From 7a996cb8ad43838e765493bc5761fa4d3a5c9bb5 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Tue, 3 Mar 2026 03:58:04 +0200 Subject: [PATCH 1/3] feat(spm): add Swift Package Manager build support Generate public header symlinks in `include/PubNub/` using the `export_for_spm.sh` script with `public-only` mode, configure `Package.swift` with header search paths and fix internal imports that relied on umbrella header transitivity. fix(presence): enforce channel and group uniqueness Replace `NSMutableArray` with `NSMutableSet` in `PNHeartbeat` manager and add `NSSet`-based deduplication in `PNSubscribeRequest` and `PNPresenceHeartbeatRequest` initializers to prevent duplicate channel and channel group entries. feat(tests): add comprehensive unit test suite Add unit tests for concurrency, requests, helpers, models, crypto modules, logger, and network reachability along with error handling and subscribe channel uniqueness tests. --- .github/workflows/run-live-tests.yml | 70 ++ .github/workflows/run-tests.yml | 3 + Framework/scripts/export_for_spm.sh | 56 +- Package.swift | 93 +- PubNub/Core/PubNub+Core.m | 2 + PubNub/Core/PubNub+Subscribe.m | 10 +- PubNub/Data/Managers/PNHeartbeat.m | 22 +- .../PNGenerateFileUploadURLStatus.h | 2 +- .../PNPresenceGlobalHereNowResult+Private.h | 1 + .../Presence/PNPresenceHereNowResult.h | 2 +- PubNub/Data/Transport/PNTransportMiddleware.h | 2 +- PubNub/Data/Transport/PNTransportMiddleware.m | 1 + .../PNTransportMiddlewareConfiguration.h | 5 +- PubNub/Misc/Helpers/PNHelpers.h | 1 + .../Crypto/Cryptors/AES/PNCCCryptorWrapper.m | 6 +- .../PNHistoryFetchRequest.m | 9 +- .../Requests/Objects/PNBaseObjectsRequest.m | 2 +- .../Presence/PNPresenceHeartbeatRequest.m | 4 +- .../Requests/Publish/PNBasePublishRequest.m | 22 + .../Network/Requests/Signal/PNSignalRequest.m | 11 + .../Requests/Subscribe/PNSubscribeRequest.m | 4 +- ...SURLSessionConfiguration+PNConfiguration.h | 1 + PubNub/include/PubNub/PNAES.h | 1 + PubNub/include/PubNub/PNAESCBCCryptor.h | 1 + PubNub/include/PubNub/PNAPICallBuilder.h | 1 + PubNub/include/PubNub/PNAPNSAPICallBuilder.h | 1 + .../PubNub/PNAPNSAuditAPICallBuilder.h | 1 + .../PubNub/PNAPNSEnabledChannelsResult.h | 1 + .../PubNub/PNAPNSModificationAPICallBuilder.h | 1 + .../PubNub/PNAPNSNotificationConfiguration.h | 1 + .../PubNub/PNAPNSNotificationPayload.h | 1 + .../include/PubNub/PNAPNSNotificationTarget.h | 1 + .../include/PubNub/PNAcknowledgmentStatus.h | 1 + .../PubNub/PNAddMessageActionAPICallBuilder.h | 1 + .../PubNub/PNAddMessageActionRequest.h | 1 + .../include/PubNub/PNAddMessageActionStatus.h | 1 + .../include/PubNub/PNBaseAppContextObject.h | 1 + .../PubNub/PNBaseMessageActionRequest.h | 1 + .../PubNub/PNBaseNotificationPayload.h | 1 + .../PubNub/PNBaseObjectsMembershipRequest.h | 1 + PubNub/include/PubNub/PNBaseObjectsRequest.h | 1 + PubNub/include/PubNub/PNBaseOperationData.h | 1 + PubNub/include/PubNub/PNBasePublishRequest.h | 1 + .../PubNub/PNBasePushNotificationsRequest.h | 1 + PubNub/include/PubNub/PNBaseRequest.h | 1 + .../PubNub/PNChannelClientStateResult.h | 1 + .../PubNub/PNChannelGroupChannelsResult.h | 1 + .../PubNub/PNChannelGroupClientStateResult.h | 1 + .../include/PubNub/PNChannelGroupFetchData.h | 1 + .../PubNub/PNChannelGroupFetchRequest.h | 1 + .../PubNub/PNChannelGroupManageRequest.h | 1 + PubNub/include/PubNub/PNChannelGroupsResult.h | 1 + PubNub/include/PubNub/PNChannelMember.h | 1 + .../PubNub/PNChannelMembersFetchData.h | 1 + .../PubNub/PNChannelMembersManageData.h | 1 + PubNub/include/PubNub/PNChannelMetadata.h | 1 + .../PubNub/PNChannelMetadataFetchAllData.h | 1 + .../PubNub/PNChannelMetadataFetchData.h | 1 + .../include/PubNub/PNChannelMetadataSetData.h | 1 + PubNub/include/PubNub/PNClientInformation.h | 1 + .../include/PubNub/PNClientStateGetResult.h | 1 + .../PubNub/PNClientStateUpdateStatus.h | 1 + PubNub/include/PubNub/PNCodable.h | 1 + PubNub/include/PubNub/PNConfiguration.h | 1 + PubNub/include/PubNub/PNCryptoModule.h | 1 + PubNub/include/PubNub/PNCryptoProvider.h | 1 + PubNub/include/PubNub/PNCryptor.h | 1 + PubNub/include/PubNub/PNCryptorInputStream.h | 1 + PubNub/include/PubNub/PNDecoder.h | 1 + PubNub/include/PubNub/PNDefines.h | 1 + .../PubNub/PNDeleteFileAPICallBuilder.h | 1 + PubNub/include/PubNub/PNDeleteFileRequest.h | 1 + .../PubNub/PNDeleteMessageAPICallBuilder.h | 1 + PubNub/include/PubNub/PNDictionaryLogEntry.h | 1 + .../PubNub/PNDownloadFileAPICallBuilder.h | 1 + PubNub/include/PubNub/PNDownloadFileRequest.h | 1 + PubNub/include/PubNub/PNDownloadFileResult.h | 1 + PubNub/include/PubNub/PNEncoder.h | 1 + PubNub/include/PubNub/PNEncryptedData.h | 1 + PubNub/include/PubNub/PNEncryptedStream.h | 1 + PubNub/include/PubNub/PNError.h | 1 + PubNub/include/PubNub/PNErrorData.h | 1 + PubNub/include/PubNub/PNErrorLogEntry.h | 1 + PubNub/include/PubNub/PNErrorStatus.h | 1 + PubNub/include/PubNub/PNEventsListener.h | 1 + .../include/PubNub/PNFCMNotificationPayload.h | 1 + ...PNFetchAllChannelsMetadataAPICallBuilder.h | 1 + .../PNFetchAllChannelsMetadataRequest.h | 1 + .../PubNub/PNFetchAllChannelsMetadataResult.h | 1 + .../PNFetchAllUUIDMetadataAPICallBuilder.h | 1 + .../PubNub/PNFetchAllUUIDMetadataRequest.h | 1 + .../PubNub/PNFetchAllUUIDMetadataResult.h | 1 + .../PNFetchChannelMembersAPICallBuilder.h | 1 + .../PubNub/PNFetchChannelMembersRequest.h | 1 + .../PubNub/PNFetchChannelMembersResult.h | 1 + .../PNFetchChannelMetadataAPICallBuilder.h | 1 + .../PubNub/PNFetchChannelMetadataRequest.h | 1 + .../PubNub/PNFetchChannelMetadataResult.h | 1 + .../PubNub/PNFetchMembershipsAPICallBuilder.h | 1 + .../PubNub/PNFetchMembershipsRequest.h | 1 + .../include/PubNub/PNFetchMembershipsResult.h | 1 + .../PubNub/PNFetchMessageActionsRequest.h | 1 + .../PubNub/PNFetchMessageActionsResult.h | 1 + .../PNFetchMessagesActionsAPICallBuilder.h | 1 + .../PNFetchUUIDMetadataAPICallBuilder.h | 1 + .../PubNub/PNFetchUUIDMetadataRequest.h | 1 + .../PubNub/PNFetchUUIDMetadataResult.h | 1 + PubNub/include/PubNub/PNFile.h | 1 + PubNub/include/PubNub/PNFileDownloadData.h | 1 + .../PubNub/PNFileDownloadURLAPICallBuilder.h | 1 + PubNub/include/PubNub/PNFileEventResult.h | 1 + PubNub/include/PubNub/PNFileListFetchData.h | 1 + PubNub/include/PubNub/PNFileLogger.h | 1 + PubNub/include/PubNub/PNFileSendData.h | 1 + PubNub/include/PubNub/PNFilesAPICallBuilder.h | 1 + PubNub/include/PubNub/PNFunctions.h | 1 + PubNub/include/PubNub/PNHereNowRequest.h | 1 + .../include/PubNub/PNHistoryAPICallBuilder.h | 1 + PubNub/include/PubNub/PNHistoryFetchData.h | 1 + PubNub/include/PubNub/PNHistoryFetchRequest.h | 1 + .../PubNub/PNHistoryMessageCountData.h | 1 + .../PubNub/PNHistoryMessagesCountRequest.h | 1 + .../PubNub/PNHistoryMessagesDeleteRequest.h | 1 + PubNub/include/PubNub/PNHistoryResult.h | 1 + PubNub/include/PubNub/PNJSONCoder.h | 1 + PubNub/include/PubNub/PNJSONSerialization.h | 1 + PubNub/include/PubNub/PNJSONSerializer.h | 1 + PubNub/include/PubNub/PNLegacyCryptor.h | 1 + .../PubNub/PNListFilesAPICallBuilder.h | 1 + PubNub/include/PubNub/PNListFilesRequest.h | 1 + PubNub/include/PubNub/PNListFilesResult.h | 1 + PubNub/include/PubNub/PNLock.h | 1 + PubNub/include/PubNub/PNLogEntry.h | 1 + PubNub/include/PubNub/PNLogger.h | 1 + PubNub/include/PubNub/PNLoggerManager.h | 1 + .../PNManageChannelMembersAPICallBuilder.h | 1 + .../PubNub/PNManageChannelMembersRequest.h | 1 + .../PubNub/PNManageChannelMembersStatus.h | 1 + .../PNManageMembershipsAPICallBuilder.h | 1 + .../PubNub/PNManageMembershipsRequest.h | 1 + .../PubNub/PNManageMembershipsStatus.h | 1 + PubNub/include/PubNub/PNMembership.h | 1 + .../include/PubNub/PNMembershipsFetchData.h | 1 + .../include/PubNub/PNMembershipsManageData.h | 1 + PubNub/include/PubNub/PNMessageAction.h | 1 + .../include/PubNub/PNMessageActionFetchData.h | 1 + PubNub/include/PubNub/PNMessageActionResult.h | 1 + .../PubNub/PNMessageActionsFetchData.h | 1 + .../PubNub/PNMessageCountAPICallBuilder.h | 1 + PubNub/include/PubNub/PNMessageCountResult.h | 1 + PubNub/include/PubNub/PNMessageResult.h | 1 + .../include/PubNub/PNNetworkRequestLogEntry.h | 1 + .../PubNub/PNNetworkResponseLogEntry.h | 1 + .../include/PubNub/PNNotificationsPayload.h | 1 + PubNub/include/PubNub/PNObjectEventResult.h | 1 + PubNub/include/PubNub/PNObjectSerializer.h | 1 + .../include/PubNub/PNObjectsAPICallBuilder.h | 1 + .../PubNub/PNObjectsPaginatedRequest.h | 1 + PubNub/include/PubNub/PNOperationResult.h | 1 + PubNub/include/PubNub/PNPAMToken.h | 1 + PubNub/include/PubNub/PNPagedAppContextData.h | 1 + .../include/PubNub/PNPresenceAPICallBuilder.h | 1 + ...resenceChannelGroupHereNowAPICallBuilder.h | 1 + .../PNPresenceChannelGroupHereNowResult.h | 1 + .../PNPresenceChannelHereNowAPICallBuilder.h | 1 + .../PubNub/PNPresenceChannelHereNowResult.h | 1 + PubNub/include/PubNub/PNPresenceEventResult.h | 1 + .../PubNub/PNPresenceGlobalHereNowResult.h | 1 + .../PNPresenceHeartbeatAPICallBuilder.h | 1 + .../PubNub/PNPresenceHeartbeatRequest.h | 1 + .../PubNub/PNPresenceHereNowAPICallBuilder.h | 1 + .../PubNub/PNPresenceHereNowFetchData.h | 1 + .../include/PubNub/PNPresenceHereNowResult.h | 1 + .../include/PubNub/PNPresenceLeaveRequest.h | 1 + .../PubNub/PNPresenceStateFetchRequest.h | 1 + .../PubNub/PNPresenceStateFetchResult.h | 1 + .../PubNub/PNPresenceStateSetRequest.h | 1 + .../PubNub/PNPresenceUserStateFetchData.h | 1 + .../PubNub/PNPresenceUserStateSetData.h | 1 + .../PubNub/PNPresenceWhereNowAPICallBuilder.h | 1 + .../PubNub/PNPresenceWhereNowFetchData.h | 1 + .../include/PubNub/PNPresenceWhereNowResult.h | 1 + .../include/PubNub/PNPublishAPICallBuilder.h | 1 + PubNub/include/PubNub/PNPublishData.h | 1 + .../PNPublishFileMessageAPICallBuilder.h | 1 + .../PubNub/PNPublishFileMessageRequest.h | 1 + PubNub/include/PubNub/PNPublishRequest.h | 1 + .../PubNub/PNPublishSizeAPICallBuilder.h | 1 + PubNub/include/PubNub/PNPublishStatus.h | 1 + .../PubNub/PNPushNotificationFetchData.h | 1 + .../PubNub/PNPushNotificationFetchRequest.h | 1 + .../PubNub/PNPushNotificationManageRequest.h | 1 + .../PNRemoveChannelMembersAPICallBuilder.h | 1 + .../PubNub/PNRemoveChannelMembersRequest.h | 1 + .../PNRemoveChannelMetadataAPICallBuilder.h | 1 + .../PubNub/PNRemoveChannelMetadataRequest.h | 1 + .../PNRemoveMembershipsAPICallBuilder.h | 1 + .../PubNub/PNRemoveMembershipsRequest.h | 1 + .../PNRemoveMessageActionAPICallBuilder.h | 1 + .../PubNub/PNRemoveMessageActionRequest.h | 1 + .../PNRemoveUUIDMetadataAPICallBuilder.h | 1 + .../PubNub/PNRemoveUUIDMetadataRequest.h | 1 + .../PubNub/PNRequestRetryConfiguration.h | 1 + PubNub/include/PubNub/PNResult.h | 1 + .../include/PubNub/PNSendFileAPICallBuilder.h | 1 + PubNub/include/PubNub/PNSendFileRequest.h | 1 + PubNub/include/PubNub/PNSendFileStatus.h | 1 + PubNub/include/PubNub/PNServiceData.h | 1 + .../PNSetChannelMembersAPICallBuilder.h | 1 + .../PubNub/PNSetChannelMembersRequest.h | 1 + .../PNSetChannelMetadataAPICallBuilder.h | 1 + .../PubNub/PNSetChannelMetadataRequest.h | 1 + .../PubNub/PNSetChannelMetadataStatus.h | 1 + .../PubNub/PNSetMembershipsAPICallBuilder.h | 1 + .../include/PubNub/PNSetMembershipsRequest.h | 1 + .../PubNub/PNSetUUIDMetadataAPICallBuilder.h | 1 + .../include/PubNub/PNSetUUIDMetadataRequest.h | 1 + .../include/PubNub/PNSetUUIDMetadataStatus.h | 1 + .../include/PubNub/PNSignalAPICallBuilder.h | 1 + PubNub/include/PubNub/PNSignalData.h | 1 + PubNub/include/PubNub/PNSignalRequest.h | 1 + PubNub/include/PubNub/PNSignalResult.h | 1 + PubNub/include/PubNub/PNSignalStatus.h | 1 + PubNub/include/PubNub/PNStateAPICallBuilder.h | 1 + .../PubNub/PNStateAuditAPICallBuilder.h | 1 + .../PNStateModificationAPICallBuilder.h | 1 + PubNub/include/PubNub/PNStatus.h | 1 + .../include/PubNub/PNStreamAPICallBuilder.h | 1 + .../PubNub/PNStreamAuditAPICallBuilder.h | 1 + .../PNStreamModificationAPICallBuilder.h | 1 + PubNub/include/PubNub/PNStringLogEntry.h | 1 + PubNub/include/PubNub/PNStructures.h | 1 + PubNub/include/PubNub/PNSubscribeAPIBuilder.h | 1 + .../PNSubscribeChannelsOrGroupsAPIBuilder.h | 1 + PubNub/include/PubNub/PNSubscribeEventData.h | 1 + .../include/PubNub/PNSubscribeFileEventData.h | 1 + .../PNSubscribeMessageActionEventData.h | 1 + .../PubNub/PNSubscribeMessageEventData.h | 1 + .../PubNub/PNSubscribeObjectEventData.h | 1 + .../PubNub/PNSubscribePresenceEventData.h | 1 + PubNub/include/PubNub/PNSubscribeRequest.h | 1 + .../PubNub/PNSubscribeSignalEventData.h | 1 + PubNub/include/PubNub/PNSubscribeStatus.h | 1 + PubNub/include/PubNub/PNTimeAPICallBuilder.h | 1 + PubNub/include/PubNub/PNTimeData.h | 1 + PubNub/include/PubNub/PNTimeRequest.h | 1 + PubNub/include/PubNub/PNTimeResult.h | 1 + PubNub/include/PubNub/PNTransport.h | 1 + .../include/PubNub/PNTransportConfiguration.h | 1 + PubNub/include/PubNub/PNTransportRequest.h | 1 + PubNub/include/PubNub/PNTransportResponse.h | 1 + PubNub/include/PubNub/PNUUIDMetadata.h | 1 + .../PubNub/PNUUIDMetadataFetchAllData.h | 1 + .../include/PubNub/PNUUIDMetadataFetchData.h | 1 + PubNub/include/PubNub/PNUUIDMetadataSetData.h | 1 + .../PubNub/PNUnsubscribeAPICallBuilder.h | 1 + ...nsubscribeChannelsOrGroupsAPICallBuilder.h | 1 + PubNub/include/PubNub/PNWhereNowRequest.h | 1 + PubNub/include/PubNub/PubNub+APNS.h | 1 + PubNub/include/PubNub/PubNub+ChannelGroup.h | 1 + PubNub/include/PubNub/PubNub+Core.h | 1 + PubNub/include/PubNub/PubNub+Files.h | 1 + PubNub/include/PubNub/PubNub+History.h | 1 + PubNub/include/PubNub/PubNub+MessageActions.h | 1 + PubNub/include/PubNub/PubNub+Objects.h | 1 + PubNub/include/PubNub/PubNub+PAM.h | 1 + PubNub/include/PubNub/PubNub+Presence.h | 1 + PubNub/include/PubNub/PubNub+Publish.h | 1 + PubNub/include/PubNub/PubNub+State.h | 1 + PubNub/include/PubNub/PubNub+Subscribe.h | 1 + PubNub/include/PubNub/PubNub+Time.h | 1 + Tests/Podfile | 2 + Tests/Podfile.lock | 10 +- Tests/PubNub Tests.xcodeproj/project.pbxproj | 1000 +++++++++++++++++ Tests/Support Files/Scripts/tests-runner.sh | 3 +- Tests/Tests/Helpers/PNRecordableTestCase.h | 8 + Tests/Tests/Helpers/PNRecordableTestCase.m | 21 +- .../PNChannelGroupIntegrationTests.m | 7 +- .../Integration/PNHistoryIntegrationTests.m | 7 +- .../Integration/PNPublishIntegrationTests.m | 5 +- .../PNPushNotificationsIntegrationTests.m | 7 +- .../Integration/PNSubscribeIntegrationTest.m | 5 +- Tests/Tests/Unit/Concurrency/PNLockTest.m | 497 ++++++++ .../Unit/Concurrency/PNThreadSafetyTest.m | 658 +++++++++++ .../ChannelGroups/PNChannelGroupErrorTest.m | 200 ++++ .../Tests/Unit/Core/Files/PNFilesErrorTest.m | 275 +++++ .../Unit/Core/History/PNHistoryErrorTest.m | 242 ++++ .../PNMessageActionsErrorTest.m | 130 +++ .../Unit/Core/Objects/PNObjectsErrorTest.m | 234 ++++ .../Unit/Core/Presence/PNPresenceErrorTest.m | 272 +++++ .../Unit/Core/Publish/PNPublishErrorTest.m | 175 +++ .../ChannelGroups/PNChannelGroupRequestTest.m | 194 ++++ .../Core/Requests/Files/PNFilesRequestTest.m | 263 +++++ .../Requests/History/PNHistoryRequestTest.m | 262 +++++ .../PNMessageActionsRequestTest.m | 245 ++++ .../Requests/Objects/PNObjectsRequestTest.m | 265 +++++ .../Requests/Objects/PNRelationsRequestTest.m | 385 +++++++ .../Requests/Presence/PNPresenceRequestTest.m | 270 +++++ .../Requests/Publish/PNPublishRequestTest.m | 163 +++ .../Core/Requests/Push/PNPushRequestTest.m | 347 ++++++ .../Subscribe/PNSubscribeRequestTest.m | 187 +++ .../Core/Requests/Time/PNTimeRequestTest.m | 77 ++ .../Core/Subscribe/PNSubscribeErrorTest.m | 120 ++ .../Unit/Core/Subscribe/PNSubscribeTest.m | 107 +- Tests/Tests/Unit/Helpers/PNArrayTest.m | 121 ++ Tests/Tests/Unit/Helpers/PNChannelTest.m | 248 ++++ Tests/Tests/Unit/Helpers/PNDataTest.m | 138 +++ Tests/Tests/Unit/Helpers/PNDateTest.m | 133 +++ Tests/Tests/Unit/Helpers/PNDictionaryTest.m | 149 +++ Tests/Tests/Unit/Helpers/PNFunctionsTest.m | 202 ++++ Tests/Tests/Unit/Helpers/PNGZIPTest.m | 216 ++++ Tests/Tests/Unit/Helpers/PNJSONHelperTest.m | 264 +++++ .../PNNotificationPayloadBuilderTest.m | 4 - Tests/Tests/Unit/Helpers/PNNumberTest.m | 95 ++ Tests/Tests/Unit/Helpers/PNStringTest.m | 238 ++++ Tests/Tests/Unit/Helpers/PNURLRequestTest.m | 101 ++ Tests/Tests/Unit/Models/PNEventResultTest.m | 371 ++++++ Tests/Tests/Unit/Models/PNResultTest.m | 326 ++++++ Tests/Tests/Unit/Models/PNStatusTest.m | 581 ++++++++++ .../Unit/Modules/Crypto/PNAESCBCCryptorTest.m | 299 +++++ .../Unit/Modules/Crypto/PNCryptoModuleTest.m | 297 +++++ .../Unit/Modules/Crypto/PNEncryptedDataTest.m | 121 ++ .../Unit/Modules/Crypto/PNLegacyCryptorTest.m | 281 +++++ .../Unit/Modules/Logger/PNConsoleLoggerTest.m | 651 +++++++++++ .../Unit/Modules/Logger/PNFileLoggerTest.m | 477 ++++++++ .../Unit/Modules/Logger/PNLogEntryTest.m | 357 ++++++ .../Unit/Modules/Logger/PNLoggerManagerTest.m | 893 +++++++++++++++ .../Network/Reachability/PNMockTransport.h | 148 +++ .../Network/Reachability/PNMockTransport.m | 410 +++++++ .../Reachability/PNNetworkReachabilityTest.m | 960 ++++++++++++++++ 330 files changed, 15201 insertions(+), 101 deletions(-) create mode 100644 .github/workflows/run-live-tests.yml create mode 120000 PubNub/include/PubNub/NSURLSessionConfiguration+PNConfiguration.h create mode 120000 PubNub/include/PubNub/PNAES.h create mode 120000 PubNub/include/PubNub/PNAESCBCCryptor.h create mode 120000 PubNub/include/PubNub/PNAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNAPNSAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNAPNSAuditAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNAPNSEnabledChannelsResult.h create mode 120000 PubNub/include/PubNub/PNAPNSModificationAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNAPNSNotificationConfiguration.h create mode 120000 PubNub/include/PubNub/PNAPNSNotificationPayload.h create mode 120000 PubNub/include/PubNub/PNAPNSNotificationTarget.h create mode 120000 PubNub/include/PubNub/PNAcknowledgmentStatus.h create mode 120000 PubNub/include/PubNub/PNAddMessageActionAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNAddMessageActionRequest.h create mode 120000 PubNub/include/PubNub/PNAddMessageActionStatus.h create mode 120000 PubNub/include/PubNub/PNBaseAppContextObject.h create mode 120000 PubNub/include/PubNub/PNBaseMessageActionRequest.h create mode 120000 PubNub/include/PubNub/PNBaseNotificationPayload.h create mode 120000 PubNub/include/PubNub/PNBaseObjectsMembershipRequest.h create mode 120000 PubNub/include/PubNub/PNBaseObjectsRequest.h create mode 120000 PubNub/include/PubNub/PNBaseOperationData.h create mode 120000 PubNub/include/PubNub/PNBasePublishRequest.h create mode 120000 PubNub/include/PubNub/PNBasePushNotificationsRequest.h create mode 120000 PubNub/include/PubNub/PNBaseRequest.h create mode 120000 PubNub/include/PubNub/PNChannelClientStateResult.h create mode 120000 PubNub/include/PubNub/PNChannelGroupChannelsResult.h create mode 120000 PubNub/include/PubNub/PNChannelGroupClientStateResult.h create mode 120000 PubNub/include/PubNub/PNChannelGroupFetchData.h create mode 120000 PubNub/include/PubNub/PNChannelGroupFetchRequest.h create mode 120000 PubNub/include/PubNub/PNChannelGroupManageRequest.h create mode 120000 PubNub/include/PubNub/PNChannelGroupsResult.h create mode 120000 PubNub/include/PubNub/PNChannelMember.h create mode 120000 PubNub/include/PubNub/PNChannelMembersFetchData.h create mode 120000 PubNub/include/PubNub/PNChannelMembersManageData.h create mode 120000 PubNub/include/PubNub/PNChannelMetadata.h create mode 120000 PubNub/include/PubNub/PNChannelMetadataFetchAllData.h create mode 120000 PubNub/include/PubNub/PNChannelMetadataFetchData.h create mode 120000 PubNub/include/PubNub/PNChannelMetadataSetData.h create mode 120000 PubNub/include/PubNub/PNClientInformation.h create mode 120000 PubNub/include/PubNub/PNClientStateGetResult.h create mode 120000 PubNub/include/PubNub/PNClientStateUpdateStatus.h create mode 120000 PubNub/include/PubNub/PNCodable.h create mode 120000 PubNub/include/PubNub/PNConfiguration.h create mode 120000 PubNub/include/PubNub/PNCryptoModule.h create mode 120000 PubNub/include/PubNub/PNCryptoProvider.h create mode 120000 PubNub/include/PubNub/PNCryptor.h create mode 120000 PubNub/include/PubNub/PNCryptorInputStream.h create mode 120000 PubNub/include/PubNub/PNDecoder.h create mode 120000 PubNub/include/PubNub/PNDefines.h create mode 120000 PubNub/include/PubNub/PNDeleteFileAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNDeleteFileRequest.h create mode 120000 PubNub/include/PubNub/PNDeleteMessageAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNDictionaryLogEntry.h create mode 120000 PubNub/include/PubNub/PNDownloadFileAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNDownloadFileRequest.h create mode 120000 PubNub/include/PubNub/PNDownloadFileResult.h create mode 120000 PubNub/include/PubNub/PNEncoder.h create mode 120000 PubNub/include/PubNub/PNEncryptedData.h create mode 120000 PubNub/include/PubNub/PNEncryptedStream.h create mode 120000 PubNub/include/PubNub/PNError.h create mode 120000 PubNub/include/PubNub/PNErrorData.h create mode 120000 PubNub/include/PubNub/PNErrorLogEntry.h create mode 120000 PubNub/include/PubNub/PNErrorStatus.h create mode 120000 PubNub/include/PubNub/PNEventsListener.h create mode 120000 PubNub/include/PubNub/PNFCMNotificationPayload.h create mode 120000 PubNub/include/PubNub/PNFetchAllChannelsMetadataAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNFetchAllChannelsMetadataRequest.h create mode 120000 PubNub/include/PubNub/PNFetchAllChannelsMetadataResult.h create mode 120000 PubNub/include/PubNub/PNFetchAllUUIDMetadataAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNFetchAllUUIDMetadataRequest.h create mode 120000 PubNub/include/PubNub/PNFetchAllUUIDMetadataResult.h create mode 120000 PubNub/include/PubNub/PNFetchChannelMembersAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNFetchChannelMembersRequest.h create mode 120000 PubNub/include/PubNub/PNFetchChannelMembersResult.h create mode 120000 PubNub/include/PubNub/PNFetchChannelMetadataAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNFetchChannelMetadataRequest.h create mode 120000 PubNub/include/PubNub/PNFetchChannelMetadataResult.h create mode 120000 PubNub/include/PubNub/PNFetchMembershipsAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNFetchMembershipsRequest.h create mode 120000 PubNub/include/PubNub/PNFetchMembershipsResult.h create mode 120000 PubNub/include/PubNub/PNFetchMessageActionsRequest.h create mode 120000 PubNub/include/PubNub/PNFetchMessageActionsResult.h create mode 120000 PubNub/include/PubNub/PNFetchMessagesActionsAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNFetchUUIDMetadataAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNFetchUUIDMetadataRequest.h create mode 120000 PubNub/include/PubNub/PNFetchUUIDMetadataResult.h create mode 120000 PubNub/include/PubNub/PNFile.h create mode 120000 PubNub/include/PubNub/PNFileDownloadData.h create mode 120000 PubNub/include/PubNub/PNFileDownloadURLAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNFileEventResult.h create mode 120000 PubNub/include/PubNub/PNFileListFetchData.h create mode 120000 PubNub/include/PubNub/PNFileLogger.h create mode 120000 PubNub/include/PubNub/PNFileSendData.h create mode 120000 PubNub/include/PubNub/PNFilesAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNFunctions.h create mode 120000 PubNub/include/PubNub/PNHereNowRequest.h create mode 120000 PubNub/include/PubNub/PNHistoryAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNHistoryFetchData.h create mode 120000 PubNub/include/PubNub/PNHistoryFetchRequest.h create mode 120000 PubNub/include/PubNub/PNHistoryMessageCountData.h create mode 120000 PubNub/include/PubNub/PNHistoryMessagesCountRequest.h create mode 120000 PubNub/include/PubNub/PNHistoryMessagesDeleteRequest.h create mode 120000 PubNub/include/PubNub/PNHistoryResult.h create mode 120000 PubNub/include/PubNub/PNJSONCoder.h create mode 120000 PubNub/include/PubNub/PNJSONSerialization.h create mode 120000 PubNub/include/PubNub/PNJSONSerializer.h create mode 120000 PubNub/include/PubNub/PNLegacyCryptor.h create mode 120000 PubNub/include/PubNub/PNListFilesAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNListFilesRequest.h create mode 120000 PubNub/include/PubNub/PNListFilesResult.h create mode 120000 PubNub/include/PubNub/PNLock.h create mode 120000 PubNub/include/PubNub/PNLogEntry.h create mode 120000 PubNub/include/PubNub/PNLogger.h create mode 120000 PubNub/include/PubNub/PNLoggerManager.h create mode 120000 PubNub/include/PubNub/PNManageChannelMembersAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNManageChannelMembersRequest.h create mode 120000 PubNub/include/PubNub/PNManageChannelMembersStatus.h create mode 120000 PubNub/include/PubNub/PNManageMembershipsAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNManageMembershipsRequest.h create mode 120000 PubNub/include/PubNub/PNManageMembershipsStatus.h create mode 120000 PubNub/include/PubNub/PNMembership.h create mode 120000 PubNub/include/PubNub/PNMembershipsFetchData.h create mode 120000 PubNub/include/PubNub/PNMembershipsManageData.h create mode 120000 PubNub/include/PubNub/PNMessageAction.h create mode 120000 PubNub/include/PubNub/PNMessageActionFetchData.h create mode 120000 PubNub/include/PubNub/PNMessageActionResult.h create mode 120000 PubNub/include/PubNub/PNMessageActionsFetchData.h create mode 120000 PubNub/include/PubNub/PNMessageCountAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNMessageCountResult.h create mode 120000 PubNub/include/PubNub/PNMessageResult.h create mode 120000 PubNub/include/PubNub/PNNetworkRequestLogEntry.h create mode 120000 PubNub/include/PubNub/PNNetworkResponseLogEntry.h create mode 120000 PubNub/include/PubNub/PNNotificationsPayload.h create mode 120000 PubNub/include/PubNub/PNObjectEventResult.h create mode 120000 PubNub/include/PubNub/PNObjectSerializer.h create mode 120000 PubNub/include/PubNub/PNObjectsAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNObjectsPaginatedRequest.h create mode 120000 PubNub/include/PubNub/PNOperationResult.h create mode 120000 PubNub/include/PubNub/PNPAMToken.h create mode 120000 PubNub/include/PubNub/PNPagedAppContextData.h create mode 120000 PubNub/include/PubNub/PNPresenceAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNPresenceChannelGroupHereNowAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNPresenceChannelGroupHereNowResult.h create mode 120000 PubNub/include/PubNub/PNPresenceChannelHereNowAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNPresenceChannelHereNowResult.h create mode 120000 PubNub/include/PubNub/PNPresenceEventResult.h create mode 120000 PubNub/include/PubNub/PNPresenceGlobalHereNowResult.h create mode 120000 PubNub/include/PubNub/PNPresenceHeartbeatAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNPresenceHeartbeatRequest.h create mode 120000 PubNub/include/PubNub/PNPresenceHereNowAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNPresenceHereNowFetchData.h create mode 120000 PubNub/include/PubNub/PNPresenceHereNowResult.h create mode 120000 PubNub/include/PubNub/PNPresenceLeaveRequest.h create mode 120000 PubNub/include/PubNub/PNPresenceStateFetchRequest.h create mode 120000 PubNub/include/PubNub/PNPresenceStateFetchResult.h create mode 120000 PubNub/include/PubNub/PNPresenceStateSetRequest.h create mode 120000 PubNub/include/PubNub/PNPresenceUserStateFetchData.h create mode 120000 PubNub/include/PubNub/PNPresenceUserStateSetData.h create mode 120000 PubNub/include/PubNub/PNPresenceWhereNowAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNPresenceWhereNowFetchData.h create mode 120000 PubNub/include/PubNub/PNPresenceWhereNowResult.h create mode 120000 PubNub/include/PubNub/PNPublishAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNPublishData.h create mode 120000 PubNub/include/PubNub/PNPublishFileMessageAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNPublishFileMessageRequest.h create mode 120000 PubNub/include/PubNub/PNPublishRequest.h create mode 120000 PubNub/include/PubNub/PNPublishSizeAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNPublishStatus.h create mode 120000 PubNub/include/PubNub/PNPushNotificationFetchData.h create mode 120000 PubNub/include/PubNub/PNPushNotificationFetchRequest.h create mode 120000 PubNub/include/PubNub/PNPushNotificationManageRequest.h create mode 120000 PubNub/include/PubNub/PNRemoveChannelMembersAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNRemoveChannelMembersRequest.h create mode 120000 PubNub/include/PubNub/PNRemoveChannelMetadataAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNRemoveChannelMetadataRequest.h create mode 120000 PubNub/include/PubNub/PNRemoveMembershipsAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNRemoveMembershipsRequest.h create mode 120000 PubNub/include/PubNub/PNRemoveMessageActionAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNRemoveMessageActionRequest.h create mode 120000 PubNub/include/PubNub/PNRemoveUUIDMetadataAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNRemoveUUIDMetadataRequest.h create mode 120000 PubNub/include/PubNub/PNRequestRetryConfiguration.h create mode 120000 PubNub/include/PubNub/PNResult.h create mode 120000 PubNub/include/PubNub/PNSendFileAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNSendFileRequest.h create mode 120000 PubNub/include/PubNub/PNSendFileStatus.h create mode 120000 PubNub/include/PubNub/PNServiceData.h create mode 120000 PubNub/include/PubNub/PNSetChannelMembersAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNSetChannelMembersRequest.h create mode 120000 PubNub/include/PubNub/PNSetChannelMetadataAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNSetChannelMetadataRequest.h create mode 120000 PubNub/include/PubNub/PNSetChannelMetadataStatus.h create mode 120000 PubNub/include/PubNub/PNSetMembershipsAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNSetMembershipsRequest.h create mode 120000 PubNub/include/PubNub/PNSetUUIDMetadataAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNSetUUIDMetadataRequest.h create mode 120000 PubNub/include/PubNub/PNSetUUIDMetadataStatus.h create mode 120000 PubNub/include/PubNub/PNSignalAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNSignalData.h create mode 120000 PubNub/include/PubNub/PNSignalRequest.h create mode 120000 PubNub/include/PubNub/PNSignalResult.h create mode 120000 PubNub/include/PubNub/PNSignalStatus.h create mode 120000 PubNub/include/PubNub/PNStateAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNStateAuditAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNStateModificationAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNStatus.h create mode 120000 PubNub/include/PubNub/PNStreamAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNStreamAuditAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNStreamModificationAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNStringLogEntry.h create mode 120000 PubNub/include/PubNub/PNStructures.h create mode 120000 PubNub/include/PubNub/PNSubscribeAPIBuilder.h create mode 120000 PubNub/include/PubNub/PNSubscribeChannelsOrGroupsAPIBuilder.h create mode 120000 PubNub/include/PubNub/PNSubscribeEventData.h create mode 120000 PubNub/include/PubNub/PNSubscribeFileEventData.h create mode 120000 PubNub/include/PubNub/PNSubscribeMessageActionEventData.h create mode 120000 PubNub/include/PubNub/PNSubscribeMessageEventData.h create mode 120000 PubNub/include/PubNub/PNSubscribeObjectEventData.h create mode 120000 PubNub/include/PubNub/PNSubscribePresenceEventData.h create mode 120000 PubNub/include/PubNub/PNSubscribeRequest.h create mode 120000 PubNub/include/PubNub/PNSubscribeSignalEventData.h create mode 120000 PubNub/include/PubNub/PNSubscribeStatus.h create mode 120000 PubNub/include/PubNub/PNTimeAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNTimeData.h create mode 120000 PubNub/include/PubNub/PNTimeRequest.h create mode 120000 PubNub/include/PubNub/PNTimeResult.h create mode 120000 PubNub/include/PubNub/PNTransport.h create mode 120000 PubNub/include/PubNub/PNTransportConfiguration.h create mode 120000 PubNub/include/PubNub/PNTransportRequest.h create mode 120000 PubNub/include/PubNub/PNTransportResponse.h create mode 120000 PubNub/include/PubNub/PNUUIDMetadata.h create mode 120000 PubNub/include/PubNub/PNUUIDMetadataFetchAllData.h create mode 120000 PubNub/include/PubNub/PNUUIDMetadataFetchData.h create mode 120000 PubNub/include/PubNub/PNUUIDMetadataSetData.h create mode 120000 PubNub/include/PubNub/PNUnsubscribeAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNUnsubscribeChannelsOrGroupsAPICallBuilder.h create mode 120000 PubNub/include/PubNub/PNWhereNowRequest.h create mode 120000 PubNub/include/PubNub/PubNub+APNS.h create mode 120000 PubNub/include/PubNub/PubNub+ChannelGroup.h create mode 120000 PubNub/include/PubNub/PubNub+Core.h create mode 120000 PubNub/include/PubNub/PubNub+Files.h create mode 120000 PubNub/include/PubNub/PubNub+History.h create mode 120000 PubNub/include/PubNub/PubNub+MessageActions.h create mode 120000 PubNub/include/PubNub/PubNub+Objects.h create mode 120000 PubNub/include/PubNub/PubNub+PAM.h create mode 120000 PubNub/include/PubNub/PubNub+Presence.h create mode 120000 PubNub/include/PubNub/PubNub+Publish.h create mode 120000 PubNub/include/PubNub/PubNub+State.h create mode 120000 PubNub/include/PubNub/PubNub+Subscribe.h create mode 120000 PubNub/include/PubNub/PubNub+Time.h create mode 100644 Tests/Tests/Unit/Concurrency/PNLockTest.m create mode 100644 Tests/Tests/Unit/Concurrency/PNThreadSafetyTest.m create mode 100644 Tests/Tests/Unit/Core/ChannelGroups/PNChannelGroupErrorTest.m create mode 100644 Tests/Tests/Unit/Core/Files/PNFilesErrorTest.m create mode 100644 Tests/Tests/Unit/Core/History/PNHistoryErrorTest.m create mode 100644 Tests/Tests/Unit/Core/MessageActions/PNMessageActionsErrorTest.m create mode 100644 Tests/Tests/Unit/Core/Objects/PNObjectsErrorTest.m create mode 100644 Tests/Tests/Unit/Core/Presence/PNPresenceErrorTest.m create mode 100644 Tests/Tests/Unit/Core/Publish/PNPublishErrorTest.m create mode 100644 Tests/Tests/Unit/Core/Requests/ChannelGroups/PNChannelGroupRequestTest.m create mode 100644 Tests/Tests/Unit/Core/Requests/Files/PNFilesRequestTest.m create mode 100644 Tests/Tests/Unit/Core/Requests/History/PNHistoryRequestTest.m create mode 100644 Tests/Tests/Unit/Core/Requests/MessageActions/PNMessageActionsRequestTest.m create mode 100644 Tests/Tests/Unit/Core/Requests/Objects/PNObjectsRequestTest.m create mode 100644 Tests/Tests/Unit/Core/Requests/Objects/PNRelationsRequestTest.m create mode 100644 Tests/Tests/Unit/Core/Requests/Presence/PNPresenceRequestTest.m create mode 100644 Tests/Tests/Unit/Core/Requests/Publish/PNPublishRequestTest.m create mode 100644 Tests/Tests/Unit/Core/Requests/Push/PNPushRequestTest.m create mode 100644 Tests/Tests/Unit/Core/Requests/Subscribe/PNSubscribeRequestTest.m create mode 100644 Tests/Tests/Unit/Core/Requests/Time/PNTimeRequestTest.m create mode 100644 Tests/Tests/Unit/Core/Subscribe/PNSubscribeErrorTest.m create mode 100644 Tests/Tests/Unit/Helpers/PNArrayTest.m create mode 100644 Tests/Tests/Unit/Helpers/PNChannelTest.m create mode 100644 Tests/Tests/Unit/Helpers/PNDataTest.m create mode 100644 Tests/Tests/Unit/Helpers/PNDateTest.m create mode 100644 Tests/Tests/Unit/Helpers/PNDictionaryTest.m create mode 100644 Tests/Tests/Unit/Helpers/PNFunctionsTest.m create mode 100644 Tests/Tests/Unit/Helpers/PNGZIPTest.m create mode 100644 Tests/Tests/Unit/Helpers/PNJSONHelperTest.m create mode 100644 Tests/Tests/Unit/Helpers/PNNumberTest.m create mode 100644 Tests/Tests/Unit/Helpers/PNStringTest.m create mode 100644 Tests/Tests/Unit/Helpers/PNURLRequestTest.m create mode 100644 Tests/Tests/Unit/Models/PNEventResultTest.m create mode 100644 Tests/Tests/Unit/Models/PNResultTest.m create mode 100644 Tests/Tests/Unit/Models/PNStatusTest.m create mode 100644 Tests/Tests/Unit/Modules/Crypto/PNAESCBCCryptorTest.m create mode 100644 Tests/Tests/Unit/Modules/Crypto/PNCryptoModuleTest.m create mode 100644 Tests/Tests/Unit/Modules/Crypto/PNEncryptedDataTest.m create mode 100644 Tests/Tests/Unit/Modules/Crypto/PNLegacyCryptorTest.m create mode 100644 Tests/Tests/Unit/Modules/Logger/PNConsoleLoggerTest.m create mode 100644 Tests/Tests/Unit/Modules/Logger/PNFileLoggerTest.m create mode 100644 Tests/Tests/Unit/Modules/Logger/PNLogEntryTest.m create mode 100644 Tests/Tests/Unit/Modules/Logger/PNLoggerManagerTest.m create mode 100644 Tests/Tests/Unit/Network/Reachability/PNMockTransport.h create mode 100644 Tests/Tests/Unit/Network/Reachability/PNMockTransport.m create mode 100644 Tests/Tests/Unit/Network/Reachability/PNNetworkReachabilityTest.m diff --git a/.github/workflows/run-live-tests.yml b/.github/workflows/run-live-tests.yml new file mode 100644 index 000000000..ae58ffcd0 --- /dev/null +++ b/.github/workflows/run-live-tests.yml @@ -0,0 +1,70 @@ +name: Live Integration Tests + +on: + workflow_dispatch: + schedule: + # Run every two weeks on Saturday at midnight UTC (1st and 3rd Saturday of each month) + - cron: '0 0 1-7 * 6' + - cron: '0 0 15-21 * 6' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +defaults: + run: + shell: bash +env: + TESTS_PAM_SUBSCRIBE_KEY: ${{ secrets.SDK_PAM_SUB_KEY }} + TESTS_PAM_PUBLISH_KEY: ${{ secrets.SDK_PAM_PUB_KEY }} + TESTS_SUBSCRIBE_KEY: ${{ secrets.SDK_SUB_KEY }} + TESTS_PUBLISH_KEY: ${{ secrets.SDK_PUB_KEY }} + LANG: en_US.UTF-8 + LANGUAGE: en_US.UTF-8 + LC_ALL: en_US.UTF-8 + LC_CTYPE: en_US.UTF-8 + +jobs: + live-tests: + name: Live integration tests + runs-on: + group: organization/macos-gh + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_TOKEN }} + - name: Checkout actions + uses: actions/checkout@v4 + with: + repository: pubnub/client-engineering-deployment-tools + ref: v1 + token: ${{ secrets.GH_TOKEN }} + path: .github/.release/actions + - name: Setup Ruby 3.2.2 + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2.2 + bundler-cache: false + - name: Setup CocoaPods + run: gem install cocoapods + - name: Cache installed Pods + uses: actions/cache@v4 + with: + path: Tests/Pods + key: ${{ runner.os }}-pods-${{ hashFiles('**/Tests/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-pods- + - name: Pre-load simulators list + run: xcrun simctl list -j + - name: Install dependencies + run: | + gem install xcpretty -v 0.3.0 + cd ./Tests && pod install && cd .. + - name: Configure test environment + run: | + ./Tests/Support\ Files/Scripts/create-configuration.sh + - name: Run iOS live integration tests + run: | + ./Tests/Support\ Files/Scripts/tests-runner.sh ios integration 1 + - name: Cancel workflow runs for commit on error + if: failure() + uses: ./.github/.release/actions/actions/utils/fast-jobs-failure diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 1d45a74f2..3eacb433b 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -64,6 +64,9 @@ jobs: - name: Configure test environment run: | ./Tests/Support\ Files/Scripts/create-configuration.sh + - name: Run ${{ matrix.environment }} unit tests + run: | + ./Tests/Support\ Files/Scripts/tests-runner.sh $(echo ${{ matrix.environment }} | tr '[:upper:]' '[:lower:]') unit 1 - name: Run ${{ matrix.environment }} integration tests run: | ./Tests/Support\ Files/Scripts/tests-runner.sh $(echo ${{ matrix.environment }} | tr '[:upper:]' '[:lower:]') mocked 1 diff --git a/Framework/scripts/export_for_spm.sh b/Framework/scripts/export_for_spm.sh index 9ad427924..8fd18a294 100755 --- a/Framework/scripts/export_for_spm.sh +++ b/Framework/scripts/export_for_spm.sh @@ -5,6 +5,7 @@ set -e WORKING_DIRECTORY="$(pwd)" SOURCES_FOLDER="$1" [[ "$2" == public-only ]] && PUBLIC_ONLY=1 || PUBLIC_ONLY=0 +UMBRELLA_HEADER="${3:-$SOURCES_FOLDER/PubNub.h}" PRIVATE_HEADERS=() PUBLIC_HEADERS=() ALL_HEADERS=() @@ -62,35 +63,28 @@ gather_imported_headers_in_file() { } +regex=".*Private.h" +# Retrieve list of all headers. +while IFS='' read -r HEADER_PATH; do + RELATIVE_PATH="${HEADER_PATH#"$WORKING_DIRECTORY/$SOURCES_FOLDER/"}" + FILENAME="$(echo "$RELATIVE_PATH" | rev | cut -d/ -f1 | rev)" + FILES+=( "$FILENAME:$RELATIVE_PATH" ) + ALL_HEADERS+=("$RELATIVE_PATH") + [[ "$RELATIVE_PATH" =~ $regex ]] && PRIVATE_HEADERS+=("$RELATIVE_PATH") +done <<< "$(find "$WORKING_DIRECTORY/$SOURCES_FOLDER" -path "*/include" -prune -o -type f ! \( -name "*.m" -o -name ".DS_Store" \) -print)" + if [[ $PUBLIC_ONLY == 1 ]]; then - regex=".*Private.h" - # Retrieve list of potentially public headers. - while IFS='' read -r HEADER_PATH; do - RELATIVE_PATH="${HEADER_PATH#"$WORKING_DIRECTORY/$SOURCES_FOLDER/"}" - FILENAME="$(echo "$RELATIVE_PATH" | rev | cut -d/ -f1 | rev)" - FILES+=( "$FILENAME:$RELATIVE_PATH" ) - ALL_HEADERS+=("$RELATIVE_PATH") - [[ "$RELATIVE_PATH" =~ $regex ]] && PRIVATE_HEADERS+=("$RELATIVE_PATH") - done <<< "$(find "$WORKING_DIRECTORY/$SOURCES_FOLDER" -type f ! \( -name "*.m" -o -name ".DS_Store" -o -name "*Private.h" \))" - - # Scan for public headers - gather_imported_headers_in_file "$SOURCES_FOLDER/PubNub.h" -else - regex=".*Private.h" - # Retrieve list of all headers. - while IFS='' read -r HEADER_PATH; do - RELATIVE_PATH="${HEADER_PATH#"$WORKING_DIRECTORY/$SOURCES_FOLDER/"}" - FILENAME="$(echo "$RELATIVE_PATH" | rev | cut -d/ -f1 | rev)" - FILES+=( "$FILENAME:$RELATIVE_PATH" ) - ALL_HEADERS+=("$RELATIVE_PATH") - [[ "$RELATIVE_PATH" =~ $regex ]] && PRIVATE_HEADERS+=("$RELATIVE_PATH") - done <<< "$(find "$WORKING_DIRECTORY/$SOURCES_FOLDER" -type f ! \( -name "*.m" -o -name ".DS_Store" \))" + # Scan umbrella header to discover public headers. + gather_imported_headers_in_file "$UMBRELLA_HEADER" fi +# Clean up previous include directory if it exists. +[[ -d "$1/include" ]] && rm -rf "$1/include" + # Create required folders structure. ! [[ -d "$WORKING_DIRECTORY/Sources" ]] && mkdir -p "$WORKING_DIRECTORY/Sources" -! [[ -d "$1/include" ]] && mkdir -p "$1/include/PubNub" +mkdir -p "$1/include/PubNub" # Create symbolic link to Objective-C SDK source files. @@ -114,20 +108,4 @@ else done fi -[[ -e "PubNub.h" ]] && rm "PubNub.h" - -cd "../" - -if [[ $PUBLIC_ONLY == 1 ]]; then - for HEADER_PATH in "${PUBLIC_HEADERS[@]}"; do - FILENAME="$(echo "$HEADER_PATH" | rev | cut -d/ -f1 | rev)" - ! [[ -e "$FILENAME" ]] && ln -s "../$HEADER_PATH" - done -else - for HEADER_PATH in "${ALL_HEADERS[@]}"; do - FILENAME="$(echo "$HEADER_PATH" | rev | cut -d/ -f1 | rev)" - ! [[ -e "$FILENAME" ]] && ln -s "../$HEADER_PATH" "$FILENAME" - done -fi - [[ -e "PubNub.h" ]] && rm "PubNub.h" \ No newline at end of file diff --git a/Package.swift b/Package.swift index e4eae4e86..2cb33195a 100644 --- a/Package.swift +++ b/Package.swift @@ -53,7 +53,98 @@ let package = Package( name: "PubNub", dependencies: [], path: "Sources/PubNub", - resources: [.copy("../../Framework/PubNub/PrivacyInfo.xcprivacy")] + resources: [.copy("../../Framework/PubNub/PrivacyInfo.xcprivacy")], + cSettings: [ + .headerSearchPath("."), + .headerSearchPath("Core"), + .headerSearchPath("Data"), + .headerSearchPath("Data/Builders"), + .headerSearchPath("Data/Builders/API Call/APNS"), + .headerSearchPath("Data/Builders/API Call/Actions/Message"), + .headerSearchPath("Data/Builders/API Call/Files"), + .headerSearchPath("Data/Builders/API Call/History"), + .headerSearchPath("Data/Builders/API Call/Objects"), + .headerSearchPath("Data/Builders/API Call/Objects/Channel"), + .headerSearchPath("Data/Builders/API Call/Objects/Membership"), + .headerSearchPath("Data/Builders/API Call/Objects/UUID"), + .headerSearchPath("Data/Builders/API Call/Presence"), + .headerSearchPath("Data/Builders/API Call/Publish"), + .headerSearchPath("Data/Builders/API Call/State"), + .headerSearchPath("Data/Builders/API Call/Stream"), + .headerSearchPath("Data/Builders/API Call/Subscribe"), + .headerSearchPath("Data/Builders/API Call/Time"), + .headerSearchPath("Data/Managers"), + .headerSearchPath("Data/Models"), + .headerSearchPath("Data/Service Objects"), + .headerSearchPath("Data/Service Objects/App Context"), + .headerSearchPath("Data/Service Objects/Channel Groups"), + .headerSearchPath("Data/Service Objects/Error"), + .headerSearchPath("Data/Service Objects/File Sharing"), + .headerSearchPath("Data/Service Objects/Message Persistence"), + .headerSearchPath("Data/Service Objects/Message Reaction"), + .headerSearchPath("Data/Service Objects/Presence"), + .headerSearchPath("Data/Service Objects/Publish"), + .headerSearchPath("Data/Service Objects/Push Notification"), + .headerSearchPath("Data/Service Objects/Signal"), + .headerSearchPath("Data/Service Objects/Subscribe"), + .headerSearchPath("Data/Service Objects/Time"), + .headerSearchPath("Data/Transport"), + .headerSearchPath("Misc"), + .headerSearchPath("Misc/Categories"), + .headerSearchPath("Misc/Helpers"), + .headerSearchPath("Misc/Helpers/Notifications Payload"), + .headerSearchPath("Misc/Helpers/Notifications Payload/APNS"), + .headerSearchPath("Misc/Logger/Additional/Console"), + .headerSearchPath("Misc/Logger/Additional/File"), + .headerSearchPath("Misc/Logger/Core"), + .headerSearchPath("Misc/Logger/Data"), + .headerSearchPath("Misc/Protocols"), + .headerSearchPath("Misc/Protocols/Serializer/JSON"), + .headerSearchPath("Misc/Protocols/Serializer/Object"), + .headerSearchPath("Modules/Crypto"), + .headerSearchPath("Modules/Crypto/Cryptors/AES"), + .headerSearchPath("Modules/Crypto/Data"), + .headerSearchPath("Modules/Crypto/Header"), + .headerSearchPath("Modules/Serializer/JSON"), + .headerSearchPath("Modules/Serializer/Object"), + .headerSearchPath("Modules/Serializer/Object/Categories"), + .headerSearchPath("Modules/Serializer/Object/Models"), + .headerSearchPath("Modules/Transport"), + .headerSearchPath("Modules/Transport/Categories"), + .headerSearchPath("Network"), + .headerSearchPath("Network/Parsers"), + .headerSearchPath("Network/Requests"), + .headerSearchPath("Network/Requests/Channel Groups"), + .headerSearchPath("Network/Requests/Files"), + .headerSearchPath("Network/Requests/Message Persistence"), + .headerSearchPath("Network/Requests/Message Reaction/Message"), + .headerSearchPath("Network/Requests/Objects"), + .headerSearchPath("Network/Requests/Objects/Channel"), + .headerSearchPath("Network/Requests/Objects/Membership"), + .headerSearchPath("Network/Requests/Objects/UUID"), + .headerSearchPath("Network/Requests/Presence"), + .headerSearchPath("Network/Requests/Publish"), + .headerSearchPath("Network/Requests/Push Notifications"), + .headerSearchPath("Network/Requests/Signal"), + .headerSearchPath("Network/Requests/Subscribe"), + .headerSearchPath("Network/Requests/Time"), + .headerSearchPath("Network/Responses"), + .headerSearchPath("Network/Responses/App Context"), + .headerSearchPath("Network/Responses/Channel Groups"), + .headerSearchPath("Network/Responses/Error"), + .headerSearchPath("Network/Responses/File Sharing"), + .headerSearchPath("Network/Responses/Message Persistence"), + .headerSearchPath("Network/Responses/Message Reaction"), + .headerSearchPath("Network/Responses/Presence"), + .headerSearchPath("Network/Responses/Publish"), + .headerSearchPath("Network/Responses/Push Notification"), + .headerSearchPath("Network/Responses/Signal"), + .headerSearchPath("Network/Responses/Subscribe"), + .headerSearchPath("Network/Responses/Time"), + .headerSearchPath("Network/Streams"), + .headerSearchPath("Protocols/Transport"), + ], + linkerSettings: [.linkedLibrary("z")] ) ], swiftLanguageVersions: [.v5] diff --git a/PubNub/Core/PubNub+Core.m b/PubNub/Core/PubNub+Core.m index 30f463bc4..bc9b06257 100644 --- a/PubNub/Core/PubNub+Core.m +++ b/PubNub/Core/PubNub+Core.m @@ -1,3 +1,5 @@ +#import "PNDictionaryLogEntry+Private.h" +#import "PNStringLogEntry+Private.h" #import "PubNub+CorePrivate.h" #define PN_CORE_PROTOCOLS PNEventsListener diff --git a/PubNub/Core/PubNub+Subscribe.m b/PubNub/Core/PubNub+Subscribe.m index 94ea1536d..14b3a83b5 100644 --- a/PubNub/Core/PubNub+Subscribe.m +++ b/PubNub/Core/PubNub+Subscribe.m @@ -132,11 +132,17 @@ - (void)removeListener:(id )listener { #pragma mark - Filtering - (NSString *)filterExpression { - return self.configuration.filterExpression; + __block NSString *expression; + [self.lock readAccessWithBlock:^{ + expression = self.configuration.filterExpression; + }]; + return expression; } - (void)setFilterExpression:(NSString *)filterExpression { - self.configuration.filterExpression = filterExpression; + [self.lock syncWriteAccessWithBlock:^{ + self.configuration.filterExpression = filterExpression; + }]; if ([self.subscriberManager allObjects].count) { [self subscribeWithRequest:[PNSubscribeRequest requestWithChannels:@[] channelGroups:@[]]]; diff --git a/PubNub/Data/Managers/PNHeartbeat.m b/PubNub/Data/Managers/PNHeartbeat.m index b30d7dd3a..2663956d9 100644 --- a/PubNub/Data/Managers/PNHeartbeat.m +++ b/PubNub/Data/Managers/PNHeartbeat.m @@ -30,14 +30,14 @@ @interface PNHeartbeat () * * @since 4.7.5 */ -@property (nonatomic, strong) NSMutableArray *presenceChannels; +@property (nonatomic, strong) NSMutableSet *presenceChannels; /** * @brief Stores reference on list of channel groups for which client's connected state has been set to \c YES. * * @since 4.7.5 */ -@property (nonatomic, strong) NSMutableArray *presenceChannelGroups; +@property (nonatomic, strong) NSMutableSet *presenceChannelGroups; /** * @brief Stores reference on timer used to trigger heartbeat requests. @@ -104,8 +104,8 @@ + (instancetype)heartbeatForClient:(PubNub *)client { - (instancetype)initForClient:(PubNub *)client { if ((self = [super init])) { _client = client; - _presenceChannels = [NSMutableArray array]; - _presenceChannelGroups = [NSMutableArray array]; + _presenceChannels = [NSMutableSet set]; + _presenceChannelGroups = [NSMutableSet set]; _resourceAccessQueue = dispatch_queue_create("com.pubnub.heartbeat", DISPATCH_QUEUE_CONCURRENT); } @@ -135,8 +135,8 @@ - (void)setHeartbeatTimer:(dispatch_source_t)heartbeatTimer { NSMutableArray *allObjects = [NSMutableArray array]; pn_safe_property_read(self.resourceAccessQueue, ^{ - [allObjects addObjectsFromArray:self->_presenceChannels]; - [allObjects addObjectsFromArray:self->_presenceChannelGroups]; + [allObjects addObjectsFromArray:self->_presenceChannels.allObjects]; + [allObjects addObjectsFromArray:self->_presenceChannelGroups.allObjects]; }); return allObjects; @@ -146,7 +146,7 @@ - (void)setHeartbeatTimer:(dispatch_source_t)heartbeatTimer { __block NSArray *channels = nil; pn_safe_property_read(self.resourceAccessQueue, ^{ - channels = self->_presenceChannels; + channels = self->_presenceChannels.allObjects; }); return channels; @@ -156,8 +156,7 @@ - (void)setHeartbeatTimer:(dispatch_source_t)heartbeatTimer { __block NSArray *channelGroups = nil; pn_safe_property_read(self.resourceAccessQueue, ^{ - channelGroups = self->_presenceChannelGroups; - + channelGroups = self->_presenceChannelGroups.allObjects; }); return channelGroups; @@ -169,7 +168,7 @@ - (void)setConnected:(BOOL)connected forChannels:(NSArray *)channels if (connected) { [self->_presenceChannels addObjectsFromArray:channels]; } else { - [self->_presenceChannels removeObjectsInArray:channels]; + [self->_presenceChannels minusSet:[NSSet setWithArray:channels]]; } }); } @@ -181,8 +180,7 @@ - (void)setConnected:(BOOL)connected forChannelGroups:(NSArray *)cha if (connected) { [self->_presenceChannelGroups addObjectsFromArray:channelGroups]; } else { - [self->_presenceChannelGroups removeObjectsInArray:channelGroups]; - + [self->_presenceChannelGroups minusSet:[NSSet setWithArray:channelGroups]]; } }); } diff --git a/PubNub/Data/Service Objects/File Sharing/PNGenerateFileUploadURLStatus.h b/PubNub/Data/Service Objects/File Sharing/PNGenerateFileUploadURLStatus.h index d8fbbf19b..41c505d69 100644 --- a/PubNub/Data/Service Objects/File Sharing/PNGenerateFileUploadURLStatus.h +++ b/PubNub/Data/Service Objects/File Sharing/PNGenerateFileUploadURLStatus.h @@ -1,5 +1,5 @@ #import -#import +#import "PNFileGenerateUploadURLData.h" NS_ASSUME_NONNULL_BEGIN diff --git a/PubNub/Data/Service Objects/Presence/PNPresenceGlobalHereNowResult+Private.h b/PubNub/Data/Service Objects/Presence/PNPresenceGlobalHereNowResult+Private.h index d3f8410dd..fb6ac5530 100644 --- a/PubNub/Data/Service Objects/Presence/PNPresenceGlobalHereNowResult+Private.h +++ b/PubNub/Data/Service Objects/Presence/PNPresenceGlobalHereNowResult+Private.h @@ -1,3 +1,4 @@ +#import "PNPresenceGlobalHereNowResult.h" #import "PNPresenceHereNowResult.h" diff --git a/PubNub/Data/Service Objects/Presence/PNPresenceHereNowResult.h b/PubNub/Data/Service Objects/Presence/PNPresenceHereNowResult.h index ca8158859..54db635a3 100644 --- a/PubNub/Data/Service Objects/Presence/PNPresenceHereNowResult.h +++ b/PubNub/Data/Service Objects/Presence/PNPresenceHereNowResult.h @@ -1,4 +1,4 @@ -#import +#import #import diff --git a/PubNub/Data/Transport/PNTransportMiddleware.h b/PubNub/Data/Transport/PNTransportMiddleware.h index 96c981abc..26c28ae84 100644 --- a/PubNub/Data/Transport/PNTransportMiddleware.h +++ b/PubNub/Data/Transport/PNTransportMiddleware.h @@ -1,6 +1,6 @@ #import -#import #import +#import "PNTransportMiddlewareConfiguration.h" NS_ASSUME_NONNULL_BEGIN diff --git a/PubNub/Data/Transport/PNTransportMiddleware.m b/PubNub/Data/Transport/PNTransportMiddleware.m index ff252dcc2..ea0af8b43 100644 --- a/PubNub/Data/Transport/PNTransportMiddleware.m +++ b/PubNub/Data/Transport/PNTransportMiddleware.m @@ -1,6 +1,7 @@ #import "PNTransportMiddleware.h" #import "PNNetworkResponseLogEntry+Private.h" #import "PNNetworkRequestLogEntry+Private.h" +#import "PNDictionaryLogEntry.h" #import "PNTransportConfiguration+Private.h" #import "PNTransportRequest+Private.h" #import "PNConfiguration+Private.h" diff --git a/PubNub/Data/Transport/PNTransportMiddlewareConfiguration.h b/PubNub/Data/Transport/PNTransportMiddlewareConfiguration.h index 61fa1cc0a..df5410f83 100644 --- a/PubNub/Data/Transport/PNTransportMiddlewareConfiguration.h +++ b/PubNub/Data/Transport/PNTransportMiddlewareConfiguration.h @@ -1,5 +1,8 @@ #import -#import +#import +#import +#import +#import NS_ASSUME_NONNULL_BEGIN diff --git a/PubNub/Misc/Helpers/PNHelpers.h b/PubNub/Misc/Helpers/PNHelpers.h index 1319c5a84..b7ff6cee8 100644 --- a/PubNub/Misc/Helpers/PNHelpers.h +++ b/PubNub/Misc/Helpers/PNHelpers.h @@ -9,6 +9,7 @@ #import "PNLockSupport.h" #import "PNURLRequest.h" #import "PNDictionary.h" +#import "PNFunctions.h" #import "PNDefines.h" #import "PNChannel.h" #import "PNString.h" diff --git a/PubNub/Modules/Crypto/Cryptors/AES/PNCCCryptorWrapper.m b/PubNub/Modules/Crypto/Cryptors/AES/PNCCCryptorWrapper.m index 87480e768..4c193320d 100644 --- a/PubNub/Modules/Crypto/Cryptors/AES/PNCCCryptorWrapper.m +++ b/PubNub/Modules/Crypto/Cryptors/AES/PNCCCryptorWrapper.m @@ -130,10 +130,12 @@ @implementation PNCCCryptorWrapper estimatedResultLength - processedDataLength, &finalisedDataLength); processedData.length = processedDataLength + finalisedDataLength; - } else { + } + + if (status != kCCSuccess) { error = [[self class] errorFromCryptorStatus:status andOperation:self.operation]; } - + return [PNResult resultWithData:processedData error:error]; } diff --git a/PubNub/Network/Requests/Message Persistence/PNHistoryFetchRequest.m b/PubNub/Network/Requests/Message Persistence/PNHistoryFetchRequest.m index 7fa71df71..86f0529e4 100644 --- a/PubNub/Network/Requests/Message Persistence/PNHistoryFetchRequest.m +++ b/PubNub/Network/Requests/Message Persistence/PNHistoryFetchRequest.m @@ -76,15 +76,18 @@ - (NSDictionary *)query { if (self.limit > 0) limitValue = MIN(self.limit, defaultLimit); if (operation == PNHistoryWithActionsOperation) { - limitValue = self.limit; limitQueryKeyName = @"max"; + defaultLimit = 25; + limitValue = self.limit > 0 ? MIN(self.limit, defaultLimit) : defaultLimit; } if (self.multipleChannels) { if (self.limit > 0 || (operation != PNHistoryWithActionsOperation && self.channels.count == 1)) { query[limitQueryKeyName] = @(limitValue).stringValue; } - } else if (self.limit > 0) query[limitQueryKeyName] = @(limitValue).stringValue; + } else if (self.limit > 0 || operation == PNHistoryWithActionsOperation) { + query[limitQueryKeyName] = @(limitValue).stringValue; + } if (startDate) query[@"start"] = [PNNumber timeTokenFromNumber:startDate].stringValue; @@ -158,7 +161,7 @@ - (PNError *)validate { ); return [PNError errorWithDomain:PNAPIErrorDomain code:PNAPIErrorUnacceptableParameters userInfo:userInfo]; - } else if (!self.multipleChannels && self.channels.count == 0) { + } else if (!self.multipleChannels && (self.channels.count == 0 || self.channels.firstObject.length == 0)) { return [self missingParameterError:@"channel" forObjectRequest:@"PNHistoryFetchRequest"]; } else if (self.multipleChannels && self.channels.count == 0) { return [self missingParameterError:@"channels" forObjectRequest:@"PNHistoryFetchRequest"]; diff --git a/PubNub/Network/Requests/Objects/PNBaseObjectsRequest.m b/PubNub/Network/Requests/Objects/PNBaseObjectsRequest.m index 1f34f3cb3..ad8062d74 100644 --- a/PubNub/Network/Requests/Objects/PNBaseObjectsRequest.m +++ b/PubNub/Network/Requests/Objects/PNBaseObjectsRequest.m @@ -157,7 +157,7 @@ - (instancetype)initWithObject:(NSString *)objectType identifier:(NSString *)ide #pragma mark - Prepare - (PNError *)validate { - if (self.identifier) { + if (self.identifier.length) { if (self.identifier.length > 92) { return [self valueTooLongErrorForParameter:self.objectType ofObjectRequest:self.objectType diff --git a/PubNub/Network/Requests/Presence/PNPresenceHeartbeatRequest.m b/PubNub/Network/Requests/Presence/PNPresenceHeartbeatRequest.m index 2e15c6731..e69e1ee92 100644 --- a/PubNub/Network/Requests/Presence/PNPresenceHeartbeatRequest.m +++ b/PubNub/Network/Requests/Presence/PNPresenceHeartbeatRequest.m @@ -87,9 +87,9 @@ - (instancetype)initWithHeartbeat:(NSInteger)heartbeat channels:(NSArray *)channels channelGroups:(NSArray *)channelGroups { if ((self = [super init])) { - _channelGroups = [channelGroups copy]; + _channelGroups = channelGroups ? [[NSSet setWithArray:channelGroups].allObjects copy] : nil; _presenceHeartbeatValue = heartbeat; - _channels = [channels copy]; + _channels = channels ? [[NSSet setWithArray:channels].allObjects copy] : nil; } return self; diff --git a/PubNub/Network/Requests/Publish/PNBasePublishRequest.m b/PubNub/Network/Requests/Publish/PNBasePublishRequest.m index e80dcaf2c..520b67a18 100644 --- a/PubNub/Network/Requests/Publish/PNBasePublishRequest.m +++ b/PubNub/Network/Requests/Publish/PNBasePublishRequest.m @@ -130,6 +130,17 @@ - (PNError *)validate { NSString *messageForPublish = @""; NSError *error = nil; + if (preFormattedMessage && ![NSJSONSerialization isValidJSONObject:@[preFormattedMessage]]) { + NSDictionary *userInfo = PNErrorUserInfo( + @"Request parameters error", + @"Message serialization did fail", + @"Ensure that only JSON-compatible values used in 'message'.", + nil + ); + + return [PNError errorWithDomain:PNAPIErrorDomain code:PNAPIErrorUnacceptableParameters userInfo:userInfo]; + } + messageForPublish = [PNJSON JSONStringFrom:preFormattedMessage withError:&error]; BOOL isMessageEncrypted = NO; @@ -183,6 +194,17 @@ - (PNError *)validate { else self.preparedMessage = messageForPublish; if (self.metadata) { + if (![NSJSONSerialization isValidJSONObject:self.metadata]) { + NSDictionary *userInfo = PNErrorUserInfo( + @"Request parameters error", + @"Metadata serialization did fail", + @"Ensure that only JSON-compatible values used in 'metadata'.", + nil + ); + + return [PNError errorWithDomain:PNAPIErrorDomain code:PNAPIErrorUnacceptableParameters userInfo:userInfo]; + } + NSString *metadataForPublish = [PNJSON JSONStringFrom:self.metadata withError:&error]; if (!error && metadataForPublish.length) self.preparedMetadata = [metadataForPublish copy]; else if (error) { diff --git a/PubNub/Network/Requests/Signal/PNSignalRequest.m b/PubNub/Network/Requests/Signal/PNSignalRequest.m index df9efc6c2..00dd39e9e 100644 --- a/PubNub/Network/Requests/Signal/PNSignalRequest.m +++ b/PubNub/Network/Requests/Signal/PNSignalRequest.m @@ -94,6 +94,17 @@ - (instancetype)initWithChannel:(NSString *)channel signal:(id)signalData { #pragma mark - Prepare - (PNError *)validate { + if (self.message && ![NSJSONSerialization isValidJSONObject:@[self.message]]) { + NSDictionary *userInfo = PNErrorUserInfo( + @"Request parameters error", + @"Message serialization did fail", + @"Ensure that only JSON-compatible values used in 'message'.", + nil + ); + + return [PNError errorWithDomain:PNAPIErrorDomain code:PNAPIErrorUnacceptableParameters userInfo:userInfo]; + } + PNError *error = nil; NSString *messageForPublish = [PNJSON JSONStringFrom:self.message withError:&error]; diff --git a/PubNub/Network/Requests/Subscribe/PNSubscribeRequest.m b/PubNub/Network/Requests/Subscribe/PNSubscribeRequest.m index 525b6499f..040bc6593 100644 --- a/PubNub/Network/Requests/Subscribe/PNSubscribeRequest.m +++ b/PubNub/Network/Requests/Subscribe/PNSubscribeRequest.m @@ -129,10 +129,10 @@ - (instancetype)initWithChannels:(NSArray *)channels channelGroups:(NSArray *)channelGroups presenceOnly:(BOOL)presenceOnly { if ((self = [super init])) { - _channelGroups = [channelGroups copy]; + _channelGroups = channelGroups ? [[NSSet setWithArray:channelGroups].allObjects copy] : nil; _observePresence = presenceOnly; _presenceOnly = presenceOnly; - _channels = [channels copy]; + _channels = channels ? [[NSSet setWithArray:channels].allObjects copy] : nil; } return self; diff --git a/PubNub/include/PubNub/NSURLSessionConfiguration+PNConfiguration.h b/PubNub/include/PubNub/NSURLSessionConfiguration+PNConfiguration.h new file mode 120000 index 000000000..6e9752c44 --- /dev/null +++ b/PubNub/include/PubNub/NSURLSessionConfiguration+PNConfiguration.h @@ -0,0 +1 @@ +../../Modules/Transport/Categories/NSURLSessionConfiguration+PNConfiguration.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNAES.h b/PubNub/include/PubNub/PNAES.h new file mode 120000 index 000000000..fba662d37 --- /dev/null +++ b/PubNub/include/PubNub/PNAES.h @@ -0,0 +1 @@ +../../Data/PNAES.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNAESCBCCryptor.h b/PubNub/include/PubNub/PNAESCBCCryptor.h new file mode 120000 index 000000000..7ca1c71cf --- /dev/null +++ b/PubNub/include/PubNub/PNAESCBCCryptor.h @@ -0,0 +1 @@ +../../Modules/Crypto/Cryptors/AES/PNAESCBCCryptor.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNAPICallBuilder.h b/PubNub/include/PubNub/PNAPICallBuilder.h new file mode 120000 index 000000000..f891f0b97 --- /dev/null +++ b/PubNub/include/PubNub/PNAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/PNAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNAPNSAPICallBuilder.h b/PubNub/include/PubNub/PNAPNSAPICallBuilder.h new file mode 120000 index 000000000..a0ba98ba6 --- /dev/null +++ b/PubNub/include/PubNub/PNAPNSAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/APNS/PNAPNSAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNAPNSAuditAPICallBuilder.h b/PubNub/include/PubNub/PNAPNSAuditAPICallBuilder.h new file mode 120000 index 000000000..4c3147e9c --- /dev/null +++ b/PubNub/include/PubNub/PNAPNSAuditAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/APNS/PNAPNSAuditAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNAPNSEnabledChannelsResult.h b/PubNub/include/PubNub/PNAPNSEnabledChannelsResult.h new file mode 120000 index 000000000..5b27d45a2 --- /dev/null +++ b/PubNub/include/PubNub/PNAPNSEnabledChannelsResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Push Notification/PNAPNSEnabledChannelsResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNAPNSModificationAPICallBuilder.h b/PubNub/include/PubNub/PNAPNSModificationAPICallBuilder.h new file mode 120000 index 000000000..93009830a --- /dev/null +++ b/PubNub/include/PubNub/PNAPNSModificationAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/APNS/PNAPNSModificationAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNAPNSNotificationConfiguration.h b/PubNub/include/PubNub/PNAPNSNotificationConfiguration.h new file mode 120000 index 000000000..5fa908a78 --- /dev/null +++ b/PubNub/include/PubNub/PNAPNSNotificationConfiguration.h @@ -0,0 +1 @@ +../../Misc/Helpers/Notifications Payload/APNS/PNAPNSNotificationConfiguration.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNAPNSNotificationPayload.h b/PubNub/include/PubNub/PNAPNSNotificationPayload.h new file mode 120000 index 000000000..d7be06f4d --- /dev/null +++ b/PubNub/include/PubNub/PNAPNSNotificationPayload.h @@ -0,0 +1 @@ +../../Misc/Helpers/Notifications Payload/APNS/PNAPNSNotificationPayload.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNAPNSNotificationTarget.h b/PubNub/include/PubNub/PNAPNSNotificationTarget.h new file mode 120000 index 000000000..78f2d93c5 --- /dev/null +++ b/PubNub/include/PubNub/PNAPNSNotificationTarget.h @@ -0,0 +1 @@ +../../Misc/Helpers/Notifications Payload/APNS/PNAPNSNotificationTarget.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNAcknowledgmentStatus.h b/PubNub/include/PubNub/PNAcknowledgmentStatus.h new file mode 120000 index 000000000..2f1a4bc0e --- /dev/null +++ b/PubNub/include/PubNub/PNAcknowledgmentStatus.h @@ -0,0 +1 @@ +../../Data/Service Objects/PNAcknowledgmentStatus.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNAddMessageActionAPICallBuilder.h b/PubNub/include/PubNub/PNAddMessageActionAPICallBuilder.h new file mode 120000 index 000000000..8b4cb14c4 --- /dev/null +++ b/PubNub/include/PubNub/PNAddMessageActionAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Actions/Message/PNAddMessageActionAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNAddMessageActionRequest.h b/PubNub/include/PubNub/PNAddMessageActionRequest.h new file mode 120000 index 000000000..93b192e3c --- /dev/null +++ b/PubNub/include/PubNub/PNAddMessageActionRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Message Reaction/Message/PNAddMessageActionRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNAddMessageActionStatus.h b/PubNub/include/PubNub/PNAddMessageActionStatus.h new file mode 120000 index 000000000..20af4c86d --- /dev/null +++ b/PubNub/include/PubNub/PNAddMessageActionStatus.h @@ -0,0 +1 @@ +../../Data/Service Objects/Message Reaction/PNAddMessageActionStatus.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNBaseAppContextObject.h b/PubNub/include/PubNub/PNBaseAppContextObject.h new file mode 120000 index 000000000..52b067477 --- /dev/null +++ b/PubNub/include/PubNub/PNBaseAppContextObject.h @@ -0,0 +1 @@ +../../Data/Models/PNBaseAppContextObject.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNBaseMessageActionRequest.h b/PubNub/include/PubNub/PNBaseMessageActionRequest.h new file mode 120000 index 000000000..ff0e30676 --- /dev/null +++ b/PubNub/include/PubNub/PNBaseMessageActionRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Message Reaction/Message/PNBaseMessageActionRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNBaseNotificationPayload.h b/PubNub/include/PubNub/PNBaseNotificationPayload.h new file mode 120000 index 000000000..96a335eff --- /dev/null +++ b/PubNub/include/PubNub/PNBaseNotificationPayload.h @@ -0,0 +1 @@ +../../Misc/Helpers/Notifications Payload/PNBaseNotificationPayload.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNBaseObjectsMembershipRequest.h b/PubNub/include/PubNub/PNBaseObjectsMembershipRequest.h new file mode 120000 index 000000000..f34266958 --- /dev/null +++ b/PubNub/include/PubNub/PNBaseObjectsMembershipRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/Membership/PNBaseObjectsMembershipRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNBaseObjectsRequest.h b/PubNub/include/PubNub/PNBaseObjectsRequest.h new file mode 120000 index 000000000..5c7c32ce7 --- /dev/null +++ b/PubNub/include/PubNub/PNBaseObjectsRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/PNBaseObjectsRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNBaseOperationData.h b/PubNub/include/PubNub/PNBaseOperationData.h new file mode 120000 index 000000000..081253231 --- /dev/null +++ b/PubNub/include/PubNub/PNBaseOperationData.h @@ -0,0 +1 @@ +../../Network/Responses/PNBaseOperationData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNBasePublishRequest.h b/PubNub/include/PubNub/PNBasePublishRequest.h new file mode 120000 index 000000000..1cd9fd659 --- /dev/null +++ b/PubNub/include/PubNub/PNBasePublishRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Publish/PNBasePublishRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNBasePushNotificationsRequest.h b/PubNub/include/PubNub/PNBasePushNotificationsRequest.h new file mode 120000 index 000000000..74e46b92e --- /dev/null +++ b/PubNub/include/PubNub/PNBasePushNotificationsRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Push Notifications/PNBasePushNotificationsRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNBaseRequest.h b/PubNub/include/PubNub/PNBaseRequest.h new file mode 120000 index 000000000..7500cfd7d --- /dev/null +++ b/PubNub/include/PubNub/PNBaseRequest.h @@ -0,0 +1 @@ +../../Network/Requests/PNBaseRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNChannelClientStateResult.h b/PubNub/include/PubNub/PNChannelClientStateResult.h new file mode 120000 index 000000000..4608f6d3e --- /dev/null +++ b/PubNub/include/PubNub/PNChannelClientStateResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Presence/PNChannelClientStateResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNChannelGroupChannelsResult.h b/PubNub/include/PubNub/PNChannelGroupChannelsResult.h new file mode 120000 index 000000000..3cd62a800 --- /dev/null +++ b/PubNub/include/PubNub/PNChannelGroupChannelsResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Channel Groups/PNChannelGroupChannelsResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNChannelGroupClientStateResult.h b/PubNub/include/PubNub/PNChannelGroupClientStateResult.h new file mode 120000 index 000000000..cbf28d6ae --- /dev/null +++ b/PubNub/include/PubNub/PNChannelGroupClientStateResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Presence/PNChannelGroupClientStateResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNChannelGroupFetchData.h b/PubNub/include/PubNub/PNChannelGroupFetchData.h new file mode 120000 index 000000000..afde779b8 --- /dev/null +++ b/PubNub/include/PubNub/PNChannelGroupFetchData.h @@ -0,0 +1 @@ +../../Network/Responses/Channel Groups/PNChannelGroupFetchData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNChannelGroupFetchRequest.h b/PubNub/include/PubNub/PNChannelGroupFetchRequest.h new file mode 120000 index 000000000..3b28f1698 --- /dev/null +++ b/PubNub/include/PubNub/PNChannelGroupFetchRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Channel Groups/PNChannelGroupFetchRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNChannelGroupManageRequest.h b/PubNub/include/PubNub/PNChannelGroupManageRequest.h new file mode 120000 index 000000000..5788cf9f7 --- /dev/null +++ b/PubNub/include/PubNub/PNChannelGroupManageRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Channel Groups/PNChannelGroupManageRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNChannelGroupsResult.h b/PubNub/include/PubNub/PNChannelGroupsResult.h new file mode 120000 index 000000000..d9243830f --- /dev/null +++ b/PubNub/include/PubNub/PNChannelGroupsResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Channel Groups/PNChannelGroupsResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNChannelMember.h b/PubNub/include/PubNub/PNChannelMember.h new file mode 120000 index 000000000..bbc2ea293 --- /dev/null +++ b/PubNub/include/PubNub/PNChannelMember.h @@ -0,0 +1 @@ +../../Data/Models/PNChannelMember.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNChannelMembersFetchData.h b/PubNub/include/PubNub/PNChannelMembersFetchData.h new file mode 120000 index 000000000..9ca3046bf --- /dev/null +++ b/PubNub/include/PubNub/PNChannelMembersFetchData.h @@ -0,0 +1 @@ +../../Network/Responses/App Context/PNChannelMembersFetchData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNChannelMembersManageData.h b/PubNub/include/PubNub/PNChannelMembersManageData.h new file mode 120000 index 000000000..a631408da --- /dev/null +++ b/PubNub/include/PubNub/PNChannelMembersManageData.h @@ -0,0 +1 @@ +../../Network/Responses/App Context/PNChannelMembersManageData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNChannelMetadata.h b/PubNub/include/PubNub/PNChannelMetadata.h new file mode 120000 index 000000000..b7faca299 --- /dev/null +++ b/PubNub/include/PubNub/PNChannelMetadata.h @@ -0,0 +1 @@ +../../Data/Models/PNChannelMetadata.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNChannelMetadataFetchAllData.h b/PubNub/include/PubNub/PNChannelMetadataFetchAllData.h new file mode 120000 index 000000000..93b494ea4 --- /dev/null +++ b/PubNub/include/PubNub/PNChannelMetadataFetchAllData.h @@ -0,0 +1 @@ +../../Network/Responses/App Context/PNChannelMetadataFetchAllData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNChannelMetadataFetchData.h b/PubNub/include/PubNub/PNChannelMetadataFetchData.h new file mode 120000 index 000000000..740cd94fd --- /dev/null +++ b/PubNub/include/PubNub/PNChannelMetadataFetchData.h @@ -0,0 +1 @@ +../../Network/Responses/App Context/PNChannelMetadataFetchData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNChannelMetadataSetData.h b/PubNub/include/PubNub/PNChannelMetadataSetData.h new file mode 120000 index 000000000..13835afbb --- /dev/null +++ b/PubNub/include/PubNub/PNChannelMetadataSetData.h @@ -0,0 +1 @@ +../../Network/Responses/App Context/PNChannelMetadataSetData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNClientInformation.h b/PubNub/include/PubNub/PNClientInformation.h new file mode 120000 index 000000000..ba18050bd --- /dev/null +++ b/PubNub/include/PubNub/PNClientInformation.h @@ -0,0 +1 @@ +../../Data/PNClientInformation.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNClientStateGetResult.h b/PubNub/include/PubNub/PNClientStateGetResult.h new file mode 120000 index 000000000..37c2b5042 --- /dev/null +++ b/PubNub/include/PubNub/PNClientStateGetResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Presence/PNClientStateGetResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNClientStateUpdateStatus.h b/PubNub/include/PubNub/PNClientStateUpdateStatus.h new file mode 120000 index 000000000..5a8a582fb --- /dev/null +++ b/PubNub/include/PubNub/PNClientStateUpdateStatus.h @@ -0,0 +1 @@ +../../Data/Service Objects/Presence/PNClientStateUpdateStatus.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNCodable.h b/PubNub/include/PubNub/PNCodable.h new file mode 120000 index 000000000..fd9ddb3b0 --- /dev/null +++ b/PubNub/include/PubNub/PNCodable.h @@ -0,0 +1 @@ +../../Misc/Protocols/Serializer/Object/PNCodable.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNConfiguration.h b/PubNub/include/PubNub/PNConfiguration.h new file mode 120000 index 000000000..2bb48094a --- /dev/null +++ b/PubNub/include/PubNub/PNConfiguration.h @@ -0,0 +1 @@ +../../Data/PNConfiguration.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNCryptoModule.h b/PubNub/include/PubNub/PNCryptoModule.h new file mode 120000 index 000000000..a74a4fdd8 --- /dev/null +++ b/PubNub/include/PubNub/PNCryptoModule.h @@ -0,0 +1 @@ +../../Modules/Crypto/PNCryptoModule.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNCryptoProvider.h b/PubNub/include/PubNub/PNCryptoProvider.h new file mode 120000 index 000000000..8c9324e15 --- /dev/null +++ b/PubNub/include/PubNub/PNCryptoProvider.h @@ -0,0 +1 @@ +../../Misc/Protocols/PNCryptoProvider.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNCryptor.h b/PubNub/include/PubNub/PNCryptor.h new file mode 120000 index 000000000..93a2c6995 --- /dev/null +++ b/PubNub/include/PubNub/PNCryptor.h @@ -0,0 +1 @@ +../../Misc/Protocols/PNCryptor.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNCryptorInputStream.h b/PubNub/include/PubNub/PNCryptorInputStream.h new file mode 120000 index 000000000..336ecd6d0 --- /dev/null +++ b/PubNub/include/PubNub/PNCryptorInputStream.h @@ -0,0 +1 @@ +../../Modules/Crypto/Data/PNCryptorInputStream.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNDecoder.h b/PubNub/include/PubNub/PNDecoder.h new file mode 120000 index 000000000..31ff3bf93 --- /dev/null +++ b/PubNub/include/PubNub/PNDecoder.h @@ -0,0 +1 @@ +../../Misc/Protocols/Serializer/Object/PNDecoder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNDefines.h b/PubNub/include/PubNub/PNDefines.h new file mode 120000 index 000000000..09e3dc8d5 --- /dev/null +++ b/PubNub/include/PubNub/PNDefines.h @@ -0,0 +1 @@ +../../Misc/Helpers/PNDefines.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNDeleteFileAPICallBuilder.h b/PubNub/include/PubNub/PNDeleteFileAPICallBuilder.h new file mode 120000 index 000000000..671894086 --- /dev/null +++ b/PubNub/include/PubNub/PNDeleteFileAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Files/PNDeleteFileAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNDeleteFileRequest.h b/PubNub/include/PubNub/PNDeleteFileRequest.h new file mode 120000 index 000000000..d974706da --- /dev/null +++ b/PubNub/include/PubNub/PNDeleteFileRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Files/PNDeleteFileRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNDeleteMessageAPICallBuilder.h b/PubNub/include/PubNub/PNDeleteMessageAPICallBuilder.h new file mode 120000 index 000000000..8ad09f525 --- /dev/null +++ b/PubNub/include/PubNub/PNDeleteMessageAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/History/PNDeleteMessageAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNDictionaryLogEntry.h b/PubNub/include/PubNub/PNDictionaryLogEntry.h new file mode 120000 index 000000000..fe61ae99d --- /dev/null +++ b/PubNub/include/PubNub/PNDictionaryLogEntry.h @@ -0,0 +1 @@ +../../Misc/Logger/Data/PNDictionaryLogEntry.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNDownloadFileAPICallBuilder.h b/PubNub/include/PubNub/PNDownloadFileAPICallBuilder.h new file mode 120000 index 000000000..caa12ee3f --- /dev/null +++ b/PubNub/include/PubNub/PNDownloadFileAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Files/PNDownloadFileAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNDownloadFileRequest.h b/PubNub/include/PubNub/PNDownloadFileRequest.h new file mode 120000 index 000000000..7a0cffc7f --- /dev/null +++ b/PubNub/include/PubNub/PNDownloadFileRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Files/PNDownloadFileRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNDownloadFileResult.h b/PubNub/include/PubNub/PNDownloadFileResult.h new file mode 120000 index 000000000..852f74c38 --- /dev/null +++ b/PubNub/include/PubNub/PNDownloadFileResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/File Sharing/PNDownloadFileResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNEncoder.h b/PubNub/include/PubNub/PNEncoder.h new file mode 120000 index 000000000..0b0ad1268 --- /dev/null +++ b/PubNub/include/PubNub/PNEncoder.h @@ -0,0 +1 @@ +../../Misc/Protocols/Serializer/Object/PNEncoder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNEncryptedData.h b/PubNub/include/PubNub/PNEncryptedData.h new file mode 120000 index 000000000..e76ffc0d8 --- /dev/null +++ b/PubNub/include/PubNub/PNEncryptedData.h @@ -0,0 +1 @@ +../../Modules/Crypto/Data/PNEncryptedData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNEncryptedStream.h b/PubNub/include/PubNub/PNEncryptedStream.h new file mode 120000 index 000000000..59955131d --- /dev/null +++ b/PubNub/include/PubNub/PNEncryptedStream.h @@ -0,0 +1 @@ +../../Modules/Crypto/Data/PNEncryptedStream.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNError.h b/PubNub/include/PubNub/PNError.h new file mode 120000 index 000000000..0f72e5d1f --- /dev/null +++ b/PubNub/include/PubNub/PNError.h @@ -0,0 +1 @@ +../../Data/PNError.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNErrorData.h b/PubNub/include/PubNub/PNErrorData.h new file mode 120000 index 000000000..c592c44d9 --- /dev/null +++ b/PubNub/include/PubNub/PNErrorData.h @@ -0,0 +1 @@ +../../Network/Responses/Error/PNErrorData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNErrorLogEntry.h b/PubNub/include/PubNub/PNErrorLogEntry.h new file mode 120000 index 000000000..7f568d353 --- /dev/null +++ b/PubNub/include/PubNub/PNErrorLogEntry.h @@ -0,0 +1 @@ +../../Misc/Logger/Data/PNErrorLogEntry.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNErrorStatus.h b/PubNub/include/PubNub/PNErrorStatus.h new file mode 120000 index 000000000..55d4d4c57 --- /dev/null +++ b/PubNub/include/PubNub/PNErrorStatus.h @@ -0,0 +1 @@ +../../Data/Service Objects/Error/PNErrorStatus.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNEventsListener.h b/PubNub/include/PubNub/PNEventsListener.h new file mode 120000 index 000000000..eeb0f0f8f --- /dev/null +++ b/PubNub/include/PubNub/PNEventsListener.h @@ -0,0 +1 @@ +../../Misc/Protocols/PNEventsListener.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFCMNotificationPayload.h b/PubNub/include/PubNub/PNFCMNotificationPayload.h new file mode 120000 index 000000000..9462a90c4 --- /dev/null +++ b/PubNub/include/PubNub/PNFCMNotificationPayload.h @@ -0,0 +1 @@ +../../Misc/Helpers/Notifications Payload/PNFCMNotificationPayload.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchAllChannelsMetadataAPICallBuilder.h b/PubNub/include/PubNub/PNFetchAllChannelsMetadataAPICallBuilder.h new file mode 120000 index 000000000..89b1765c6 --- /dev/null +++ b/PubNub/include/PubNub/PNFetchAllChannelsMetadataAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Objects/Channel/PNFetchAllChannelsMetadataAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchAllChannelsMetadataRequest.h b/PubNub/include/PubNub/PNFetchAllChannelsMetadataRequest.h new file mode 120000 index 000000000..770753835 --- /dev/null +++ b/PubNub/include/PubNub/PNFetchAllChannelsMetadataRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/Channel/PNFetchAllChannelsMetadataRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchAllChannelsMetadataResult.h b/PubNub/include/PubNub/PNFetchAllChannelsMetadataResult.h new file mode 120000 index 000000000..2f23adbb3 --- /dev/null +++ b/PubNub/include/PubNub/PNFetchAllChannelsMetadataResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/App Context/PNFetchAllChannelsMetadataResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchAllUUIDMetadataAPICallBuilder.h b/PubNub/include/PubNub/PNFetchAllUUIDMetadataAPICallBuilder.h new file mode 120000 index 000000000..38251aa8d --- /dev/null +++ b/PubNub/include/PubNub/PNFetchAllUUIDMetadataAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Objects/UUID/PNFetchAllUUIDMetadataAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchAllUUIDMetadataRequest.h b/PubNub/include/PubNub/PNFetchAllUUIDMetadataRequest.h new file mode 120000 index 000000000..2288a5ec9 --- /dev/null +++ b/PubNub/include/PubNub/PNFetchAllUUIDMetadataRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/UUID/PNFetchAllUUIDMetadataRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchAllUUIDMetadataResult.h b/PubNub/include/PubNub/PNFetchAllUUIDMetadataResult.h new file mode 120000 index 000000000..3c32aed12 --- /dev/null +++ b/PubNub/include/PubNub/PNFetchAllUUIDMetadataResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/App Context/PNFetchAllUUIDMetadataResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchChannelMembersAPICallBuilder.h b/PubNub/include/PubNub/PNFetchChannelMembersAPICallBuilder.h new file mode 120000 index 000000000..39920b0ac --- /dev/null +++ b/PubNub/include/PubNub/PNFetchChannelMembersAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Objects/Membership/PNFetchChannelMembersAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchChannelMembersRequest.h b/PubNub/include/PubNub/PNFetchChannelMembersRequest.h new file mode 120000 index 000000000..20760cf6a --- /dev/null +++ b/PubNub/include/PubNub/PNFetchChannelMembersRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/Membership/PNFetchChannelMembersRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchChannelMembersResult.h b/PubNub/include/PubNub/PNFetchChannelMembersResult.h new file mode 120000 index 000000000..361e92d58 --- /dev/null +++ b/PubNub/include/PubNub/PNFetchChannelMembersResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/App Context/PNFetchChannelMembersResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchChannelMetadataAPICallBuilder.h b/PubNub/include/PubNub/PNFetchChannelMetadataAPICallBuilder.h new file mode 120000 index 000000000..448f8ddb5 --- /dev/null +++ b/PubNub/include/PubNub/PNFetchChannelMetadataAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Objects/Channel/PNFetchChannelMetadataAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchChannelMetadataRequest.h b/PubNub/include/PubNub/PNFetchChannelMetadataRequest.h new file mode 120000 index 000000000..fb6961a4e --- /dev/null +++ b/PubNub/include/PubNub/PNFetchChannelMetadataRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/Channel/PNFetchChannelMetadataRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchChannelMetadataResult.h b/PubNub/include/PubNub/PNFetchChannelMetadataResult.h new file mode 120000 index 000000000..add3fcc91 --- /dev/null +++ b/PubNub/include/PubNub/PNFetchChannelMetadataResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/App Context/PNFetchChannelMetadataResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchMembershipsAPICallBuilder.h b/PubNub/include/PubNub/PNFetchMembershipsAPICallBuilder.h new file mode 120000 index 000000000..72a1ae34b --- /dev/null +++ b/PubNub/include/PubNub/PNFetchMembershipsAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Objects/Membership/PNFetchMembershipsAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchMembershipsRequest.h b/PubNub/include/PubNub/PNFetchMembershipsRequest.h new file mode 120000 index 000000000..1bccf0cde --- /dev/null +++ b/PubNub/include/PubNub/PNFetchMembershipsRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/Membership/PNFetchMembershipsRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchMembershipsResult.h b/PubNub/include/PubNub/PNFetchMembershipsResult.h new file mode 120000 index 000000000..17fadffa5 --- /dev/null +++ b/PubNub/include/PubNub/PNFetchMembershipsResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/App Context/PNFetchMembershipsResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchMessageActionsRequest.h b/PubNub/include/PubNub/PNFetchMessageActionsRequest.h new file mode 120000 index 000000000..2111d03ec --- /dev/null +++ b/PubNub/include/PubNub/PNFetchMessageActionsRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Message Reaction/Message/PNFetchMessageActionsRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchMessageActionsResult.h b/PubNub/include/PubNub/PNFetchMessageActionsResult.h new file mode 120000 index 000000000..668cb66a5 --- /dev/null +++ b/PubNub/include/PubNub/PNFetchMessageActionsResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Message Reaction/PNFetchMessageActionsResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchMessagesActionsAPICallBuilder.h b/PubNub/include/PubNub/PNFetchMessagesActionsAPICallBuilder.h new file mode 120000 index 000000000..e11d5be75 --- /dev/null +++ b/PubNub/include/PubNub/PNFetchMessagesActionsAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Actions/Message/PNFetchMessagesActionsAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchUUIDMetadataAPICallBuilder.h b/PubNub/include/PubNub/PNFetchUUIDMetadataAPICallBuilder.h new file mode 120000 index 000000000..10f17091c --- /dev/null +++ b/PubNub/include/PubNub/PNFetchUUIDMetadataAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Objects/UUID/PNFetchUUIDMetadataAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchUUIDMetadataRequest.h b/PubNub/include/PubNub/PNFetchUUIDMetadataRequest.h new file mode 120000 index 000000000..aefc9e1fc --- /dev/null +++ b/PubNub/include/PubNub/PNFetchUUIDMetadataRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/UUID/PNFetchUUIDMetadataRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFetchUUIDMetadataResult.h b/PubNub/include/PubNub/PNFetchUUIDMetadataResult.h new file mode 120000 index 000000000..eebafe0f9 --- /dev/null +++ b/PubNub/include/PubNub/PNFetchUUIDMetadataResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/App Context/PNFetchUUIDMetadataResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFile.h b/PubNub/include/PubNub/PNFile.h new file mode 120000 index 000000000..96aa3465b --- /dev/null +++ b/PubNub/include/PubNub/PNFile.h @@ -0,0 +1 @@ +../../Data/Models/PNFile.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFileDownloadData.h b/PubNub/include/PubNub/PNFileDownloadData.h new file mode 120000 index 000000000..238b4a539 --- /dev/null +++ b/PubNub/include/PubNub/PNFileDownloadData.h @@ -0,0 +1 @@ +../../Network/Responses/File Sharing/PNFileDownloadData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFileDownloadURLAPICallBuilder.h b/PubNub/include/PubNub/PNFileDownloadURLAPICallBuilder.h new file mode 120000 index 000000000..4e4a1a09a --- /dev/null +++ b/PubNub/include/PubNub/PNFileDownloadURLAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Files/PNFileDownloadURLAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFileEventResult.h b/PubNub/include/PubNub/PNFileEventResult.h new file mode 120000 index 000000000..e9dc96a3b --- /dev/null +++ b/PubNub/include/PubNub/PNFileEventResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Subscribe/PNFileEventResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFileListFetchData.h b/PubNub/include/PubNub/PNFileListFetchData.h new file mode 120000 index 000000000..85fbba6d4 --- /dev/null +++ b/PubNub/include/PubNub/PNFileListFetchData.h @@ -0,0 +1 @@ +../../Network/Responses/File Sharing/PNFileListFetchData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFileLogger.h b/PubNub/include/PubNub/PNFileLogger.h new file mode 120000 index 000000000..15e780b4e --- /dev/null +++ b/PubNub/include/PubNub/PNFileLogger.h @@ -0,0 +1 @@ +../../Misc/Logger/Additional/File/PNFileLogger.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFileSendData.h b/PubNub/include/PubNub/PNFileSendData.h new file mode 120000 index 000000000..656603140 --- /dev/null +++ b/PubNub/include/PubNub/PNFileSendData.h @@ -0,0 +1 @@ +../../Network/Responses/File Sharing/PNFileSendData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFilesAPICallBuilder.h b/PubNub/include/PubNub/PNFilesAPICallBuilder.h new file mode 120000 index 000000000..9caa42f1b --- /dev/null +++ b/PubNub/include/PubNub/PNFilesAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Files/PNFilesAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNFunctions.h b/PubNub/include/PubNub/PNFunctions.h new file mode 120000 index 000000000..3c81eb793 --- /dev/null +++ b/PubNub/include/PubNub/PNFunctions.h @@ -0,0 +1 @@ +../../Misc/Helpers/PNFunctions.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNHereNowRequest.h b/PubNub/include/PubNub/PNHereNowRequest.h new file mode 120000 index 000000000..d5448be1e --- /dev/null +++ b/PubNub/include/PubNub/PNHereNowRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Presence/PNHereNowRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNHistoryAPICallBuilder.h b/PubNub/include/PubNub/PNHistoryAPICallBuilder.h new file mode 120000 index 000000000..4574d4566 --- /dev/null +++ b/PubNub/include/PubNub/PNHistoryAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/History/PNHistoryAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNHistoryFetchData.h b/PubNub/include/PubNub/PNHistoryFetchData.h new file mode 120000 index 000000000..9575944c2 --- /dev/null +++ b/PubNub/include/PubNub/PNHistoryFetchData.h @@ -0,0 +1 @@ +../../Network/Responses/Message Persistence/PNHistoryFetchData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNHistoryFetchRequest.h b/PubNub/include/PubNub/PNHistoryFetchRequest.h new file mode 120000 index 000000000..55e3ebd9c --- /dev/null +++ b/PubNub/include/PubNub/PNHistoryFetchRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Message Persistence/PNHistoryFetchRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNHistoryMessageCountData.h b/PubNub/include/PubNub/PNHistoryMessageCountData.h new file mode 120000 index 000000000..a17f036a9 --- /dev/null +++ b/PubNub/include/PubNub/PNHistoryMessageCountData.h @@ -0,0 +1 @@ +../../Network/Responses/Message Persistence/PNHistoryMessageCountData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNHistoryMessagesCountRequest.h b/PubNub/include/PubNub/PNHistoryMessagesCountRequest.h new file mode 120000 index 000000000..882f12e1e --- /dev/null +++ b/PubNub/include/PubNub/PNHistoryMessagesCountRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Message Persistence/PNHistoryMessagesCountRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNHistoryMessagesDeleteRequest.h b/PubNub/include/PubNub/PNHistoryMessagesDeleteRequest.h new file mode 120000 index 000000000..910b03ceb --- /dev/null +++ b/PubNub/include/PubNub/PNHistoryMessagesDeleteRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Message Persistence/PNHistoryMessagesDeleteRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNHistoryResult.h b/PubNub/include/PubNub/PNHistoryResult.h new file mode 120000 index 000000000..82ff9857c --- /dev/null +++ b/PubNub/include/PubNub/PNHistoryResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Message Persistence/PNHistoryResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNJSONCoder.h b/PubNub/include/PubNub/PNJSONCoder.h new file mode 120000 index 000000000..d3542f01e --- /dev/null +++ b/PubNub/include/PubNub/PNJSONCoder.h @@ -0,0 +1 @@ +../../Modules/Serializer/Object/PNJSONCoder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNJSONSerialization.h b/PubNub/include/PubNub/PNJSONSerialization.h new file mode 120000 index 000000000..8c5d1b943 --- /dev/null +++ b/PubNub/include/PubNub/PNJSONSerialization.h @@ -0,0 +1 @@ +../../Modules/Serializer/JSON/PNJSONSerialization.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNJSONSerializer.h b/PubNub/include/PubNub/PNJSONSerializer.h new file mode 120000 index 000000000..d23561432 --- /dev/null +++ b/PubNub/include/PubNub/PNJSONSerializer.h @@ -0,0 +1 @@ +../../Misc/Protocols/Serializer/JSON/PNJSONSerializer.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNLegacyCryptor.h b/PubNub/include/PubNub/PNLegacyCryptor.h new file mode 120000 index 000000000..ee7312183 --- /dev/null +++ b/PubNub/include/PubNub/PNLegacyCryptor.h @@ -0,0 +1 @@ +../../Modules/Crypto/Cryptors/AES/PNLegacyCryptor.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNListFilesAPICallBuilder.h b/PubNub/include/PubNub/PNListFilesAPICallBuilder.h new file mode 120000 index 000000000..2c2b6bb5d --- /dev/null +++ b/PubNub/include/PubNub/PNListFilesAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Files/PNListFilesAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNListFilesRequest.h b/PubNub/include/PubNub/PNListFilesRequest.h new file mode 120000 index 000000000..4ce32e744 --- /dev/null +++ b/PubNub/include/PubNub/PNListFilesRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Files/PNListFilesRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNListFilesResult.h b/PubNub/include/PubNub/PNListFilesResult.h new file mode 120000 index 000000000..6f29693cd --- /dev/null +++ b/PubNub/include/PubNub/PNListFilesResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/File Sharing/PNListFilesResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNLock.h b/PubNub/include/PubNub/PNLock.h new file mode 120000 index 000000000..6f45de6cd --- /dev/null +++ b/PubNub/include/PubNub/PNLock.h @@ -0,0 +1 @@ +../../Misc/Helpers/PNLock.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNLogEntry.h b/PubNub/include/PubNub/PNLogEntry.h new file mode 120000 index 000000000..6cb74d3ed --- /dev/null +++ b/PubNub/include/PubNub/PNLogEntry.h @@ -0,0 +1 @@ +../../Misc/Logger/Core/PNLogEntry.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNLogger.h b/PubNub/include/PubNub/PNLogger.h new file mode 120000 index 000000000..5c866bddd --- /dev/null +++ b/PubNub/include/PubNub/PNLogger.h @@ -0,0 +1 @@ +../../Misc/Protocols/PNLogger.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNLoggerManager.h b/PubNub/include/PubNub/PNLoggerManager.h new file mode 120000 index 000000000..3797a6b45 --- /dev/null +++ b/PubNub/include/PubNub/PNLoggerManager.h @@ -0,0 +1 @@ +../../Data/Managers/PNLoggerManager.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNManageChannelMembersAPICallBuilder.h b/PubNub/include/PubNub/PNManageChannelMembersAPICallBuilder.h new file mode 120000 index 000000000..4d2d3ed11 --- /dev/null +++ b/PubNub/include/PubNub/PNManageChannelMembersAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Objects/Membership/PNManageChannelMembersAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNManageChannelMembersRequest.h b/PubNub/include/PubNub/PNManageChannelMembersRequest.h new file mode 120000 index 000000000..8f6c458fe --- /dev/null +++ b/PubNub/include/PubNub/PNManageChannelMembersRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/Membership/PNManageChannelMembersRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNManageChannelMembersStatus.h b/PubNub/include/PubNub/PNManageChannelMembersStatus.h new file mode 120000 index 000000000..e521452b0 --- /dev/null +++ b/PubNub/include/PubNub/PNManageChannelMembersStatus.h @@ -0,0 +1 @@ +../../Data/Service Objects/App Context/PNManageChannelMembersStatus.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNManageMembershipsAPICallBuilder.h b/PubNub/include/PubNub/PNManageMembershipsAPICallBuilder.h new file mode 120000 index 000000000..b4ca9afba --- /dev/null +++ b/PubNub/include/PubNub/PNManageMembershipsAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Objects/Membership/PNManageMembershipsAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNManageMembershipsRequest.h b/PubNub/include/PubNub/PNManageMembershipsRequest.h new file mode 120000 index 000000000..cd9a0dc33 --- /dev/null +++ b/PubNub/include/PubNub/PNManageMembershipsRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/Membership/PNManageMembershipsRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNManageMembershipsStatus.h b/PubNub/include/PubNub/PNManageMembershipsStatus.h new file mode 120000 index 000000000..828d1a834 --- /dev/null +++ b/PubNub/include/PubNub/PNManageMembershipsStatus.h @@ -0,0 +1 @@ +../../Data/Service Objects/App Context/PNManageMembershipsStatus.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNMembership.h b/PubNub/include/PubNub/PNMembership.h new file mode 120000 index 000000000..7c3029f14 --- /dev/null +++ b/PubNub/include/PubNub/PNMembership.h @@ -0,0 +1 @@ +../../Data/Models/PNMembership.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNMembershipsFetchData.h b/PubNub/include/PubNub/PNMembershipsFetchData.h new file mode 120000 index 000000000..5979480c8 --- /dev/null +++ b/PubNub/include/PubNub/PNMembershipsFetchData.h @@ -0,0 +1 @@ +../../Network/Responses/App Context/PNMembershipsFetchData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNMembershipsManageData.h b/PubNub/include/PubNub/PNMembershipsManageData.h new file mode 120000 index 000000000..bb404a5c5 --- /dev/null +++ b/PubNub/include/PubNub/PNMembershipsManageData.h @@ -0,0 +1 @@ +../../Network/Responses/App Context/PNMembershipsManageData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNMessageAction.h b/PubNub/include/PubNub/PNMessageAction.h new file mode 120000 index 000000000..c6018ec95 --- /dev/null +++ b/PubNub/include/PubNub/PNMessageAction.h @@ -0,0 +1 @@ +../../Network/Responses/Message Reaction/PNMessageAction.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNMessageActionFetchData.h b/PubNub/include/PubNub/PNMessageActionFetchData.h new file mode 120000 index 000000000..cba4e531d --- /dev/null +++ b/PubNub/include/PubNub/PNMessageActionFetchData.h @@ -0,0 +1 @@ +../../Network/Responses/Message Reaction/PNMessageActionFetchData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNMessageActionResult.h b/PubNub/include/PubNub/PNMessageActionResult.h new file mode 120000 index 000000000..c7ea2a7dc --- /dev/null +++ b/PubNub/include/PubNub/PNMessageActionResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Subscribe/PNMessageActionResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNMessageActionsFetchData.h b/PubNub/include/PubNub/PNMessageActionsFetchData.h new file mode 120000 index 000000000..3a3167003 --- /dev/null +++ b/PubNub/include/PubNub/PNMessageActionsFetchData.h @@ -0,0 +1 @@ +../../Network/Responses/Message Reaction/PNMessageActionsFetchData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNMessageCountAPICallBuilder.h b/PubNub/include/PubNub/PNMessageCountAPICallBuilder.h new file mode 120000 index 000000000..a324ddddb --- /dev/null +++ b/PubNub/include/PubNub/PNMessageCountAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/History/PNMessageCountAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNMessageCountResult.h b/PubNub/include/PubNub/PNMessageCountResult.h new file mode 120000 index 000000000..85f8b2c90 --- /dev/null +++ b/PubNub/include/PubNub/PNMessageCountResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Message Persistence/PNMessageCountResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNMessageResult.h b/PubNub/include/PubNub/PNMessageResult.h new file mode 120000 index 000000000..cd24a2e60 --- /dev/null +++ b/PubNub/include/PubNub/PNMessageResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Subscribe/PNMessageResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNNetworkRequestLogEntry.h b/PubNub/include/PubNub/PNNetworkRequestLogEntry.h new file mode 120000 index 000000000..30d9dc51a --- /dev/null +++ b/PubNub/include/PubNub/PNNetworkRequestLogEntry.h @@ -0,0 +1 @@ +../../Misc/Logger/Data/PNNetworkRequestLogEntry.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNNetworkResponseLogEntry.h b/PubNub/include/PubNub/PNNetworkResponseLogEntry.h new file mode 120000 index 000000000..c3d0ecdda --- /dev/null +++ b/PubNub/include/PubNub/PNNetworkResponseLogEntry.h @@ -0,0 +1 @@ +../../Misc/Logger/Data/PNNetworkResponseLogEntry.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNNotificationsPayload.h b/PubNub/include/PubNub/PNNotificationsPayload.h new file mode 120000 index 000000000..5ca0c39fd --- /dev/null +++ b/PubNub/include/PubNub/PNNotificationsPayload.h @@ -0,0 +1 @@ +../../Misc/Helpers/Notifications Payload/PNNotificationsPayload.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNObjectEventResult.h b/PubNub/include/PubNub/PNObjectEventResult.h new file mode 120000 index 000000000..a036de0c5 --- /dev/null +++ b/PubNub/include/PubNub/PNObjectEventResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Subscribe/PNObjectEventResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNObjectSerializer.h b/PubNub/include/PubNub/PNObjectSerializer.h new file mode 120000 index 000000000..8ec7f57b0 --- /dev/null +++ b/PubNub/include/PubNub/PNObjectSerializer.h @@ -0,0 +1 @@ +../../Misc/Protocols/Serializer/Object/PNObjectSerializer.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNObjectsAPICallBuilder.h b/PubNub/include/PubNub/PNObjectsAPICallBuilder.h new file mode 120000 index 000000000..39f69477a --- /dev/null +++ b/PubNub/include/PubNub/PNObjectsAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Objects/PNObjectsAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNObjectsPaginatedRequest.h b/PubNub/include/PubNub/PNObjectsPaginatedRequest.h new file mode 120000 index 000000000..c1acd9fdf --- /dev/null +++ b/PubNub/include/PubNub/PNObjectsPaginatedRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/PNObjectsPaginatedRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNOperationResult.h b/PubNub/include/PubNub/PNOperationResult.h new file mode 120000 index 000000000..1d1dcc9fb --- /dev/null +++ b/PubNub/include/PubNub/PNOperationResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/PNOperationResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPAMToken.h b/PubNub/include/PubNub/PNPAMToken.h new file mode 120000 index 000000000..04cb56ab8 --- /dev/null +++ b/PubNub/include/PubNub/PNPAMToken.h @@ -0,0 +1 @@ +../../Data/Models/PNPAMToken.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPagedAppContextData.h b/PubNub/include/PubNub/PNPagedAppContextData.h new file mode 120000 index 000000000..031754289 --- /dev/null +++ b/PubNub/include/PubNub/PNPagedAppContextData.h @@ -0,0 +1 @@ +../../Network/Responses/App Context/PNPagedAppContextData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceAPICallBuilder.h b/PubNub/include/PubNub/PNPresenceAPICallBuilder.h new file mode 120000 index 000000000..10a32854d --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Presence/PNPresenceAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceChannelGroupHereNowAPICallBuilder.h b/PubNub/include/PubNub/PNPresenceChannelGroupHereNowAPICallBuilder.h new file mode 120000 index 000000000..77d77c913 --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceChannelGroupHereNowAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Presence/PNPresenceChannelGroupHereNowAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceChannelGroupHereNowResult.h b/PubNub/include/PubNub/PNPresenceChannelGroupHereNowResult.h new file mode 120000 index 000000000..3b78491c6 --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceChannelGroupHereNowResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Presence/PNPresenceChannelGroupHereNowResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceChannelHereNowAPICallBuilder.h b/PubNub/include/PubNub/PNPresenceChannelHereNowAPICallBuilder.h new file mode 120000 index 000000000..36954fa50 --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceChannelHereNowAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Presence/PNPresenceChannelHereNowAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceChannelHereNowResult.h b/PubNub/include/PubNub/PNPresenceChannelHereNowResult.h new file mode 120000 index 000000000..d8eb844b4 --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceChannelHereNowResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Presence/PNPresenceChannelHereNowResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceEventResult.h b/PubNub/include/PubNub/PNPresenceEventResult.h new file mode 120000 index 000000000..7f9fb30f5 --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceEventResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Subscribe/PNPresenceEventResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceGlobalHereNowResult.h b/PubNub/include/PubNub/PNPresenceGlobalHereNowResult.h new file mode 120000 index 000000000..aeb0ee4fe --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceGlobalHereNowResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Presence/PNPresenceGlobalHereNowResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceHeartbeatAPICallBuilder.h b/PubNub/include/PubNub/PNPresenceHeartbeatAPICallBuilder.h new file mode 120000 index 000000000..0adcf109c --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceHeartbeatAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Presence/PNPresenceHeartbeatAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceHeartbeatRequest.h b/PubNub/include/PubNub/PNPresenceHeartbeatRequest.h new file mode 120000 index 000000000..8fbf36d99 --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceHeartbeatRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Presence/PNPresenceHeartbeatRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceHereNowAPICallBuilder.h b/PubNub/include/PubNub/PNPresenceHereNowAPICallBuilder.h new file mode 120000 index 000000000..fd90c6461 --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceHereNowAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Presence/PNPresenceHereNowAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceHereNowFetchData.h b/PubNub/include/PubNub/PNPresenceHereNowFetchData.h new file mode 120000 index 000000000..0b0a6b0c6 --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceHereNowFetchData.h @@ -0,0 +1 @@ +../../Network/Responses/Presence/PNPresenceHereNowFetchData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceHereNowResult.h b/PubNub/include/PubNub/PNPresenceHereNowResult.h new file mode 120000 index 000000000..247f0713d --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceHereNowResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Presence/PNPresenceHereNowResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceLeaveRequest.h b/PubNub/include/PubNub/PNPresenceLeaveRequest.h new file mode 120000 index 000000000..1a60f241c --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceLeaveRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Presence/PNPresenceLeaveRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceStateFetchRequest.h b/PubNub/include/PubNub/PNPresenceStateFetchRequest.h new file mode 120000 index 000000000..6920179b6 --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceStateFetchRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Presence/PNPresenceStateFetchRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceStateFetchResult.h b/PubNub/include/PubNub/PNPresenceStateFetchResult.h new file mode 120000 index 000000000..98617b16a --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceStateFetchResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Presence/PNPresenceStateFetchResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceStateSetRequest.h b/PubNub/include/PubNub/PNPresenceStateSetRequest.h new file mode 120000 index 000000000..591dd94fb --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceStateSetRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Presence/PNPresenceStateSetRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceUserStateFetchData.h b/PubNub/include/PubNub/PNPresenceUserStateFetchData.h new file mode 120000 index 000000000..37855bead --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceUserStateFetchData.h @@ -0,0 +1 @@ +../../Network/Responses/Presence/PNPresenceUserStateFetchData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceUserStateSetData.h b/PubNub/include/PubNub/PNPresenceUserStateSetData.h new file mode 120000 index 000000000..fddeddb72 --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceUserStateSetData.h @@ -0,0 +1 @@ +../../Network/Responses/Presence/PNPresenceUserStateSetData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceWhereNowAPICallBuilder.h b/PubNub/include/PubNub/PNPresenceWhereNowAPICallBuilder.h new file mode 120000 index 000000000..c63e42212 --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceWhereNowAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Presence/PNPresenceWhereNowAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceWhereNowFetchData.h b/PubNub/include/PubNub/PNPresenceWhereNowFetchData.h new file mode 120000 index 000000000..a745941be --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceWhereNowFetchData.h @@ -0,0 +1 @@ +../../Network/Responses/Presence/PNPresenceWhereNowFetchData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPresenceWhereNowResult.h b/PubNub/include/PubNub/PNPresenceWhereNowResult.h new file mode 120000 index 000000000..1e3c649d7 --- /dev/null +++ b/PubNub/include/PubNub/PNPresenceWhereNowResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Presence/PNPresenceWhereNowResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPublishAPICallBuilder.h b/PubNub/include/PubNub/PNPublishAPICallBuilder.h new file mode 120000 index 000000000..7ef4dab6d --- /dev/null +++ b/PubNub/include/PubNub/PNPublishAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Publish/PNPublishAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPublishData.h b/PubNub/include/PubNub/PNPublishData.h new file mode 120000 index 000000000..d1b8839e0 --- /dev/null +++ b/PubNub/include/PubNub/PNPublishData.h @@ -0,0 +1 @@ +../../Network/Responses/Publish/PNPublishData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPublishFileMessageAPICallBuilder.h b/PubNub/include/PubNub/PNPublishFileMessageAPICallBuilder.h new file mode 120000 index 000000000..0db99fbae --- /dev/null +++ b/PubNub/include/PubNub/PNPublishFileMessageAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Publish/PNPublishFileMessageAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPublishFileMessageRequest.h b/PubNub/include/PubNub/PNPublishFileMessageRequest.h new file mode 120000 index 000000000..2b65367bc --- /dev/null +++ b/PubNub/include/PubNub/PNPublishFileMessageRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Publish/PNPublishFileMessageRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPublishRequest.h b/PubNub/include/PubNub/PNPublishRequest.h new file mode 120000 index 000000000..b16409abb --- /dev/null +++ b/PubNub/include/PubNub/PNPublishRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Publish/PNPublishRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPublishSizeAPICallBuilder.h b/PubNub/include/PubNub/PNPublishSizeAPICallBuilder.h new file mode 120000 index 000000000..d168ee932 --- /dev/null +++ b/PubNub/include/PubNub/PNPublishSizeAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Publish/PNPublishSizeAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPublishStatus.h b/PubNub/include/PubNub/PNPublishStatus.h new file mode 120000 index 000000000..939a8b91b --- /dev/null +++ b/PubNub/include/PubNub/PNPublishStatus.h @@ -0,0 +1 @@ +../../Data/Service Objects/Publish/PNPublishStatus.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPushNotificationFetchData.h b/PubNub/include/PubNub/PNPushNotificationFetchData.h new file mode 120000 index 000000000..12b2fb057 --- /dev/null +++ b/PubNub/include/PubNub/PNPushNotificationFetchData.h @@ -0,0 +1 @@ +../../Network/Responses/Push Notification/PNPushNotificationFetchData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPushNotificationFetchRequest.h b/PubNub/include/PubNub/PNPushNotificationFetchRequest.h new file mode 120000 index 000000000..1d4773a72 --- /dev/null +++ b/PubNub/include/PubNub/PNPushNotificationFetchRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Push Notifications/PNPushNotificationFetchRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNPushNotificationManageRequest.h b/PubNub/include/PubNub/PNPushNotificationManageRequest.h new file mode 120000 index 000000000..8634faf0d --- /dev/null +++ b/PubNub/include/PubNub/PNPushNotificationManageRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Push Notifications/PNPushNotificationManageRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNRemoveChannelMembersAPICallBuilder.h b/PubNub/include/PubNub/PNRemoveChannelMembersAPICallBuilder.h new file mode 120000 index 000000000..3f53a99a2 --- /dev/null +++ b/PubNub/include/PubNub/PNRemoveChannelMembersAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Objects/Membership/PNRemoveChannelMembersAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNRemoveChannelMembersRequest.h b/PubNub/include/PubNub/PNRemoveChannelMembersRequest.h new file mode 120000 index 000000000..e1d7f9042 --- /dev/null +++ b/PubNub/include/PubNub/PNRemoveChannelMembersRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/Membership/PNRemoveChannelMembersRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNRemoveChannelMetadataAPICallBuilder.h b/PubNub/include/PubNub/PNRemoveChannelMetadataAPICallBuilder.h new file mode 120000 index 000000000..2dc5ea75f --- /dev/null +++ b/PubNub/include/PubNub/PNRemoveChannelMetadataAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Objects/Channel/PNRemoveChannelMetadataAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNRemoveChannelMetadataRequest.h b/PubNub/include/PubNub/PNRemoveChannelMetadataRequest.h new file mode 120000 index 000000000..7c2093bee --- /dev/null +++ b/PubNub/include/PubNub/PNRemoveChannelMetadataRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/Channel/PNRemoveChannelMetadataRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNRemoveMembershipsAPICallBuilder.h b/PubNub/include/PubNub/PNRemoveMembershipsAPICallBuilder.h new file mode 120000 index 000000000..72c1cbadb --- /dev/null +++ b/PubNub/include/PubNub/PNRemoveMembershipsAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Objects/Membership/PNRemoveMembershipsAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNRemoveMembershipsRequest.h b/PubNub/include/PubNub/PNRemoveMembershipsRequest.h new file mode 120000 index 000000000..e9963761a --- /dev/null +++ b/PubNub/include/PubNub/PNRemoveMembershipsRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/Membership/PNRemoveMembershipsRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNRemoveMessageActionAPICallBuilder.h b/PubNub/include/PubNub/PNRemoveMessageActionAPICallBuilder.h new file mode 120000 index 000000000..2f23c089b --- /dev/null +++ b/PubNub/include/PubNub/PNRemoveMessageActionAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Actions/Message/PNRemoveMessageActionAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNRemoveMessageActionRequest.h b/PubNub/include/PubNub/PNRemoveMessageActionRequest.h new file mode 120000 index 000000000..6ad1fc40b --- /dev/null +++ b/PubNub/include/PubNub/PNRemoveMessageActionRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Message Reaction/Message/PNRemoveMessageActionRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNRemoveUUIDMetadataAPICallBuilder.h b/PubNub/include/PubNub/PNRemoveUUIDMetadataAPICallBuilder.h new file mode 120000 index 000000000..45daf5e93 --- /dev/null +++ b/PubNub/include/PubNub/PNRemoveUUIDMetadataAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Objects/UUID/PNRemoveUUIDMetadataAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNRemoveUUIDMetadataRequest.h b/PubNub/include/PubNub/PNRemoveUUIDMetadataRequest.h new file mode 120000 index 000000000..4f27ef722 --- /dev/null +++ b/PubNub/include/PubNub/PNRemoveUUIDMetadataRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/UUID/PNRemoveUUIDMetadataRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNRequestRetryConfiguration.h b/PubNub/include/PubNub/PNRequestRetryConfiguration.h new file mode 120000 index 000000000..7e00c7bf5 --- /dev/null +++ b/PubNub/include/PubNub/PNRequestRetryConfiguration.h @@ -0,0 +1 @@ +../../Data/Transport/PNRequestRetryConfiguration.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNResult.h b/PubNub/include/PubNub/PNResult.h new file mode 120000 index 000000000..f48168936 --- /dev/null +++ b/PubNub/include/PubNub/PNResult.h @@ -0,0 +1 @@ +../../Data/PNResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSendFileAPICallBuilder.h b/PubNub/include/PubNub/PNSendFileAPICallBuilder.h new file mode 120000 index 000000000..3c7feab0b --- /dev/null +++ b/PubNub/include/PubNub/PNSendFileAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Files/PNSendFileAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSendFileRequest.h b/PubNub/include/PubNub/PNSendFileRequest.h new file mode 120000 index 000000000..44e990332 --- /dev/null +++ b/PubNub/include/PubNub/PNSendFileRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Files/PNSendFileRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSendFileStatus.h b/PubNub/include/PubNub/PNSendFileStatus.h new file mode 120000 index 000000000..c22f61de8 --- /dev/null +++ b/PubNub/include/PubNub/PNSendFileStatus.h @@ -0,0 +1 @@ +../../Data/Service Objects/File Sharing/PNSendFileStatus.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNServiceData.h b/PubNub/include/PubNub/PNServiceData.h new file mode 120000 index 000000000..4156d9218 --- /dev/null +++ b/PubNub/include/PubNub/PNServiceData.h @@ -0,0 +1 @@ +../../Data/Service Objects/PNServiceData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSetChannelMembersAPICallBuilder.h b/PubNub/include/PubNub/PNSetChannelMembersAPICallBuilder.h new file mode 120000 index 000000000..7389af564 --- /dev/null +++ b/PubNub/include/PubNub/PNSetChannelMembersAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Objects/Membership/PNSetChannelMembersAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSetChannelMembersRequest.h b/PubNub/include/PubNub/PNSetChannelMembersRequest.h new file mode 120000 index 000000000..c216bed80 --- /dev/null +++ b/PubNub/include/PubNub/PNSetChannelMembersRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/Membership/PNSetChannelMembersRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSetChannelMetadataAPICallBuilder.h b/PubNub/include/PubNub/PNSetChannelMetadataAPICallBuilder.h new file mode 120000 index 000000000..92d027a0c --- /dev/null +++ b/PubNub/include/PubNub/PNSetChannelMetadataAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Objects/Channel/PNSetChannelMetadataAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSetChannelMetadataRequest.h b/PubNub/include/PubNub/PNSetChannelMetadataRequest.h new file mode 120000 index 000000000..ee84afb24 --- /dev/null +++ b/PubNub/include/PubNub/PNSetChannelMetadataRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/Channel/PNSetChannelMetadataRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSetChannelMetadataStatus.h b/PubNub/include/PubNub/PNSetChannelMetadataStatus.h new file mode 120000 index 000000000..a1c1ab30c --- /dev/null +++ b/PubNub/include/PubNub/PNSetChannelMetadataStatus.h @@ -0,0 +1 @@ +../../Data/Service Objects/App Context/PNSetChannelMetadataStatus.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSetMembershipsAPICallBuilder.h b/PubNub/include/PubNub/PNSetMembershipsAPICallBuilder.h new file mode 120000 index 000000000..14d839b2b --- /dev/null +++ b/PubNub/include/PubNub/PNSetMembershipsAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Objects/Membership/PNSetMembershipsAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSetMembershipsRequest.h b/PubNub/include/PubNub/PNSetMembershipsRequest.h new file mode 120000 index 000000000..6c7bef73d --- /dev/null +++ b/PubNub/include/PubNub/PNSetMembershipsRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/Membership/PNSetMembershipsRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSetUUIDMetadataAPICallBuilder.h b/PubNub/include/PubNub/PNSetUUIDMetadataAPICallBuilder.h new file mode 120000 index 000000000..08c1feb06 --- /dev/null +++ b/PubNub/include/PubNub/PNSetUUIDMetadataAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Objects/UUID/PNSetUUIDMetadataAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSetUUIDMetadataRequest.h b/PubNub/include/PubNub/PNSetUUIDMetadataRequest.h new file mode 120000 index 000000000..86ffab054 --- /dev/null +++ b/PubNub/include/PubNub/PNSetUUIDMetadataRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Objects/UUID/PNSetUUIDMetadataRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSetUUIDMetadataStatus.h b/PubNub/include/PubNub/PNSetUUIDMetadataStatus.h new file mode 120000 index 000000000..47abfe1d7 --- /dev/null +++ b/PubNub/include/PubNub/PNSetUUIDMetadataStatus.h @@ -0,0 +1 @@ +../../Data/Service Objects/App Context/PNSetUUIDMetadataStatus.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSignalAPICallBuilder.h b/PubNub/include/PubNub/PNSignalAPICallBuilder.h new file mode 120000 index 000000000..75540dc68 --- /dev/null +++ b/PubNub/include/PubNub/PNSignalAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Publish/PNSignalAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSignalData.h b/PubNub/include/PubNub/PNSignalData.h new file mode 120000 index 000000000..1eaeb6221 --- /dev/null +++ b/PubNub/include/PubNub/PNSignalData.h @@ -0,0 +1 @@ +../../Network/Responses/Signal/PNSignalData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSignalRequest.h b/PubNub/include/PubNub/PNSignalRequest.h new file mode 120000 index 000000000..73c98c6cc --- /dev/null +++ b/PubNub/include/PubNub/PNSignalRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Signal/PNSignalRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSignalResult.h b/PubNub/include/PubNub/PNSignalResult.h new file mode 120000 index 000000000..265e78288 --- /dev/null +++ b/PubNub/include/PubNub/PNSignalResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Subscribe/PNSignalResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSignalStatus.h b/PubNub/include/PubNub/PNSignalStatus.h new file mode 120000 index 000000000..adc8b5c45 --- /dev/null +++ b/PubNub/include/PubNub/PNSignalStatus.h @@ -0,0 +1 @@ +../../Data/Service Objects/Signal/PNSignalStatus.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNStateAPICallBuilder.h b/PubNub/include/PubNub/PNStateAPICallBuilder.h new file mode 120000 index 000000000..428d7a383 --- /dev/null +++ b/PubNub/include/PubNub/PNStateAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/State/PNStateAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNStateAuditAPICallBuilder.h b/PubNub/include/PubNub/PNStateAuditAPICallBuilder.h new file mode 120000 index 000000000..30ae004f4 --- /dev/null +++ b/PubNub/include/PubNub/PNStateAuditAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/State/PNStateAuditAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNStateModificationAPICallBuilder.h b/PubNub/include/PubNub/PNStateModificationAPICallBuilder.h new file mode 120000 index 000000000..737c0a9d8 --- /dev/null +++ b/PubNub/include/PubNub/PNStateModificationAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/State/PNStateModificationAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNStatus.h b/PubNub/include/PubNub/PNStatus.h new file mode 120000 index 000000000..705ae1b87 --- /dev/null +++ b/PubNub/include/PubNub/PNStatus.h @@ -0,0 +1 @@ +../../Data/Service Objects/PNStatus.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNStreamAPICallBuilder.h b/PubNub/include/PubNub/PNStreamAPICallBuilder.h new file mode 120000 index 000000000..951cd4675 --- /dev/null +++ b/PubNub/include/PubNub/PNStreamAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Stream/PNStreamAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNStreamAuditAPICallBuilder.h b/PubNub/include/PubNub/PNStreamAuditAPICallBuilder.h new file mode 120000 index 000000000..97d2dfb95 --- /dev/null +++ b/PubNub/include/PubNub/PNStreamAuditAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Stream/PNStreamAuditAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNStreamModificationAPICallBuilder.h b/PubNub/include/PubNub/PNStreamModificationAPICallBuilder.h new file mode 120000 index 000000000..37d7cb52b --- /dev/null +++ b/PubNub/include/PubNub/PNStreamModificationAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Stream/PNStreamModificationAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNStringLogEntry.h b/PubNub/include/PubNub/PNStringLogEntry.h new file mode 120000 index 000000000..63342deb2 --- /dev/null +++ b/PubNub/include/PubNub/PNStringLogEntry.h @@ -0,0 +1 @@ +../../Misc/Logger/Data/PNStringLogEntry.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNStructures.h b/PubNub/include/PubNub/PNStructures.h new file mode 120000 index 000000000..b2a1bae17 --- /dev/null +++ b/PubNub/include/PubNub/PNStructures.h @@ -0,0 +1 @@ +../../Misc/PNStructures.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSubscribeAPIBuilder.h b/PubNub/include/PubNub/PNSubscribeAPIBuilder.h new file mode 120000 index 000000000..804c4e986 --- /dev/null +++ b/PubNub/include/PubNub/PNSubscribeAPIBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Subscribe/PNSubscribeAPIBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSubscribeChannelsOrGroupsAPIBuilder.h b/PubNub/include/PubNub/PNSubscribeChannelsOrGroupsAPIBuilder.h new file mode 120000 index 000000000..fd872f91f --- /dev/null +++ b/PubNub/include/PubNub/PNSubscribeChannelsOrGroupsAPIBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Subscribe/PNSubscribeChannelsOrGroupsAPIBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSubscribeEventData.h b/PubNub/include/PubNub/PNSubscribeEventData.h new file mode 120000 index 000000000..48b31903c --- /dev/null +++ b/PubNub/include/PubNub/PNSubscribeEventData.h @@ -0,0 +1 @@ +../../Network/Responses/Subscribe/PNSubscribeEventData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSubscribeFileEventData.h b/PubNub/include/PubNub/PNSubscribeFileEventData.h new file mode 120000 index 000000000..29fe911b8 --- /dev/null +++ b/PubNub/include/PubNub/PNSubscribeFileEventData.h @@ -0,0 +1 @@ +../../Network/Responses/Subscribe/PNSubscribeFileEventData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSubscribeMessageActionEventData.h b/PubNub/include/PubNub/PNSubscribeMessageActionEventData.h new file mode 120000 index 000000000..a88bff5a8 --- /dev/null +++ b/PubNub/include/PubNub/PNSubscribeMessageActionEventData.h @@ -0,0 +1 @@ +../../Network/Responses/Subscribe/PNSubscribeMessageActionEventData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSubscribeMessageEventData.h b/PubNub/include/PubNub/PNSubscribeMessageEventData.h new file mode 120000 index 000000000..d5065e53a --- /dev/null +++ b/PubNub/include/PubNub/PNSubscribeMessageEventData.h @@ -0,0 +1 @@ +../../Network/Responses/Subscribe/PNSubscribeMessageEventData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSubscribeObjectEventData.h b/PubNub/include/PubNub/PNSubscribeObjectEventData.h new file mode 120000 index 000000000..15e5d7b65 --- /dev/null +++ b/PubNub/include/PubNub/PNSubscribeObjectEventData.h @@ -0,0 +1 @@ +../../Network/Responses/Subscribe/PNSubscribeObjectEventData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSubscribePresenceEventData.h b/PubNub/include/PubNub/PNSubscribePresenceEventData.h new file mode 120000 index 000000000..352fcaa27 --- /dev/null +++ b/PubNub/include/PubNub/PNSubscribePresenceEventData.h @@ -0,0 +1 @@ +../../Network/Responses/Subscribe/PNSubscribePresenceEventData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSubscribeRequest.h b/PubNub/include/PubNub/PNSubscribeRequest.h new file mode 120000 index 000000000..26250be34 --- /dev/null +++ b/PubNub/include/PubNub/PNSubscribeRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Subscribe/PNSubscribeRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSubscribeSignalEventData.h b/PubNub/include/PubNub/PNSubscribeSignalEventData.h new file mode 120000 index 000000000..f763187ee --- /dev/null +++ b/PubNub/include/PubNub/PNSubscribeSignalEventData.h @@ -0,0 +1 @@ +../../Network/Responses/Subscribe/PNSubscribeSignalEventData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNSubscribeStatus.h b/PubNub/include/PubNub/PNSubscribeStatus.h new file mode 120000 index 000000000..4efec5f54 --- /dev/null +++ b/PubNub/include/PubNub/PNSubscribeStatus.h @@ -0,0 +1 @@ +../../Data/Service Objects/Subscribe/PNSubscribeStatus.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNTimeAPICallBuilder.h b/PubNub/include/PubNub/PNTimeAPICallBuilder.h new file mode 120000 index 000000000..e1862698f --- /dev/null +++ b/PubNub/include/PubNub/PNTimeAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Time/PNTimeAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNTimeData.h b/PubNub/include/PubNub/PNTimeData.h new file mode 120000 index 000000000..03e3f7eba --- /dev/null +++ b/PubNub/include/PubNub/PNTimeData.h @@ -0,0 +1 @@ +../../Network/Responses/Time/PNTimeData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNTimeRequest.h b/PubNub/include/PubNub/PNTimeRequest.h new file mode 120000 index 000000000..1dcb301d3 --- /dev/null +++ b/PubNub/include/PubNub/PNTimeRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Time/PNTimeRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNTimeResult.h b/PubNub/include/PubNub/PNTimeResult.h new file mode 120000 index 000000000..c165e5c51 --- /dev/null +++ b/PubNub/include/PubNub/PNTimeResult.h @@ -0,0 +1 @@ +../../Data/Service Objects/Time/PNTimeResult.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNTransport.h b/PubNub/include/PubNub/PNTransport.h new file mode 120000 index 000000000..590a14040 --- /dev/null +++ b/PubNub/include/PubNub/PNTransport.h @@ -0,0 +1 @@ +../../Protocols/Transport/PNTransport.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNTransportConfiguration.h b/PubNub/include/PubNub/PNTransportConfiguration.h new file mode 120000 index 000000000..99cbc4595 --- /dev/null +++ b/PubNub/include/PubNub/PNTransportConfiguration.h @@ -0,0 +1 @@ +../../Data/Transport/PNTransportConfiguration.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNTransportRequest.h b/PubNub/include/PubNub/PNTransportRequest.h new file mode 120000 index 000000000..eb075cbc4 --- /dev/null +++ b/PubNub/include/PubNub/PNTransportRequest.h @@ -0,0 +1 @@ +../../Data/Transport/PNTransportRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNTransportResponse.h b/PubNub/include/PubNub/PNTransportResponse.h new file mode 120000 index 000000000..229dee28c --- /dev/null +++ b/PubNub/include/PubNub/PNTransportResponse.h @@ -0,0 +1 @@ +../../Protocols/Transport/PNTransportResponse.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNUUIDMetadata.h b/PubNub/include/PubNub/PNUUIDMetadata.h new file mode 120000 index 000000000..514b6a68c --- /dev/null +++ b/PubNub/include/PubNub/PNUUIDMetadata.h @@ -0,0 +1 @@ +../../Data/Models/PNUUIDMetadata.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNUUIDMetadataFetchAllData.h b/PubNub/include/PubNub/PNUUIDMetadataFetchAllData.h new file mode 120000 index 000000000..a7a36d800 --- /dev/null +++ b/PubNub/include/PubNub/PNUUIDMetadataFetchAllData.h @@ -0,0 +1 @@ +../../Network/Responses/App Context/PNUUIDMetadataFetchAllData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNUUIDMetadataFetchData.h b/PubNub/include/PubNub/PNUUIDMetadataFetchData.h new file mode 120000 index 000000000..a3cc8ba24 --- /dev/null +++ b/PubNub/include/PubNub/PNUUIDMetadataFetchData.h @@ -0,0 +1 @@ +../../Network/Responses/App Context/PNUUIDMetadataFetchData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNUUIDMetadataSetData.h b/PubNub/include/PubNub/PNUUIDMetadataSetData.h new file mode 120000 index 000000000..c8c4292d7 --- /dev/null +++ b/PubNub/include/PubNub/PNUUIDMetadataSetData.h @@ -0,0 +1 @@ +../../Network/Responses/App Context/PNUUIDMetadataSetData.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNUnsubscribeAPICallBuilder.h b/PubNub/include/PubNub/PNUnsubscribeAPICallBuilder.h new file mode 120000 index 000000000..be816ad81 --- /dev/null +++ b/PubNub/include/PubNub/PNUnsubscribeAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Subscribe/PNUnsubscribeAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNUnsubscribeChannelsOrGroupsAPICallBuilder.h b/PubNub/include/PubNub/PNUnsubscribeChannelsOrGroupsAPICallBuilder.h new file mode 120000 index 000000000..ebd68b80c --- /dev/null +++ b/PubNub/include/PubNub/PNUnsubscribeChannelsOrGroupsAPICallBuilder.h @@ -0,0 +1 @@ +../../Data/Builders/API Call/Subscribe/PNUnsubscribeChannelsOrGroupsAPICallBuilder.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PNWhereNowRequest.h b/PubNub/include/PubNub/PNWhereNowRequest.h new file mode 120000 index 000000000..0a362a33b --- /dev/null +++ b/PubNub/include/PubNub/PNWhereNowRequest.h @@ -0,0 +1 @@ +../../Network/Requests/Presence/PNWhereNowRequest.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PubNub+APNS.h b/PubNub/include/PubNub/PubNub+APNS.h new file mode 120000 index 000000000..ae9d0a2f8 --- /dev/null +++ b/PubNub/include/PubNub/PubNub+APNS.h @@ -0,0 +1 @@ +../../Core/PubNub+APNS.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PubNub+ChannelGroup.h b/PubNub/include/PubNub/PubNub+ChannelGroup.h new file mode 120000 index 000000000..aa7f8c26e --- /dev/null +++ b/PubNub/include/PubNub/PubNub+ChannelGroup.h @@ -0,0 +1 @@ +../../Core/PubNub+ChannelGroup.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PubNub+Core.h b/PubNub/include/PubNub/PubNub+Core.h new file mode 120000 index 000000000..c67854cc6 --- /dev/null +++ b/PubNub/include/PubNub/PubNub+Core.h @@ -0,0 +1 @@ +../../Core/PubNub+Core.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PubNub+Files.h b/PubNub/include/PubNub/PubNub+Files.h new file mode 120000 index 000000000..c6bbe1f80 --- /dev/null +++ b/PubNub/include/PubNub/PubNub+Files.h @@ -0,0 +1 @@ +../../Core/PubNub+Files.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PubNub+History.h b/PubNub/include/PubNub/PubNub+History.h new file mode 120000 index 000000000..c23bd37b7 --- /dev/null +++ b/PubNub/include/PubNub/PubNub+History.h @@ -0,0 +1 @@ +../../Core/PubNub+History.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PubNub+MessageActions.h b/PubNub/include/PubNub/PubNub+MessageActions.h new file mode 120000 index 000000000..803692dbf --- /dev/null +++ b/PubNub/include/PubNub/PubNub+MessageActions.h @@ -0,0 +1 @@ +../../Core/PubNub+MessageActions.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PubNub+Objects.h b/PubNub/include/PubNub/PubNub+Objects.h new file mode 120000 index 000000000..17ea5f1fa --- /dev/null +++ b/PubNub/include/PubNub/PubNub+Objects.h @@ -0,0 +1 @@ +../../Core/PubNub+Objects.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PubNub+PAM.h b/PubNub/include/PubNub/PubNub+PAM.h new file mode 120000 index 000000000..64baf55b8 --- /dev/null +++ b/PubNub/include/PubNub/PubNub+PAM.h @@ -0,0 +1 @@ +../../Core/PubNub+PAM.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PubNub+Presence.h b/PubNub/include/PubNub/PubNub+Presence.h new file mode 120000 index 000000000..c5681ae25 --- /dev/null +++ b/PubNub/include/PubNub/PubNub+Presence.h @@ -0,0 +1 @@ +../../Core/PubNub+Presence.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PubNub+Publish.h b/PubNub/include/PubNub/PubNub+Publish.h new file mode 120000 index 000000000..693919d65 --- /dev/null +++ b/PubNub/include/PubNub/PubNub+Publish.h @@ -0,0 +1 @@ +../../Core/PubNub+Publish.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PubNub+State.h b/PubNub/include/PubNub/PubNub+State.h new file mode 120000 index 000000000..a702f6120 --- /dev/null +++ b/PubNub/include/PubNub/PubNub+State.h @@ -0,0 +1 @@ +../../Core/PubNub+State.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PubNub+Subscribe.h b/PubNub/include/PubNub/PubNub+Subscribe.h new file mode 120000 index 000000000..84405d08b --- /dev/null +++ b/PubNub/include/PubNub/PubNub+Subscribe.h @@ -0,0 +1 @@ +../../Core/PubNub+Subscribe.h \ No newline at end of file diff --git a/PubNub/include/PubNub/PubNub+Time.h b/PubNub/include/PubNub/PubNub+Time.h new file mode 120000 index 000000000..d9f4f4936 --- /dev/null +++ b/PubNub/include/PubNub/PubNub+Time.h @@ -0,0 +1 @@ +../../Core/PubNub+Time.h \ No newline at end of file diff --git a/Tests/Podfile b/Tests/Podfile index 851a60baf..7febeb0b9 100644 --- a/Tests/Podfile +++ b/Tests/Podfile @@ -35,6 +35,8 @@ end target '[iOS] Integration Tests' do platform :ios, '14.0' pod 'PubNub', :path => '../' + pod 'OCMock', '= 3.6' + pod 'YAHTTPVCR' end diff --git a/Tests/Podfile.lock b/Tests/Podfile.lock index b7a132267..d0eda56e8 100644 --- a/Tests/Podfile.lock +++ b/Tests/Podfile.lock @@ -1,9 +1,9 @@ PODS: - Cucumberish (1.4.0) - OCMock (3.6) - - PubNub (5.8.0): - - PubNub/Core (= 5.8.0) - - PubNub/Core (5.8.0) + - PubNub (6.1.1): + - PubNub/Core (= 6.1.1) + - PubNub/Core (6.1.1) - YAHTTPVCR (1.5.1) DEPENDENCIES: @@ -25,9 +25,9 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Cucumberish: 6cbd0c1f50306b369acebfe7d9f514c9c287d26c OCMock: 5ea90566be239f179ba766fd9fbae5885040b992 - PubNub: 97be21b701517f8bf1ae962ecb25e6f056b03e6b + PubNub: 1e5c67523c074ad2830859ce39e45b73e52310d8 YAHTTPVCR: 000a59a2ce38ae24dea6ca2a769e1cf81d629093 -PODFILE CHECKSUM: 7ee58f1df93d3b0f3d5c9c3d7fcf22aa7e7b071d +PODFILE CHECKSUM: 0e3863f9d6b5136703e6b2aa81fbf196ecb8055b COCOAPODS: 1.16.2 diff --git a/Tests/PubNub Tests.xcodeproj/project.pbxproj b/Tests/PubNub Tests.xcodeproj/project.pbxproj index 4aef3e98e..8a50d5e97 100644 --- a/Tests/PubNub Tests.xcodeproj/project.pbxproj +++ b/Tests/PubNub Tests.xcodeproj/project.pbxproj @@ -7,6 +7,65 @@ objects = { /* Begin PBXBuildFile section */ + 04BC9E9121DA9952E0432246 /* PNPushRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E10F799FD1078C2F9A4B212 /* PNPushRequestTest.m */; }; + 058538B0A821FF0034FD3F48 /* PNPublishRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = A532BE184FA3B8C7B01E32DE /* PNPublishRequestTest.m */; }; + 058FBBF60DD08FC740998174 /* PNResultTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 9EB116DE2F3241E748F6174A /* PNResultTest.m */; }; + 062F4437206A7532B22817F2 /* PNSubscribeErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 19BF7C177326B1C6CDF3DA10 /* PNSubscribeErrorTest.m */; }; + 0F9367763694D9F9119B8A1B /* PNTimeRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B02A4F0F50A4C9342039266D /* PNTimeRequestTest.m */; }; + 12F82D5B71425C9D98B3DE04 /* PNHistoryRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 31F20174EB19D78E1FD97599 /* PNHistoryRequestTest.m */; }; + 162C1DCA65589A9F83FA1328 /* PNSubscribeRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = C6E503A1DE0FDA061E31B74A /* PNSubscribeRequestTest.m */; }; + 193FD75C837B66D19A3B886E /* Pods_MockableTests__tvOS__Unit_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CCCBDE0DC8023B3FEA8BA2A5 /* Pods_MockableTests__tvOS__Unit_Tests.framework */; }; + 1B562E2394C6D5708F5FC7C9 /* PNSubscribeErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 19BF7C177326B1C6CDF3DA10 /* PNSubscribeErrorTest.m */; }; + 1CD01667C2A5B2931DC0AA78 /* PNThreadSafetyTest.m in Sources */ = {isa = PBXBuildFile; fileRef = FD175125650C0611EEEEA7F9 /* PNThreadSafetyTest.m */; }; + 1FD4EEE14CB8FBEE698C187F /* Pods_MockableTests__macOS__Mocked_Integration_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C67E6D8CFAD7122AEF9B9F3B /* Pods_MockableTests__macOS__Mocked_Integration_Tests.framework */; }; + 237F24FAEAACB2B4BD898FAE /* PNCryptoModuleTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 093FE0A4C2D6AEECB0F9679E /* PNCryptoModuleTest.m */; }; + 2662B0A2DAD4F7CCA9C3215F /* PNHistoryErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 80297BA7962383B632F60CB9 /* PNHistoryErrorTest.m */; }; + 2694FB777645089272313DB7 /* PNMockTransport.m in Sources */ = {isa = PBXBuildFile; fileRef = 62D67A5C9C84B246FE62141C /* PNMockTransport.m */; }; + 270C969F5BB07F540E5017A5 /* PNHistoryRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 31F20174EB19D78E1FD97599 /* PNHistoryRequestTest.m */; }; + 282A2F3E76BB2114EA62F40C /* PNPresenceRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BD87A37DBF18CA8914A73C9 /* PNPresenceRequestTest.m */; }; + 298914DB5FB0F533B2E35715 /* PNPushRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E10F799FD1078C2F9A4B212 /* PNPushRequestTest.m */; }; + 2EDEF855F8EFB17E0B84FA99 /* PNStatusTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 165DEF4A7D063584BDD27C17 /* PNStatusTest.m */; }; + 2FBF0763AE95E08BAF9118DE /* PNFilesErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 04A089AC6BB27DBDB3901F7C /* PNFilesErrorTest.m */; }; + 300EDFC452DC9C74A458F3C6 /* PNThreadSafetyTest.m in Sources */ = {isa = PBXBuildFile; fileRef = FD175125650C0611EEEEA7F9 /* PNThreadSafetyTest.m */; }; + 3563561AA89A7130C2171761 /* PNObjectsRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 526A09E4456A59C2B2481A39 /* PNObjectsRequestTest.m */; }; + 387756F2A5F34648F5943740 /* PNSubscribeErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 19BF7C177326B1C6CDF3DA10 /* PNSubscribeErrorTest.m */; }; + 39097D404BA5812A63BE4A1E /* Pods_ContractTests__iOS__Contract_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6525EDE436B0654BA479F783 /* Pods_ContractTests__iOS__Contract_Tests.framework */; }; + 39D956CF4A5715357DDB3D35 /* PNMessageActionsErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = D1F2BD9F8EC8C75ACFDFE2BB /* PNMessageActionsErrorTest.m */; }; + 3B6D6E21F968A21FC765816A /* PNMessageActionsErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = D1F2BD9F8EC8C75ACFDFE2BB /* PNMessageActionsErrorTest.m */; }; + 40E8F187501278AF56405925 /* PNPresenceErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DDD622C9ADDEE8D1C1DF8A6F /* PNPresenceErrorTest.m */; }; + 41DDE35AB4B6F51779AD0AED /* PNConsoleLoggerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = A23F08EC70398AF2082E698E /* PNConsoleLoggerTest.m */; }; + 44B7B3DEC0D495C38CA73E43 /* Pods_MockableTests__iOS__Unit_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7D30F78DC5D7C2E9D8B17B7C /* Pods_MockableTests__iOS__Unit_Tests.framework */; }; + 465210D19F96C8F1B6F3719E /* PNEncryptedDataTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 64BF29B938B31ADA4DD658C4 /* PNEncryptedDataTest.m */; }; + 467AD85A45B720BEB8F51DBA /* PNNetworkReachabilityTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F876C90B1703896A31964AD8 /* PNNetworkReachabilityTest.m */; }; + 467CB93F40B4DA9596F785C9 /* PNNetworkReachabilityTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F876C90B1703896A31964AD8 /* PNNetworkReachabilityTest.m */; }; + 46AC80C8E1C3E7C8C55A6D8D /* PNRelationsRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 952F37A94003D0BF689B1429 /* PNRelationsRequestTest.m */; }; + 4745A637B1211A322F3657A6 /* PNHistoryErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 80297BA7962383B632F60CB9 /* PNHistoryErrorTest.m */; }; + 476A652B261C64426B13677A /* PNFileLoggerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE32A535F1EEC78F77BB80F7 /* PNFileLoggerTest.m */; }; + 47E47D1397EECE01B80F570C /* PNRelationsRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 952F37A94003D0BF689B1429 /* PNRelationsRequestTest.m */; }; + 48E61C64BC1A2DAF25730014 /* PNEncryptedDataTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 64BF29B938B31ADA4DD658C4 /* PNEncryptedDataTest.m */; }; + 4BCCE6959DBED1F83D20A886 /* PNTimeRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B02A4F0F50A4C9342039266D /* PNTimeRequestTest.m */; }; + 4C0043C033103575F195BDB9 /* PNFilesErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 04A089AC6BB27DBDB3901F7C /* PNFilesErrorTest.m */; }; + 4E071D388C01CCBBEF4DE3CB /* PNPublishErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 56AA27CE7081C5CE2181EEBC /* PNPublishErrorTest.m */; }; + 5356F7077A4FE8A430B98AE5 /* PNRelationsRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 952F37A94003D0BF689B1429 /* PNRelationsRequestTest.m */; }; + 5636212D520497EAC489FB9B /* PNThreadSafetyTest.m in Sources */ = {isa = PBXBuildFile; fileRef = FD175125650C0611EEEEA7F9 /* PNThreadSafetyTest.m */; }; + 564319F90A0CBCB0AB09C6C4 /* Pods_ContractTests__iOS__Contract_Tests_Beta.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 947A44E5142056BAE3081C97 /* Pods_ContractTests__iOS__Contract_Tests_Beta.framework */; }; + 569E2DE2ACEFFFE4D34D41EB /* PNPublishErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 56AA27CE7081C5CE2181EEBC /* PNPublishErrorTest.m */; }; + 56DD00486694EC77052B58FC /* PNNetworkReachabilityTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F876C90B1703896A31964AD8 /* PNNetworkReachabilityTest.m */; }; + 60B7D04D0F8B9CC5FFC89964 /* PNPublishRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = A532BE184FA3B8C7B01E32DE /* PNPublishRequestTest.m */; }; + 60FC4D068CE1F13FCC8071C7 /* PNCryptoModuleTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 093FE0A4C2D6AEECB0F9679E /* PNCryptoModuleTest.m */; }; + 616CEF9EB86BBE2DA7E0D0EA /* PNHistoryErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 80297BA7962383B632F60CB9 /* PNHistoryErrorTest.m */; }; + 623FF93991A555AE8C8512B4 /* PNObjectsErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 80CC146647A79FE6FC209B24 /* PNObjectsErrorTest.m */; }; + 63BF18FA88A7E19A20D99306 /* PNPresenceRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BD87A37DBF18CA8914A73C9 /* PNPresenceRequestTest.m */; }; + 64274928442B2ED295B2206D /* PNEncryptedDataTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 64BF29B938B31ADA4DD658C4 /* PNEncryptedDataTest.m */; }; + 6667C1B496EC1B049CEDB34A /* PNLegacyCryptorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 32B90B876DCD3863117CC5FA /* PNLegacyCryptorTest.m */; }; + 66BFB66C9746515470AC2B72 /* PNMockTransport.m in Sources */ = {isa = PBXBuildFile; fileRef = 62D67A5C9C84B246FE62141C /* PNMockTransport.m */; }; + 6BE828A024F484227C0784AD /* PNPushRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E10F799FD1078C2F9A4B212 /* PNPushRequestTest.m */; }; + 73902F9404B2D5D0ED20AA40 /* PNEventResultTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A66E35544C7F66CEDA34E1F /* PNEventResultTest.m */; }; + 7553E0A136475DD7963E4DBF /* PNConsoleLoggerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = A23F08EC70398AF2082E698E /* PNConsoleLoggerTest.m */; }; + 75F94EA97C629DBC13263DC9 /* PNObjectsRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 526A09E4456A59C2B2481A39 /* PNObjectsRequestTest.m */; }; + 7632F08681CA1457FB229439 /* PNMessageActionsRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B129B1EE9735B885316F0D4 /* PNMessageActionsRequestTest.m */; }; + 76F8D4AF3CBC4DDBA87008A7 /* PNPresenceRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BD87A37DBF18CA8914A73C9 /* PNPresenceRequestTest.m */; }; + 780B481401DC08659804A92B /* PNChannelGroupRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 62A6B4886DE2AA8C28208D73 /* PNChannelGroupRequestTest.m */; }; 79657A7B2719CD5B00BACEC5 /* PNAccessContractTestSteps.m in Sources */ = {isa = PBXBuildFile; fileRef = 79BB4BDE270A66E100EDC466 /* PNAccessContractTestSteps.m */; }; 79657A7C2719CD5B00BACEC5 /* PNMessageActionsContractTestSteps.m in Sources */ = {isa = PBXBuildFile; fileRef = 79BB4BD6270A60A900EDC466 /* PNMessageActionsContractTestSteps.m */; }; 79657A7D2719CD5B00BACEC5 /* PNSubscribeContractTestSteps.m in Sources */ = {isa = PBXBuildFile; fileRef = 796E6538270895F5001B57F4 /* PNSubscribeContractTestSteps.m */; }; @@ -31,6 +90,7 @@ 797ABBE524C9BECA0008CA1E /* PNFilesIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 797ABBE324C9BECA0008CA1E /* PNFilesIntegrationTests.m */; }; 797ABBE624C9BECA0008CA1E /* PNFilesIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 797ABBE324C9BECA0008CA1E /* PNFilesIntegrationTests.m */; }; 797ABBE724C9BECA0008CA1E /* PNFilesIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 797ABBE324C9BECA0008CA1E /* PNFilesIntegrationTests.m */; }; + 79B37BEAB9DF52613FE5ED01 /* PNObjectsErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 80CC146647A79FE6FC209B24 /* PNObjectsErrorTest.m */; }; 79BB4BCF2709D70C00EDC466 /* PNPublishContractTestSteps.m in Sources */ = {isa = PBXBuildFile; fileRef = 79BB4BCE2709D70C00EDC466 /* PNPublishContractTestSteps.m */; }; 79BB4BD32709EA5400EDC466 /* PNPushContractTestSteps.m in Sources */ = {isa = PBXBuildFile; fileRef = 79BB4BD22709EA5400EDC466 /* PNPushContractTestSteps.m */; }; 79BB4BD7270A60A900EDC466 /* PNMessageActionsContractTestSteps.m in Sources */ = {isa = PBXBuildFile; fileRef = 79BB4BD6270A60A900EDC466 /* PNMessageActionsContractTestSteps.m */; }; @@ -43,6 +103,24 @@ 79DDA2B0278DC75E00A5B24C /* PNConfigurationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 79DDA2AF278DC75E00A5B24C /* PNConfigurationTest.m */; }; 79DDA2B1278DC75E00A5B24C /* PNConfigurationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 79DDA2AF278DC75E00A5B24C /* PNConfigurationTest.m */; }; 79DDA2B2278DC75E00A5B24C /* PNConfigurationTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 79DDA2AF278DC75E00A5B24C /* PNConfigurationTest.m */; }; + 7B88EDEF352946D429891A5D /* PNObjectsRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 526A09E4456A59C2B2481A39 /* PNObjectsRequestTest.m */; }; + 7D0AA1A0155E4B9A78710023 /* PNFilesErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 04A089AC6BB27DBDB3901F7C /* PNFilesErrorTest.m */; }; + 7D5524168B386391F112003F /* PNLoggerManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 647419614AE0C530B2239D40 /* PNLoggerManagerTest.m */; }; + 7F7600F3D673DFE43FB92530 /* PNStatusTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 165DEF4A7D063584BDD27C17 /* PNStatusTest.m */; }; + 805C6EC12BCD5D58CEA95956 /* PNObjectsErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 80CC146647A79FE6FC209B24 /* PNObjectsErrorTest.m */; }; + 81C3DFA3C1EEB56AC8BFAE0B /* PNFileLoggerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE32A535F1EEC78F77BB80F7 /* PNFileLoggerTest.m */; }; + 84ECFAD5772CC715EED23E11 /* PNPublishRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = A532BE184FA3B8C7B01E32DE /* PNPublishRequestTest.m */; }; + 8849AD288B9EC6F155DE367B /* Pods_MockableTests__iOS__Mocked_Integration_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 81D4F301DAA6A91D40D23457 /* Pods_MockableTests__iOS__Mocked_Integration_Tests.framework */; }; + 8F2C19F503D211176B71B109 /* PNAESCBCCryptorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 1791A709672BABB38657D9DF /* PNAESCBCCryptorTest.m */; }; + 971C4586064200646C3DC561 /* PNLockTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AAD89BD8238516462734950 /* PNLockTest.m */; }; + 981E23D8B2D55CAAE7FC3572 /* PNPublishErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 56AA27CE7081C5CE2181EEBC /* PNPublishErrorTest.m */; }; + 9AE002BD58D28F0FE9D63AB4 /* PNEventResultTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A66E35544C7F66CEDA34E1F /* PNEventResultTest.m */; }; + 9BB7D860DAE77ECC6FE02EFF /* PNLoggerManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 647419614AE0C530B2239D40 /* PNLoggerManagerTest.m */; }; + 9E88A450D47057CF287848F7 /* PNFilesRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A51FE3097F33740EB9A2498 /* PNFilesRequestTest.m */; }; + 9FF9796557639DA7C21C09A6 /* PNHistoryRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 31F20174EB19D78E1FD97599 /* PNHistoryRequestTest.m */; }; + A05F34B9DA96AB370EC77B25 /* PNLegacyCryptorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 32B90B876DCD3863117CC5FA /* PNLegacyCryptorTest.m */; }; + A0C29915B07D27CA71DFC53A /* PNConsoleLoggerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = A23F08EC70398AF2082E698E /* PNConsoleLoggerTest.m */; }; + A1102DE62241FFCD62A1D1E8 /* PNEventResultTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A66E35544C7F66CEDA34E1F /* PNEventResultTest.m */; }; A529271023B181FE00FF46DD /* PNRecordableTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = A529270F23B181FE00FF46DD /* PNRecordableTestCase.m */; }; A529271123B181FE00FF46DD /* PNRecordableTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = A529270F23B181FE00FF46DD /* PNRecordableTestCase.m */; }; A529271223B181FE00FF46DD /* PNRecordableTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = A529270F23B181FE00FF46DD /* PNRecordableTestCase.m */; }; @@ -197,9 +275,102 @@ A5F8E9DC2476D47A007F79AB /* PNObjectsAPICallBuilderTest.m in Sources */ = {isa = PBXBuildFile; fileRef = A5F8E9DA2476D46D007F79AB /* PNObjectsAPICallBuilderTest.m */; }; A5F8E9DD2476D47C007F79AB /* PNObjectsAPICallBuilderTest.m in Sources */ = {isa = PBXBuildFile; fileRef = A5F8E9DA2476D46D007F79AB /* PNObjectsAPICallBuilderTest.m */; }; A5F8E9DE2476D47D007F79AB /* PNObjectsAPICallBuilderTest.m in Sources */ = {isa = PBXBuildFile; fileRef = A5F8E9DA2476D46D007F79AB /* PNObjectsAPICallBuilderTest.m */; }; + A6E34A67CC2A097449B9ED12 /* PNAESCBCCryptorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 1791A709672BABB38657D9DF /* PNAESCBCCryptorTest.m */; }; + A8DFD8AA707B7A06955D91A3 /* PNLockTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AAD89BD8238516462734950 /* PNLockTest.m */; }; + AFADD04ED859E4D0ADA51774 /* PNLogEntryTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 971257940752F215B480B28A /* PNLogEntryTest.m */; }; + B10A000126F00001001E72AF /* PNGZIPTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A000026F00001001E72AF /* PNGZIPTest.m */; }; + B10A000226F00001001E72AF /* PNGZIPTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A000026F00001001E72AF /* PNGZIPTest.m */; }; + B10A000326F00001001E72AF /* PNGZIPTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A000026F00001001E72AF /* PNGZIPTest.m */; }; + B10A000526F00002001E72AF /* PNStringTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A000426F00002001E72AF /* PNStringTest.m */; }; + B10A000626F00002001E72AF /* PNStringTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A000426F00002001E72AF /* PNStringTest.m */; }; + B10A000726F00002001E72AF /* PNStringTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A000426F00002001E72AF /* PNStringTest.m */; }; + B10A000926F00003001E72AF /* PNDictionaryTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A000826F00003001E72AF /* PNDictionaryTest.m */; }; + B10A000A26F00003001E72AF /* PNDictionaryTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A000826F00003001E72AF /* PNDictionaryTest.m */; }; + B10A000B26F00003001E72AF /* PNDictionaryTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A000826F00003001E72AF /* PNDictionaryTest.m */; }; + B10A000D26F00004001E72AF /* PNChannelTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A000C26F00004001E72AF /* PNChannelTest.m */; }; + B10A000E26F00004001E72AF /* PNChannelTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A000C26F00004001E72AF /* PNChannelTest.m */; }; + B10A000F26F00004001E72AF /* PNChannelTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A000C26F00004001E72AF /* PNChannelTest.m */; }; + B10A001126F00005001E72AF /* PNDateTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A001026F00005001E72AF /* PNDateTest.m */; }; + B10A001226F00005001E72AF /* PNDateTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A001026F00005001E72AF /* PNDateTest.m */; }; + B10A001326F00005001E72AF /* PNDateTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A001026F00005001E72AF /* PNDateTest.m */; }; + B10A001526F00006001E72AF /* PNJSONHelperTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A001426F00006001E72AF /* PNJSONHelperTest.m */; }; + B10A001626F00006001E72AF /* PNJSONHelperTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A001426F00006001E72AF /* PNJSONHelperTest.m */; }; + B10A001726F00006001E72AF /* PNJSONHelperTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A001426F00006001E72AF /* PNJSONHelperTest.m */; }; + B10A001926F00007001E72AF /* PNArrayTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A001826F00007001E72AF /* PNArrayTest.m */; }; + B10A001A26F00007001E72AF /* PNArrayTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A001826F00007001E72AF /* PNArrayTest.m */; }; + B10A001B26F00007001E72AF /* PNArrayTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A001826F00007001E72AF /* PNArrayTest.m */; }; + B10A001D26F00008001E72AF /* PNURLRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A001C26F00008001E72AF /* PNURLRequestTest.m */; }; + B10A001E26F00008001E72AF /* PNURLRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A001C26F00008001E72AF /* PNURLRequestTest.m */; }; + B10A001F26F00008001E72AF /* PNURLRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A001C26F00008001E72AF /* PNURLRequestTest.m */; }; + B10A002126F00009001E72AF /* PNDataTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A002026F00009001E72AF /* PNDataTest.m */; }; + B10A002226F00009001E72AF /* PNDataTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A002026F00009001E72AF /* PNDataTest.m */; }; + B10A002326F00009001E72AF /* PNDataTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A002026F00009001E72AF /* PNDataTest.m */; }; + B10A002526F0000A001E72AF /* PNNumberTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A002426F0000A001E72AF /* PNNumberTest.m */; }; + B10A002626F0000A001E72AF /* PNNumberTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A002426F0000A001E72AF /* PNNumberTest.m */; }; + B10A002726F0000A001E72AF /* PNNumberTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A002426F0000A001E72AF /* PNNumberTest.m */; }; + B10A002926F0000B001E72AF /* PNFunctionsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A002826F0000B001E72AF /* PNFunctionsTest.m */; }; + B10A002A26F0000B001E72AF /* PNFunctionsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A002826F0000B001E72AF /* PNFunctionsTest.m */; }; + B10A002B26F0000B001E72AF /* PNFunctionsTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B10A002826F0000B001E72AF /* PNFunctionsTest.m */; }; + B1BC20E88E54228CACBDD2DA /* PNCryptoModuleTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 093FE0A4C2D6AEECB0F9679E /* PNCryptoModuleTest.m */; }; + B5DF93E316DCC294BBC9EFE2 /* PNChannelGroupRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 62A6B4886DE2AA8C28208D73 /* PNChannelGroupRequestTest.m */; }; + B8F976AFEA38339B1398E743 /* Pods__iOS__Integration_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D82F3EDD9B779712BB30F6CF /* Pods__iOS__Integration_Tests.framework */; }; + BC88C073FAB54B116644A596 /* PNLogEntryTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 971257940752F215B480B28A /* PNLogEntryTest.m */; }; + BC9F3DC13A60CFDA1D009FEE /* PNSubscribeRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = C6E503A1DE0FDA061E31B74A /* PNSubscribeRequestTest.m */; }; + BEA0092DE3C3BC014A9E7D9E /* PNTimeRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B02A4F0F50A4C9342039266D /* PNTimeRequestTest.m */; }; + CB54F4A634CD739CE8A17249 /* PNMessageActionsRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B129B1EE9735B885316F0D4 /* PNMessageActionsRequestTest.m */; }; + CBB76F3172ACA55C91C80CE2 /* PNChannelGroupErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 64FFAD4A2656869CC895ECF7 /* PNChannelGroupErrorTest.m */; }; + CC67974165E9CBC701722D7D /* PNMessageActionsErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = D1F2BD9F8EC8C75ACFDFE2BB /* PNMessageActionsErrorTest.m */; }; + CC8F37BC6D08707441AE6A23 /* PNChannelGroupErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 64FFAD4A2656869CC895ECF7 /* PNChannelGroupErrorTest.m */; }; + CD2CBFFF17F44B2900025694 /* PNAESCBCCryptorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 1791A709672BABB38657D9DF /* PNAESCBCCryptorTest.m */; }; + CDDF13424502756DBC2E2889 /* PNSubscribeRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = C6E503A1DE0FDA061E31B74A /* PNSubscribeRequestTest.m */; }; + D179179876010515FABF3534 /* PNChannelGroupRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 62A6B4886DE2AA8C28208D73 /* PNChannelGroupRequestTest.m */; }; + D1DBC99745A63BB23339BAC0 /* PNLockTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AAD89BD8238516462734950 /* PNLockTest.m */; }; + D211216B7F5D635C0440B7BC /* PNFilesRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A51FE3097F33740EB9A2498 /* PNFilesRequestTest.m */; }; + D21BDE27E902AE9A8E3C361D /* PNResultTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 9EB116DE2F3241E748F6174A /* PNResultTest.m */; }; + D87FE9BAEE57FAE4E790C294 /* PNPresenceErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DDD622C9ADDEE8D1C1DF8A6F /* PNPresenceErrorTest.m */; }; + DEECE528F6AF398E911BCC2C /* PNFileLoggerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE32A535F1EEC78F77BB80F7 /* PNFileLoggerTest.m */; }; + DF19678C9972950E60CB11AF /* PNPresenceErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DDD622C9ADDEE8D1C1DF8A6F /* PNPresenceErrorTest.m */; }; + E4A9EBBAD8EEFC9C8F8DB1E7 /* PNLoggerManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 647419614AE0C530B2239D40 /* PNLoggerManagerTest.m */; }; + E5FABBFBFAE3DBE4D0FC7E91 /* PNResultTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 9EB116DE2F3241E748F6174A /* PNResultTest.m */; }; + E91EA41C3C70DD66DF9A91BA /* PNStatusTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 165DEF4A7D063584BDD27C17 /* PNStatusTest.m */; }; + EFD833C07B11D39222612B81 /* PNChannelGroupErrorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 64FFAD4A2656869CC895ECF7 /* PNChannelGroupErrorTest.m */; }; + F4D4E3EDE940F20D89E99A1B /* Pods_MockableTests__macOS__Unit_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 94110E8F4D9E60C81C89230D /* Pods_MockableTests__macOS__Unit_Tests.framework */; }; + F5C17A633F93A87BE8230B09 /* Pods_MockableTests__tvOS__Mocked_Integration_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 205C7F4879904D40A9E13489 /* Pods_MockableTests__tvOS__Mocked_Integration_Tests.framework */; }; + F63B2B636C672D5C7AC41015 /* PNMockTransport.m in Sources */ = {isa = PBXBuildFile; fileRef = 62D67A5C9C84B246FE62141C /* PNMockTransport.m */; }; + F981DA01D20ECB01839D64C6 /* PNFilesRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A51FE3097F33740EB9A2498 /* PNFilesRequestTest.m */; }; + F9C90DC751EE4B5FE4DEEBDE /* PNMessageActionsRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B129B1EE9735B885316F0D4 /* PNMessageActionsRequestTest.m */; }; + FE368482933A15CB3139C4A9 /* PNLegacyCryptorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 32B90B876DCD3863117CC5FA /* PNLegacyCryptorTest.m */; }; + FF9129721EA3EB95C95FE113 /* PNLogEntryTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 971257940752F215B480B28A /* PNLogEntryTest.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 01FD138F1D9143FB281121EC /* Pods-[iOS] Integration Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-[iOS] Integration Tests.release.xcconfig"; path = "Target Support Files/Pods-[iOS] Integration Tests/Pods-[iOS] Integration Tests.release.xcconfig"; sourceTree = ""; }; + 04A089AC6BB27DBDB3901F7C /* PNFilesErrorTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNFilesErrorTest.m; sourceTree = ""; }; + 093FE0A4C2D6AEECB0F9679E /* PNCryptoModuleTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNCryptoModuleTest.m; sourceTree = ""; }; + 13FDDE6376033A34187C1FC1 /* Pods-MockableTests-[iOS] Unit Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MockableTests-[iOS] Unit Tests.debug.xcconfig"; path = "Target Support Files/Pods-MockableTests-[iOS] Unit Tests/Pods-MockableTests-[iOS] Unit Tests.debug.xcconfig"; sourceTree = ""; }; + 165DEF4A7D063584BDD27C17 /* PNStatusTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNStatusTest.m; sourceTree = ""; }; + 1791A709672BABB38657D9DF /* PNAESCBCCryptorTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNAESCBCCryptorTest.m; sourceTree = ""; }; + 19BF7C177326B1C6CDF3DA10 /* PNSubscribeErrorTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNSubscribeErrorTest.m; sourceTree = ""; }; + 1AAD89BD8238516462734950 /* PNLockTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNLockTest.m; sourceTree = ""; }; + 205C7F4879904D40A9E13489 /* Pods_MockableTests__tvOS__Mocked_Integration_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MockableTests__tvOS__Mocked_Integration_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2E10F799FD1078C2F9A4B212 /* PNPushRequestTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNPushRequestTest.m; sourceTree = ""; }; + 31F20174EB19D78E1FD97599 /* PNHistoryRequestTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNHistoryRequestTest.m; sourceTree = ""; }; + 32B90B876DCD3863117CC5FA /* PNLegacyCryptorTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNLegacyCryptorTest.m; sourceTree = ""; }; + 33BEB83945085F2AEBB08569 /* Pods-MockableTests-[iOS] Mocked Integration Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MockableTests-[iOS] Mocked Integration Tests.release.xcconfig"; path = "Target Support Files/Pods-MockableTests-[iOS] Mocked Integration Tests/Pods-MockableTests-[iOS] Mocked Integration Tests.release.xcconfig"; sourceTree = ""; }; + 37E54F107BD09641FA12F7D0 /* Pods-MockableTests-[macOS] Mocked Integration Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MockableTests-[macOS] Mocked Integration Tests.release.xcconfig"; path = "Target Support Files/Pods-MockableTests-[macOS] Mocked Integration Tests/Pods-MockableTests-[macOS] Mocked Integration Tests.release.xcconfig"; sourceTree = ""; }; + 41E0773DB321A7C37C16CE47 /* Pods-ContractTests-[iOS] Contract Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ContractTests-[iOS] Contract Tests.release.xcconfig"; path = "Target Support Files/Pods-ContractTests-[iOS] Contract Tests/Pods-ContractTests-[iOS] Contract Tests.release.xcconfig"; sourceTree = ""; }; + 4A51FE3097F33740EB9A2498 /* PNFilesRequestTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNFilesRequestTest.m; sourceTree = ""; }; + 4BC7B27C62C862E41744AC12 /* Pods-MockableTests-[tvOS] Mocked Integration Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MockableTests-[tvOS] Mocked Integration Tests.release.xcconfig"; path = "Target Support Files/Pods-MockableTests-[tvOS] Mocked Integration Tests/Pods-MockableTests-[tvOS] Mocked Integration Tests.release.xcconfig"; sourceTree = ""; }; + 526A09E4456A59C2B2481A39 /* PNObjectsRequestTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNObjectsRequestTest.m; sourceTree = ""; }; + 56AA27CE7081C5CE2181EEBC /* PNPublishErrorTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNPublishErrorTest.m; sourceTree = ""; }; + 5F7B03D6706F383B4D1016C8 /* Pods-[iOS] Integration Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-[iOS] Integration Tests.debug.xcconfig"; path = "Target Support Files/Pods-[iOS] Integration Tests/Pods-[iOS] Integration Tests.debug.xcconfig"; sourceTree = ""; }; + 62A6B4886DE2AA8C28208D73 /* PNChannelGroupRequestTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNChannelGroupRequestTest.m; sourceTree = ""; }; + 62D67A5C9C84B246FE62141C /* PNMockTransport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNMockTransport.m; sourceTree = ""; }; + 647419614AE0C530B2239D40 /* PNLoggerManagerTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNLoggerManagerTest.m; sourceTree = ""; }; + 64BF29B938B31ADA4DD658C4 /* PNEncryptedDataTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNEncryptedDataTest.m; sourceTree = ""; }; + 64FFAD4A2656869CC895ECF7 /* PNChannelGroupErrorTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNChannelGroupErrorTest.m; sourceTree = ""; }; + 6525EDE436B0654BA479F783 /* Pods_ContractTests__iOS__Contract_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ContractTests__iOS__Contract_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6B67CB35405BA8A5D64355F1 /* Pods-ContractTests-[iOS] Contract Tests Beta.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ContractTests-[iOS] Contract Tests Beta.debug.xcconfig"; path = "Target Support Files/Pods-ContractTests-[iOS] Contract Tests Beta/Pods-ContractTests-[iOS] Contract Tests Beta.debug.xcconfig"; sourceTree = ""; }; 7941EE5E270C73B30054D9EF /* ios-contract-tests.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "ios-contract-tests.plist"; sourceTree = ""; }; 79657A8F2719CD5B00BACEC5 /* [iOS] Contract Tests Beta.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "[iOS] Contract Tests Beta.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 796E65292707B33A001B57F4 /* PNContractCucumberTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNContractCucumberTest.m; sourceTree = ""; }; @@ -226,6 +397,22 @@ 79BB4BE2270A6FBF00EDC466 /* PNFilesContractTestSteps.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNFilesContractTestSteps.m; sourceTree = ""; }; 79CFA2D626DE25CD00D206D4 /* PNPAMTokenTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNPAMTokenTest.m; sourceTree = ""; }; 79DDA2AF278DC75E00A5B24C /* PNConfigurationTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNConfigurationTest.m; sourceTree = ""; }; + 7A66E35544C7F66CEDA34E1F /* PNEventResultTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNEventResultTest.m; sourceTree = ""; }; + 7B129B1EE9735B885316F0D4 /* PNMessageActionsRequestTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNMessageActionsRequestTest.m; sourceTree = ""; }; + 7D30F78DC5D7C2E9D8B17B7C /* Pods_MockableTests__iOS__Unit_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MockableTests__iOS__Unit_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 80297BA7962383B632F60CB9 /* PNHistoryErrorTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNHistoryErrorTest.m; sourceTree = ""; }; + 80CC146647A79FE6FC209B24 /* PNObjectsErrorTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNObjectsErrorTest.m; sourceTree = ""; }; + 81D4F301DAA6A91D40D23457 /* Pods_MockableTests__iOS__Mocked_Integration_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MockableTests__iOS__Mocked_Integration_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 828EFFC330107C0A76C78310 /* Pods-MockableTests-[tvOS] Unit Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MockableTests-[tvOS] Unit Tests.debug.xcconfig"; path = "Target Support Files/Pods-MockableTests-[tvOS] Unit Tests/Pods-MockableTests-[tvOS] Unit Tests.debug.xcconfig"; sourceTree = ""; }; + 8F86174540901D21E718D8F1 /* Pods-MockableTests-[iOS] Unit Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MockableTests-[iOS] Unit Tests.release.xcconfig"; path = "Target Support Files/Pods-MockableTests-[iOS] Unit Tests/Pods-MockableTests-[iOS] Unit Tests.release.xcconfig"; sourceTree = ""; }; + 911C9D8D1CDF9A6875A4E22F /* Pods-MockableTests-[macOS] Mocked Integration Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MockableTests-[macOS] Mocked Integration Tests.debug.xcconfig"; path = "Target Support Files/Pods-MockableTests-[macOS] Mocked Integration Tests/Pods-MockableTests-[macOS] Mocked Integration Tests.debug.xcconfig"; sourceTree = ""; }; + 94110E8F4D9E60C81C89230D /* Pods_MockableTests__macOS__Unit_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MockableTests__macOS__Unit_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 947A44E5142056BAE3081C97 /* Pods_ContractTests__iOS__Contract_Tests_Beta.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ContractTests__iOS__Contract_Tests_Beta.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 952F37A94003D0BF689B1429 /* PNRelationsRequestTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNRelationsRequestTest.m; sourceTree = ""; }; + 971257940752F215B480B28A /* PNLogEntryTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNLogEntryTest.m; sourceTree = ""; }; + 9BD87A37DBF18CA8914A73C9 /* PNPresenceRequestTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNPresenceRequestTest.m; sourceTree = ""; }; + 9EB116DE2F3241E748F6174A /* PNResultTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNResultTest.m; sourceTree = ""; }; + A23F08EC70398AF2082E698E /* PNConsoleLoggerTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNConsoleLoggerTest.m; sourceTree = ""; }; A529268323B0D07500FF46DD /* [iOS] Code Coverage.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "[iOS] Code Coverage.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; A52926D823B0E3CF00FF46DD /* ios-tests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "ios-tests.plist"; sourceTree = ""; }; A52926DF23B0E6C500FF46DD /* [tvOS] Mocked Integration Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "[tvOS] Mocked Integration Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -242,6 +429,7 @@ A531DFD82D64A06D00AEE218 /* PNAppContextObjectsRelationMetadataContractTestSteps.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PNAppContextObjectsRelationMetadataContractTestSteps.h; sourceTree = ""; }; A531DFD92D64A06D00AEE218 /* PNAppContextObjectsRelationMetadataContractTestSteps.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNAppContextObjectsRelationMetadataContractTestSteps.m; sourceTree = ""; }; A532490F2C304F90003510FF /* PNSubscribeTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNSubscribeTest.m; sourceTree = ""; }; + A532BE184FA3B8C7B01E32DE /* PNPublishRequestTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNPublishRequestTest.m; sourceTree = ""; }; A53D0AED23E9BF60001E72AF /* PNMembershipsObjectsAPICallBuilderTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNMembershipsObjectsAPICallBuilderTest.m; sourceTree = ""; }; A53D0AF123E9F42B001E72AF /* PNChannelMembersObjectsAPICallBuilderTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNChannelMembersObjectsAPICallBuilderTest.m; sourceTree = ""; }; A53D0AF523E9F7D7001E72AF /* PNChannelMetadataAPICallBuilderTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNChannelMetadataAPICallBuilderTest.m; sourceTree = ""; }; @@ -283,6 +471,35 @@ A5E3BA012B2FA76700D3AA18 /* PNRequestRetryConfigurationTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNRequestRetryConfigurationTest.m; sourceTree = ""; }; A5E3BA062B3030BB00D3AA18 /* PNRequestRetryConfigurationIntegrationTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNRequestRetryConfigurationIntegrationTest.m; sourceTree = ""; }; A5F8E9DA2476D46D007F79AB /* PNObjectsAPICallBuilderTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNObjectsAPICallBuilderTest.m; sourceTree = ""; }; + B02A4F0F50A4C9342039266D /* PNTimeRequestTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNTimeRequestTest.m; sourceTree = ""; }; + B10A000026F00001001E72AF /* PNGZIPTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNGZIPTest.m; sourceTree = ""; }; + B10A000426F00002001E72AF /* PNStringTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNStringTest.m; sourceTree = ""; }; + B10A000826F00003001E72AF /* PNDictionaryTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNDictionaryTest.m; sourceTree = ""; }; + B10A000C26F00004001E72AF /* PNChannelTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNChannelTest.m; sourceTree = ""; }; + B10A001026F00005001E72AF /* PNDateTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNDateTest.m; sourceTree = ""; }; + B10A001426F00006001E72AF /* PNJSONHelperTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNJSONHelperTest.m; sourceTree = ""; }; + B10A001826F00007001E72AF /* PNArrayTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNArrayTest.m; sourceTree = ""; }; + B10A001C26F00008001E72AF /* PNURLRequestTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNURLRequestTest.m; sourceTree = ""; }; + B10A002026F00009001E72AF /* PNDataTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNDataTest.m; sourceTree = ""; }; + B10A002426F0000A001E72AF /* PNNumberTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNNumberTest.m; sourceTree = ""; }; + B10A002826F0000B001E72AF /* PNFunctionsTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PNFunctionsTest.m; sourceTree = ""; }; + BB1BF4F52A6788409FCABA1A /* Pods-MockableTests-[tvOS] Mocked Integration Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MockableTests-[tvOS] Mocked Integration Tests.debug.xcconfig"; path = "Target Support Files/Pods-MockableTests-[tvOS] Mocked Integration Tests/Pods-MockableTests-[tvOS] Mocked Integration Tests.debug.xcconfig"; sourceTree = ""; }; + C0CCA7BD2B4C6CD160949FF7 /* Pods-MockableTests-[iOS] Mocked Integration Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MockableTests-[iOS] Mocked Integration Tests.debug.xcconfig"; path = "Target Support Files/Pods-MockableTests-[iOS] Mocked Integration Tests/Pods-MockableTests-[iOS] Mocked Integration Tests.debug.xcconfig"; sourceTree = ""; }; + C478061E7C0E54A479A59738 /* Pods-ContractTests-[iOS] Contract Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ContractTests-[iOS] Contract Tests.debug.xcconfig"; path = "Target Support Files/Pods-ContractTests-[iOS] Contract Tests/Pods-ContractTests-[iOS] Contract Tests.debug.xcconfig"; sourceTree = ""; }; + C4C69FE3D81194BB29FC5A04 /* Pods-MockableTests-[macOS] Unit Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MockableTests-[macOS] Unit Tests.release.xcconfig"; path = "Target Support Files/Pods-MockableTests-[macOS] Unit Tests/Pods-MockableTests-[macOS] Unit Tests.release.xcconfig"; sourceTree = ""; }; + C67E6D8CFAD7122AEF9B9F3B /* Pods_MockableTests__macOS__Mocked_Integration_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MockableTests__macOS__Mocked_Integration_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C6E503A1DE0FDA061E31B74A /* PNSubscribeRequestTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNSubscribeRequestTest.m; sourceTree = ""; }; + CCCBDE0DC8023B3FEA8BA2A5 /* Pods_MockableTests__tvOS__Unit_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MockableTests__tvOS__Unit_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D1F2BD9F8EC8C75ACFDFE2BB /* PNMessageActionsErrorTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNMessageActionsErrorTest.m; sourceTree = ""; }; + D4FE56602E20124B56F1266C /* Pods-MockableTests-[macOS] Unit Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MockableTests-[macOS] Unit Tests.debug.xcconfig"; path = "Target Support Files/Pods-MockableTests-[macOS] Unit Tests/Pods-MockableTests-[macOS] Unit Tests.debug.xcconfig"; sourceTree = ""; }; + D82F3EDD9B779712BB30F6CF /* Pods__iOS__Integration_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods__iOS__Integration_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DD0FCB2DA90E435F25B87C6C /* Pods-ContractTests-[iOS] Contract Tests Beta.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ContractTests-[iOS] Contract Tests Beta.release.xcconfig"; path = "Target Support Files/Pods-ContractTests-[iOS] Contract Tests Beta/Pods-ContractTests-[iOS] Contract Tests Beta.release.xcconfig"; sourceTree = ""; }; + DDD622C9ADDEE8D1C1DF8A6F /* PNPresenceErrorTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNPresenceErrorTest.m; sourceTree = ""; }; + DE32A535F1EEC78F77BB80F7 /* PNFileLoggerTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNFileLoggerTest.m; sourceTree = ""; }; + E743CC9BF8671E22FF0B6BDE /* Pods-MockableTests-[tvOS] Unit Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MockableTests-[tvOS] Unit Tests.release.xcconfig"; path = "Target Support Files/Pods-MockableTests-[tvOS] Unit Tests/Pods-MockableTests-[tvOS] Unit Tests.release.xcconfig"; sourceTree = ""; }; + F765D4A0CBB489A473F525EC /* PNMockTransport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = PNMockTransport.h; sourceTree = ""; }; + F876C90B1703896A31964AD8 /* PNNetworkReachabilityTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNNetworkReachabilityTest.m; sourceTree = ""; }; + FD175125650C0611EEEEA7F9 /* PNThreadSafetyTest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = PNThreadSafetyTest.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -290,6 +507,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 564319F90A0CBCB0AB09C6C4 /* Pods_ContractTests__iOS__Contract_Tests_Beta.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -297,6 +515,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 39097D404BA5812A63BE4A1E /* Pods_ContractTests__iOS__Contract_Tests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -311,6 +530,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F5C17A633F93A87BE8230B09 /* Pods_MockableTests__tvOS__Mocked_Integration_Tests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -318,6 +538,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 193FD75C837B66D19A3B886E /* Pods_MockableTests__tvOS__Unit_Tests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -325,6 +546,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 1FD4EEE14CB8FBEE698C187F /* Pods_MockableTests__macOS__Mocked_Integration_Tests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -332,6 +554,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F4D4E3EDE940F20D89E99A1B /* Pods_MockableTests__macOS__Unit_Tests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -339,6 +562,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 44B7B3DEC0D495C38CA73E43 /* Pods_MockableTests__iOS__Unit_Tests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -346,6 +570,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 8849AD288B9EC6F155DE367B /* Pods_MockableTests__iOS__Mocked_Integration_Tests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -353,12 +578,95 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B8F976AFEA38339B1398E743 /* Pods__iOS__Integration_Tests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0C888D8CE986A1295244F2E2 /* MessageActions */ = { + isa = PBXGroup; + children = ( + D1F2BD9F8EC8C75ACFDFE2BB /* PNMessageActionsErrorTest.m */, + ); + path = MessageActions; + sourceTree = ""; + }; + 25126C6D9057DBEF349C7446 /* Presence */ = { + isa = PBXGroup; + children = ( + DDD622C9ADDEE8D1C1DF8A6F /* PNPresenceErrorTest.m */, + ); + path = Presence; + sourceTree = ""; + }; + 384DBB3A0D8B805891F2AFB6 /* ChannelGroups */ = { + isa = PBXGroup; + children = ( + 64FFAD4A2656869CC895ECF7 /* PNChannelGroupErrorTest.m */, + ); + path = ChannelGroups; + sourceTree = ""; + }; + 4196B2D4214CEEE7044D8D6A /* Subscribe */ = { + isa = PBXGroup; + children = ( + C6E503A1DE0FDA061E31B74A /* PNSubscribeRequestTest.m */, + ); + path = Subscribe; + sourceTree = ""; + }; + 49AFB0F08AD46D0A180B08E5 /* Time */ = { + isa = PBXGroup; + children = ( + B02A4F0F50A4C9342039266D /* PNTimeRequestTest.m */, + ); + path = Time; + sourceTree = ""; + }; + 55883B5C336BAC9248E00E44 /* Push */ = { + isa = PBXGroup; + children = ( + 2E10F799FD1078C2F9A4B212 /* PNPushRequestTest.m */, + ); + path = Push; + sourceTree = ""; + }; + 6DA5C342E25722601751F750 /* Reachability */ = { + isa = PBXGroup; + children = ( + F876C90B1703896A31964AD8 /* PNNetworkReachabilityTest.m */, + 62D67A5C9C84B246FE62141C /* PNMockTransport.m */, + F765D4A0CBB489A473F525EC /* PNMockTransport.h */, + ); + path = Reachability; + sourceTree = ""; + }; + 7032925208352665E9607218 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 6525EDE436B0654BA479F783 /* Pods_ContractTests__iOS__Contract_Tests.framework */, + 947A44E5142056BAE3081C97 /* Pods_ContractTests__iOS__Contract_Tests_Beta.framework */, + 81D4F301DAA6A91D40D23457 /* Pods_MockableTests__iOS__Mocked_Integration_Tests.framework */, + 7D30F78DC5D7C2E9D8B17B7C /* Pods_MockableTests__iOS__Unit_Tests.framework */, + C67E6D8CFAD7122AEF9B9F3B /* Pods_MockableTests__macOS__Mocked_Integration_Tests.framework */, + 94110E8F4D9E60C81C89230D /* Pods_MockableTests__macOS__Unit_Tests.framework */, + 205C7F4879904D40A9E13489 /* Pods_MockableTests__tvOS__Mocked_Integration_Tests.framework */, + CCCBDE0DC8023B3FEA8BA2A5 /* Pods_MockableTests__tvOS__Unit_Tests.framework */, + D82F3EDD9B779712BB30F6CF /* Pods__iOS__Integration_Tests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 74C094835BA8E2329BCA287A /* ChannelGroups */ = { + isa = PBXGroup; + children = ( + 62A6B4886DE2AA8C28208D73 /* PNChannelGroupRequestTest.m */, + ); + path = ChannelGroups; + sourceTree = ""; + }; 796E65282707A864001B57F4 /* Steps */ = { isa = PBXGroup; children = ( @@ -481,6 +789,43 @@ path = Data; sourceTree = ""; }; + 8C4E400FD78DD7C75A822587 /* Crypto */ = { + isa = PBXGroup; + children = ( + 32B90B876DCD3863117CC5FA /* PNLegacyCryptorTest.m */, + 64BF29B938B31ADA4DD658C4 /* PNEncryptedDataTest.m */, + 1791A709672BABB38657D9DF /* PNAESCBCCryptorTest.m */, + 093FE0A4C2D6AEECB0F9679E /* PNCryptoModuleTest.m */, + ); + path = Crypto; + sourceTree = ""; + }; + 9134BE35F920B07165FDDF14 /* Modules */ = { + isa = PBXGroup; + children = ( + 8C4E400FD78DD7C75A822587 /* Crypto */, + D3BCB1ACDED7ED9CBB61E72F /* Logger */, + ); + path = Modules; + sourceTree = ""; + }; + 939D9165977A9204F5AB6DAB /* Files */ = { + isa = PBXGroup; + children = ( + 4A51FE3097F33740EB9A2498 /* PNFilesRequestTest.m */, + ); + path = Files; + sourceTree = ""; + }; + 9AA4BA5323DEDBCD57037954 /* Objects */ = { + isa = PBXGroup; + children = ( + 952F37A94003D0BF689B1429 /* PNRelationsRequestTest.m */, + 526A09E4456A59C2B2481A39 /* PNObjectsRequestTest.m */, + ); + path = Objects; + sourceTree = ""; + }; A52926AF23B0D63100FF46DD /* Support Files */ = { isa = PBXGroup; children = ( @@ -506,8 +851,11 @@ A52926B523B0D67C00FF46DD /* Unit */ = { isa = PBXGroup; children = ( + DEE8C411DCE820AB5E484AE2 /* Concurrency */, A5E3BA002B2FA73B00D3AA18 /* Network */, + 9134BE35F920B07165FDDF14 /* Modules */, A53D0B2923EA0FDC001E72AF /* Helpers */, + CF2CDCC0D7E5242AE97268F9 /* Models */, A59ECFDA23BB56E900E84300 /* Core */, 79DDA2AE278DC6F100A5B24C /* Data */, ); @@ -560,6 +908,7 @@ isa = PBXGroup; children = ( A532490F2C304F90003510FF /* PNSubscribeTest.m */, + 19BF7C177326B1C6CDF3DA10 /* PNSubscribeErrorTest.m */, ); path = Subscribe; sourceTree = ""; @@ -626,6 +975,7 @@ A53D0B1223EA0A07001E72AF /* PNChannelMemberObjectsTest.m */, A53D0B1623EA0AB4001E72AF /* PNChannelMetadataObjectsTest.m */, A53D0B1A23EA0C5C001E72AF /* PNUUIDMetadataObjectsTest.m */, + 80CC146647A79FE6FC209B24 /* PNObjectsErrorTest.m */, ); path = Objects; sourceTree = ""; @@ -650,6 +1000,7 @@ isa = PBXGroup; children = ( A53D0B2523EA0EAD001E72AF /* PNMessageCountTest.m */, + 80297BA7962383B632F60CB9 /* PNHistoryErrorTest.m */, ); path = History; sourceTree = ""; @@ -657,7 +1008,18 @@ A53D0B2923EA0FDC001E72AF /* Helpers */ = { isa = PBXGroup; children = ( + B10A001826F00007001E72AF /* PNArrayTest.m */, + B10A000C26F00004001E72AF /* PNChannelTest.m */, + B10A002026F00009001E72AF /* PNDataTest.m */, + B10A001026F00005001E72AF /* PNDateTest.m */, + B10A000826F00003001E72AF /* PNDictionaryTest.m */, + B10A002826F0000B001E72AF /* PNFunctionsTest.m */, + B10A000026F00001001E72AF /* PNGZIPTest.m */, + B10A001426F00006001E72AF /* PNJSONHelperTest.m */, A53D0B2A23EA0FEF001E72AF /* PNNotificationPayloadBuilderTest.m */, + B10A002426F0000A001E72AF /* PNNumberTest.m */, + B10A000426F00002001E72AF /* PNStringTest.m */, + B10A001C26F00008001E72AF /* PNURLRequestTest.m */, ); path = Helpers; sourceTree = ""; @@ -683,6 +1045,11 @@ A53D0B1E23EA0DA1001E72AF /* Actions */, A59ECFDB23BB56F100E84300 /* Publish */, A53D0B2423EA0E92001E72AF /* History */, + 384DBB3A0D8B805891F2AFB6 /* ChannelGroups */, + B382A6F8A912484A76350780 /* Requests */, + 25126C6D9057DBEF349C7446 /* Presence */, + B92773A2F183C762E69C7BA8 /* Files */, + 0C888D8CE986A1295244F2E2 /* MessageActions */, ); path = Core; sourceTree = ""; @@ -691,6 +1058,7 @@ isa = PBXGroup; children = ( A59ECFDC23BB571200E84300 /* PNSignalTest.m */, + 56AA27CE7081C5CE2181EEBC /* PNPublishErrorTest.m */, ); path = Publish; sourceTree = ""; @@ -710,6 +1078,7 @@ A52926AF23B0D63100FF46DD /* Support Files */, A5B65D5523B03DB1006B7BFB /* Products */, DC23E0797E4D29A69E09CCB4 /* Pods */, + 7032925208352665E9607218 /* Frameworks */, ); sourceTree = ""; }; @@ -773,17 +1142,123 @@ isa = PBXGroup; children = ( A5E3BA012B2FA76700D3AA18 /* PNRequestRetryConfigurationTest.m */, + 6DA5C342E25722601751F750 /* Reachability */, ); path = Network; sourceTree = ""; }; + B382A6F8A912484A76350780 /* Requests */ = { + isa = PBXGroup; + children = ( + 74C094835BA8E2329BCA287A /* ChannelGroups */, + 9AA4BA5323DEDBCD57037954 /* Objects */, + 49AFB0F08AD46D0A180B08E5 /* Time */, + 4196B2D4214CEEE7044D8D6A /* Subscribe */, + 55883B5C336BAC9248E00E44 /* Push */, + B7C55E1709FA0A5D774171B4 /* Publish */, + E32CA04C870CBD05F633C5BA /* Presence */, + FA73158339A27CB6E745507C /* History */, + 939D9165977A9204F5AB6DAB /* Files */, + FE69D17CC04B7DF363AE2535 /* MessageActions */, + ); + path = Requests; + sourceTree = ""; + }; + B7C55E1709FA0A5D774171B4 /* Publish */ = { + isa = PBXGroup; + children = ( + A532BE184FA3B8C7B01E32DE /* PNPublishRequestTest.m */, + ); + path = Publish; + sourceTree = ""; + }; + B92773A2F183C762E69C7BA8 /* Files */ = { + isa = PBXGroup; + children = ( + 04A089AC6BB27DBDB3901F7C /* PNFilesErrorTest.m */, + ); + path = Files; + sourceTree = ""; + }; + CF2CDCC0D7E5242AE97268F9 /* Models */ = { + isa = PBXGroup; + children = ( + 165DEF4A7D063584BDD27C17 /* PNStatusTest.m */, + 7A66E35544C7F66CEDA34E1F /* PNEventResultTest.m */, + 9EB116DE2F3241E748F6174A /* PNResultTest.m */, + ); + path = Models; + sourceTree = ""; + }; + D3BCB1ACDED7ED9CBB61E72F /* Logger */ = { + isa = PBXGroup; + children = ( + A23F08EC70398AF2082E698E /* PNConsoleLoggerTest.m */, + 971257940752F215B480B28A /* PNLogEntryTest.m */, + DE32A535F1EEC78F77BB80F7 /* PNFileLoggerTest.m */, + 647419614AE0C530B2239D40 /* PNLoggerManagerTest.m */, + ); + path = Logger; + sourceTree = ""; + }; DC23E0797E4D29A69E09CCB4 /* Pods */ = { isa = PBXGroup; children = ( + C478061E7C0E54A479A59738 /* Pods-ContractTests-[iOS] Contract Tests.debug.xcconfig */, + 41E0773DB321A7C37C16CE47 /* Pods-ContractTests-[iOS] Contract Tests.release.xcconfig */, + 6B67CB35405BA8A5D64355F1 /* Pods-ContractTests-[iOS] Contract Tests Beta.debug.xcconfig */, + DD0FCB2DA90E435F25B87C6C /* Pods-ContractTests-[iOS] Contract Tests Beta.release.xcconfig */, + C0CCA7BD2B4C6CD160949FF7 /* Pods-MockableTests-[iOS] Mocked Integration Tests.debug.xcconfig */, + 33BEB83945085F2AEBB08569 /* Pods-MockableTests-[iOS] Mocked Integration Tests.release.xcconfig */, + 13FDDE6376033A34187C1FC1 /* Pods-MockableTests-[iOS] Unit Tests.debug.xcconfig */, + 8F86174540901D21E718D8F1 /* Pods-MockableTests-[iOS] Unit Tests.release.xcconfig */, + 911C9D8D1CDF9A6875A4E22F /* Pods-MockableTests-[macOS] Mocked Integration Tests.debug.xcconfig */, + 37E54F107BD09641FA12F7D0 /* Pods-MockableTests-[macOS] Mocked Integration Tests.release.xcconfig */, + D4FE56602E20124B56F1266C /* Pods-MockableTests-[macOS] Unit Tests.debug.xcconfig */, + C4C69FE3D81194BB29FC5A04 /* Pods-MockableTests-[macOS] Unit Tests.release.xcconfig */, + BB1BF4F52A6788409FCABA1A /* Pods-MockableTests-[tvOS] Mocked Integration Tests.debug.xcconfig */, + 4BC7B27C62C862E41744AC12 /* Pods-MockableTests-[tvOS] Mocked Integration Tests.release.xcconfig */, + 828EFFC330107C0A76C78310 /* Pods-MockableTests-[tvOS] Unit Tests.debug.xcconfig */, + E743CC9BF8671E22FF0B6BDE /* Pods-MockableTests-[tvOS] Unit Tests.release.xcconfig */, + 5F7B03D6706F383B4D1016C8 /* Pods-[iOS] Integration Tests.debug.xcconfig */, + 01FD138F1D9143FB281121EC /* Pods-[iOS] Integration Tests.release.xcconfig */, ); path = Pods; sourceTree = ""; }; + DEE8C411DCE820AB5E484AE2 /* Concurrency */ = { + isa = PBXGroup; + children = ( + FD175125650C0611EEEEA7F9 /* PNThreadSafetyTest.m */, + 1AAD89BD8238516462734950 /* PNLockTest.m */, + ); + path = Concurrency; + sourceTree = ""; + }; + E32CA04C870CBD05F633C5BA /* Presence */ = { + isa = PBXGroup; + children = ( + 9BD87A37DBF18CA8914A73C9 /* PNPresenceRequestTest.m */, + ); + path = Presence; + sourceTree = ""; + }; + FA73158339A27CB6E745507C /* History */ = { + isa = PBXGroup; + children = ( + 31F20174EB19D78E1FD97599 /* PNHistoryRequestTest.m */, + ); + path = History; + sourceTree = ""; + }; + FE69D17CC04B7DF363AE2535 /* MessageActions */ = { + isa = PBXGroup; + children = ( + 7B129B1EE9735B885316F0D4 /* PNMessageActionsRequestTest.m */, + ); + path = MessageActions; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -791,9 +1266,11 @@ isa = PBXNativeTarget; buildConfigurationList = 79657A8C2719CD5B00BACEC5 /* Build configuration list for PBXNativeTarget "[iOS] Contract Tests Beta" */; buildPhases = ( + AB7182EAD1A61D20D3989AC6 /* [CP] Check Pods Manifest.lock */, 79657A7A2719CD5B00BACEC5 /* Sources */, 79657A852719CD5B00BACEC5 /* Frameworks */, 79657A872719CD5B00BACEC5 /* Resources */, + FC5BB3C042E98A9647C68269 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -808,9 +1285,11 @@ isa = PBXNativeTarget; buildConfigurationList = 796F6307270344DF00DE6F07 /* Build configuration list for PBXNativeTarget "[iOS] Contract Tests" */; buildPhases = ( + DF9B0259CDD274CD59761913 /* [CP] Check Pods Manifest.lock */, 796F62F1270344DF00DE6F07 /* Sources */, 796F6303270344DF00DE6F07 /* Frameworks */, 796F6304270344DF00DE6F07 /* Resources */, + 07AC1933C6075A17BB69F6FA /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -842,9 +1321,11 @@ isa = PBXNativeTarget; buildConfigurationList = A52926E423B0E6C500FF46DD /* Build configuration list for PBXNativeTarget "[tvOS] Mocked Integration Tests" */; buildPhases = ( + B19BE4F3126ED2F6FAD7CDA4 /* [CP] Check Pods Manifest.lock */, A52926DB23B0E6C500FF46DD /* Sources */, A52926DC23B0E6C500FF46DD /* Frameworks */, A52926DD23B0E6C500FF46DD /* Resources */, + 7A1129B7078F4B3B21B9E50C /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -859,9 +1340,11 @@ isa = PBXNativeTarget; buildConfigurationList = A52926F023B0E6DD00FF46DD /* Build configuration list for PBXNativeTarget "[tvOS] Unit Tests" */; buildPhases = ( + 35956CDA52542FE656C9F34D /* [CP] Check Pods Manifest.lock */, A52926E723B0E6DC00FF46DD /* Sources */, A52926E823B0E6DC00FF46DD /* Frameworks */, A52926E923B0E6DC00FF46DD /* Resources */, + 02751715E529E4036BAF1590 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -876,9 +1359,11 @@ isa = PBXNativeTarget; buildConfigurationList = A52926FC23B0E71700FF46DD /* Build configuration list for PBXNativeTarget "[macOS] Mocked Integration Tests" */; buildPhases = ( + 8AF8B1681C3DCD2C68E9D869 /* [CP] Check Pods Manifest.lock */, A52926F323B0E71700FF46DD /* Sources */, A52926F423B0E71700FF46DD /* Frameworks */, A52926F523B0E71700FF46DD /* Resources */, + 7B8371BC29754BFFB9022CDD /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -893,9 +1378,11 @@ isa = PBXNativeTarget; buildConfigurationList = A529270823B0E72500FF46DD /* Build configuration list for PBXNativeTarget "[macOS] Unit Tests" */; buildPhases = ( + 07B0ECB1B8FB5DF7E01CB24E /* [CP] Check Pods Manifest.lock */, A52926FF23B0E72500FF46DD /* Sources */, A529270023B0E72500FF46DD /* Frameworks */, A529270123B0E72500FF46DD /* Resources */, + BD3BA944E10797F2715C91FB /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -910,9 +1397,11 @@ isa = PBXNativeTarget; buildConfigurationList = A5B65D7B23B03DB4006B7BFB /* Build configuration list for PBXNativeTarget "[iOS] Unit Tests" */; buildPhases = ( + 8DB0A6CA5E21A7D93101D039 /* [CP] Check Pods Manifest.lock */, A5B65D6B23B03DB4006B7BFB /* Sources */, A5B65D6C23B03DB4006B7BFB /* Frameworks */, A5B65D6D23B03DB4006B7BFB /* Resources */, + EA4A14C57D031490EDDAE937 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -927,9 +1416,11 @@ isa = PBXNativeTarget; buildConfigurationList = A5B65D8723B03FFD006B7BFB /* Build configuration list for PBXNativeTarget "[iOS] Mocked Integration Tests" */; buildPhases = ( + ADB1817F8D94781BBD844ADF /* [CP] Check Pods Manifest.lock */, A5B65D7E23B03FFD006B7BFB /* Sources */, A5B65D7F23B03FFD006B7BFB /* Frameworks */, A5B65D8023B03FFD006B7BFB /* Resources */, + 33AEB79FC97D1C88F0CBD8A6 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -944,9 +1435,11 @@ isa = PBXNativeTarget; buildConfigurationList = A5B65D9423B04148006B7BFB /* Build configuration list for PBXNativeTarget "[iOS] Integration Tests" */; buildPhases = ( + FE36CC054B1BFEF6BB2B1CA1 /* [CP] Check Pods Manifest.lock */, A5B65D8B23B04148006B7BFB /* Sources */, A5B65D8C23B04148006B7BFB /* Frameworks */, A5B65D8D23B04148006B7BFB /* Resources */, + 5DBCBF820B2F34714594FA81 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -1112,6 +1605,360 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 02751715E529E4036BAF1590 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-MockableTests-[tvOS] Unit Tests/Pods-MockableTests-[tvOS] Unit Tests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-MockableTests-[tvOS] Unit Tests/Pods-MockableTests-[tvOS] Unit Tests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-MockableTests-[tvOS] Unit Tests/Pods-MockableTests-[tvOS] Unit Tests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 07AC1933C6075A17BB69F6FA /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ContractTests-[iOS] Contract Tests/Pods-ContractTests-[iOS] Contract Tests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ContractTests-[iOS] Contract Tests/Pods-ContractTests-[iOS] Contract Tests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ContractTests-[iOS] Contract Tests/Pods-ContractTests-[iOS] Contract Tests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 07B0ECB1B8FB5DF7E01CB24E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-MockableTests-[macOS] Unit Tests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 33AEB79FC97D1C88F0CBD8A6 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-MockableTests-[iOS] Mocked Integration Tests/Pods-MockableTests-[iOS] Mocked Integration Tests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-MockableTests-[iOS] Mocked Integration Tests/Pods-MockableTests-[iOS] Mocked Integration Tests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-MockableTests-[iOS] Mocked Integration Tests/Pods-MockableTests-[iOS] Mocked Integration Tests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 35956CDA52542FE656C9F34D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-MockableTests-[tvOS] Unit Tests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 5DBCBF820B2F34714594FA81 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-[iOS] Integration Tests/Pods-[iOS] Integration Tests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-[iOS] Integration Tests/Pods-[iOS] Integration Tests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-[iOS] Integration Tests/Pods-[iOS] Integration Tests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 7A1129B7078F4B3B21B9E50C /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-MockableTests-[tvOS] Mocked Integration Tests/Pods-MockableTests-[tvOS] Mocked Integration Tests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-MockableTests-[tvOS] Mocked Integration Tests/Pods-MockableTests-[tvOS] Mocked Integration Tests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-MockableTests-[tvOS] Mocked Integration Tests/Pods-MockableTests-[tvOS] Mocked Integration Tests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 7B8371BC29754BFFB9022CDD /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-MockableTests-[macOS] Mocked Integration Tests/Pods-MockableTests-[macOS] Mocked Integration Tests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-MockableTests-[macOS] Mocked Integration Tests/Pods-MockableTests-[macOS] Mocked Integration Tests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-MockableTests-[macOS] Mocked Integration Tests/Pods-MockableTests-[macOS] Mocked Integration Tests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 8AF8B1681C3DCD2C68E9D869 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-MockableTests-[macOS] Mocked Integration Tests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 8DB0A6CA5E21A7D93101D039 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-MockableTests-[iOS] Unit Tests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + AB7182EAD1A61D20D3989AC6 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ContractTests-[iOS] Contract Tests Beta-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + ADB1817F8D94781BBD844ADF /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-MockableTests-[iOS] Mocked Integration Tests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + B19BE4F3126ED2F6FAD7CDA4 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-MockableTests-[tvOS] Mocked Integration Tests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + BD3BA944E10797F2715C91FB /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-MockableTests-[macOS] Unit Tests/Pods-MockableTests-[macOS] Unit Tests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-MockableTests-[macOS] Unit Tests/Pods-MockableTests-[macOS] Unit Tests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-MockableTests-[macOS] Unit Tests/Pods-MockableTests-[macOS] Unit Tests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + DF9B0259CDD274CD59761913 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ContractTests-[iOS] Contract Tests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + EA4A14C57D031490EDDAE937 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-MockableTests-[iOS] Unit Tests/Pods-MockableTests-[iOS] Unit Tests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-MockableTests-[iOS] Unit Tests/Pods-MockableTests-[iOS] Unit Tests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-MockableTests-[iOS] Unit Tests/Pods-MockableTests-[iOS] Unit Tests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + FC5BB3C042E98A9647C68269 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ContractTests-[iOS] Contract Tests Beta/Pods-ContractTests-[iOS] Contract Tests Beta-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ContractTests-[iOS] Contract Tests Beta/Pods-ContractTests-[iOS] Contract Tests Beta-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ContractTests-[iOS] Contract Tests Beta/Pods-ContractTests-[iOS] Contract Tests Beta-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + FE36CC054B1BFEF6BB2B1CA1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-[iOS] Integration Tests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 79657A7A2719CD5B00BACEC5 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -1195,6 +2042,17 @@ A5E3BA032B2FA76700D3AA18 /* PNRequestRetryConfigurationTest.m in Sources */, A53D0B1023EA07E5001E72AF /* PNMembershipObjectsTest.m in Sources */, A53D0B2C23EA0FEF001E72AF /* PNNotificationPayloadBuilderTest.m in Sources */, + B10A000226F00001001E72AF /* PNGZIPTest.m in Sources */, + B10A000626F00002001E72AF /* PNStringTest.m in Sources */, + B10A000A26F00003001E72AF /* PNDictionaryTest.m in Sources */, + B10A000E26F00004001E72AF /* PNChannelTest.m in Sources */, + B10A001226F00005001E72AF /* PNDateTest.m in Sources */, + B10A001626F00006001E72AF /* PNJSONHelperTest.m in Sources */, + B10A001A26F00007001E72AF /* PNArrayTest.m in Sources */, + B10A001E26F00008001E72AF /* PNURLRequestTest.m in Sources */, + B10A002226F00009001E72AF /* PNDataTest.m in Sources */, + B10A002626F0000A001E72AF /* PNNumberTest.m in Sources */, + B10A002A26F0000B001E72AF /* PNFunctionsTest.m in Sources */, A53D0AFB23E9FBE4001E72AF /* PNUUIDMetadataAPICallBuilderTest.m in Sources */, A5DB1E9623B2D212009B1B23 /* NSInvocation+PNTest.m in Sources */, A59ECFDE23BB571200E84300 /* PNSignalTest.m in Sources */, @@ -1215,6 +2073,40 @@ 79DDA2B1278DC75E00A5B24C /* PNConfigurationTest.m in Sources */, A53D0AF323E9F42B001E72AF /* PNChannelMembersObjectsAPICallBuilderTest.m in Sources */, A53D0B1823EA0AB4001E72AF /* PNChannelMetadataObjectsTest.m in Sources */, + CBB76F3172ACA55C91C80CE2 /* PNChannelGroupErrorTest.m in Sources */, + 79B37BEAB9DF52613FE5ED01 /* PNObjectsErrorTest.m in Sources */, + D179179876010515FABF3534 /* PNChannelGroupRequestTest.m in Sources */, + 5356F7077A4FE8A430B98AE5 /* PNRelationsRequestTest.m in Sources */, + 3563561AA89A7130C2171761 /* PNObjectsRequestTest.m in Sources */, + 0F9367763694D9F9119B8A1B /* PNTimeRequestTest.m in Sources */, + CDDF13424502756DBC2E2889 /* PNSubscribeRequestTest.m in Sources */, + 04BC9E9121DA9952E0432246 /* PNPushRequestTest.m in Sources */, + 84ECFAD5772CC715EED23E11 /* PNPublishRequestTest.m in Sources */, + 282A2F3E76BB2114EA62F40C /* PNPresenceRequestTest.m in Sources */, + 270C969F5BB07F540E5017A5 /* PNHistoryRequestTest.m in Sources */, + F981DA01D20ECB01839D64C6 /* PNFilesRequestTest.m in Sources */, + CB54F4A634CD739CE8A17249 /* PNMessageActionsRequestTest.m in Sources */, + 1B562E2394C6D5708F5FC7C9 /* PNSubscribeErrorTest.m in Sources */, + 4E071D388C01CCBBEF4DE3CB /* PNPublishErrorTest.m in Sources */, + D87FE9BAEE57FAE4E790C294 /* PNPresenceErrorTest.m in Sources */, + 2662B0A2DAD4F7CCA9C3215F /* PNHistoryErrorTest.m in Sources */, + 7D0AA1A0155E4B9A78710023 /* PNFilesErrorTest.m in Sources */, + CC67974165E9CBC701722D7D /* PNMessageActionsErrorTest.m in Sources */, + 467AD85A45B720BEB8F51DBA /* PNNetworkReachabilityTest.m in Sources */, + 66BFB66C9746515470AC2B72 /* PNMockTransport.m in Sources */, + E91EA41C3C70DD66DF9A91BA /* PNStatusTest.m in Sources */, + 9AE002BD58D28F0FE9D63AB4 /* PNEventResultTest.m in Sources */, + D21BDE27E902AE9A8E3C361D /* PNResultTest.m in Sources */, + 300EDFC452DC9C74A458F3C6 /* PNThreadSafetyTest.m in Sources */, + 971C4586064200646C3DC561 /* PNLockTest.m in Sources */, + A05F34B9DA96AB370EC77B25 /* PNLegacyCryptorTest.m in Sources */, + 48E61C64BC1A2DAF25730014 /* PNEncryptedDataTest.m in Sources */, + A6E34A67CC2A097449B9ED12 /* PNAESCBCCryptorTest.m in Sources */, + 237F24FAEAACB2B4BD898FAE /* PNCryptoModuleTest.m in Sources */, + 7553E0A136475DD7963E4DBF /* PNConsoleLoggerTest.m in Sources */, + FF9129721EA3EB95C95FE113 /* PNLogEntryTest.m in Sources */, + DEECE528F6AF398E911BCC2C /* PNFileLoggerTest.m in Sources */, + 9BB7D860DAE77ECC6FE02EFF /* PNLoggerManagerTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1250,6 +2142,17 @@ A5E3BA022B2FA76700D3AA18 /* PNRequestRetryConfigurationTest.m in Sources */, A53D0B0F23EA07E5001E72AF /* PNMembershipObjectsTest.m in Sources */, A53D0B2B23EA0FEF001E72AF /* PNNotificationPayloadBuilderTest.m in Sources */, + B10A000126F00001001E72AF /* PNGZIPTest.m in Sources */, + B10A000526F00002001E72AF /* PNStringTest.m in Sources */, + B10A000926F00003001E72AF /* PNDictionaryTest.m in Sources */, + B10A000D26F00004001E72AF /* PNChannelTest.m in Sources */, + B10A001126F00005001E72AF /* PNDateTest.m in Sources */, + B10A001526F00006001E72AF /* PNJSONHelperTest.m in Sources */, + B10A001926F00007001E72AF /* PNArrayTest.m in Sources */, + B10A001D26F00008001E72AF /* PNURLRequestTest.m in Sources */, + B10A002126F00009001E72AF /* PNDataTest.m in Sources */, + B10A002526F0000A001E72AF /* PNNumberTest.m in Sources */, + B10A002926F0000B001E72AF /* PNFunctionsTest.m in Sources */, A53D0AFA23E9FBE4001E72AF /* PNUUIDMetadataAPICallBuilderTest.m in Sources */, A5DB1E9423B2D212009B1B23 /* NSInvocation+PNTest.m in Sources */, A59ECFDD23BB571200E84300 /* PNSignalTest.m in Sources */, @@ -1270,6 +2173,40 @@ 79DDA2B0278DC75E00A5B24C /* PNConfigurationTest.m in Sources */, A53D0AF223E9F42B001E72AF /* PNChannelMembersObjectsAPICallBuilderTest.m in Sources */, A53D0B1723EA0AB4001E72AF /* PNChannelMetadataObjectsTest.m in Sources */, + EFD833C07B11D39222612B81 /* PNChannelGroupErrorTest.m in Sources */, + 623FF93991A555AE8C8512B4 /* PNObjectsErrorTest.m in Sources */, + 780B481401DC08659804A92B /* PNChannelGroupRequestTest.m in Sources */, + 46AC80C8E1C3E7C8C55A6D8D /* PNRelationsRequestTest.m in Sources */, + 7B88EDEF352946D429891A5D /* PNObjectsRequestTest.m in Sources */, + BEA0092DE3C3BC014A9E7D9E /* PNTimeRequestTest.m in Sources */, + BC9F3DC13A60CFDA1D009FEE /* PNSubscribeRequestTest.m in Sources */, + 298914DB5FB0F533B2E35715 /* PNPushRequestTest.m in Sources */, + 058538B0A821FF0034FD3F48 /* PNPublishRequestTest.m in Sources */, + 63BF18FA88A7E19A20D99306 /* PNPresenceRequestTest.m in Sources */, + 9FF9796557639DA7C21C09A6 /* PNHistoryRequestTest.m in Sources */, + 9E88A450D47057CF287848F7 /* PNFilesRequestTest.m in Sources */, + 7632F08681CA1457FB229439 /* PNMessageActionsRequestTest.m in Sources */, + 387756F2A5F34648F5943740 /* PNSubscribeErrorTest.m in Sources */, + 981E23D8B2D55CAAE7FC3572 /* PNPublishErrorTest.m in Sources */, + 40E8F187501278AF56405925 /* PNPresenceErrorTest.m in Sources */, + 4745A637B1211A322F3657A6 /* PNHistoryErrorTest.m in Sources */, + 4C0043C033103575F195BDB9 /* PNFilesErrorTest.m in Sources */, + 3B6D6E21F968A21FC765816A /* PNMessageActionsErrorTest.m in Sources */, + 56DD00486694EC77052B58FC /* PNNetworkReachabilityTest.m in Sources */, + F63B2B636C672D5C7AC41015 /* PNMockTransport.m in Sources */, + 7F7600F3D673DFE43FB92530 /* PNStatusTest.m in Sources */, + 73902F9404B2D5D0ED20AA40 /* PNEventResultTest.m in Sources */, + 058FBBF60DD08FC740998174 /* PNResultTest.m in Sources */, + 1CD01667C2A5B2931DC0AA78 /* PNThreadSafetyTest.m in Sources */, + D1DBC99745A63BB23339BAC0 /* PNLockTest.m in Sources */, + 6667C1B496EC1B049CEDB34A /* PNLegacyCryptorTest.m in Sources */, + 64274928442B2ED295B2206D /* PNEncryptedDataTest.m in Sources */, + 8F2C19F503D211176B71B109 /* PNAESCBCCryptorTest.m in Sources */, + 60FC4D068CE1F13FCC8071C7 /* PNCryptoModuleTest.m in Sources */, + 41DDE35AB4B6F51779AD0AED /* PNConsoleLoggerTest.m in Sources */, + AFADD04ED859E4D0ADA51774 /* PNLogEntryTest.m in Sources */, + 476A652B261C64426B13677A /* PNFileLoggerTest.m in Sources */, + 7D5524168B386391F112003F /* PNLoggerManagerTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1280,6 +2217,17 @@ A5E3BA042B2FA76700D3AA18 /* PNRequestRetryConfigurationTest.m in Sources */, A53D0B1123EA07E5001E72AF /* PNMembershipObjectsTest.m in Sources */, A53D0B2D23EA0FEF001E72AF /* PNNotificationPayloadBuilderTest.m in Sources */, + B10A000326F00001001E72AF /* PNGZIPTest.m in Sources */, + B10A000726F00002001E72AF /* PNStringTest.m in Sources */, + B10A000B26F00003001E72AF /* PNDictionaryTest.m in Sources */, + B10A000F26F00004001E72AF /* PNChannelTest.m in Sources */, + B10A001326F00005001E72AF /* PNDateTest.m in Sources */, + B10A001726F00006001E72AF /* PNJSONHelperTest.m in Sources */, + B10A001B26F00007001E72AF /* PNArrayTest.m in Sources */, + B10A001F26F00008001E72AF /* PNURLRequestTest.m in Sources */, + B10A002326F00009001E72AF /* PNDataTest.m in Sources */, + B10A002726F0000A001E72AF /* PNNumberTest.m in Sources */, + B10A002B26F0000B001E72AF /* PNFunctionsTest.m in Sources */, A53D0AFC23E9FBE4001E72AF /* PNUUIDMetadataAPICallBuilderTest.m in Sources */, A5DB1E9A23B2D212009B1B23 /* NSInvocation+PNTest.m in Sources */, A59ECFDF23BB571200E84300 /* PNSignalTest.m in Sources */, @@ -1300,6 +2248,40 @@ 79DDA2B2278DC75E00A5B24C /* PNConfigurationTest.m in Sources */, A53D0AF423E9F42B001E72AF /* PNChannelMembersObjectsAPICallBuilderTest.m in Sources */, A53D0B1923EA0AB4001E72AF /* PNChannelMetadataObjectsTest.m in Sources */, + CC8F37BC6D08707441AE6A23 /* PNChannelGroupErrorTest.m in Sources */, + 805C6EC12BCD5D58CEA95956 /* PNObjectsErrorTest.m in Sources */, + B5DF93E316DCC294BBC9EFE2 /* PNChannelGroupRequestTest.m in Sources */, + 47E47D1397EECE01B80F570C /* PNRelationsRequestTest.m in Sources */, + 75F94EA97C629DBC13263DC9 /* PNObjectsRequestTest.m in Sources */, + 4BCCE6959DBED1F83D20A886 /* PNTimeRequestTest.m in Sources */, + 162C1DCA65589A9F83FA1328 /* PNSubscribeRequestTest.m in Sources */, + 6BE828A024F484227C0784AD /* PNPushRequestTest.m in Sources */, + 60B7D04D0F8B9CC5FFC89964 /* PNPublishRequestTest.m in Sources */, + 76F8D4AF3CBC4DDBA87008A7 /* PNPresenceRequestTest.m in Sources */, + 12F82D5B71425C9D98B3DE04 /* PNHistoryRequestTest.m in Sources */, + D211216B7F5D635C0440B7BC /* PNFilesRequestTest.m in Sources */, + F9C90DC751EE4B5FE4DEEBDE /* PNMessageActionsRequestTest.m in Sources */, + 062F4437206A7532B22817F2 /* PNSubscribeErrorTest.m in Sources */, + 569E2DE2ACEFFFE4D34D41EB /* PNPublishErrorTest.m in Sources */, + DF19678C9972950E60CB11AF /* PNPresenceErrorTest.m in Sources */, + 616CEF9EB86BBE2DA7E0D0EA /* PNHistoryErrorTest.m in Sources */, + 2FBF0763AE95E08BAF9118DE /* PNFilesErrorTest.m in Sources */, + 39D956CF4A5715357DDB3D35 /* PNMessageActionsErrorTest.m in Sources */, + 467CB93F40B4DA9596F785C9 /* PNNetworkReachabilityTest.m in Sources */, + 2694FB777645089272313DB7 /* PNMockTransport.m in Sources */, + 2EDEF855F8EFB17E0B84FA99 /* PNStatusTest.m in Sources */, + A1102DE62241FFCD62A1D1E8 /* PNEventResultTest.m in Sources */, + E5FABBFBFAE3DBE4D0FC7E91 /* PNResultTest.m in Sources */, + 5636212D520497EAC489FB9B /* PNThreadSafetyTest.m in Sources */, + A8DFD8AA707B7A06955D91A3 /* PNLockTest.m in Sources */, + FE368482933A15CB3139C4A9 /* PNLegacyCryptorTest.m in Sources */, + 465210D19F96C8F1B6F3719E /* PNEncryptedDataTest.m in Sources */, + CD2CBFFF17F44B2900025694 /* PNAESCBCCryptorTest.m in Sources */, + B1BC20E88E54228CACBDD2DA /* PNCryptoModuleTest.m in Sources */, + A0C29915B07D27CA71DFC53A /* PNConsoleLoggerTest.m in Sources */, + BC88C073FAB54B116644A596 /* PNLogEntryTest.m in Sources */, + 81C3DFA3C1EEB56AC8BFAE0B /* PNFileLoggerTest.m in Sources */, + E4A9EBBAD8EEFC9C8F8DB1E7 /* PNLoggerManagerTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1358,6 +2340,7 @@ /* Begin XCBuildConfiguration section */ 79657A8D2719CD5B00BACEC5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 6B67CB35405BA8A5D64355F1 /* Pods-ContractTests-[iOS] Contract Tests Beta.debug.xcconfig */; buildSettings = { CLANG_ENABLE_CODE_COVERAGE = YES; CODE_SIGN_IDENTITY = "-"; @@ -1388,6 +2371,7 @@ }; 79657A8E2719CD5B00BACEC5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = DD0FCB2DA90E435F25B87C6C /* Pods-ContractTests-[iOS] Contract Tests Beta.release.xcconfig */; buildSettings = { CLANG_ENABLE_CODE_COVERAGE = YES; CODE_SIGN_IDENTITY = "-"; @@ -1413,6 +2397,7 @@ }; 796F6308270344DF00DE6F07 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = C478061E7C0E54A479A59738 /* Pods-ContractTests-[iOS] Contract Tests.debug.xcconfig */; buildSettings = { CLANG_ENABLE_CODE_COVERAGE = YES; CODE_SIGN_IDENTITY = "-"; @@ -1438,6 +2423,7 @@ }; 796F6309270344DF00DE6F07 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 41E0773DB321A7C37C16CE47 /* Pods-ContractTests-[iOS] Contract Tests.release.xcconfig */; buildSettings = { CLANG_ENABLE_CODE_COVERAGE = YES; CODE_SIGN_IDENTITY = "-"; @@ -1511,6 +2497,7 @@ }; A52926E523B0E6C500FF46DD /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = BB1BF4F52A6788409FCABA1A /* Pods-MockableTests-[tvOS] Mocked Integration Tests.debug.xcconfig */; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; @@ -1537,6 +2524,7 @@ }; A52926E623B0E6C500FF46DD /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 4BC7B27C62C862E41744AC12 /* Pods-MockableTests-[tvOS] Mocked Integration Tests.release.xcconfig */; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; @@ -1564,6 +2552,7 @@ }; A52926F123B0E6DD00FF46DD /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 828EFFC330107C0A76C78310 /* Pods-MockableTests-[tvOS] Unit Tests.debug.xcconfig */; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; @@ -1590,6 +2579,7 @@ }; A52926F223B0E6DD00FF46DD /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = E743CC9BF8671E22FF0B6BDE /* Pods-MockableTests-[tvOS] Unit Tests.release.xcconfig */; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; @@ -1617,6 +2607,7 @@ }; A52926FD23B0E71700FF46DD /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 911C9D8D1CDF9A6875A4E22F /* Pods-MockableTests-[macOS] Mocked Integration Tests.debug.xcconfig */; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; @@ -1642,6 +2633,7 @@ }; A52926FE23B0E71700FF46DD /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 37E54F107BD09641FA12F7D0 /* Pods-MockableTests-[macOS] Mocked Integration Tests.release.xcconfig */; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; @@ -1668,6 +2660,7 @@ }; A529270923B0E72500FF46DD /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = D4FE56602E20124B56F1266C /* Pods-MockableTests-[macOS] Unit Tests.debug.xcconfig */; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; @@ -1693,6 +2686,7 @@ }; A529270A23B0E72500FF46DD /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = C4C69FE3D81194BB29FC5A04 /* Pods-MockableTests-[macOS] Unit Tests.release.xcconfig */; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; @@ -1843,6 +2837,7 @@ }; A5B65D7C23B03DB4006B7BFB /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 13FDDE6376033A34187C1FC1 /* Pods-MockableTests-[iOS] Unit Tests.debug.xcconfig */; buildSettings = { CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Manual; @@ -1867,6 +2862,7 @@ }; A5B65D7D23B03DB4006B7BFB /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 8F86174540901D21E718D8F1 /* Pods-MockableTests-[iOS] Unit Tests.release.xcconfig */; buildSettings = { CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Manual; @@ -1891,6 +2887,7 @@ }; A5B65D8823B03FFD006B7BFB /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = C0CCA7BD2B4C6CD160949FF7 /* Pods-MockableTests-[iOS] Mocked Integration Tests.debug.xcconfig */; buildSettings = { CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Manual; @@ -1916,6 +2913,7 @@ }; A5B65D8923B03FFD006B7BFB /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 33BEB83945085F2AEBB08569 /* Pods-MockableTests-[iOS] Mocked Integration Tests.release.xcconfig */; buildSettings = { CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Manual; @@ -1941,6 +2939,7 @@ }; A5B65D9523B04148006B7BFB /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 5F7B03D6706F383B4D1016C8 /* Pods-[iOS] Integration Tests.debug.xcconfig */; buildSettings = { CLANG_ENABLE_CODE_COVERAGE = YES; CODE_SIGN_IDENTITY = "-"; @@ -1966,6 +2965,7 @@ }; A5B65D9623B04148006B7BFB /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 01FD138F1D9143FB281121EC /* Pods-[iOS] Integration Tests.release.xcconfig */; buildSettings = { CLANG_ENABLE_CODE_COVERAGE = YES; CODE_SIGN_IDENTITY = "-"; diff --git a/Tests/Support Files/Scripts/tests-runner.sh b/Tests/Support Files/Scripts/tests-runner.sh index 779769b6a..390b4faba 100755 --- a/Tests/Support Files/Scripts/tests-runner.sh +++ b/Tests/Support Files/Scripts/tests-runner.sh @@ -26,6 +26,7 @@ TEST_SCHEME_TYPE="Mocked Integration Tests" [[ $2 == coverage ]] && TEST_SCHEME_TYPE="Code Coverage" [[ $2 == contract ]] && TEST_SCHEME_TYPE="Contract Tests" [[ $2 == contract-beta ]] && TEST_SCHEME_TYPE="Contract Tests Beta" +[[ $2 == unit ]] && TEST_SCHEME_TYPE="Unit Tests" # Maximum number of tests which should run for same device type (various versions). [[ -n $3 ]] && MAXIMUM_DESTINATIONS="$3" || MAXIMUM_DESTINATIONS=3 @@ -103,7 +104,7 @@ for destinationPlatformIdx in "${!DESTINATIONS[@]}"; do -scheme "[$PLATFORM] $TEST_SCHEME_TYPE" \ -destination "$DESTINATION_PLATFORM" \ -parallel-testing-enabled NO \ - test | xcpretty --simple && XCODE_BUILD_EXITCODE="${PIPESTATUS[0]}" + test 2>&1 | if command -v xcpretty &>/dev/null; then xcpretty --simple; else cat; fi && XCODE_BUILD_EXITCODE="${PIPESTATUS[0]}" if [[ $2 == contract || $2 == contract-beta ]]; then REPORT_FILENAME="$CUCUMBER_REPORTS_PATH/CucumberishTestResults-[$PLATFORM] $TEST_SCHEME_TYPE.json" diff --git a/Tests/Tests/Helpers/PNRecordableTestCase.h b/Tests/Tests/Helpers/PNRecordableTestCase.h index 5ba4b42d6..38a378e20 100644 --- a/Tests/Tests/Helpers/PNRecordableTestCase.h +++ b/Tests/Tests/Helpers/PNRecordableTestCase.h @@ -106,6 +106,14 @@ typedef void (^PNTClientDidReceiveStatusHandler)(PubNub *client, PNSubscribeStat */ @property (nonatomic, readonly, nullable, strong) PubNub *client; +/** + * @brief Whether the current test suite is a mocked integration test suite or not. + * + * @discussion Uses the test bundle identifier to determine if the suite is running as part of + * the mocked integration test target. + */ +@property (nonatomic, readonly, getter=isMockedIntegrationTestSuite) BOOL mockedIntegrationTestSuite; + /** * @brief Whether current test case uses mocked objects or not. * diff --git a/Tests/Tests/Helpers/PNRecordableTestCase.m b/Tests/Tests/Helpers/PNRecordableTestCase.m index cff63e24a..1a78a7851 100644 --- a/Tests/Tests/Helpers/PNRecordableTestCase.m +++ b/Tests/Tests/Helpers/PNRecordableTestCase.m @@ -478,6 +478,16 @@ - (PNConfiguration *)configurationForTestCaseWithName:(NSString *)name { #pragma mark - VCR configuration +- (BOOL)isMockedIntegrationTestSuite { + static BOOL isMocked; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *bundleIdentifier = [NSBundle bundleForClass:[self class]].bundleIdentifier; + isMocked = [bundleIdentifier pnt_includesString:@"mocked-integration"]; + }); + return isMocked; +} + - (BOOL)shouldSetupVCR { NSString *bundleIdentifier = [NSBundle bundleForClass:[self class]].bundleIdentifier; BOOL shouldSetupVCR = [bundleIdentifier rangeOfString:@"mocked-integration"].location != NSNotFound; @@ -2204,16 +2214,9 @@ - (XCTestExpectation *)waitTask:(NSString *)taskName completionFor:(NSTimeInterv #pragma mark - Helpers - (BOOL)shouldSkipTestWithManuallyModifiedMockedResponse { - static BOOL _isMockedIntegration; - static dispatch_once_t onceToken; BOOL shouldSkip = NO; - - dispatch_once(&onceToken, ^{ - NSString *bundleIdentifier = [NSBundle bundleForClass:[self class]].bundleIdentifier; - _isMockedIntegration = [bundleIdentifier pnt_includesString:@"mocked-integration"]; - }); - - if (_isMockedIntegration) { + + if (self.isMockedIntegrationTestSuite) { if (YHVVCR.cassette.isNewCassette) { NSString *logSeparator1 = @"\n------------\n\n\n"; NSString *logSeparator2 = @"\n\n\n------------"; diff --git a/Tests/Tests/Integration/PNChannelGroupIntegrationTests.m b/Tests/Tests/Integration/PNChannelGroupIntegrationTests.m index c696e246f..57a536367 100644 --- a/Tests/Tests/Integration/PNChannelGroupIntegrationTests.m +++ b/Tests/Tests/Integration/PNChannelGroupIntegrationTests.m @@ -42,16 +42,17 @@ @implementation PNChannelGroupIntegrationTests - (BOOL)shouldSetupVCR { BOOL shouldSetupVCR = [super shouldSetupVCR]; - + if (!shouldSetupVCR && !self.isMockedIntegrationTestSuite) return NO; + if (!shouldSetupVCR) { NSArray *testNames = @[ @"ShouldNotRemoveChannelsFromGroupAndReceiveBadRequestStatusWhenChannelGroupIsNil", @"ShouldNotRemoveAllChannelsFromChannelGroupAndReceiveBadRequestStatusWhenChannelGroupIsNil" ]; - + shouldSetupVCR = [self.name pnt_includesAnyString:testNames]; } - + return shouldSetupVCR; } diff --git a/Tests/Tests/Integration/PNHistoryIntegrationTests.m b/Tests/Tests/Integration/PNHistoryIntegrationTests.m index e9ae8c182..ff79d0d48 100644 --- a/Tests/Tests/Integration/PNHistoryIntegrationTests.m +++ b/Tests/Tests/Integration/PNHistoryIntegrationTests.m @@ -42,18 +42,19 @@ @implementation PNHistoryIntegrationTests - (BOOL)shouldSetupVCR { BOOL shouldSetupVCR = [super shouldSetupVCR]; - + if (!shouldSetupVCR && !self.isMockedIntegrationTestSuite) return NO; + if (!shouldSetupVCR) { NSArray *testNames = @[ @"ShouldNotDeleteMessagesForChannelAndReceiveBadRequestStatusWhenChannelIsNil", @"ItShouldFetchHistoryForChannelWithEncryptedMessagesAndDecrypt" ]; - + shouldSetupVCR = [self.name pnt_includesAnyString:testNames]; } else if ([self.name rangeOfString:@"RandomIV"].location != NSNotFound) { shouldSetupVCR = NO; } - + return shouldSetupVCR; } diff --git a/Tests/Tests/Integration/PNPublishIntegrationTests.m b/Tests/Tests/Integration/PNPublishIntegrationTests.m index 0ea0b6ec3..e220bcfe7 100644 --- a/Tests/Tests/Integration/PNPublishIntegrationTests.m +++ b/Tests/Tests/Integration/PNPublishIntegrationTests.m @@ -40,11 +40,12 @@ @implementation PNPublishIntegrationTests - (BOOL)shouldSetupVCR { BOOL shouldSetupVCR = [super shouldSetupVCR]; - + if (!shouldSetupVCR && !self.isMockedIntegrationTestSuite) return NO; + if ([self.name pnt_includesString:@"Size"] || [self.name pnt_includesString:@"RandomIV"]) { shouldSetupVCR = NO; } - + return shouldSetupVCR; } diff --git a/Tests/Tests/Integration/PNPushNotificationsIntegrationTests.m b/Tests/Tests/Integration/PNPushNotificationsIntegrationTests.m index dc001e159..99824755b 100644 --- a/Tests/Tests/Integration/PNPushNotificationsIntegrationTests.m +++ b/Tests/Tests/Integration/PNPushNotificationsIntegrationTests.m @@ -85,7 +85,8 @@ @implementation PNPushNotificationsIntegrationTests - (BOOL)shouldSetupVCR { BOOL shouldSetupVCR = [super shouldSetupVCR]; - + if (!shouldSetupVCR && !self.isMockedIntegrationTestSuite) return NO; + if (!shouldSetupVCR) { NSArray *testNames = @[ @"ShouldNotAddPushNotificationsAndReceiveBadRequestStatusWhenChannelsIsNil", @@ -93,10 +94,10 @@ - (BOOL)shouldSetupVCR { @"ShouldNotRemovePushNotificationsAndReceiveBadRequestStatusWhenChannelsIsNil", @"ShouldNotRemovePushNotificationsUsingV2APIAndReceiveBadRequestStatusWhenChannelsIsNil" ]; - + shouldSetupVCR = [self.name pnt_includesAnyString:testNames]; } - + return shouldSetupVCR; } diff --git a/Tests/Tests/Integration/PNSubscribeIntegrationTest.m b/Tests/Tests/Integration/PNSubscribeIntegrationTest.m index 6e84be61c..a65794127 100644 --- a/Tests/Tests/Integration/PNSubscribeIntegrationTest.m +++ b/Tests/Tests/Integration/PNSubscribeIntegrationTest.m @@ -36,11 +36,12 @@ @implementation PNSubscribeIntegrationTest - (BOOL)shouldSetupVCR { BOOL shouldSetupVCR = [super shouldSetupVCR]; - + if (!shouldSetupVCR && !self.isMockedIntegrationTestSuite) return NO; + if ([self.name pnt_includesString:@"RandomIV"]) { shouldSetupVCR = NO; } - + return shouldSetupVCR; } diff --git a/Tests/Tests/Unit/Concurrency/PNLockTest.m b/Tests/Tests/Unit/Concurrency/PNLockTest.m new file mode 100644 index 000000000..e0f07cbda --- /dev/null +++ b/Tests/Tests/Unit/Concurrency/PNLockTest.m @@ -0,0 +1,497 @@ +/** + * @brief Thread safety tests for PNLock (GCD-based MRSW lock) and PNLockSupport (pthread mutex helpers). + * + * These tests verify that the locking primitives work correctly under contention, do not deadlock, + * and properly enforce mutual exclusion and reader-writer semantics. + * + * @author PubNub Tests + * @copyright 2010-2026 PubNub, Inc. + */ +#import +#import +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNLockTest : XCTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNLockTest + + +#pragma mark - Tests :: PNLock :: Basic read/write + +- (void)testPNLockBasicSyncRead { + PNLock *lock = [PNLock lockWithIsolationQueueName:@"basic-read" + subsystemQueueIdentifier:@"com.pubnub.test.lock"]; + __block BOOL blockExecuted = NO; + + [lock syncReadAccessWithBlock:^{ + blockExecuted = YES; + }]; + + XCTAssertTrue(blockExecuted, @"Synchronous read block should execute."); +} + +- (void)testPNLockBasicSyncWrite { + PNLock *lock = [PNLock lockWithIsolationQueueName:@"basic-write" + subsystemQueueIdentifier:@"com.pubnub.test.lock"]; + __block BOOL blockExecuted = NO; + + [lock syncWriteAccessWithBlock:^{ + blockExecuted = YES; + }]; + + XCTAssertTrue(blockExecuted, @"Synchronous write block should execute."); +} + +- (void)testPNLockBasicAsyncRead { + PNLock *lock = [PNLock lockWithIsolationQueueName:@"async-read" + subsystemQueueIdentifier:@"com.pubnub.test.lock"]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Async read completes"]; + + [lock asyncReadAccessWithBlock:^{ + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testPNLockBasicAsyncWrite { + PNLock *lock = [PNLock lockWithIsolationQueueName:@"async-write" + subsystemQueueIdentifier:@"com.pubnub.test.lock"]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Async write completes"]; + + [lock asyncWriteAccessWithBlock:^{ + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + +- (void)testPNLockShorthandReadUsesSync { + PNLock *lock = [PNLock lockWithIsolationQueueName:@"shorthand-read" + subsystemQueueIdentifier:@"com.pubnub.test.lock"]; + __block BOOL executed = NO; + + [lock readAccessWithBlock:^{ + executed = YES; + }]; + + // readAccessWithBlock is synchronous, so it should have completed by now. + XCTAssertTrue(executed, @"readAccessWithBlock should be synchronous."); +} + +- (void)testPNLockShorthandWriteUsesAsync { + PNLock *lock = [PNLock lockWithIsolationQueueName:@"shorthand-write" + subsystemQueueIdentifier:@"com.pubnub.test.lock"]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Shorthand write completes"]; + + [lock writeAccessWithBlock:^{ + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + + +#pragma mark - Tests :: PNLock :: Contention + +- (void)testPNLockMultipleThreadsCompeteForWriteAccess { + PNLock *lock = [PNLock lockWithIsolationQueueName:@"contention-write" + subsystemQueueIdentifier:@"com.pubnub.test.lock.contention"]; + __block _Atomic(int32_t) counter = 0; + int iterations = 200; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [lock syncWriteAccessWithBlock:^{ + atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed); + }]; + dispatch_group_leave(group); + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"All write operations should complete without deadlock."); + XCTAssertEqual(counter, iterations, @"All %d write blocks should have executed.", iterations); +} + +- (void)testPNLockMultipleThreadsCompeteForReadAccess { + PNLock *lock = [PNLock lockWithIsolationQueueName:@"contention-read" + subsystemQueueIdentifier:@"com.pubnub.test.lock.contention"]; + __block _Atomic(int32_t) counter = 0; + int iterations = 200; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [lock syncReadAccessWithBlock:^{ + atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed); + }]; + dispatch_group_leave(group); + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"All read operations should complete without deadlock."); + XCTAssertEqual(counter, iterations, @"All %d read blocks should have executed.", iterations); +} + + +#pragma mark - Tests :: PNLock :: Read-Write pattern + +- (void)testPNLockConcurrentReadsWithExclusiveWrites { + PNLock *lock = [PNLock lockWithIsolationQueueName:@"rw-pattern" + subsystemQueueIdentifier:@"com.pubnub.test.lock.rw"]; + __block NSMutableArray *sharedArray = [NSMutableArray new]; + int readIterations = 100; + int writeIterations = 50; + dispatch_group_t group = dispatch_group_create(); + + // Launch concurrent reads. + for (int i = 0; i < readIterations; i++) { + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [lock syncReadAccessWithBlock:^{ + // Just read the count; should never see a partially modified array. + __unused NSUInteger count = sharedArray.count; + }]; + dispatch_group_leave(group); + }); + } + + // Launch exclusive writes. + for (int i = 0; i < writeIterations; i++) { + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [lock syncWriteAccessWithBlock:^{ + [sharedArray addObject:@(i)]; + }]; + dispatch_group_leave(group); + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"All operations should complete without deadlock."); + + // Read the final state under the lock. + __block NSUInteger finalCount; + [lock syncReadAccessWithBlock:^{ + finalCount = sharedArray.count; + }]; + XCTAssertEqual(finalCount, (NSUInteger)writeIterations, @"All writes should have been applied."); +} + +- (void)testPNLockInterleavedReadsAndWrites { + PNLock *lock = [PNLock lockWithIsolationQueueName:@"interleaved" + subsystemQueueIdentifier:@"com.pubnub.test.lock.interleaved"]; + __block NSInteger sharedValue = 0; + int iterations = 200; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if (i % 3 == 0) { + [lock syncWriteAccessWithBlock:^{ + sharedValue++; + }]; + } else { + [lock syncReadAccessWithBlock:^{ + __unused NSInteger val = sharedValue; + }]; + } + dispatch_group_leave(group); + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"Interleaved reads and writes should complete without deadlock."); +} + + +#pragma mark - Tests :: PNLock :: Deadlock detection via timeout + +- (void)testPNLockDoesNotDeadlockUnderContention { + PNLock *lock = [PNLock lockWithIsolationQueueName:@"deadlock-test" + subsystemQueueIdentifier:@"com.pubnub.test.lock.deadlock"]; + XCTestExpectation *expectation = [self expectationWithDescription:@"No deadlock under contention"]; + int iterations = 100; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if (i % 2 == 0) { + [lock syncWriteAccessWithBlock:^{ + [NSThread sleepForTimeInterval:0.001]; + }]; + } else { + [lock syncReadAccessWithBlock:^{ + [NSThread sleepForTimeInterval:0.001]; + }]; + } + dispatch_group_leave(group); + }); + } + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + + +#pragma mark - Tests :: PNLock :: Multiple lock instances + +- (void)testMultiplePNLockInstancesDoNotInterfere { + PNLock *lockA = [PNLock lockWithIsolationQueueName:@"lockA" + subsystemQueueIdentifier:@"com.pubnub.test.lock.multi"]; + PNLock *lockB = [PNLock lockWithIsolationQueueName:@"lockB" + subsystemQueueIdentifier:@"com.pubnub.test.lock.multi"]; + __block _Atomic(int32_t) counterA = 0; + __block _Atomic(int32_t) counterB = 0; + int iterations = 100; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [lockA syncWriteAccessWithBlock:^{ + atomic_fetch_add_explicit(&counterA, 1, memory_order_relaxed); + }]; + dispatch_group_leave(group); + }); + + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [lockB syncWriteAccessWithBlock:^{ + atomic_fetch_add_explicit(&counterB, 1, memory_order_relaxed); + }]; + dispatch_group_leave(group); + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"All operations should complete."); + XCTAssertEqual(counterA, iterations, @"Lock A operations should all complete."); + XCTAssertEqual(counterB, iterations, @"Lock B operations should all complete."); +} + + +#pragma mark - Tests :: PNLockSupport :: Basic lock/unlock + +- (void)testPNLockSupportBasicLockUnlock { + pthread_mutex_t mutex; + pthread_mutex_init(&mutex, NULL); + __block BOOL executed = NO; + + pn_lock(&mutex, ^{ + executed = YES; + }); + + XCTAssertTrue(executed, @"Block within pn_lock should execute."); + pthread_mutex_destroy(&mutex); +} + +- (void)testPNLockSupportTryLock { + pthread_mutex_t mutex; + pthread_mutex_init(&mutex, NULL); + __block BOOL executed = NO; + + pn_trylock(&mutex, ^{ + executed = YES; + }); + + XCTAssertTrue(executed, @"Block within pn_trylock should execute when lock is available."); + pthread_mutex_destroy(&mutex); +} + + +#pragma mark - Tests :: PNLockSupport :: Contention + +- (void)testPNLockSupportContention { + __block pthread_mutex_t mutex; + pthread_mutex_init(&mutex, NULL); + __block _Atomic(int32_t) counter = 0; + int iterations = 200; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + pn_lock(&mutex, ^{ + atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed); + }); + dispatch_group_leave(group); + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"All pn_lock operations should complete."); + XCTAssertEqual(counter, iterations, @"All %d blocks should have executed under pn_lock.", iterations); + pthread_mutex_destroy(&mutex); +} + +- (void)testPNLockSupportTryLockContention { + __block pthread_mutex_t mutex; + pthread_mutex_init(&mutex, NULL); + __block _Atomic(int32_t) counter = 0; + int iterations = 200; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // trylock may skip the block if lock is held, but should not crash. + pn_trylock(&mutex, ^{ + atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed); + }); + dispatch_group_leave(group); + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"All pn_trylock operations should complete (even if some skipped)."); + // counter may be less than iterations because trylock can fail to acquire. + XCTAssertGreaterThan(counter, 0, @"At least some trylock operations should succeed."); + pthread_mutex_destroy(&mutex); +} + + +#pragma mark - Tests :: PNLockSupport :: Async lock + +- (void)testPNLockSupportAsyncLock { + pthread_mutex_t mutex; + pthread_mutex_init(&mutex, NULL); + XCTestExpectation *expectation = [self expectationWithDescription:@"Async lock completes"]; + + pn_lock_async(&mutex, ^(dispatch_block_t complete) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // Simulate async work. + [NSThread sleepForTimeInterval:0.01]; + complete(); + [expectation fulfill]; + }); + }); + + [self waitForExpectationsWithTimeout:5 handler:nil]; + pthread_mutex_destroy(&mutex); +} + +- (void)testPNLockSupportAsyncLockContention { + __block pthread_mutex_t mutex; + pthread_mutex_init(&mutex, NULL); + __block _Atomic(int32_t) counter = 0; + int iterations = 50; + XCTestExpectation *expectation = [self expectationWithDescription:@"All async lock operations complete"]; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + pn_lock_async(&mutex, ^(dispatch_block_t complete) { + atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed); + complete(); + dispatch_group_leave(group); + }); + }); + } + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:30 handler:nil]; + XCTAssertEqual(counter, iterations, @"All async lock blocks should execute."); + pthread_mutex_destroy(&mutex); +} + + +#pragma mark - Tests :: PNLockSupport :: Deadlock detection + +- (void)testPNLockSupportDoesNotDeadlockUnderContention { + __block pthread_mutex_t mutex; + pthread_mutex_init(&mutex, NULL); + XCTestExpectation *expectation = [self expectationWithDescription:@"No deadlock with pn_lock"]; + int iterations = 100; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + pn_lock(&mutex, ^{ + [NSThread sleepForTimeInterval:0.001]; + }); + dispatch_group_leave(group); + }); + } + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:30 handler:nil]; + pthread_mutex_destroy(&mutex); +} + + +#pragma mark - Tests :: PNLockSupport :: Data integrity under mutex + +- (void)testPNLockSupportProtectsSharedDataIntegrity { + __block pthread_mutex_t mutex; + pthread_mutex_init(&mutex, NULL); + NSMutableArray *sharedArray = [NSMutableArray new]; + __block _Atomic(int32_t) counter = 0; + int iterations = 100; + XCTestExpectation *expectation = [self expectationWithDescription:@"All lock operations complete"]; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + pn_lock(&mutex, ^{ + [sharedArray addObject:@(counter)]; + atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed); + }); + dispatch_group_leave(group); + }); + } + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:30 handler:nil]; + XCTAssertEqual(counter, iterations, @"Counter should reach %d.", iterations); + XCTAssertEqual(sharedArray.count, (NSUInteger)iterations, + @"Array should contain exactly %d elements without corruption.", iterations); + pthread_mutex_destroy(&mutex); +} + +#pragma mark - + + +@end diff --git a/Tests/Tests/Unit/Concurrency/PNThreadSafetyTest.m b/Tests/Tests/Unit/Concurrency/PNThreadSafetyTest.m new file mode 100644 index 000000000..97d4dff19 --- /dev/null +++ b/Tests/Tests/Unit/Concurrency/PNThreadSafetyTest.m @@ -0,0 +1,658 @@ +/** + * @brief Thread safety tests for the PubNub client. + * + * These tests verify that concurrent access to the PubNub client does not cause crashes, + * deadlocks, or data corruption. They exercise listener management, channel list access, + * configuration copying, publish, subscribe/unsubscribe, callback queue verification, and + * concurrent property access. + * + * @author PubNub Tests + * @copyright 2010-2026 PubNub, Inc. + */ +#import "PNRecordableTestCase.h" +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Types and structures + +/// Simple listener object that conforms to PNEventsListener. +/// +/// Used by thread-safety tests to add/remove listeners concurrently. +@interface PNTestListener : NSObject + +/// Track whether any callback was received. +@property (nonatomic, assign) BOOL didReceiveStatus; + +@end + +@implementation PNTestListener + +- (void)client:(PubNub *)client didReceiveStatus:(PNStatus *)status { + self.didReceiveStatus = YES; +} + +@end + + +#pragma mark - Interface declaration + +@interface PNThreadSafetyTest : PNRecordableTestCase + + +#pragma mark - Properties + +/// All PubNub clients created during the current test. +/// +/// Tracked so ``tearDown`` can ensure proper cleanup. Without this, residual async operations from +/// previous tests' clients accumulate on the shared ``PNLock`` subsystem queues +/// (`com.pubnub.subscriber`, `com.pubnub.transport`, `com.pubnub.serializer`) and eventually exhaust +/// the GCD thread pool — causing `dispatch_sync` callers to stall (deadlock via thread starvation). +@property (nonatomic, strong) NSMutableArray *testClients; + +/// Lock that protects concurrent access to ``testClients`` array. +@property (nonatomic, strong) NSLock *testClientsLock; + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNThreadSafetyTest + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + + +#pragma mark - Setup / Teardown + +- (void)setUp { + [super setUp]; + + self.testClients = [NSMutableArray new]; + self.testClientsLock = [NSLock new]; +} + +- (void)tearDown { + // Unsubscribe and remove self-listeners for every tracked client so that in-flight subscribe + // cycles and network requests are cancelled as quickly as possible. + [self.testClientsLock lock]; + NSArray *clients = [self.testClients copy]; + [self.testClients removeAllObjects]; + [self.testClientsLock unlock]; + + // Use @autoreleasepool to ensure clients are deallocated immediately after cleanup. + // Client dealloc calls -[PNURLSessionTransport invalidate] which cancels all pending HTTP + // requests via -[NSURLSession invalidateAndCancel]. Releasing references BEFORE the drain + // wait ensures the invalidation actually happens during that window — otherwise pending + // requests from earlier tests accumulate on the shared PNLock subsystem target queues + // (com.pubnub.transport, com.pubnub.subscriber, etc.) and can exhaust the GCD thread pool. + @autoreleasepool { + for (PubNub *client in clients) { + [client unsubscribeFromAll]; + [client removeListener:client]; + } + + clients = nil; + } + + // Wait for GCD to process dealloc-triggered network invalidation and drain remaining async + // work (dispatch_barrier_async for transport invalidation, subscribe cycle teardown, etc.). + [self waitTask:@"asyncOperationsDrain" completionFor:0.5]; + + [super tearDown]; +} + + +#pragma mark - VCR configuration + +- (BOOL)shouldSetupVCR { + return NO; +} + + +#pragma mark - Helpers + +/// Create a standalone PubNub client for testing. +/// +/// Creates a client with "demo" keys on a background callback queue, suitable for concurrency +/// testing without any VCR/network dependency. +- (PubNub *)createTestClient { + PNConfiguration *configuration = [PNConfiguration configurationWithPublishKey:@"demo" + subscribeKey:@"demo" + userID:[[NSUUID UUID] UUIDString]]; + dispatch_queue_t callbackQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + PubNub *client = [PubNub clientWithConfiguration:configuration callbackQueue:callbackQueue]; + [self trackClient:client]; + return client; +} + +/// Keep a strong reference so ``tearDown`` can clean the client up. +- (void)trackClient:(PubNub *)client { + [self.testClientsLock lock]; + [self.testClients addObject:client]; + [self.testClientsLock unlock]; +} + + +#pragma mark - Tests :: Concurrent listener management + +- (void)testConcurrentAddAndRemoveListeners { + PubNub *client = [self createTestClient]; + int iterations = 200; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + PNTestListener *listener = [PNTestListener new]; + [client addListener:listener]; + [client removeListener:listener]; + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"Concurrent listener add/remove should complete without deadlock."); +} + +- (void)testConcurrentAddListenersFromMultipleThreads { + PubNub *client = [self createTestClient]; + int iterations = 100; + dispatch_group_t group = dispatch_group_create(); + NSMutableArray *listeners = [NSMutableArray new]; + NSLock *arrayLock = [NSLock new]; + + for (int i = 0; i < iterations; i++) { + PNTestListener *listener = [PNTestListener new]; + [arrayLock lock]; + [listeners addObject:listener]; + [arrayLock unlock]; + + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [client addListener:listener]; + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"Concurrent addListener should complete without deadlock."); + + // Clean up: remove all listeners. + for (PNTestListener *listener in listeners) { + [client removeListener:listener]; + } +} + +- (void)testConcurrentAddRemoveWithInterleavedOperations { + PubNub *client = [self createTestClient]; + int iterations = 150; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + PNTestListener *listener = [PNTestListener new]; + [client addListener:listener]; + + // Interleave with a property read. + __unused NSArray *channels = [client channels]; + + [client removeListener:listener]; + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"Interleaved listener and channel access should not deadlock."); +} + + +#pragma mark - Tests :: Concurrent channel list access + +- (void)testConcurrentChannelAndChannelGroupAccess { + PubNub *client = [self createTestClient]; + int iterations = 200; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + __unused NSArray *channels = [client channels]; + __unused NSArray *groups = [client channelGroups]; + __unused NSArray *presenceChannels = [client presenceChannels]; + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"Concurrent channel list reads should complete without crash."); +} + +- (void)testConcurrentChannelReadWithSubscribeUnsubscribe { + PubNub *client = [self createTestClient]; + int iterations = 20; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSString *channel = [NSString stringWithFormat:@"test-channel-%d", i]; + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:@[channel] + channelGroups:nil]; + [client subscribeWithRequest:request]; + }); + + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + __unused NSArray *channels = [client channels]; + __unused NSArray *groups = [client channelGroups]; + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"Concurrent subscribe + channel read should not deadlock or crash."); + + [client unsubscribeFromAll]; +} + + +#pragma mark - Tests :: Concurrent configuration copy + +- (void)testConcurrentCopyWithConfiguration { + PubNub *client = [self createTestClient]; + int iterations = 50; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + PNConfiguration *config = [client currentConfiguration]; + config.userID = [NSString stringWithFormat:@"user-%d-%@", i, [[NSUUID UUID] UUIDString]]; + + [client copyWithConfiguration:config completion:^(PubNub *newClient) { + XCTAssertNotNil(newClient, @"Copied client should not be nil."); + [self trackClient:newClient]; + dispatch_group_leave(group); + }]; + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"Concurrent copyWithConfiguration should complete without deadlock."); +} + +- (void)testConcurrentCopyWithConfigurationAndCallbackQueue { + PubNub *client = [self createTestClient]; + int iterations = 30; + dispatch_group_t group = dispatch_group_create(); + dispatch_queue_t customQueue = dispatch_queue_create("com.pubnub.test.custom-callback", DISPATCH_QUEUE_SERIAL); + + for (int i = 0; i < iterations; i++) { + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + PNConfiguration *config = [client currentConfiguration]; + config.userID = [NSString stringWithFormat:@"user-%d-%@", i, [[NSUUID UUID] UUIDString]]; + + [client copyWithConfiguration:config callbackQueue:customQueue completion:^(PubNub *newClient) { + XCTAssertNotNil(newClient, @"Copied client should not be nil."); + [self trackClient:newClient]; + dispatch_group_leave(group); + }]; + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"Concurrent copyWithConfiguration with custom queue should complete."); +} + + +#pragma mark - Tests :: Concurrent publish + +- (void)testConcurrentPublishFromMultipleThreads { + PubNub *client = [self createTestClient]; + int iterations = 20; + dispatch_group_t group = dispatch_group_create(); + __block _Atomic(int32_t) completionCount = 0; + + for (int i = 0; i < iterations; i++) { + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@"test-channel"]; + request.message = @{@"index": @(i), @"text": @"concurrent test"}; + + [client publishWithRequest:request completion:^(PNPublishStatus *status) { + // We expect errors since "demo" keys may not support publish, but no crashes. + atomic_fetch_add_explicit(&completionCount, 1, memory_order_relaxed); + dispatch_group_leave(group); + }]; + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"All concurrent publishes should complete without deadlock."); + XCTAssertEqual(completionCount, iterations, @"All publish completion blocks should fire."); +} + +- (void)testConcurrentPublishToMultipleChannels { + PubNub *client = [self createTestClient]; + int iterations = 20; + dispatch_group_t group = dispatch_group_create(); + __block _Atomic(int32_t) completionCount = 0; + + for (int i = 0; i < iterations; i++) { + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSString *channel = [NSString stringWithFormat:@"channel-%d", i]; + PNPublishRequest *request = [PNPublishRequest requestWithChannel:channel]; + request.message = @{@"data": @"test"}; + + [client publishWithRequest:request completion:^(PNPublishStatus *status) { + atomic_fetch_add_explicit(&completionCount, 1, memory_order_relaxed); + dispatch_group_leave(group); + }]; + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"All publishes to different channels should complete."); + XCTAssertEqual(completionCount, iterations, @"All completion blocks should fire."); +} + + +#pragma mark - Tests :: Concurrent subscribe/unsubscribe + +- (void)testConcurrentSubscribeAndUnsubscribe { + PubNub *client = [self createTestClient]; + int iterations = 20; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSString *channel = [NSString stringWithFormat:@"sub-channel-%d", i % 20]; + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:@[channel] + channelGroups:nil]; + [client subscribeWithRequest:request]; + }); + + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSString *channel = [NSString stringWithFormat:@"sub-channel-%d", i % 20]; + PNPresenceLeaveRequest *request = [PNPresenceLeaveRequest requestWithChannels:@[channel] + channelGroups:nil]; + [client unsubscribeWithRequest:request]; + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"Concurrent subscribe/unsubscribe should not deadlock or crash."); + + [client unsubscribeFromAll]; +} + +- (void)testConcurrentSubscribeToChannelsAndGroups { + PubNub *client = [self createTestClient]; + int iterations = 20; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSString *channel = [NSString stringWithFormat:@"channel-%d", i]; + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:@[channel] + channelGroups:nil]; + [client subscribeWithRequest:request]; + }); + + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSString *channelGroup = [NSString stringWithFormat:@"group-%d", i]; + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:nil + channelGroups:@[channelGroup]]; + [client subscribeWithRequest:request]; + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"Concurrent channel + group subscribes should complete."); + + [client unsubscribeFromAll]; +} + +- (void)testConcurrentUnsubscribeFromAll { + PubNub *client = [self createTestClient]; + int iterations = 20; + dispatch_group_t group = dispatch_group_create(); + + // First subscribe to some channels. + for (int i = 0; i < 10; i++) { + NSString *channel = [NSString stringWithFormat:@"unsub-channel-%d", i]; + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:@[channel] channelGroups:nil]; + [client subscribeWithRequest:request]; + } + + // Concurrently unsubscribe from all. + for (int i = 0; i < iterations; i++) { + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [client unsubscribeFromAll]; + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"Concurrent unsubscribeFromAll should not deadlock."); +} + + +#pragma mark - Tests :: Listener callback thread verification + +- (void)testCallbackArrivesOnSpecifiedQueue { + dispatch_queue_t callbackQueue = dispatch_queue_create("com.pubnub.test.callback-verify", + DISPATCH_QUEUE_SERIAL); + dispatch_queue_set_specific(callbackQueue, + (__bridge const void *)@"test-queue-key", + (__bridge void *)@YES, + NULL); + + PNConfiguration *configuration = [PNConfiguration configurationWithPublishKey:@"demo" + subscribeKey:@"demo" + userID:[[NSUUID UUID] UUIDString]]; + PubNub *client = [PubNub clientWithConfiguration:configuration callbackQueue:callbackQueue]; + [self trackClient:client]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback on expected queue"]; + + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@"test-queue-channel"]; + request.message = @"test"; + + [client publishWithRequest:request completion:^(PNPublishStatus *status) { + BOOL onExpectedQueue = (dispatch_get_specific((__bridge const void *)@"test-queue-key") != NULL); + XCTAssertTrue(onExpectedQueue, + @"Publish callback should arrive on the specified callbackQueue."); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + +- (void)testDefaultCallbackQueueIsMainQueue { + PNConfiguration *configuration = [PNConfiguration configurationWithPublishKey:@"demo" + subscribeKey:@"demo" + userID:[[NSUUID UUID] UUIDString]]; + // Pass nil for callbackQueue; should default to main. + PubNub *client = [PubNub clientWithConfiguration:configuration]; + [self trackClient:client]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Callback on main queue"]; + + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@"main-queue-channel"]; + request.message = @"test"; + + [client publishWithRequest:request completion:^(PNPublishStatus *status) { + XCTAssertTrue([NSThread isMainThread], @"Default callback should arrive on main thread."); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; +} + + +#pragma mark - Tests :: Concurrent state access + +- (void)testConcurrentCurrentConfigurationAccess { + PubNub *client = [self createTestClient]; + int iterations = 200; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + PNConfiguration *config = [client currentConfiguration]; + XCTAssertNotNil(config, @"Configuration should not be nil."); + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"Concurrent configuration access should complete."); +} + +- (void)testConcurrentUserIDAccess { + PubNub *client = [self createTestClient]; + int iterations = 200; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSString *userID = [client userID]; + XCTAssertNotNil(userID, @"userID should not be nil."); + XCTAssertGreaterThan(userID.length, 0u, @"userID should not be empty."); + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"Concurrent userID access should complete."); +} + +- (void)testConcurrentFilterExpressionAccess { + PubNub *client = [self createTestClient]; + int iterations = 200; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if (i % 2 == 0) { + client.filterExpression = [NSString stringWithFormat:@"type == 'test-%d'", i]; + } else { + __unused NSString *filter = client.filterExpression; + } + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"Concurrent filterExpression read/write should not crash."); +} + +- (void)testConcurrentStateAccessWhilePublishing { + PubNub *client = [self createTestClient]; + int iterations = 20; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + // Read state from one thread. + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + __unused NSString *userID = [client userID]; + __unused PNConfiguration *config = [client currentConfiguration]; + __unused NSString *filter = client.filterExpression; + __unused NSArray *channels = [client channels]; + }); + + // Publish from another thread. + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@"state-test"]; + request.message = @{@"i": @(i)}; + + [client publishWithRequest:request completion:^(PNPublishStatus *status) { + dispatch_group_leave(group); + }]; + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"Concurrent state access while publishing should not crash or deadlock."); +} + + +#pragma mark - Tests :: Mixed concurrent operations + +- (void)testMixedConcurrentOperations { + PubNub *client = [self createTestClient]; + int iterations = 20; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + // Add/remove listeners. + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + PNTestListener *listener = [PNTestListener new]; + [client addListener:listener]; + [client removeListener:listener]; + }); + + // Read channels. + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + __unused NSArray *channels = [client channels]; + __unused NSArray *groups = [client channelGroups]; + }); + + // Access configuration properties. + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + __unused NSString *userID = [client userID]; + __unused PNConfiguration *config = [client currentConfiguration]; + }); + + // Publish. + dispatch_group_enter(group); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@"mixed-ops"]; + request.message = @(i); + + [client publishWithRequest:request completion:^(PNPublishStatus *status) { + dispatch_group_leave(group); + }]; + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, @"Mixed concurrent operations should complete without deadlock or crash."); +} + +- (void)testRapidSubscribeUnsubscribeWithListenerManagement { + PubNub *client = [self createTestClient]; + int iterations = 20; + dispatch_group_t group = dispatch_group_create(); + + for (int i = 0; i < iterations; i++) { + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSString *channel = [NSString stringWithFormat:@"rapid-%d", i % 10]; + PNSubscribeRequest *subReq = [PNSubscribeRequest requestWithChannels:@[channel] + channelGroups:nil]; + [client subscribeWithRequest:subReq]; + }); + + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSString *channel = [NSString stringWithFormat:@"rapid-%d", i % 10]; + PNPresenceLeaveRequest *leaveReq = [PNPresenceLeaveRequest requestWithChannels:@[channel] + channelGroups:nil]; + [client unsubscribeWithRequest:leaveReq]; + }); + + dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + PNTestListener *listener = [PNTestListener new]; + [client addListener:listener]; + [client removeListener:listener]; + }); + } + + long result = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 30 * NSEC_PER_SEC)); + XCTAssertEqual(result, 0, + @"Rapid subscribe/unsubscribe with listener management should not crash."); + + [client unsubscribeFromAll]; +} + +#pragma mark - + +#pragma clang diagnostic pop + +@end diff --git a/Tests/Tests/Unit/Core/ChannelGroups/PNChannelGroupErrorTest.m b/Tests/Tests/Unit/Core/ChannelGroups/PNChannelGroupErrorTest.m new file mode 100644 index 000000000..4d4b43fa3 --- /dev/null +++ b/Tests/Tests/Unit/Core/ChannelGroups/PNChannelGroupErrorTest.m @@ -0,0 +1,200 @@ +/** + * @brief Error / negative path tests for Channel Groups operations. + * + * @author PubNub Tests + * @copyright (c) 2010-2026 PubNub, Inc. + */ +#import +#import +#import +#import "PNRecordableTestCase.h" +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNChannelGroupErrorTest : PNRecordableTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNChannelGroupErrorTest + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + + +#pragma mark - VCR configuration + +- (BOOL)shouldSetupVCR { + return NO; +} + + +#pragma mark - Tests :: Add Channels :: Missing group name + +- (void)testItShouldReturnValidationErrorWhenAddChannelsToGroupWithEmptyGroupName { + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToAddChannels:@[@"ch-a"] + toChannelGroup:@""]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channelGroup"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenAddChannelsToGroupWithNilGroupName { + NSString *channelGroup = nil; + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToAddChannels:@[@"ch-a"] + toChannelGroup:channelGroup]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channelGroup"].location, NSNotFound); +} + + +#pragma mark - Tests :: Add Channels :: Nil channels array + +- (void)testItShouldNotReturnValidationErrorWhenAddChannelsWithNilChannelsArray { + // SDK treats nil channels as "remove group" operation; group name is provided so it should pass. + NSArray *channels = nil; + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToAddChannels:channels + toChannelGroup:@"test-group"]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - Tests :: Add Channels :: Valid request + +- (void)testItShouldNotReturnValidationErrorWhenAddChannelsParamsAreValid { + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToAddChannels:@[@"ch-a", @"ch-b"] + toChannelGroup:@"test-group"]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - Tests :: Remove Channels :: Missing group name + +- (void)testItShouldReturnValidationErrorWhenRemoveChannelsFromGroupWithEmptyGroupName { + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToRemoveChannels:@[@"ch-a"] + fromChannelGroup:@""]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channelGroup"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenRemoveChannelsFromGroupWithNilGroupName { + NSString *channelGroup = nil; + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToRemoveChannels:@[@"ch-a"] + fromChannelGroup:channelGroup]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channelGroup"].location, NSNotFound); +} + + +#pragma mark - Tests :: Remove Channel Group :: Missing group name + +- (void)testItShouldReturnValidationErrorWhenRemoveChannelGroupWithEmptyGroupName { + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToRemoveChannelGroup:@""]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channelGroup"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenRemoveChannelGroupWithNilGroupName { + NSString *channelGroup = nil; + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToRemoveChannelGroup:channelGroup]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channelGroup"].location, NSNotFound); +} + + +#pragma mark - Tests :: Remove Channel Group :: Valid request + +- (void)testItShouldNotReturnValidationErrorWhenRemoveChannelGroupIsValid { + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToRemoveChannelGroup:@"test-group"]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - Tests :: Fetch Channels :: Missing group name + +- (void)testItShouldReturnValidationErrorWhenFetchChannelsForGroupWithEmptyGroupName { + PNChannelGroupFetchRequest *request = [PNChannelGroupFetchRequest requestWithChannelGroup:@""]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channelGroup"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenFetchChannelsForGroupWithNilGroupName { + NSString *channelGroup = nil; + PNChannelGroupFetchRequest *request = [PNChannelGroupFetchRequest requestWithChannelGroup:channelGroup]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channelGroup"].location, NSNotFound); +} + + +#pragma mark - Tests :: Fetch Channels :: Valid request + +- (void)testItShouldNotReturnValidationErrorWhenFetchChannelsForGroupIsValid { + PNChannelGroupFetchRequest *request = [PNChannelGroupFetchRequest requestWithChannelGroup:@"test-group"]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - Tests :: Fetch Channel Groups :: No group required + +- (void)testItShouldNotReturnValidationErrorWhenFetchAllChannelGroups { + PNChannelGroupFetchRequest *request = [PNChannelGroupFetchRequest requestChannelGroups]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - + +#pragma clang diagnostic pop + +@end diff --git a/Tests/Tests/Unit/Core/Files/PNFilesErrorTest.m b/Tests/Tests/Unit/Core/Files/PNFilesErrorTest.m new file mode 100644 index 000000000..1e9aa1a98 --- /dev/null +++ b/Tests/Tests/Unit/Core/Files/PNFilesErrorTest.m @@ -0,0 +1,275 @@ +/** + * @brief Error / negative path tests for Files operations (Send, List, Download, Delete). + * + * @author PubNub Tests + * @copyright (c) 2010-2026 PubNub, Inc. + */ +#import +#import +#import +#import +#import +#import "PNRecordableTestCase.h" +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNFilesErrorTest : PNRecordableTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNFilesErrorTest + + +#pragma mark - VCR configuration + +- (BOOL)shouldSetupVCR { + return NO; +} + + +#pragma mark - Tests :: Send File :: Empty data + +- (void)testItShouldReturnValidationErrorWhenSendFileWithEmptyData { + PNSendFileRequest *request = [PNSendFileRequest requestWithChannel:@"test-channel" + fileName:@"test.txt" + data:[NSData data]]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"empty"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenSendFileWithNilData { + NSData *data = nil; + PNSendFileRequest *request = [PNSendFileRequest requestWithChannel:@"test-channel" + fileName:@"test.txt" + data:data]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); +} + + +#pragma mark - Tests :: Send File :: Invalid file URL + +- (void)testItShouldReturnValidationErrorWhenSendFileWithNonExistentFileURL { + NSURL *fakeURL = [NSURL fileURLWithPath:@"/tmp/nonexistent_file_for_pubnub_test.txt"]; + PNSendFileRequest *request = [PNSendFileRequest requestWithChannel:@"test-channel" fileURL:fakeURL]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"file"].location, NSNotFound); +} + + +#pragma mark - Tests :: Send File :: Directory URL instead of file + +- (void)testItShouldReturnValidationErrorWhenSendFileWithDirectoryURL { + NSURL *dirURL = [NSURL fileURLWithPath:NSTemporaryDirectory()]; + PNSendFileRequest *request = [PNSendFileRequest requestWithChannel:@"test-channel" fileURL:dirURL]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"directory"].location, NSNotFound); +} + + +#pragma mark - Tests :: List Files :: Missing channel + +- (void)testItShouldReturnValidationErrorWhenListFilesChannelIsEmpty { + PNListFilesRequest *request = [PNListFilesRequest requestWithChannel:@""]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channel"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenListFilesChannelIsNil { + NSString *channel = nil; + PNListFilesRequest *request = [PNListFilesRequest requestWithChannel:channel]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channel"].location, NSNotFound); +} + + +#pragma mark - Tests :: List Files :: Valid request + +- (void)testItShouldNotReturnValidationErrorWhenListFilesChannelIsValid { + PNListFilesRequest *request = [PNListFilesRequest requestWithChannel:@"test-channel"]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - Tests :: Download File :: Missing identifier + +- (void)testItShouldReturnValidationErrorWhenDownloadFileIdentifierIsEmpty { + PNDownloadFileRequest *request = [PNDownloadFileRequest requestWithChannel:@"test-channel" + identifier:@"" + name:@"test.txt"]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"identifier"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenDownloadFileIdentifierIsNil { + NSString *identifier = nil; + PNDownloadFileRequest *request = [PNDownloadFileRequest requestWithChannel:@"test-channel" + identifier:identifier + name:@"test.txt"]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"identifier"].location, NSNotFound); +} + + +#pragma mark - Tests :: Download File :: Missing channel + +- (void)testItShouldReturnValidationErrorWhenDownloadFileChannelIsEmpty { + PNDownloadFileRequest *request = [PNDownloadFileRequest requestWithChannel:@"" + identifier:@"file-id" + name:@"test.txt"]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channel"].location, NSNotFound); +} + + +#pragma mark - Tests :: Download File :: Missing name + +- (void)testItShouldReturnValidationErrorWhenDownloadFileNameIsEmpty { + PNDownloadFileRequest *request = [PNDownloadFileRequest requestWithChannel:@"test-channel" + identifier:@"file-id" + name:@""]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"name"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenDownloadFileNameIsNil { + NSString *name = nil; + PNDownloadFileRequest *request = [PNDownloadFileRequest requestWithChannel:@"test-channel" + identifier:@"file-id" + name:name]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"name"].location, NSNotFound); +} + + +#pragma mark - Tests :: Download File :: Valid request + +- (void)testItShouldNotReturnValidationErrorWhenDownloadFileParamsAreValid { + PNDownloadFileRequest *request = [PNDownloadFileRequest requestWithChannel:@"test-channel" + identifier:@"file-id" + name:@"test.txt"]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - Tests :: Delete File :: Missing identifier + +- (void)testItShouldReturnValidationErrorWhenDeleteFileIdentifierIsEmpty { + PNDeleteFileRequest *request = [PNDeleteFileRequest requestWithChannel:@"test-channel" + identifier:@"" + name:@"test.txt"]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"identifier"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenDeleteFileIdentifierIsNil { + NSString *identifier = nil; + PNDeleteFileRequest *request = [PNDeleteFileRequest requestWithChannel:@"test-channel" + identifier:identifier + name:@"test.txt"]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"identifier"].location, NSNotFound); +} + + +#pragma mark - Tests :: Delete File :: Missing channel + +- (void)testItShouldReturnValidationErrorWhenDeleteFileChannelIsEmpty { + PNDeleteFileRequest *request = [PNDeleteFileRequest requestWithChannel:@"" + identifier:@"file-id" + name:@"test.txt"]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channel"].location, NSNotFound); +} + + +#pragma mark - Tests :: Delete File :: Missing name + +- (void)testItShouldReturnValidationErrorWhenDeleteFileNameIsEmpty { + PNDeleteFileRequest *request = [PNDeleteFileRequest requestWithChannel:@"test-channel" + identifier:@"file-id" + name:@""]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"name"].location, NSNotFound); +} + + +#pragma mark - Tests :: Delete File :: Valid request + +- (void)testItShouldNotReturnValidationErrorWhenDeleteFileParamsAreValid { + PNDeleteFileRequest *request = [PNDeleteFileRequest requestWithChannel:@"test-channel" + identifier:@"file-id" + name:@"test.txt"]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Core/History/PNHistoryErrorTest.m b/Tests/Tests/Unit/Core/History/PNHistoryErrorTest.m new file mode 100644 index 000000000..459d079da --- /dev/null +++ b/Tests/Tests/Unit/Core/History/PNHistoryErrorTest.m @@ -0,0 +1,242 @@ +/** + * @brief Error / negative path tests for History, Message Count, and Delete Messages operations. + * + * @author PubNub Tests + * @copyright (c) 2010-2026 PubNub, Inc. + */ +#import +#import +#import +#import +#import "PNRecordableTestCase.h" +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNHistoryErrorTest : PNRecordableTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNHistoryErrorTest + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + + +#pragma mark - VCR configuration + +- (BOOL)shouldSetupVCR { + return NO; +} + + +#pragma mark - Tests :: History :: Missing channel (single channel request) + +- (void)testItShouldReturnValidationErrorWhenHistoryFetchChannelIsEmpty { + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannel:@""]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channel"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenHistoryFetchChannelIsNil { + NSString *channel = nil; + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannel:channel]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channel"].location, NSNotFound); +} + + +#pragma mark - Tests :: History :: Missing channels (multi-channel request) + +- (void)testItShouldReturnValidationErrorWhenHistoryFetchChannelsIsEmpty { + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannels:@[]]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channels"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenHistoryFetchChannelsIsNil { + NSArray *channels = nil; + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannels:channels]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channels"].location, NSNotFound); +} + + +#pragma mark - Tests :: History :: Message actions with multiple channels + +- (void)testItShouldReturnValidationErrorWhenMessageActionsUsedWithMultipleChannels { + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannels:@[@"ch-a", @"ch-b"]]; + request.includeMessageActions = YES; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"includeMessageActions"].location, NSNotFound); +} + + +#pragma mark - Tests :: History :: Valid single channel request + +- (void)testItShouldNotReturnValidationErrorWhenHistoryFetchChannelIsValid { + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannel:@"test-channel"]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - Tests :: Messages Count :: Missing channels + +- (void)testItShouldReturnValidationErrorWhenMessageCountChannelsIsEmpty { + PNHistoryMessagesCountRequest *request = [PNHistoryMessagesCountRequest requestWithChannels:@[] + timetokens:@[@(123456)]]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channels"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenMessageCountChannelsIsNil { + NSArray *channels = nil; + PNHistoryMessagesCountRequest *request = [PNHistoryMessagesCountRequest requestWithChannels:channels + timetokens:@[@(123456)]]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channels"].location, NSNotFound); +} + + +#pragma mark - Tests :: Messages Count :: Missing timetokens + +- (void)testItShouldReturnValidationErrorWhenMessageCountTimetokensIsEmpty { + PNHistoryMessagesCountRequest *request = [PNHistoryMessagesCountRequest requestWithChannels:@[@"ch-a"] + timetokens:@[]]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"timetokens"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenMessageCountTimetokensIsNil { + NSArray *timetokens = nil; + PNHistoryMessagesCountRequest *request = [PNHistoryMessagesCountRequest requestWithChannels:@[@"ch-a"] + timetokens:timetokens]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"timetokens"].location, NSNotFound); +} + + +#pragma mark - Tests :: Messages Count :: Mismatched channels and timetokens count + +- (void)testItShouldReturnValidationErrorWhenChannelCountDoesNotMatchTimetokenCount { + PNHistoryMessagesCountRequest *request = [PNHistoryMessagesCountRequest requestWithChannels:@[@"ch-a", @"ch-b"] + timetokens:@[@(1), @(2), @(3)]]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"doesn't match"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenOneChannelHasMultipleTimetokens { + PNHistoryMessagesCountRequest *request = [PNHistoryMessagesCountRequest requestWithChannels:@[@"ch-a"] + timetokens:@[@(1), @(2)]]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); +} + + +#pragma mark - Tests :: Messages Count :: Valid request + +- (void)testItShouldNotReturnValidationErrorWhenMessageCountParamsAreValid { + PNHistoryMessagesCountRequest *request = [PNHistoryMessagesCountRequest requestWithChannels:@[@"ch-a"] + timetokens:@[@(123456)]]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + +- (void)testItShouldNotReturnValidationErrorWhenMultipleChannelsMatchTimetokens { + PNHistoryMessagesCountRequest *request = [PNHistoryMessagesCountRequest requestWithChannels:@[@"ch-a", @"ch-b"] + timetokens:@[@(1), @(2)]]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - Tests :: Delete Messages :: Missing channel + +- (void)testItShouldReturnValidationErrorWhenDeleteMessagesChannelIsEmpty { + PNHistoryMessagesDeleteRequest *request = [PNHistoryMessagesDeleteRequest requestWithChannel:@""]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channel"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenDeleteMessagesChannelIsNil { + NSString *channel = nil; + PNHistoryMessagesDeleteRequest *request = [PNHistoryMessagesDeleteRequest requestWithChannel:channel]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channel"].location, NSNotFound); +} + + +#pragma mark - Tests :: Delete Messages :: Valid request + +- (void)testItShouldNotReturnValidationErrorWhenDeleteMessagesChannelIsValid { + PNHistoryMessagesDeleteRequest *request = [PNHistoryMessagesDeleteRequest requestWithChannel:@"test-channel"]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - + +#pragma clang diagnostic pop + +@end diff --git a/Tests/Tests/Unit/Core/MessageActions/PNMessageActionsErrorTest.m b/Tests/Tests/Unit/Core/MessageActions/PNMessageActionsErrorTest.m new file mode 100644 index 000000000..736617642 --- /dev/null +++ b/Tests/Tests/Unit/Core/MessageActions/PNMessageActionsErrorTest.m @@ -0,0 +1,130 @@ +/** + * @brief Error / negative path tests for Message Actions operations. + * + * @author PubNub Tests + * @copyright (c) 2010-2026 PubNub, Inc. + */ +#import +#import +#import +#import "PNRecordableTestCase.h" +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNMessageActionsErrorTest : PNRecordableTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNMessageActionsErrorTest + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + + +#pragma mark - VCR configuration + +- (BOOL)shouldSetupVCR { + return NO; +} + + +#pragma mark - Tests :: Add Message Action :: Non-serializable value + +- (void)testItShouldNotAddMessageActionWhenValueIsNotSerializable { + NSNumber *messageTimetoken = @([NSDate date].timeIntervalSince1970 * 1000); + NSString *channel = [NSUUID UUID].UUIDString; + NSString *nonSerializableValue = (id)[NSDate date]; + + [self waitToCompleteIn:self.testCompletionDelay codeBlock:^(dispatch_block_t handler) { + PNAddMessageActionRequest *request = [PNAddMessageActionRequest requestWithChannel:channel + messageTimetoken:messageTimetoken]; + request.type = @"custom"; + request.value = nonSerializableValue; + + [self.client addMessageActionWithRequest:request completion:^(PNAddMessageActionStatus *status) { + XCTAssertTrue(status.isError); + XCTAssertNotEqual([status.errorData.information rangeOfString:@"Message action"].location, + NSNotFound); + + handler(); + }]; + }]; +} + + +#pragma mark - Tests :: Fetch Message Actions with request :: Missing channel + +- (void)testItShouldReturnErrorWhenFetchMessageActionsWithRequestAndChannelIsNil { + NSString *channel = nil; + PNFetchMessageActionsRequest *request = [PNFetchMessageActionsRequest requestWithChannel:channel]; + + [self waitToCompleteIn:self.testCompletionDelay codeBlock:^(dispatch_block_t handler) { + [self.client fetchMessageActionsWithRequest:request + completion:^(PNFetchMessageActionsResult *result, PNErrorStatus *status) { + XCTAssertNotNil(status); + XCTAssertTrue(status.isError); + XCTAssertNotEqual([status.errorData.information rangeOfString:@"channel"].location, + NSNotFound); + + handler(); + }]; + }]; +} + +- (void)testItShouldReturnErrorWhenFetchMessageActionsWithRequestAndChannelIsEmpty { + PNFetchMessageActionsRequest *request = [PNFetchMessageActionsRequest requestWithChannel:@""]; + + [self waitToCompleteIn:self.testCompletionDelay codeBlock:^(dispatch_block_t handler) { + [self.client fetchMessageActionsWithRequest:request + completion:^(PNFetchMessageActionsResult *result, PNErrorStatus *status) { + XCTAssertNotNil(status); + XCTAssertTrue(status.isError); + XCTAssertNotEqual([status.errorData.information rangeOfString:@"channel"].location, + NSNotFound); + + handler(); + }]; + }]; +} + + +#pragma mark - Tests :: Add Message Action with request :: Missing channel + +- (void)testItShouldReturnErrorWhenAddMessageActionWithRequestAndChannelIsNil { + NSString *channel = nil; + PNAddMessageActionRequest *request = [PNAddMessageActionRequest requestWithChannel:channel + messageTimetoken:@(1234567890)]; + request.type = @"custom"; + request.value = @"smile"; + + [self waitToCompleteIn:self.testCompletionDelay codeBlock:^(dispatch_block_t handler) { + [self.client addMessageActionWithRequest:request completion:^(PNAddMessageActionStatus *status) { + XCTAssertTrue(status.isError); + XCTAssertNotEqual([status.errorData.information rangeOfString:@"channel"].location, + NSNotFound); + + handler(); + }]; + }]; +} + + +#pragma mark - + +#pragma clang diagnostic pop + +@end diff --git a/Tests/Tests/Unit/Core/Objects/PNObjectsErrorTest.m b/Tests/Tests/Unit/Core/Objects/PNObjectsErrorTest.m new file mode 100644 index 000000000..3694a6868 --- /dev/null +++ b/Tests/Tests/Unit/Core/Objects/PNObjectsErrorTest.m @@ -0,0 +1,234 @@ +/** + * @brief Error / negative path tests for App Context (Objects) operations. + * + * @author PubNub Tests + * @copyright (c) 2010-2026 PubNub, Inc. + */ +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import "PNRecordableTestCase.h" +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNObjectsErrorTest : PNRecordableTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNObjectsErrorTest + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + + +#pragma mark - VCR configuration + +- (BOOL)shouldSetupVCR { + return NO; +} + + +#pragma mark - Tests :: Set UUID Metadata :: Missing identifier + +- (void)testItShouldReturnValidationErrorWhenSetUUIDMetadataIdentifierIsNil { + NSString *uuid = nil; + PNSetUUIDMetadataRequest *request = [PNSetUUIDMetadataRequest requestWithUUID:uuid]; + request.name = @"Test User"; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"identifier"].location, NSNotFound); +} + + +#pragma mark - Tests :: Set UUID Metadata :: Identifier too long + +- (void)testItShouldReturnValidationErrorWhenSetUUIDMetadataIdentifierIsTooLong { + NSString *longId = [@[ + [NSUUID UUID].UUIDString, [NSUUID UUID].UUIDString, [NSUUID UUID].UUIDString, + [NSUUID UUID].UUIDString, [NSUUID UUID].UUIDString, [NSUUID UUID].UUIDString, + [NSUUID UUID].UUIDString, [NSUUID UUID].UUIDString, [NSUUID UUID].UUIDString, + [NSUUID UUID].UUIDString, [NSUUID UUID].UUIDString, [NSUUID UUID].UUIDString, + ] componentsJoinedByString:@""]; + + PNSetUUIDMetadataRequest *request = [PNSetUUIDMetadataRequest requestWithUUID:longId]; + request.name = @"Test User"; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"too long"].location, NSNotFound); +} + + +#pragma mark - Tests :: Set UUID Metadata :: Unsupported custom data types + +- (void)testItShouldReturnValidationErrorWhenSetUUIDMetadataCustomContainsUnsupportedTypes { + PNSetUUIDMetadataRequest *request = [PNSetUUIDMetadataRequest requestWithUUID:@"test-uuid"]; + request.name = @"Test User"; + request.custom = @{ @"date": [NSDate date] }; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"custom"].location, NSNotFound); +} + + +#pragma mark - Tests :: Set UUID Metadata :: Valid request + +- (void)testItShouldNotReturnValidationErrorWhenSetUUIDMetadataParamsAreValid { + PNSetUUIDMetadataRequest *request = [PNSetUUIDMetadataRequest requestWithUUID:@"test-uuid"]; + request.name = @"Test User"; + request.custom = @{ @"key": @"value" }; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - Tests :: Set Channel Metadata :: Missing identifier + +- (void)testItShouldReturnValidationErrorWhenSetChannelMetadataChannelIsNil { + NSString *channel = nil; + PNSetChannelMetadataRequest *request = [PNSetChannelMetadataRequest requestWithChannel:channel]; + request.name = @"Test Channel"; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"identifier"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenSetChannelMetadataChannelIsEmpty { + PNSetChannelMetadataRequest *request = [PNSetChannelMetadataRequest requestWithChannel:@""]; + request.name = @"Test Channel"; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); +} + + +#pragma mark - Tests :: Set Channel Metadata :: Unsupported custom data types + +- (void)testItShouldReturnValidationErrorWhenSetChannelMetadataCustomContainsUnsupportedTypes { + PNSetChannelMetadataRequest *request = [PNSetChannelMetadataRequest requestWithChannel:@"test-ch"]; + request.name = @"Test Channel"; + request.custom = @{ @"date": [NSDate date] }; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"custom"].location, NSNotFound); +} + + +#pragma mark - Tests :: Set Channel Metadata :: Identifier too long + +- (void)testItShouldReturnValidationErrorWhenSetChannelMetadataChannelIsTooLong { + NSString *longChannel = [@[ + [NSUUID UUID].UUIDString, [NSUUID UUID].UUIDString, [NSUUID UUID].UUIDString, + [NSUUID UUID].UUIDString, [NSUUID UUID].UUIDString, [NSUUID UUID].UUIDString, + [NSUUID UUID].UUIDString, [NSUUID UUID].UUIDString, [NSUUID UUID].UUIDString, + [NSUUID UUID].UUIDString, [NSUUID UUID].UUIDString, [NSUUID UUID].UUIDString, + ] componentsJoinedByString:@""]; + + PNSetChannelMetadataRequest *request = [PNSetChannelMetadataRequest requestWithChannel:longChannel]; + request.name = @"Test Channel"; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"too long"].location, NSNotFound); +} + + +#pragma mark - Tests :: Set Channel Metadata :: Valid request + +- (void)testItShouldNotReturnValidationErrorWhenSetChannelMetadataParamsAreValid { + PNSetChannelMetadataRequest *request = [PNSetChannelMetadataRequest requestWithChannel:@"test-channel"]; + request.name = @"Test Channel"; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - Tests :: Remove UUID Metadata :: Missing identifier (using builder) + +- (void)testItShouldReturnValidationErrorWhenRemoveUUIDMetadataIdentifierIsNil { + NSString *uuid = nil; + PNRemoveUUIDMetadataRequest *request = [PNRemoveUUIDMetadataRequest requestWithUUID:uuid]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); +} + + +#pragma mark - Tests :: Fetch UUID Metadata :: Missing identifier + +- (void)testItShouldReturnValidationErrorWhenFetchUUIDMetadataIdentifierIsNil { + NSString *uuid = nil; + PNFetchUUIDMetadataRequest *request = [PNFetchUUIDMetadataRequest requestWithUUID:uuid]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); +} + + +#pragma mark - Tests :: Remove Channel Metadata :: Missing channel + +- (void)testItShouldReturnValidationErrorWhenRemoveChannelMetadataChannelIsEmpty { + PNRemoveChannelMetadataRequest *request = [PNRemoveChannelMetadataRequest requestWithChannel:@""]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); +} + + +#pragma mark - Tests :: Fetch Channel Metadata :: Missing channel + +- (void)testItShouldReturnValidationErrorWhenFetchChannelMetadataChannelIsEmpty { + PNFetchChannelMetadataRequest *request = [PNFetchChannelMetadataRequest requestWithChannel:@""]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); +} + + +#pragma mark - + +#pragma clang diagnostic pop + +@end diff --git a/Tests/Tests/Unit/Core/Presence/PNPresenceErrorTest.m b/Tests/Tests/Unit/Core/Presence/PNPresenceErrorTest.m new file mode 100644 index 000000000..08f07db68 --- /dev/null +++ b/Tests/Tests/Unit/Core/Presence/PNPresenceErrorTest.m @@ -0,0 +1,272 @@ +/** + * @brief Error / negative path tests for Presence operations (HereNow, WhereNow, State, Heartbeat). + * + * @author PubNub Tests + * @copyright (c) 2010-2026 PubNub, Inc. + */ +#import +#import +#import +#import +#import +#import +#import "PNRecordableTestCase.h" +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNPresenceErrorTest : PNRecordableTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNPresenceErrorTest + + +#pragma mark - VCR configuration + +- (BOOL)shouldSetupVCR { + return NO; +} + + +#pragma mark - Tests :: HereNow :: Missing channels (channel-specific request) + +- (void)testItShouldReturnValidationErrorWhenHereNowForChannelsIsEmpty { + PNHereNowRequest *request = [PNHereNowRequest requestForChannels:@[]]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channels"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenHereNowForChannelsIsNil { + NSArray *channels = nil; + PNHereNowRequest *request = [PNHereNowRequest requestForChannels:channels]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channels"].location, NSNotFound); +} + + +#pragma mark - Tests :: HereNow :: Missing channel groups (channel-group request) + +- (void)testItShouldReturnValidationErrorWhenHereNowForChannelGroupsIsEmpty { + PNHereNowRequest *request = [PNHereNowRequest requestForChannelGroups:@[]]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channelGroup"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenHereNowForChannelGroupsIsNil { + NSArray *channelGroups = nil; + PNHereNowRequest *request = [PNHereNowRequest requestForChannelGroups:channelGroups]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channelGroup"].location, NSNotFound); +} + + +#pragma mark - Tests :: HereNow :: Global request should pass + +- (void)testItShouldNotReturnValidationErrorForGlobalHereNow { + PNHereNowRequest *request = [PNHereNowRequest requestGlobal]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - Tests :: HereNow :: Valid channel request + +- (void)testItShouldNotReturnValidationErrorWhenHereNowChannelsAreValid { + PNHereNowRequest *request = [PNHereNowRequest requestForChannels:@[@"test-channel"]]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - Tests :: WhereNow :: Missing userId + +- (void)testItShouldReturnValidationErrorWhenWhereNowUserIdIsEmpty { + PNWhereNowRequest *request = [PNWhereNowRequest requestForUserId:@""]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"userId"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenWhereNowUserIdIsNil { + NSString *userId = nil; + PNWhereNowRequest *request = [PNWhereNowRequest requestForUserId:userId]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"userId"].location, NSNotFound); +} + + +#pragma mark - Tests :: WhereNow :: Valid request + +- (void)testItShouldNotReturnValidationErrorWhenWhereNowUserIdIsValid { + PNWhereNowRequest *request = [PNWhereNowRequest requestForUserId:@"user-123"]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - Tests :: Set State :: Missing userId + +- (void)testItShouldReturnValidationErrorWhenSetStateUserIdIsEmpty { + PNPresenceStateSetRequest *request = [PNPresenceStateSetRequest requestWithUserId:@""]; + request.channels = @[@"test-channel"]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"userId"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenSetStateUserIdIsNil { + NSString *userId = nil; + PNPresenceStateSetRequest *request = [PNPresenceStateSetRequest requestWithUserId:userId]; + request.channels = @[@"test-channel"]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"userId"].location, NSNotFound); +} + + +#pragma mark - Tests :: Set State :: Missing channels + +- (void)testItShouldReturnValidationErrorWhenSetStateChannelsAndGroupsAreEmpty { + PNPresenceStateSetRequest *request = [PNPresenceStateSetRequest requestWithUserId:@"user-123"]; + // No channels or groups set + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channels"].location, NSNotFound); +} + + +#pragma mark - Tests :: Set State :: Valid request + +- (void)testItShouldNotReturnValidationErrorWhenSetStateParamsAreValid { + PNPresenceStateSetRequest *request = [PNPresenceStateSetRequest requestWithUserId:@"user-123"]; + request.channels = @[@"test-channel"]; + request.state = @{ @"mood": @"happy" }; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - Tests :: Fetch State :: Missing userId + +- (void)testItShouldReturnValidationErrorWhenFetchStateUserIdIsEmpty { + PNPresenceStateFetchRequest *request = [PNPresenceStateFetchRequest requestWithUserId:@""]; + request.channels = @[@"test-channel"]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"userId"].location, NSNotFound); +} + + +#pragma mark - Tests :: Fetch State :: Missing channels + +- (void)testItShouldReturnValidationErrorWhenFetchStateChannelsAndGroupsAreEmpty { + PNPresenceStateFetchRequest *request = [PNPresenceStateFetchRequest requestWithUserId:@"user-123"]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channels"].location, NSNotFound); +} + + +#pragma mark - Tests :: Heartbeat :: Missing channels + +- (void)testItShouldReturnValidationErrorWhenHeartbeatChannelsAndGroupsAreEmpty { + PNPresenceHeartbeatRequest *request = [PNPresenceHeartbeatRequest requestWithHeartbeat:300 + channels:@[] + channelGroups:@[]]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channels"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenHeartbeatChannelsAndGroupsAreNil { + NSArray *channels = nil; + NSArray *channelGroups = nil; + PNPresenceHeartbeatRequest *request = [PNPresenceHeartbeatRequest requestWithHeartbeat:300 + channels:channels + channelGroups:channelGroups]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channels"].location, NSNotFound); +} + + +#pragma mark - Tests :: Heartbeat :: Valid request + +- (void)testItShouldNotReturnValidationErrorWhenHeartbeatChannelsAreValid { + NSArray *channelGroups = nil; + PNPresenceHeartbeatRequest *request = [PNPresenceHeartbeatRequest requestWithHeartbeat:300 + channels:@[@"ch-a"] + channelGroups:channelGroups]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + +- (void)testItShouldNotReturnValidationErrorWhenHeartbeatChannelGroupsAreValid { + NSArray *channels = nil; + PNPresenceHeartbeatRequest *request = [PNPresenceHeartbeatRequest requestWithHeartbeat:300 + channels:channels + channelGroups:@[@"group-a"]]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Core/Publish/PNPublishErrorTest.m b/Tests/Tests/Unit/Core/Publish/PNPublishErrorTest.m new file mode 100644 index 000000000..670c919d3 --- /dev/null +++ b/Tests/Tests/Unit/Core/Publish/PNPublishErrorTest.m @@ -0,0 +1,175 @@ +/** + * @brief Error / negative path tests for Publish, Signal, and Fire operations. + * + * @author PubNub Tests + * @copyright (c) 2010-2026 PubNub, Inc. + */ +#import +#import +#import +#import +#import "PNRecordableTestCase.h" +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNPublishErrorTest : PNRecordableTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNPublishErrorTest + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + + +#pragma mark - VCR configuration + +- (BOOL)shouldSetupVCR { + return NO; +} + + +#pragma mark - Tests :: Publish :: Missing channel + +- (void)testItShouldReturnValidationErrorWhenPublishRequestChannelIsEmpty { + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@""]; + request.message = @"Hello"; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channel"].location, NSNotFound); +} + + +#pragma mark - Tests :: Publish :: Missing message + +- (void)testItShouldReturnValidationErrorWhenPublishRequestMessageIsNil { + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@"test-channel"]; + // message is nil by default + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"message"].location, NSNotFound); +} + + +#pragma mark - Tests :: Publish :: Non-serializable message + +- (void)testItShouldReturnValidationErrorWhenPublishMessageIsNotSerializable { + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@"test-channel"]; + request.message = (id)[NSDate date]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); +} + + +#pragma mark - Tests :: Signal :: Validate request + +- (void)testItShouldReturnValidationErrorWhenSignalMessageIsNil { + id signal = nil; + PNSignalRequest *request = [PNSignalRequest requestWithChannel:@"test-channel" signal:signal]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"message"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenSignalMessageIsNotSerializable { + PNSignalRequest *request = [PNSignalRequest requestWithChannel:@"test-channel" signal:(id)[NSDate date]]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); +} + + +#pragma mark - Tests :: Publish :: Non-serializable metadata + +- (void)testItShouldReturnValidationErrorWhenPublishMetadataIsNotSerializable { + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@"test-channel"]; + request.message = @"Hello"; + request.metadata = @{ @"date": [NSDate date] }; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); +} + + +#pragma mark - Tests :: Publish with request :: Missing channel + +- (void)testItShouldReturnErrorWhenPublishWithRequestAndChannelIsEmpty { + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@""]; + request.message = @"Hello"; + + [self waitToCompleteIn:self.testCompletionDelay codeBlock:^(dispatch_block_t handler) { + [self.client publishWithRequest:request completion:^(PNPublishStatus *status) { + XCTAssertTrue(status.isError); + XCTAssertNotEqual([status.errorData.information rangeOfString:@"channel"].location, + NSNotFound); + + handler(); + }]; + }]; +} + + +#pragma mark - Tests :: Publish with request :: Missing message + +- (void)testItShouldReturnErrorWhenPublishWithRequestAndMessageIsNil { + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@"test-channel"]; + + [self waitToCompleteIn:self.testCompletionDelay codeBlock:^(dispatch_block_t handler) { + [self.client publishWithRequest:request completion:^(PNPublishStatus *status) { + XCTAssertTrue(status.isError); + XCTAssertNotEqual([status.errorData.information rangeOfString:@"message"].location, + NSNotFound); + + handler(); + }]; + }]; +} + + +#pragma mark - Tests :: Signal with request :: Nil message + +- (void)testItShouldReturnErrorWhenSendSignalWithRequestAndMessageIsNil { + id signal = nil; + PNSignalRequest *request = [PNSignalRequest requestWithChannel:@"test-channel" signal:signal]; + + [self waitToCompleteIn:self.testCompletionDelay codeBlock:^(dispatch_block_t handler) { + [self.client sendSignalWithRequest:request completion:^(PNSignalStatus *status) { + XCTAssertTrue(status.isError); + XCTAssertNotEqual([status.errorData.information rangeOfString:@"message"].location, + NSNotFound); + + handler(); + }]; + }]; +} + + +#pragma mark - + +#pragma clang diagnostic pop + +@end diff --git a/Tests/Tests/Unit/Core/Requests/ChannelGroups/PNChannelGroupRequestTest.m b/Tests/Tests/Unit/Core/Requests/ChannelGroups/PNChannelGroupRequestTest.m new file mode 100644 index 000000000..71f368de8 --- /dev/null +++ b/Tests/Tests/Unit/Core/Requests/ChannelGroups/PNChannelGroupRequestTest.m @@ -0,0 +1,194 @@ +#import +#import +#import +#import "PNBaseRequest+Private.h" + + +#pragma mark Interface declaration + +@interface PNChannelGroupRequestTest : XCTestCase + +@end + + +#pragma mark - Tests + +@implementation PNChannelGroupRequestTest + + +#pragma mark - PNChannelGroupFetchRequest :: List all channel groups + +- (void)testItShouldCreateListAllChannelGroupsRequest { + PNChannelGroupFetchRequest *request = [PNChannelGroupFetchRequest requestChannelGroups]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldHaveNilChannelGroupWhenListingAllGroups { + PNChannelGroupFetchRequest *request = [PNChannelGroupFetchRequest requestChannelGroups]; + + XCTAssertNil(request.channelGroup, @"channelGroup should be nil for list all groups request"); +} + +- (void)testItShouldPassValidationWhenListingAllGroups { + PNChannelGroupFetchRequest *request = [PNChannelGroupFetchRequest requestChannelGroups]; + + XCTAssertNil([request validate], @"List all groups should pass validation without channelGroup"); +} + + +#pragma mark - PNChannelGroupFetchRequest :: List channel group channels + +- (void)testItShouldCreateFetchRequestWhenChannelGroupProvided { + PNChannelGroupFetchRequest *request = [PNChannelGroupFetchRequest requestWithChannelGroup:@"my-group"]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.channelGroup, @"my-group"); +} + +- (void)testItShouldHaveNilQueryWhenFetchRequestCreated { + PNChannelGroupFetchRequest *request = [PNChannelGroupFetchRequest requestWithChannelGroup:@"grp"]; + + XCTAssertNil(request.query); +} + +- (void)testItShouldIncludeArbitraryParametersInFetchQuery { + PNChannelGroupFetchRequest *request = [PNChannelGroupFetchRequest requestWithChannelGroup:@"grp"]; + request.arbitraryQueryParameters = @{ @"key": @"value" }; + + XCTAssertEqualObjects(request.query[@"key"], @"value"); +} + +- (void)testItShouldRetainChannelGroupNameWhenCreated { + PNChannelGroupFetchRequest *request = [PNChannelGroupFetchRequest requestWithChannelGroup:@"my-special-group"]; + + XCTAssertEqualObjects(request.channelGroup, @"my-special-group"); +} + +- (void)testItShouldHandleSpecialCharactersInChannelGroupNameWhenCreated { + PNChannelGroupFetchRequest *request = [PNChannelGroupFetchRequest requestWithChannelGroup:@"group_with-special.chars"]; + + XCTAssertEqualObjects(request.channelGroup, @"group_with-special.chars"); +} + + +#pragma mark - PNChannelGroupFetchRequest :: Validation + +- (void)testItShouldPassValidationWhenChannelGroupProvided { + PNChannelGroupFetchRequest *request = [PNChannelGroupFetchRequest requestWithChannelGroup:@"grp"]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldFailValidationWhenChannelGroupIsEmpty { + PNChannelGroupFetchRequest *request = [PNChannelGroupFetchRequest requestWithChannelGroup:@""]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty channel group"); +} + + +#pragma mark - PNChannelGroupManageRequest :: Add channels + +- (void)testItShouldCreateAddChannelsRequestWhenChannelsAndGroupProvided { + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToAddChannels:@[@"ch1", @"ch2"] + toChannelGroup:@"my-group"]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.channelGroup, @"my-group"); +} + +- (void)testItShouldIncludeChannelsInAddQuery { + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToAddChannels:@[@"ch1", @"ch2"] + toChannelGroup:@"grp"]; + + NSDictionary *query = request.query; + + XCTAssertNotNil(query[@"add"]); + XCTAssertTrue([query[@"add"] containsString:@"ch1"]); + XCTAssertTrue([query[@"add"] containsString:@"ch2"]); +} + +- (void)testItShouldIncludeArbitraryParametersInAddChannelsQuery { + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToAddChannels:@[@"ch"] + toChannelGroup:@"grp"]; + request.arbitraryQueryParameters = @{ @"custom": @"param" }; + + XCTAssertEqualObjects(request.query[@"custom"], @"param"); +} + + +#pragma mark - PNChannelGroupManageRequest :: Remove channels + +- (void)testItShouldCreateRemoveChannelsRequestWhenChannelsAndGroupProvided { + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToRemoveChannels:@[@"ch1"] + fromChannelGroup:@"my-group"]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.channelGroup, @"my-group"); +} + +- (void)testItShouldIncludeChannelsInRemoveQuery { + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToRemoveChannels:@[@"ch1"] + fromChannelGroup:@"my-group"]; + + XCTAssertEqualObjects(request.query[@"remove"], @"ch1"); +} + + +#pragma mark - PNChannelGroupManageRequest :: Remove channel group + +- (void)testItShouldCreateRemoveChannelGroupRequestWhenGroupProvided { + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToRemoveChannelGroup:@"my-group"]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.channelGroup, @"my-group"); + XCTAssertNil(request.channels, @"channels should be nil for remove group request"); +} + + +#pragma mark - PNChannelGroupManageRequest :: Validation + +- (void)testItShouldPassValidationWhenAddChannelsToGroupProvided { + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToAddChannels:@[@"ch"] + toChannelGroup:@"grp"]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldPassValidationWhenRemoveChannelsFromGroupProvided { + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToRemoveChannels:@[@"ch"] + fromChannelGroup:@"grp"]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldPassValidationWhenRemoveGroupProvided { + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToRemoveChannelGroup:@"grp"]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldFailValidationWhenAddChannelsGroupNameIsEmpty { + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToAddChannels:@[@"ch"] + toChannelGroup:@""]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty channel group name"); +} + +- (void)testItShouldFailValidationWhenRemoveChannelsGroupNameIsEmpty { + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToRemoveChannels:@[@"ch"] + fromChannelGroup:@""]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty channel group name"); +} + +- (void)testItShouldFailValidationWhenRemoveGroupNameIsEmpty { + PNChannelGroupManageRequest *request = [PNChannelGroupManageRequest requestToRemoveChannelGroup:@""]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty channel group name"); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Core/Requests/Files/PNFilesRequestTest.m b/Tests/Tests/Unit/Core/Requests/Files/PNFilesRequestTest.m new file mode 100644 index 000000000..1b848599e --- /dev/null +++ b/Tests/Tests/Unit/Core/Requests/Files/PNFilesRequestTest.m @@ -0,0 +1,263 @@ +#import +#import +#import +#import +#import +#import "PNBaseRequest+Private.h" + + +#pragma mark Interface declaration + +@interface PNFilesRequestTest : XCTestCase + +@end + + +#pragma mark - Tests + +@implementation PNFilesRequestTest + + +#pragma mark - PNSendFileRequest :: Construction (data) + +- (void)testItShouldCreateSendFileRequestWhenChannelAndDataProvided { + NSData *data = [@"Hello, World!" dataUsingEncoding:NSUTF8StringEncoding]; + PNSendFileRequest *request = [PNSendFileRequest requestWithChannel:@"test-channel" + fileName:@"test.txt" + data:data]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.channel, @"test-channel"); + XCTAssertEqualObjects(request.filename, @"test.txt"); +} + +- (void)testItShouldHaveDefaultValuesWhenSendFileRequestCreated { + NSData *data = [@"data" dataUsingEncoding:NSUTF8StringEncoding]; + PNSendFileRequest *request = [PNSendFileRequest requestWithChannel:@"ch" fileName:@"f.txt" data:data]; + + XCTAssertTrue(request.fileMessageStore, @"fileMessageStore should default to YES"); + XCTAssertEqual(request.fileMessageTTL, 0, @"fileMessageTTL should default to 0"); + XCTAssertNil(request.message, @"message should default to nil"); + XCTAssertNil(request.fileMessageMetadata, @"fileMessageMetadata should default to nil"); + XCTAssertNil(request.customMessageType, @"customMessageType should default to nil"); +} + +- (void)testItShouldIncludeArbitraryParametersInSendFileQuery { + NSData *data = [@"data" dataUsingEncoding:NSUTF8StringEncoding]; + PNSendFileRequest *request = [PNSendFileRequest requestWithChannel:@"ch" fileName:@"f.txt" data:data]; + request.arbitraryQueryParameters = @{ @"key": @"value" }; + + XCTAssertEqualObjects(request.query[@"key"], @"value"); +} + +- (void)testItShouldPassValidationWhenValidDataProvided { + NSData *data = [@"content" dataUsingEncoding:NSUTF8StringEncoding]; + PNSendFileRequest *request = [PNSendFileRequest requestWithChannel:@"ch" fileName:@"f.txt" data:data]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldFailValidationWhenEmptyDataProvided { + NSData *data = [NSData data]; + PNSendFileRequest *request = [PNSendFileRequest requestWithChannel:@"ch" fileName:@"f.txt" data:data]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty data"); +} + + +#pragma mark - PNSendFileRequest :: Construction (stream) + +- (void)testItShouldCreateSendFileRequestWhenStreamProvided { + NSData *data = [@"stream data" dataUsingEncoding:NSUTF8StringEncoding]; + NSInputStream *stream = [NSInputStream inputStreamWithData:data]; + PNSendFileRequest *request = [PNSendFileRequest requestWithChannel:@"ch" + fileName:@"stream.bin" + stream:stream + size:data.length]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.channel, @"ch"); + XCTAssertEqualObjects(request.filename, @"stream.bin"); +} + +- (void)testItShouldFailValidationWhenStreamSizeIsZero { + NSData *data = [@"data" dataUsingEncoding:NSUTF8StringEncoding]; + NSInputStream *stream = [NSInputStream inputStreamWithData:data]; + PNSendFileRequest *request = [PNSendFileRequest requestWithChannel:@"ch" + fileName:@"empty.bin" + stream:stream + size:0]; + + XCTAssertNotNil([request validate], @"Validation should fail with zero size stream"); +} + + +#pragma mark - PNListFilesRequest :: Construction + +- (void)testItShouldCreateListFilesRequestWhenChannelProvided { + PNListFilesRequest *request = [PNListFilesRequest requestWithChannel:@"test-channel"]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.channel, @"test-channel"); +} + +- (void)testItShouldHaveDefaultValuesWhenListFilesCreated { + PNListFilesRequest *request = [PNListFilesRequest requestWithChannel:@"ch"]; + + XCTAssertNil(request.next, @"next should default to nil"); +} + + +#pragma mark - PNListFilesRequest :: Query parameters + +- (void)testItShouldIncludeLimitInListFilesQuery { + PNListFilesRequest *request = [PNListFilesRequest requestWithChannel:@"ch"]; + request.limit = 50; + + XCTAssertEqualObjects(request.query[@"limit"], @(50).stringValue); +} + +- (void)testItShouldIncludeNextCursorInListFilesQuery { + PNListFilesRequest *request = [PNListFilesRequest requestWithChannel:@"ch"]; + request.next = @"next-page-cursor"; + + XCTAssertNotNil(request.query[@"next"]); +} + +- (void)testItShouldIncludeArbitraryParametersInListFilesQuery { + PNListFilesRequest *request = [PNListFilesRequest requestWithChannel:@"ch"]; + request.arbitraryQueryParameters = @{ @"custom": @"param" }; + + XCTAssertEqualObjects(request.query[@"custom"], @"param"); +} + + +#pragma mark - PNListFilesRequest :: Validation + +- (void)testItShouldPassValidationWhenListFilesChannelProvided { + PNListFilesRequest *request = [PNListFilesRequest requestWithChannel:@"ch"]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldFailValidationWhenListFilesChannelIsEmpty { + PNListFilesRequest *request = [PNListFilesRequest requestWithChannel:@""]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty channel"); +} + + +#pragma mark - PNDownloadFileRequest :: Construction + +- (void)testItShouldCreateDownloadFileRequestWhenAllParamsProvided { + PNDownloadFileRequest *request = [PNDownloadFileRequest requestWithChannel:@"ch" + identifier:@"file-id-123" + name:@"photo.jpg"]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldHaveDefaultValuesWhenDownloadFileCreated { + PNDownloadFileRequest *request = [PNDownloadFileRequest requestWithChannel:@"ch" + identifier:@"id" + name:@"file.txt"]; + + XCTAssertNil(request.targetURL, @"targetURL should default to nil"); + XCTAssertNil(request.cipherKey, @"cipherKey should default to nil"); +} + + +#pragma mark - PNDownloadFileRequest :: Validation + +- (void)testItShouldPassValidationWhenAllDownloadParamsProvided { + PNDownloadFileRequest *request = [PNDownloadFileRequest requestWithChannel:@"ch" + identifier:@"file-id" + name:@"file.txt"]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldFailValidationWhenDownloadChannelIsEmpty { + PNDownloadFileRequest *request = [PNDownloadFileRequest requestWithChannel:@"" + identifier:@"file-id" + name:@"file.txt"]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty channel"); +} + +- (void)testItShouldFailValidationWhenDownloadIdentifierIsEmpty { + PNDownloadFileRequest *request = [PNDownloadFileRequest requestWithChannel:@"ch" + identifier:@"" + name:@"file.txt"]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty identifier"); +} + +- (void)testItShouldFailValidationWhenDownloadNameIsEmpty { + PNDownloadFileRequest *request = [PNDownloadFileRequest requestWithChannel:@"ch" + identifier:@"file-id" + name:@""]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty name"); +} + + +#pragma mark - PNDeleteFileRequest :: Construction + +- (void)testItShouldCreateDeleteFileRequestWhenAllParamsProvided { + PNDeleteFileRequest *request = [PNDeleteFileRequest requestWithChannel:@"ch" + identifier:@"file-id-123" + name:@"photo.jpg"]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.identifier, @"file-id-123"); +} + +- (void)testItShouldIncludeArbitraryParametersInDeleteFileQuery { + PNDeleteFileRequest *request = [PNDeleteFileRequest requestWithChannel:@"ch" + identifier:@"id" + name:@"file.txt"]; + request.arbitraryQueryParameters = @{ @"key": @"value" }; + + XCTAssertEqualObjects(request.query[@"key"], @"value"); +} + + +#pragma mark - PNDeleteFileRequest :: Validation + +- (void)testItShouldPassValidationWhenAllDeleteParamsProvided { + PNDeleteFileRequest *request = [PNDeleteFileRequest requestWithChannel:@"ch" + identifier:@"file-id" + name:@"file.txt"]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldFailValidationWhenDeleteChannelIsEmpty { + PNDeleteFileRequest *request = [PNDeleteFileRequest requestWithChannel:@"" + identifier:@"file-id" + name:@"file.txt"]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty channel"); +} + +- (void)testItShouldFailValidationWhenDeleteIdentifierIsEmpty { + PNDeleteFileRequest *request = [PNDeleteFileRequest requestWithChannel:@"ch" + identifier:@"" + name:@"file.txt"]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty identifier"); +} + +- (void)testItShouldFailValidationWhenDeleteNameIsEmpty { + PNDeleteFileRequest *request = [PNDeleteFileRequest requestWithChannel:@"ch" + identifier:@"file-id" + name:@""]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty name"); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Core/Requests/History/PNHistoryRequestTest.m b/Tests/Tests/Unit/Core/Requests/History/PNHistoryRequestTest.m new file mode 100644 index 000000000..fab4a7614 --- /dev/null +++ b/Tests/Tests/Unit/Core/Requests/History/PNHistoryRequestTest.m @@ -0,0 +1,262 @@ +#import +#import +#import +#import +#import "PNBaseRequest+Private.h" + + +#pragma mark Interface declaration + +@interface PNHistoryRequestTest : XCTestCase + +@end + + +#pragma mark - Tests + +@implementation PNHistoryRequestTest + + +#pragma mark - PNHistoryFetchRequest :: Construction (single channel) + +- (void)testItShouldCreateFetchRequestWhenSingleChannelProvided { + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannel:@"test-channel"]; + + XCTAssertNotNil(request); + XCTAssertEqual(request.channels.count, 1); + XCTAssertEqualObjects(request.channels.firstObject, @"test-channel"); +} + +- (void)testItShouldHaveDefaultValuesWhenSingleChannelFetchRequestCreated { + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannel:@"ch"]; + + XCTAssertTrue(request.includeMessageType, @"includeMessageType should default to YES"); + XCTAssertTrue(request.includeUUID, @"includeUUID should default to YES"); + XCTAssertFalse(request.includeMessageActions, @"includeMessageActions should default to NO"); + XCTAssertFalse(request.includeCustomMessageType, @"includeCustomMessageType should default to NO"); + XCTAssertFalse(request.includeMetadata, @"includeMetadata should default to NO"); + XCTAssertFalse(request.includeTimeToken, @"includeTimeToken should default to NO"); + XCTAssertFalse(request.reverse, @"reverse should default to NO"); + XCTAssertEqual(request.limit, 0, @"limit should default to 0"); + XCTAssertNil(request.start, @"start should default to nil"); + XCTAssertNil(request.end, @"end should default to nil"); +} + + +#pragma mark - PNHistoryFetchRequest :: Construction (multiple channels) + +- (void)testItShouldCreateFetchRequestWhenMultipleChannelsProvided { + NSArray *channels = @[@"ch1", @"ch2", @"ch3"]; + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannels:channels]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.channels, channels); +} + + +#pragma mark - PNHistoryFetchRequest :: Query parameters + +- (void)testItShouldIncludeStartAndEndTimetokensInQuery { + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannel:@"ch"]; + request.start = @(16000000000000000); + request.end = @(16100000000000000); + + NSDictionary *query = request.query; + + XCTAssertNotNil(query[@"start"]); + XCTAssertNotNil(query[@"end"]); +} + +- (void)testItShouldIncludeLimitInSingleChannelQuery { + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannel:@"ch"]; + request.limit = 50; + + XCTAssertEqualObjects(request.query[@"count"], @(50).stringValue); +} + +- (void)testItShouldUseDifferentLimitKeyForMultiChannelQuery { + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannels:@[@"ch1", @"ch2"]]; + request.limit = 10; + + XCTAssertNotNil(request.query[@"max"], @"Multi-channel should use 'max' limit key"); + XCTAssertNil(request.query[@"count"], @"Multi-channel should not use 'count' limit key"); +} + +- (void)testItShouldIncludeReverseInQuery { + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannel:@"ch"]; + request.reverse = YES; + + XCTAssertEqualObjects(request.query[@"reverse"], @"true"); +} + +- (void)testItShouldIncludeMetadataFlagInQuery { + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannel:@"ch"]; + request.includeMetadata = YES; + + XCTAssertEqualObjects(request.query[@"include_meta"], @"true"); +} + +- (void)testItShouldIncludeTimeTokenFlagInQuery { + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannel:@"ch"]; + request.includeTimeToken = YES; + + XCTAssertEqualObjects(request.query[@"include_token"], @"true"); +} + +- (void)testItShouldUseLimitKeyMaxWhenMessageActionsEnabled { + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannel:@"ch"]; + request.includeMessageActions = YES; + + XCTAssertEqualObjects(request.query[@"max"], @(25).stringValue, @"With-actions default limit should be 25"); + XCTAssertNil(request.query[@"count"], @"With-actions should not use 'count' limit key"); +} + + +#pragma mark - PNHistoryFetchRequest :: Validation + +- (void)testItShouldPassValidationWhenSingleChannelProvided { + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannel:@"ch"]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldPassValidationWhenMultipleChannelsProvided { + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannels:@[@"ch1", @"ch2"]]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldFailValidationWhenEmptyChannelsArrayProvided { + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannels:@[]]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty channels array"); +} + +- (void)testItShouldFailValidationWhenMultiChannelRequestHasMessageActionsEnabled { + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannels:@[@"ch1", @"ch2"]]; + request.includeMessageActions = YES; + + XCTAssertNotNil([request validate], + @"Validation should fail with messageActions on multi-channel request"); +} + +- (void)testItShouldPassValidationWhenSingleChannelHasMessageActionsEnabled { + PNHistoryFetchRequest *request = [PNHistoryFetchRequest requestWithChannel:@"ch"]; + request.includeMessageActions = YES; + + XCTAssertNil([request validate], + @"Validation should pass with messageActions on single channel request"); +} + + +#pragma mark - PNHistoryMessagesCountRequest :: Construction + +- (void)testItShouldCreateMessagesCountRequestWhenChannelsAndTimetokensProvided { + NSArray *channels = @[@"ch1", @"ch2"]; + NSArray *timetokens = @[@(1550140202)]; + PNHistoryMessagesCountRequest *request = [PNHistoryMessagesCountRequest requestWithChannels:channels + timetokens:timetokens]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.channels, channels); + XCTAssertEqualObjects(request.timetokens, timetokens); +} + +- (void)testItShouldRetainMultipleTimetokensWhenProvided { + NSArray *channels = @[@"ch1", @"ch2"]; + NSArray *timetokens = @[@(1550140202), @(1550140204)]; + PNHistoryMessagesCountRequest *request = [PNHistoryMessagesCountRequest requestWithChannels:channels + timetokens:timetokens]; + + XCTAssertEqual(request.timetokens.count, 2); +} + + +#pragma mark - PNHistoryMessagesCountRequest :: Validation + +- (void)testItShouldPassValidationWhenChannelsAndSingleTimetokenProvided { + PNHistoryMessagesCountRequest *request = [PNHistoryMessagesCountRequest requestWithChannels:@[@"ch1", @"ch2"] + timetokens:@[@(1550140202)]]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldPassValidationWhenChannelsMatchTimetokensCount { + PNHistoryMessagesCountRequest *request = [PNHistoryMessagesCountRequest requestWithChannels:@[@"ch1", @"ch2"] + timetokens:@[@(100), @(200)]]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldFailValidationWhenNoChannelsProvided { + PNHistoryMessagesCountRequest *request = [PNHistoryMessagesCountRequest requestWithChannels:@[] + timetokens:@[@(100)]]; + + XCTAssertNotNil([request validate], @"Validation should fail without channels"); +} + +- (void)testItShouldFailValidationWhenNoTimetokensProvided { + PNHistoryMessagesCountRequest *request = [PNHistoryMessagesCountRequest requestWithChannels:@[@"ch1"] + timetokens:@[]]; + + XCTAssertNotNil([request validate], @"Validation should fail without timetokens"); +} + +- (void)testItShouldFailValidationWhenTimetokensCountMismatchesChannels { + PNHistoryMessagesCountRequest *request = [PNHistoryMessagesCountRequest requestWithChannels:@[@"ch1", @"ch2"] + timetokens:@[@(100), @(200), @(300)]]; + + XCTAssertNotNil([request validate], + @"Validation should fail when timetoken count doesn't match channel count"); +} + + +#pragma mark - PNHistoryMessagesDeleteRequest :: Construction + +- (void)testItShouldCreateDeleteRequestWhenChannelProvided { + PNHistoryMessagesDeleteRequest *request = [PNHistoryMessagesDeleteRequest requestWithChannel:@"test-channel"]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.channel, @"test-channel"); +} + +- (void)testItShouldHaveDefaultValuesWhenDeleteRequestCreated { + PNHistoryMessagesDeleteRequest *request = [PNHistoryMessagesDeleteRequest requestWithChannel:@"ch"]; + + XCTAssertNil(request.start, @"start should default to nil"); + XCTAssertNil(request.end, @"end should default to nil"); +} + + +#pragma mark - PNHistoryMessagesDeleteRequest :: Query parameters + +- (void)testItShouldIncludeStartAndEndInDeleteQuery { + PNHistoryMessagesDeleteRequest *request = [PNHistoryMessagesDeleteRequest requestWithChannel:@"ch"]; + request.start = @(16000000000000000); + request.end = @(16100000000000000); + + NSDictionary *query = request.query; + + XCTAssertNotNil(query[@"start"]); + XCTAssertNotNil(query[@"end"]); +} + + +#pragma mark - PNHistoryMessagesDeleteRequest :: Validation + +- (void)testItShouldPassValidationWhenDeleteChannelProvided { + PNHistoryMessagesDeleteRequest *request = [PNHistoryMessagesDeleteRequest requestWithChannel:@"ch"]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldFailValidationWhenDeleteChannelIsEmpty { + PNHistoryMessagesDeleteRequest *request = [PNHistoryMessagesDeleteRequest requestWithChannel:@""]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty channel"); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Core/Requests/MessageActions/PNMessageActionsRequestTest.m b/Tests/Tests/Unit/Core/Requests/MessageActions/PNMessageActionsRequestTest.m new file mode 100644 index 000000000..34694238b --- /dev/null +++ b/Tests/Tests/Unit/Core/Requests/MessageActions/PNMessageActionsRequestTest.m @@ -0,0 +1,245 @@ +#import +#import +#import +#import +#import "PNBaseRequest+Private.h" + + +#pragma mark Interface declaration + +@interface PNMessageActionsRequestTest : XCTestCase + +@end + + +#pragma mark - Tests + +@implementation PNMessageActionsRequestTest + + +#pragma mark - PNAddMessageActionRequest :: Construction + +- (void)testItShouldCreateAddMessageActionRequestWhenChannelAndTimetokenProvided { + PNAddMessageActionRequest *request = [PNAddMessageActionRequest requestWithChannel:@"test-channel" + messageTimetoken:@(16000000000000000)]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldIncludeTypeAndValueInBodyWhenAddActionValidated { + PNAddMessageActionRequest *request = [PNAddMessageActionRequest requestWithChannel:@"ch" + messageTimetoken:@(16000000000000000)]; + request.type = @"reaction"; + request.value = @"smiley"; + + PNError *error = [request validate]; + + XCTAssertNil(error); + XCTAssertNotNil(request.body, @"Body should be built during validation"); + + NSDictionary *body = [NSJSONSerialization JSONObjectWithData:request.body options:0 error:nil]; + XCTAssertEqualObjects(body[@"type"], @"reaction"); + XCTAssertEqualObjects(body[@"value"], @"smiley"); +} + +- (void)testItShouldHaveNilTypeAndValueWhenAddActionCreated { + PNAddMessageActionRequest *request = [PNAddMessageActionRequest requestWithChannel:@"ch" + messageTimetoken:@(16000000000000000)]; + + XCTAssertNil(request.type, @"type should default to nil"); + XCTAssertNil(request.value, @"value should default to nil"); +} + + +#pragma mark - PNAddMessageActionRequest :: Validation + +- (void)testItShouldPassValidationWhenAllRequiredParamsProvided { + PNAddMessageActionRequest *request = [PNAddMessageActionRequest requestWithChannel:@"ch" + messageTimetoken:@(16000000000000000)]; + request.type = @"reaction"; + request.value = @"smiley"; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldFailValidationWhenChannelIsEmpty { + PNAddMessageActionRequest *request = [PNAddMessageActionRequest requestWithChannel:@"" + messageTimetoken:@(16000000000000000)]; + request.type = @"reaction"; + request.value = @"smiley"; + + XCTAssertNotNil([request validate], @"Validation should fail with empty channel"); +} + +- (void)testItShouldFailValidationWhenMessageTimetokenIsZero { + PNAddMessageActionRequest *request = [PNAddMessageActionRequest requestWithChannel:@"ch" + messageTimetoken:@(0)]; + request.type = @"reaction"; + request.value = @"smiley"; + + XCTAssertNotNil([request validate], @"Validation should fail with zero timetoken"); +} + +- (void)testItShouldFailValidationWhenTypeIsMissing { + PNAddMessageActionRequest *request = [PNAddMessageActionRequest requestWithChannel:@"ch" + messageTimetoken:@(16000000000000000)]; + request.value = @"smiley"; + + XCTAssertNotNil([request validate], @"Validation should fail without type"); +} + +- (void)testItShouldFailValidationWhenTypeIsEmpty { + PNAddMessageActionRequest *request = [PNAddMessageActionRequest requestWithChannel:@"ch" + messageTimetoken:@(16000000000000000)]; + request.type = @""; + request.value = @"smiley"; + + XCTAssertNotNil([request validate], @"Validation should fail with empty type"); +} + +- (void)testItShouldFailValidationWhenTypeExceedsMaxLength { + PNAddMessageActionRequest *request = [PNAddMessageActionRequest requestWithChannel:@"ch" + messageTimetoken:@(16000000000000000)]; + request.type = @"this-type-is-way-too-long"; + request.value = @"smiley"; + + XCTAssertNotNil([request validate], @"Validation should fail when type exceeds 15 characters"); +} + +- (void)testItShouldPassValidationWhenTypeIsExactlyMaxLength { + PNAddMessageActionRequest *request = [PNAddMessageActionRequest requestWithChannel:@"ch" + messageTimetoken:@(16000000000000000)]; + request.type = @"123456789012345"; + request.value = @"value"; + + XCTAssertNil([request validate], @"Validation should pass when type is exactly 15 characters"); +} + +- (void)testItShouldFailValidationWhenValueIsMissing { + PNAddMessageActionRequest *request = [PNAddMessageActionRequest requestWithChannel:@"ch" + messageTimetoken:@(16000000000000000)]; + request.type = @"reaction"; + + XCTAssertNotNil([request validate], @"Validation should fail without value"); +} + +- (void)testItShouldFailValidationWhenValueIsEmpty { + PNAddMessageActionRequest *request = [PNAddMessageActionRequest requestWithChannel:@"ch" + messageTimetoken:@(16000000000000000)]; + request.type = @"reaction"; + request.value = @""; + + XCTAssertNotNil([request validate], @"Validation should fail with empty value"); +} + + +#pragma mark - PNRemoveMessageActionRequest :: Construction + +- (void)testItShouldCreateRemoveMessageActionRequestWhenAllParamsProvided { + PNRemoveMessageActionRequest *request = [PNRemoveMessageActionRequest requestWithChannel:@"ch" + messageTimetoken:@(16000000000000000) + actionTimetoken:@(16000000000000001)]; + + XCTAssertNotNil(request); +} + + +#pragma mark - PNRemoveMessageActionRequest :: Validation + +- (void)testItShouldPassValidationWhenRemoveActionAllParamsProvided { + PNRemoveMessageActionRequest *request = [PNRemoveMessageActionRequest requestWithChannel:@"ch" + messageTimetoken:@(16000000000000000) + actionTimetoken:@(16000000000000001)]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldFailValidationWhenRemoveActionChannelIsEmpty { + PNRemoveMessageActionRequest *request = [PNRemoveMessageActionRequest requestWithChannel:@"" + messageTimetoken:@(16000000000000000) + actionTimetoken:@(16000000000000001)]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty channel"); +} + +- (void)testItShouldFailValidationWhenRemoveActionMessageTimetokenIsZero { + PNRemoveMessageActionRequest *request = [PNRemoveMessageActionRequest requestWithChannel:@"ch" + messageTimetoken:@(0) + actionTimetoken:@(16000000000000001)]; + + XCTAssertNotNil([request validate], @"Validation should fail with zero message timetoken"); +} + +- (void)testItShouldFailValidationWhenRemoveActionActionTimetokenIsZero { + PNRemoveMessageActionRequest *request = [PNRemoveMessageActionRequest requestWithChannel:@"ch" + messageTimetoken:@(16000000000000000) + actionTimetoken:@(0)]; + + XCTAssertNotNil([request validate], @"Validation should fail with zero action timetoken"); +} + + +#pragma mark - PNFetchMessageActionsRequest :: Construction + +- (void)testItShouldCreateFetchMessageActionsRequestWhenChannelProvided { + PNFetchMessageActionsRequest *request = [PNFetchMessageActionsRequest requestWithChannel:@"test-channel"]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldHaveDefaultLimitWhenFetchActionsCreated { + PNFetchMessageActionsRequest *request = [PNFetchMessageActionsRequest requestWithChannel:@"ch"]; + + XCTAssertEqual(request.limit, 100, @"limit should default to 100"); +} + +- (void)testItShouldHaveDefaultValuesWhenFetchActionsCreated { + PNFetchMessageActionsRequest *request = [PNFetchMessageActionsRequest requestWithChannel:@"ch"]; + + XCTAssertNil(request.start, @"start should default to nil"); + XCTAssertNil(request.end, @"end should default to nil"); +} + + +#pragma mark - PNFetchMessageActionsRequest :: Query parameters + +- (void)testItShouldIncludeStartInFetchActionsQuery { + PNFetchMessageActionsRequest *request = [PNFetchMessageActionsRequest requestWithChannel:@"ch"]; + request.start = @(16000000000000000); + + XCTAssertNotNil(request.query[@"start"]); +} + +- (void)testItShouldIncludeEndInFetchActionsQuery { + PNFetchMessageActionsRequest *request = [PNFetchMessageActionsRequest requestWithChannel:@"ch"]; + request.end = @(16100000000000000); + + XCTAssertNotNil(request.query[@"end"]); +} + +- (void)testItShouldIncludeLimitInFetchActionsQuery { + PNFetchMessageActionsRequest *request = [PNFetchMessageActionsRequest requestWithChannel:@"ch"]; + request.limit = 50; + + XCTAssertEqualObjects(request.query[@"limit"], @(50)); +} + + +#pragma mark - PNFetchMessageActionsRequest :: Validation + +- (void)testItShouldPassValidationWhenFetchActionsChannelProvided { + PNFetchMessageActionsRequest *request = [PNFetchMessageActionsRequest requestWithChannel:@"ch"]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldFailValidationWhenFetchActionsChannelIsEmpty { + PNFetchMessageActionsRequest *request = [PNFetchMessageActionsRequest requestWithChannel:@""]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty channel"); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Core/Requests/Objects/PNObjectsRequestTest.m b/Tests/Tests/Unit/Core/Requests/Objects/PNObjectsRequestTest.m new file mode 100644 index 000000000..6853aee87 --- /dev/null +++ b/Tests/Tests/Unit/Core/Requests/Objects/PNObjectsRequestTest.m @@ -0,0 +1,265 @@ +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import "PNBaseRequest+Private.h" + + +#pragma mark Interface declaration + +@interface PNObjectsRequestTest : XCTestCase + +@end + + +#pragma mark - Tests + +@implementation PNObjectsRequestTest + + +#pragma mark - PNSetUUIDMetadataRequest :: Construction + +- (void)testItShouldCreateSetUUIDMetadataRequestWhenUUIDProvided { + PNSetUUIDMetadataRequest *request = [PNSetUUIDMetadataRequest requestWithUUID:@"uuid-123"]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldCreateSetUUIDMetadataRequestWhenNilUUIDProvided { + PNSetUUIDMetadataRequest *request = [PNSetUUIDMetadataRequest requestWithUUID:nil]; + + XCTAssertNotNil(request, @"Should create request with nil UUID (will use config UUID)"); +} + +- (void)testItShouldIncludeDefaultFieldsInSetUUIDQuery { + PNSetUUIDMetadataRequest *request = [PNSetUUIDMetadataRequest requestWithUUID:@"uuid"]; + + NSString *include = request.query[@"include"]; + + XCTAssertTrue([include containsString:@"custom"]); + XCTAssertTrue([include containsString:@"status"]); + XCTAssertTrue([include containsString:@"type"]); +} + + +#pragma mark - PNSetUUIDMetadataRequest :: Body + +- (void)testItShouldIncludeAllPropertiesInBodyWhenValidated { + PNSetUUIDMetadataRequest *request = [PNSetUUIDMetadataRequest requestWithUUID:@"uuid-123"]; + request.name = @"John Doe"; + request.email = @"john@example.com"; + request.externalId = @"ext-456"; + request.profileUrl = @"https://example.com/profile"; + request.custom = @{ @"key": @"value" }; + request.status = @"active"; + request.type = @"admin"; + + PNError *error = [request validate]; + + XCTAssertNil(error); + XCTAssertNotNil(request.body); + + NSDictionary *body = [NSJSONSerialization JSONObjectWithData:request.body options:0 error:nil]; + XCTAssertEqualObjects(body[@"name"], @"John Doe"); + XCTAssertEqualObjects(body[@"email"], @"john@example.com"); + XCTAssertEqualObjects(body[@"externalId"], @"ext-456"); + XCTAssertEqualObjects(body[@"profileUrl"], @"https://example.com/profile"); + XCTAssertEqualObjects(body[@"custom"][@"key"], @"value"); + XCTAssertEqualObjects(body[@"status"], @"active"); + XCTAssertEqualObjects(body[@"type"], @"admin"); +} + + +#pragma mark - PNFetchUUIDMetadataRequest :: Construction + +- (void)testItShouldCreateFetchUUIDMetadataRequestWhenUUIDProvided { + PNFetchUUIDMetadataRequest *request = [PNFetchUUIDMetadataRequest requestWithUUID:@"uuid-123"]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldCreateFetchUUIDMetadataRequestWhenNilUUID { + PNFetchUUIDMetadataRequest *request = [PNFetchUUIDMetadataRequest requestWithUUID:nil]; + + XCTAssertNotNil(request, @"Should create request with nil UUID (will use config UUID)"); +} + +- (void)testItShouldIncludeFieldsInFetchUUIDQuery { + PNFetchUUIDMetadataRequest *request = [PNFetchUUIDMetadataRequest requestWithUUID:@"uuid"]; + request.includeFields = PNUUIDCustomField; + + NSString *include = request.query[@"include"]; + + XCTAssertTrue([include containsString:@"custom"]); +} + + +#pragma mark - PNRemoveUUIDMetadataRequest :: Construction + +- (void)testItShouldCreateRemoveUUIDMetadataRequestWhenUUIDProvided { + PNRemoveUUIDMetadataRequest *request = [PNRemoveUUIDMetadataRequest requestWithUUID:@"uuid-123"]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldCreateRemoveUUIDMetadataRequestWhenNilUUID { + PNRemoveUUIDMetadataRequest *request = [PNRemoveUUIDMetadataRequest requestWithUUID:nil]; + + XCTAssertNotNil(request, @"Should create request with nil UUID (will use config UUID)"); +} + + +#pragma mark - PNFetchAllUUIDMetadataRequest :: Construction + +- (void)testItShouldCreateFetchAllUUIDMetadataRequest { + PNFetchAllUUIDMetadataRequest *request = [PNFetchAllUUIDMetadataRequest new]; + + XCTAssertNotNil(request); +} + + +#pragma mark - PNFetchAllUUIDMetadataRequest :: Query parameters + +- (void)testItShouldIncludePaginationParametersInFetchAllUUIDQuery { + PNFetchAllUUIDMetadataRequest *request = [PNFetchAllUUIDMetadataRequest new]; + request.limit = 50; + request.start = @"cursor-abc"; + request.end = @"cursor-xyz"; + request.filter = @"name == 'John'"; + request.sort = @[@"name:asc", @"updated:desc"]; + + NSDictionary *query = request.query; + + XCTAssertEqualObjects(query[@"limit"], @(50).stringValue); + XCTAssertEqualObjects(query[@"start"], @"cursor-abc"); + XCTAssertEqualObjects(query[@"end"], @"cursor-xyz"); + XCTAssertEqualObjects(query[@"filter"], @"name == 'John'"); + XCTAssertNotNil(query[@"sort"]); + XCTAssertTrue([query[@"sort"] containsString:@"name:asc"]); +} + +- (void)testItShouldIncludeFieldsAndCountInFetchAllUUIDQuery { + PNFetchAllUUIDMetadataRequest *request = [PNFetchAllUUIDMetadataRequest new]; + request.includeFields = PNUUIDCustomField | PNUUIDTotalCountField; + + NSDictionary *query = request.query; + + XCTAssertTrue([query[@"include"] containsString:@"custom"]); + XCTAssertEqualObjects(query[@"count"], @"1"); +} + + +#pragma mark - PNSetChannelMetadataRequest :: Construction + +- (void)testItShouldCreateSetChannelMetadataRequestWhenChannelProvided { + PNSetChannelMetadataRequest *request = [PNSetChannelMetadataRequest requestWithChannel:@"my-channel"]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldIncludeDefaultFieldsInSetChannelQuery { + PNSetChannelMetadataRequest *request = [PNSetChannelMetadataRequest requestWithChannel:@"ch"]; + + NSString *include = request.query[@"include"]; + + XCTAssertTrue([include containsString:@"custom"]); + XCTAssertTrue([include containsString:@"status"]); + XCTAssertTrue([include containsString:@"type"]); +} + + +#pragma mark - PNSetChannelMetadataRequest :: Body + +- (void)testItShouldIncludeAllChannelMetadataPropertiesInBodyWhenValidated { + PNSetChannelMetadataRequest *request = [PNSetChannelMetadataRequest requestWithChannel:@"ch"]; + request.name = @"General Chat"; + request.information = @"A channel for general discussion"; + request.custom = @{ @"theme": @"dark" }; + request.status = @"active"; + request.type = @"group"; + + PNError *error = [request validate]; + + XCTAssertNil(error); + XCTAssertNotNil(request.body); + + NSDictionary *body = [NSJSONSerialization JSONObjectWithData:request.body options:0 error:nil]; + XCTAssertEqualObjects(body[@"name"], @"General Chat"); + XCTAssertEqualObjects(body[@"description"], @"A channel for general discussion"); + XCTAssertEqualObjects(body[@"custom"][@"theme"], @"dark"); + XCTAssertEqualObjects(body[@"status"], @"active"); + XCTAssertEqualObjects(body[@"type"], @"group"); +} + + +#pragma mark - PNFetchChannelMetadataRequest :: Construction + +- (void)testItShouldCreateFetchChannelMetadataRequestWhenChannelProvided { + PNFetchChannelMetadataRequest *request = [PNFetchChannelMetadataRequest requestWithChannel:@"ch"]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldIncludeFieldsInFetchChannelQuery { + PNFetchChannelMetadataRequest *request = [PNFetchChannelMetadataRequest requestWithChannel:@"ch"]; + request.includeFields = PNChannelCustomField; + + XCTAssertTrue([request.query[@"include"] containsString:@"custom"]); +} + + +#pragma mark - PNRemoveChannelMetadataRequest :: Construction + +- (void)testItShouldCreateRemoveChannelMetadataRequestWhenChannelProvided { + PNRemoveChannelMetadataRequest *request = [PNRemoveChannelMetadataRequest requestWithChannel:@"ch"]; + + XCTAssertNotNil(request); +} + + +#pragma mark - PNFetchAllChannelsMetadataRequest :: Construction + +- (void)testItShouldCreateFetchAllChannelsMetadataRequest { + PNFetchAllChannelsMetadataRequest *request = [PNFetchAllChannelsMetadataRequest new]; + + XCTAssertNotNil(request); +} + + +#pragma mark - PNFetchAllChannelsMetadataRequest :: Query parameters + +- (void)testItShouldIncludePaginationParametersInFetchAllChannelsQuery { + PNFetchAllChannelsMetadataRequest *request = [PNFetchAllChannelsMetadataRequest new]; + request.limit = 25; + request.start = @"cursor-start"; + request.filter = @"name LIKE 'gen*'"; + request.sort = @[@"name:desc"]; + + NSDictionary *query = request.query; + + XCTAssertEqualObjects(query[@"limit"], @(25).stringValue); + XCTAssertEqualObjects(query[@"start"], @"cursor-start"); + XCTAssertEqualObjects(query[@"filter"], @"name LIKE 'gen*'"); + XCTAssertTrue([query[@"sort"] containsString:@"name:desc"]); +} + +- (void)testItShouldIncludeFieldsAndCountInFetchAllChannelsQuery { + PNFetchAllChannelsMetadataRequest *request = [PNFetchAllChannelsMetadataRequest new]; + request.includeFields = PNChannelCustomField | PNChannelTotalCountField; + + NSDictionary *query = request.query; + + XCTAssertTrue([query[@"include"] containsString:@"custom"]); + XCTAssertEqualObjects(query[@"count"], @"1"); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Core/Requests/Objects/PNRelationsRequestTest.m b/Tests/Tests/Unit/Core/Requests/Objects/PNRelationsRequestTest.m new file mode 100644 index 000000000..68fbbca12 --- /dev/null +++ b/Tests/Tests/Unit/Core/Requests/Objects/PNRelationsRequestTest.m @@ -0,0 +1,385 @@ +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import "PNBaseRequest+Private.h" + + +#pragma mark Interface declaration + +@interface PNRelationsRequestTest : XCTestCase + +@end + + +#pragma mark - Tests + +@implementation PNRelationsRequestTest + + +#pragma mark - PNSetMembershipsRequest :: Construction + +- (void)testItShouldCreateSetMembershipsRequestWhenUUIDAndChannelsProvided { + NSArray *channels = @[@{ @"channel": @"ch1" }, @{ @"channel": @"ch2" }]; + PNSetMembershipsRequest *request = [PNSetMembershipsRequest requestWithUUID:@"uuid-123" channels:channels]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldCreateSetMembershipsRequestWhenNilUUIDProvided { + NSArray *channels = @[@{ @"channel": @"ch1" }]; + PNSetMembershipsRequest *request = [PNSetMembershipsRequest requestWithUUID:nil channels:channels]; + + XCTAssertNotNil(request, @"Should create request with nil UUID (will use config UUID)"); +} + +- (void)testItShouldIncludeDefaultFieldsInSetMembershipsQuery { + NSArray *channels = @[@{ @"channel": @"ch1" }]; + PNSetMembershipsRequest *request = [PNSetMembershipsRequest requestWithUUID:@"uuid" channels:channels]; + + NSDictionary *query = request.query; + + XCTAssertEqualObjects(query[@"count"], @"1"); + XCTAssertTrue([query[@"include"] containsString:@"status"]); + XCTAssertTrue([query[@"include"] containsString:@"type"]); +} + +- (void)testItShouldIncludeFieldsInSetMembershipsQuery { + NSArray *channels = @[@{ @"channel": @"ch1" }]; + PNSetMembershipsRequest *request = [PNSetMembershipsRequest requestWithUUID:@"uuid" channels:channels]; + request.includeFields = PNMembershipCustomField | PNMembershipChannelField; + + NSDictionary *query = request.query; + + XCTAssertTrue([query[@"include"] containsString:@"custom"]); + XCTAssertTrue([query[@"include"] containsString:@"channel"]); +} + +- (void)testItShouldAcceptChannelsWithCustomDataWhenSetMembershipsCreated { + NSArray *channels = @[@{ @"channel": @"ch1", @"custom": @{ @"role": @"admin" } }]; + PNSetMembershipsRequest *request = [PNSetMembershipsRequest requestWithUUID:@"uuid" channels:channels]; + + XCTAssertNotNil(request); +} + + +#pragma mark - PNRemoveMembershipsRequest :: Construction + +- (void)testItShouldCreateRemoveMembershipsRequestWhenUUIDAndChannelsProvided { + NSArray *channels = @[@"ch1", @"ch2"]; + PNRemoveMembershipsRequest *request = [PNRemoveMembershipsRequest requestWithUUID:@"uuid-123" channels:channels]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldCreateRemoveMembershipsRequestWhenNilUUIDProvided { + PNRemoveMembershipsRequest *request = [PNRemoveMembershipsRequest requestWithUUID:nil channels:@[@"ch1"]]; + + XCTAssertNotNil(request, @"Should create request with nil UUID (will use config UUID)"); +} + +- (void)testItShouldIncludeDefaultFieldsInRemoveMembershipsQuery { + PNRemoveMembershipsRequest *request = [PNRemoveMembershipsRequest requestWithUUID:@"uuid" channels:@[@"ch1"]]; + + NSDictionary *query = request.query; + + XCTAssertEqualObjects(query[@"count"], @"1"); + XCTAssertTrue([query[@"include"] containsString:@"status"]); + XCTAssertTrue([query[@"include"] containsString:@"type"]); +} + +- (void)testItShouldIncludeFieldsInRemoveMembershipsQuery { + PNRemoveMembershipsRequest *request = [PNRemoveMembershipsRequest requestWithUUID:@"uuid" channels:@[@"ch1"]]; + request.includeFields = PNMembershipCustomField; + + XCTAssertTrue([request.query[@"include"] containsString:@"custom"]); +} + + +#pragma mark - PNFetchMembershipsRequest :: Construction + +- (void)testItShouldCreateFetchMembershipsRequestWhenUUIDProvided { + PNFetchMembershipsRequest *request = [PNFetchMembershipsRequest requestWithUUID:@"uuid-123"]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldCreateFetchMembershipsRequestWhenNilUUIDProvided { + PNFetchMembershipsRequest *request = [PNFetchMembershipsRequest requestWithUUID:nil]; + + XCTAssertNotNil(request, @"Should create request with nil UUID (will use config UUID)"); +} + +- (void)testItShouldIncludeDefaultFieldsInFetchMembershipsQuery { + PNFetchMembershipsRequest *request = [PNFetchMembershipsRequest requestWithUUID:@"uuid"]; + + NSDictionary *query = request.query; + + XCTAssertEqualObjects(query[@"count"], @"1"); + XCTAssertTrue([query[@"include"] containsString:@"status"]); + XCTAssertTrue([query[@"include"] containsString:@"type"]); +} + +- (void)testItShouldIncludePaginationParametersInFetchMembershipsQuery { + PNFetchMembershipsRequest *request = [PNFetchMembershipsRequest requestWithUUID:@"uuid"]; + request.limit = 50; + request.start = @"cursor-abc"; + request.end = @"cursor-xyz"; + request.filter = @"channel.name == 'General'"; + request.sort = @[@"channel.name:asc"]; + + NSDictionary *query = request.query; + + XCTAssertEqualObjects(query[@"limit"], @(50).stringValue); + XCTAssertEqualObjects(query[@"start"], @"cursor-abc"); + XCTAssertEqualObjects(query[@"end"], @"cursor-xyz"); + XCTAssertEqualObjects(query[@"filter"], @"channel.name == 'General'"); + XCTAssertTrue([query[@"sort"] containsString:@"channel.name:asc"]); +} + +- (void)testItShouldIncludeFieldsInFetchMembershipsQuery { + PNFetchMembershipsRequest *request = [PNFetchMembershipsRequest requestWithUUID:@"uuid"]; + request.includeFields = PNMembershipCustomField | PNMembershipChannelField | PNMembershipChannelCustomField; + + NSString *include = request.query[@"include"]; + + XCTAssertTrue([include containsString:@"custom"]); + XCTAssertTrue([include containsString:@"channel"]); + XCTAssertTrue([include containsString:@"channel.custom"]); +} + + +#pragma mark - PNManageMembershipsRequest :: Construction + +- (void)testItShouldCreateManageMembershipsRequestWhenUUIDProvided { + PNManageMembershipsRequest *request = [PNManageMembershipsRequest requestWithUUID:@"uuid-123"]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldCreateManageMembershipsRequestWhenNilUUIDProvided { + PNManageMembershipsRequest *request = [PNManageMembershipsRequest requestWithUUID:nil]; + + XCTAssertNotNil(request, @"Should create request with nil UUID (will use config UUID)"); +} + +- (void)testItShouldIncludeDefaultFieldsInManageMembershipsQuery { + PNManageMembershipsRequest *request = [PNManageMembershipsRequest requestWithUUID:@"uuid"]; + + NSDictionary *query = request.query; + + XCTAssertEqualObjects(query[@"count"], @"1"); + XCTAssertTrue([query[@"include"] containsString:@"status"]); + XCTAssertTrue([query[@"include"] containsString:@"type"]); +} + +- (void)testItShouldSetSetChannelsWhenManageMembershipsConfigured { + PNManageMembershipsRequest *request = [PNManageMembershipsRequest requestWithUUID:@"uuid"]; + request.setChannels = @[@{ @"channel": @"ch1", @"custom": @{ @"role": @"member" } }]; + + XCTAssertNotNil(request.setChannels); + XCTAssertEqual(request.setChannels.count, 1); +} + +- (void)testItShouldSetRemoveChannelsWhenManageMembershipsConfigured { + PNManageMembershipsRequest *request = [PNManageMembershipsRequest requestWithUUID:@"uuid"]; + request.removeChannels = @[@"ch-old-1", @"ch-old-2"]; + + XCTAssertNotNil(request.removeChannels); + XCTAssertEqual(request.removeChannels.count, 2); +} + +- (void)testItShouldSetBothSetAndRemoveChannelsWhenManageMembershipsConfigured { + PNManageMembershipsRequest *request = [PNManageMembershipsRequest requestWithUUID:@"uuid"]; + request.setChannels = @[@{ @"channel": @"ch-new" }]; + request.removeChannels = @[@"ch-old"]; + + XCTAssertNotNil(request.setChannels); + XCTAssertNotNil(request.removeChannels); +} + + +#pragma mark - PNSetChannelMembersRequest :: Construction + +- (void)testItShouldCreateSetChannelMembersRequestWhenChannelAndUUIDsProvided { + NSArray *uuids = @[@{ @"uuid": @"uuid-1" }, @{ @"uuid": @"uuid-2" }]; + PNSetChannelMembersRequest *request = [PNSetChannelMembersRequest requestWithChannel:@"my-channel" uuids:uuids]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldIncludeDefaultFieldsInSetChannelMembersQuery { + NSArray *uuids = @[@{ @"uuid": @"uuid-1" }]; + PNSetChannelMembersRequest *request = [PNSetChannelMembersRequest requestWithChannel:@"ch" uuids:uuids]; + + NSDictionary *query = request.query; + + XCTAssertEqualObjects(query[@"count"], @"1"); + XCTAssertTrue([query[@"include"] containsString:@"status"]); + XCTAssertTrue([query[@"include"] containsString:@"type"]); +} + +- (void)testItShouldIncludeFieldsInSetChannelMembersQuery { + NSArray *uuids = @[@{ @"uuid": @"uuid-1" }]; + PNSetChannelMembersRequest *request = [PNSetChannelMembersRequest requestWithChannel:@"ch" uuids:uuids]; + request.includeFields = PNChannelMemberCustomField | PNChannelMemberUUIDField; + + NSString *include = request.query[@"include"]; + + XCTAssertTrue([include containsString:@"custom"]); + XCTAssertTrue([include containsString:@"uuid"]); +} + +- (void)testItShouldAcceptUUIDsWithCustomDataWhenSetChannelMembersCreated { + NSArray *uuids = @[@{ @"uuid": @"uuid-1", @"custom": @{ @"role": @"moderator" } }]; + PNSetChannelMembersRequest *request = [PNSetChannelMembersRequest requestWithChannel:@"ch" uuids:uuids]; + + XCTAssertNotNil(request); +} + + +#pragma mark - PNRemoveChannelMembersRequest :: Construction + +- (void)testItShouldCreateRemoveChannelMembersRequestWhenChannelAndUUIDsProvided { + NSArray *uuids = @[@"uuid-1", @"uuid-2"]; + PNRemoveChannelMembersRequest *request = [PNRemoveChannelMembersRequest requestWithChannel:@"my-channel" + uuids:uuids]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldIncludeDefaultFieldsInRemoveChannelMembersQuery { + PNRemoveChannelMembersRequest *request = [PNRemoveChannelMembersRequest requestWithChannel:@"ch" + uuids:@[@"uuid-1"]]; + + NSDictionary *query = request.query; + + XCTAssertEqualObjects(query[@"count"], @"1"); + XCTAssertTrue([query[@"include"] containsString:@"status"]); + XCTAssertTrue([query[@"include"] containsString:@"type"]); +} + +- (void)testItShouldIncludeFieldsInRemoveChannelMembersQuery { + PNRemoveChannelMembersRequest *request = [PNRemoveChannelMembersRequest requestWithChannel:@"ch" + uuids:@[@"uuid-1"]]; + request.includeFields = PNChannelMemberCustomField; + + XCTAssertTrue([request.query[@"include"] containsString:@"custom"]); +} + + +#pragma mark - PNFetchChannelMembersRequest :: Construction + +- (void)testItShouldCreateFetchChannelMembersRequestWhenChannelProvided { + PNFetchChannelMembersRequest *request = [PNFetchChannelMembersRequest requestWithChannel:@"my-channel"]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldIncludeDefaultFieldsInFetchChannelMembersQuery { + PNFetchChannelMembersRequest *request = [PNFetchChannelMembersRequest requestWithChannel:@"ch"]; + + NSDictionary *query = request.query; + + XCTAssertEqualObjects(query[@"count"], @"1"); + XCTAssertTrue([query[@"include"] containsString:@"status"]); + XCTAssertTrue([query[@"include"] containsString:@"type"]); +} + +- (void)testItShouldIncludePaginationParametersInFetchChannelMembersQuery { + PNFetchChannelMembersRequest *request = [PNFetchChannelMembersRequest requestWithChannel:@"ch"]; + request.limit = 25; + request.start = @"cursor-start"; + request.end = @"cursor-end"; + request.filter = @"uuid.name LIKE 'John*'"; + request.sort = @[@"uuid.name:asc", @"updated:desc"]; + + NSDictionary *query = request.query; + + XCTAssertEqualObjects(query[@"limit"], @(25).stringValue); + XCTAssertEqualObjects(query[@"start"], @"cursor-start"); + XCTAssertEqualObjects(query[@"end"], @"cursor-end"); + XCTAssertEqualObjects(query[@"filter"], @"uuid.name LIKE 'John*'"); + XCTAssertTrue([query[@"sort"] containsString:@"uuid.name:asc"]); +} + +- (void)testItShouldIncludeFieldsInFetchChannelMembersQuery { + PNFetchChannelMembersRequest *request = [PNFetchChannelMembersRequest requestWithChannel:@"ch"]; + request.includeFields = PNChannelMemberCustomField | PNChannelMemberUUIDField | PNChannelMemberUUIDCustomField; + + NSString *include = request.query[@"include"]; + + XCTAssertTrue([include containsString:@"custom"]); + XCTAssertTrue([include containsString:@"uuid"]); + XCTAssertTrue([include containsString:@"uuid.custom"]); +} + + +#pragma mark - PNManageChannelMembersRequest :: Construction + +- (void)testItShouldCreateManageChannelMembersRequestWhenChannelProvided { + PNManageChannelMembersRequest *request = [PNManageChannelMembersRequest requestWithChannel:@"my-channel"]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldIncludeDefaultFieldsInManageChannelMembersQuery { + PNManageChannelMembersRequest *request = [PNManageChannelMembersRequest requestWithChannel:@"ch"]; + + NSDictionary *query = request.query; + + XCTAssertEqualObjects(query[@"count"], @"1"); + XCTAssertTrue([query[@"include"] containsString:@"status"]); + XCTAssertTrue([query[@"include"] containsString:@"type"]); +} + +- (void)testItShouldSetSetMembersWhenManageChannelMembersConfigured { + PNManageChannelMembersRequest *request = [PNManageChannelMembersRequest requestWithChannel:@"ch"]; + request.setMembers = @[@{ @"uuid": @"uuid-1", @"custom": @{ @"role": @"admin" } }]; + + XCTAssertNotNil(request.setMembers); + XCTAssertEqual(request.setMembers.count, 1); +} + +- (void)testItShouldSetRemoveMembersWhenManageChannelMembersConfigured { + PNManageChannelMembersRequest *request = [PNManageChannelMembersRequest requestWithChannel:@"ch"]; + request.removeMembers = @[@"uuid-old-1", @"uuid-old-2"]; + + XCTAssertNotNil(request.removeMembers); + XCTAssertEqual(request.removeMembers.count, 2); +} + +- (void)testItShouldSetBothSetAndRemoveMembersWhenManageChannelMembersConfigured { + PNManageChannelMembersRequest *request = [PNManageChannelMembersRequest requestWithChannel:@"ch"]; + request.setMembers = @[@{ @"uuid": @"uuid-new" }]; + request.removeMembers = @[@"uuid-old"]; + + XCTAssertNotNil(request.setMembers); + XCTAssertNotNil(request.removeMembers); +} + +- (void)testItShouldIncludePaginationParametersInManageChannelMembersQuery { + PNManageChannelMembersRequest *request = [PNManageChannelMembersRequest requestWithChannel:@"ch"]; + request.limit = 10; + request.start = @"page-start"; + request.filter = @"uuid.name == 'test'"; + request.sort = @[@"updated:desc"]; + + NSDictionary *query = request.query; + + XCTAssertEqualObjects(query[@"limit"], @(10).stringValue); + XCTAssertEqualObjects(query[@"start"], @"page-start"); + XCTAssertEqualObjects(query[@"filter"], @"uuid.name == 'test'"); + XCTAssertTrue([query[@"sort"] containsString:@"updated:desc"]); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Core/Requests/Presence/PNPresenceRequestTest.m b/Tests/Tests/Unit/Core/Requests/Presence/PNPresenceRequestTest.m new file mode 100644 index 000000000..e6d5a195e --- /dev/null +++ b/Tests/Tests/Unit/Core/Requests/Presence/PNPresenceRequestTest.m @@ -0,0 +1,270 @@ +#import +#import +#import +#import +#import +#import +#import +#import "PNBaseRequest+Private.h" + + +#pragma mark Interface declaration + +@interface PNPresenceRequestTest : XCTestCase + +@end + + +#pragma mark - Tests + +@implementation PNPresenceRequestTest + + +#pragma mark - PNHereNowRequest :: Construction (channels) + +- (void)testItShouldCreateHereNowRequestWhenChannelsProvided { + PNHereNowRequest *request = [PNHereNowRequest requestForChannels:@[@"ch1", @"ch2"]]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.channels, (@[@"ch1", @"ch2"])); +} + +- (void)testItShouldHaveDefaultVerbosityInQueryWhenChannelHereNowCreated { + PNHereNowRequest *request = [PNHereNowRequest requestForChannels:@[@"ch1"]]; + + NSDictionary *query = request.query; + + // Default PNHereNowState: UUIDs enabled, state enabled. + XCTAssertEqualObjects(query[@"disable_uuids"], @"0"); + XCTAssertEqualObjects(query[@"state"], @"1"); +} + +- (void)testItShouldHaveDefaultLimitInQueryWhenChannelHereNowCreated { + PNHereNowRequest *request = [PNHereNowRequest requestForChannels:@[@"ch1"]]; + + XCTAssertEqualObjects(request.query[@"limit"], @(1000).stringValue, @"Default limit should be 1000"); +} + + +#pragma mark - PNHereNowRequest :: Construction (channel groups) + +- (void)testItShouldCreateHereNowRequestWhenChannelGroupsProvided { + PNHereNowRequest *request = [PNHereNowRequest requestForChannelGroups:@[@"grp1"]]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.query[@"channel-group"], @"grp1"); +} + + +#pragma mark - PNHereNowRequest :: Construction (global) + +- (void)testItShouldCreateGlobalHereNowRequestWhenRequested { + PNHereNowRequest *request = [PNHereNowRequest requestGlobal]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldPassValidationWhenGlobalRequest { + PNHereNowRequest *request = [PNHereNowRequest requestGlobal]; + + XCTAssertNil([request validate], @"Global here now request should pass validation"); +} + + +#pragma mark - PNHereNowRequest :: Query parameters + +- (void)testItShouldSetOccupancyOnlyInQueryWhenVerbosityConfigured { + PNHereNowRequest *request = [PNHereNowRequest requestForChannels:@[@"ch"]]; + request.verbosityLevel = PNHereNowOccupancy; + + NSDictionary *query = request.query; + + // Occupancy: no UUIDs, no state. + XCTAssertEqualObjects(query[@"disable_uuids"], @"1"); + XCTAssertEqualObjects(query[@"state"], @"0"); +} + +- (void)testItShouldCapLimitInQueryWhenExceedingMaximum { + PNHereNowRequest *request = [PNHereNowRequest requestForChannels:@[@"ch"]]; + request.limit = 5000; + + XCTAssertEqualObjects(request.query[@"limit"], @(1000).stringValue, @"Limit should be capped at 1000"); +} + +- (void)testItShouldIncludeLimitInQueryWhenBelowMaximum { + PNHereNowRequest *request = [PNHereNowRequest requestForChannels:@[@"ch"]]; + request.limit = 500; + + XCTAssertEqualObjects(request.query[@"limit"], @(500).stringValue); +} + +- (void)testItShouldIncludeArbitraryParametersInHereNowQuery { + PNHereNowRequest *request = [PNHereNowRequest requestForChannels:@[@"ch"]]; + request.arbitraryQueryParameters = @{ @"key": @"value" }; + + XCTAssertEqualObjects(request.query[@"key"], @"value"); +} + + +#pragma mark - PNHereNowRequest :: Validation + +- (void)testItShouldPassValidationWhenChannelsProvided { + PNHereNowRequest *request = [PNHereNowRequest requestForChannels:@[@"ch1"]]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldFailValidationWhenEmptyChannelsArrayProvided { + PNHereNowRequest *request = [PNHereNowRequest requestForChannels:@[]]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty channels"); +} + +- (void)testItShouldPassValidationWhenChannelGroupsProvided { + PNHereNowRequest *request = [PNHereNowRequest requestForChannelGroups:@[@"grp1"]]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldFailValidationWhenEmptyChannelGroupsProvided { + PNHereNowRequest *request = [PNHereNowRequest requestForChannelGroups:@[]]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty channel groups"); +} + + +#pragma mark - PNWhereNowRequest :: Construction + +- (void)testItShouldCreateWhereNowRequestWhenUserIdProvided { + PNWhereNowRequest *request = [PNWhereNowRequest requestForUserId:@"user-123"]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.userId, @"user-123"); +} + + +#pragma mark - PNWhereNowRequest :: Validation + +- (void)testItShouldPassValidationWhenUserIdProvided { + PNWhereNowRequest *request = [PNWhereNowRequest requestForUserId:@"user-123"]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldFailValidationWhenUserIdIsEmpty { + PNWhereNowRequest *request = [PNWhereNowRequest requestForUserId:@""]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty userId"); +} + + +#pragma mark - PNPresenceHeartbeatRequest :: Construction + +- (void)testItShouldCreateHeartbeatRequestWhenChannelsProvided { + PNPresenceHeartbeatRequest *request = [PNPresenceHeartbeatRequest requestWithHeartbeat:300 + channels:@[@"ch1"] + channelGroups:nil]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.channels, @[@"ch1"]); + XCTAssertEqual(request.presenceHeartbeatValue, 300); +} + +- (void)testItShouldIncludeHeartbeatValueInQuery { + PNPresenceHeartbeatRequest *request = [PNPresenceHeartbeatRequest requestWithHeartbeat:300 + channels:@[@"ch"] + channelGroups:nil]; + + XCTAssertEqualObjects(request.query[@"heartbeat"], @(300).stringValue); +} + +- (void)testItShouldCreateHeartbeatRequestWhenChannelGroupsProvided { + PNPresenceHeartbeatRequest *request = [PNPresenceHeartbeatRequest requestWithHeartbeat:120 + channels:nil + channelGroups:@[@"grp1"]]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.query[@"channel-group"], @"grp1"); +} + +- (void)testItShouldIncludeStateInHeartbeatQuery { + PNPresenceHeartbeatRequest *request = [PNPresenceHeartbeatRequest requestWithHeartbeat:300 + channels:@[@"ch"] + channelGroups:nil]; + request.state = @{ @"ch": @{ @"key": @"value" } }; + + XCTAssertNotNil(request.query[@"state"]); +} + + +#pragma mark - PNPresenceStateSetRequest :: Construction + +- (void)testItShouldCreateStateSetRequestWhenUserIdProvided { + PNPresenceStateSetRequest *request = [PNPresenceStateSetRequest requestWithUserId:@"user-123"]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.userId, @"user-123"); +} + +- (void)testItShouldHaveDefaultValuesWhenStateSetCreated { + PNPresenceStateSetRequest *request = [PNPresenceStateSetRequest requestWithUserId:@"user-123"]; + + XCTAssertNil(request.channels); + XCTAssertNil(request.channelGroups); + XCTAssertNil(request.state); +} + + +#pragma mark - PNPresenceStateSetRequest :: Query parameters + +- (void)testItShouldIncludeStateInSetStateQuery { + PNPresenceStateSetRequest *request = [PNPresenceStateSetRequest requestWithUserId:@"user-123"]; + request.channels = @[@"ch1", @"ch2"]; + request.state = @{ @"mood": @"happy" }; + + NSString *stateJSON = request.query[@"state"]; + + XCTAssertNotNil(stateJSON); + NSData *stateData = [stateJSON dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *state = [NSJSONSerialization JSONObjectWithData:stateData options:0 error:nil]; + XCTAssertEqualObjects(state[@"mood"], @"happy"); +} + +- (void)testItShouldIncludeChannelGroupsInSetStateQuery { + PNPresenceStateSetRequest *request = [PNPresenceStateSetRequest requestWithUserId:@"user-123"]; + request.channelGroups = @[@"grp1"]; + + XCTAssertEqualObjects(request.query[@"channel-group"], @"grp1"); +} + + +#pragma mark - PNPresenceStateFetchRequest :: Construction + +- (void)testItShouldCreateStateFetchRequestWhenUserIdProvided { + PNPresenceStateFetchRequest *request = [PNPresenceStateFetchRequest requestWithUserId:@"user-123"]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.userId, @"user-123"); +} + +- (void)testItShouldHaveDefaultValuesWhenStateFetchCreated { + PNPresenceStateFetchRequest *request = [PNPresenceStateFetchRequest requestWithUserId:@"user-123"]; + + XCTAssertNil(request.channels); + XCTAssertNil(request.channelGroups); +} + + +#pragma mark - PNPresenceStateFetchRequest :: Query parameters + +- (void)testItShouldIncludeChannelGroupsInStateFetchQuery { + PNPresenceStateFetchRequest *request = [PNPresenceStateFetchRequest requestWithUserId:@"user-123"]; + request.channelGroups = @[@"grp1"]; + + XCTAssertEqualObjects(request.query[@"channel-group"], @"grp1"); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Core/Requests/Publish/PNPublishRequestTest.m b/Tests/Tests/Unit/Core/Requests/Publish/PNPublishRequestTest.m new file mode 100644 index 000000000..6ea956c2a --- /dev/null +++ b/Tests/Tests/Unit/Core/Requests/Publish/PNPublishRequestTest.m @@ -0,0 +1,163 @@ +#import +#import +#import +#import +#import +#import "PNBaseRequest+Private.h" + + +#pragma mark Interface declaration + +@interface PNPublishRequestTest : XCTestCase + +@end + + +#pragma mark - Tests + +@implementation PNPublishRequestTest + + +#pragma mark - PNPublishRequest :: Construction + +- (void)testItShouldCreatePublishRequestWhenChannelProvided { + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@"test-channel"]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.channel, @"test-channel"); +} + +- (void)testItShouldHaveDefaultValuesWhenPublishRequestCreated { + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@"test-channel"]; + + XCTAssertTrue(request.shouldReplicate, @"Replicate should default to YES"); + XCTAssertTrue(request.shouldStore, @"Store should default to YES"); + XCTAssertFalse(request.shouldCompress, @"Compress should default to NO"); + XCTAssertEqual(request.ttl, 0, @"TTL should default to 0"); + XCTAssertNil(request.metadata, @"Metadata should default to nil"); + XCTAssertNil(request.message, @"Message should default to nil"); + XCTAssertNil(request.payloads, @"Payloads should default to nil"); + XCTAssertNil(request.customMessageType, @"customMessageType should default to nil"); +} + + +#pragma mark - PNPublishRequest :: Query parameters + +- (void)testItShouldIncludeStoreDisabledInQuery { + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@"ch"]; + request.store = NO; + + XCTAssertEqualObjects(request.query[@"store"], @"0"); +} + +- (void)testItShouldIncludeReplicateDisabledInQuery { + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@"ch"]; + request.replicate = NO; + + XCTAssertEqualObjects(request.query[@"norep"], @"true"); +} + +- (void)testItShouldIncludeTTLInQueryWhenStoredWithTTL { + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@"ch"]; + request.store = YES; + request.ttl = 300; + + XCTAssertEqualObjects(request.query[@"ttl"], @(300).stringValue); +} + +- (void)testItShouldNotIncludeTTLInQueryWhenStoreDisabled { + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@"ch"]; + request.store = NO; + request.ttl = 300; + + XCTAssertNil(request.query[@"ttl"], @"TTL should not appear when store is disabled"); +} + +- (void)testItShouldIncludeCustomMessageTypeInQuery { + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@"ch"]; + request.customMessageType = @"text-message"; + + XCTAssertEqualObjects(request.query[@"custom_message_type"], @"text-message"); +} + +- (void)testItShouldIncludeArbitraryParametersInPublishQuery { + PNPublishRequest *request = [PNPublishRequest requestWithChannel:@"ch"]; + request.arbitraryQueryParameters = @{ @"key1": @"value1" }; + + XCTAssertEqualObjects(request.query[@"key1"], @"value1"); +} + + +#pragma mark - PNSignalRequest :: Construction + +- (void)testItShouldCreateSignalRequestWhenChannelAndDataProvided { + PNSignalRequest *request = [PNSignalRequest requestWithChannel:@"signal-ch" signal:@"status-update"]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.channel, @"signal-ch"); +} + +- (void)testItShouldAcceptDictionarySignalDataWhenCreated { + NSDictionary *signalData = @{ @"temperature": @72.5 }; + PNSignalRequest *request = [PNSignalRequest requestWithChannel:@"sensors" signal:signalData]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.channel, @"sensors"); +} + + +#pragma mark - PNSignalRequest :: Query parameters + +- (void)testItShouldIncludeCustomMessageTypeInSignalQuery { + PNSignalRequest *request = [PNSignalRequest requestWithChannel:@"ch" signal:@"data"]; + request.customMessageType = @"sensor-update"; + + XCTAssertEqualObjects(request.query[@"custom_message_type"], @"sensor-update"); +} + + +#pragma mark - PNPublishFileMessageRequest :: Construction + +- (void)testItShouldCreateFileMessageRequestWhenAllRequiredParamsProvided { + PNPublishFileMessageRequest *request = [PNPublishFileMessageRequest requestWithChannel:@"file-ch" + fileIdentifier:@"file-id-123" + name:@"photo.jpg"]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.channel, @"file-ch"); + XCTAssertEqualObjects(request.identifier, @"file-id-123"); + XCTAssertEqualObjects(request.filename, @"photo.jpg"); +} + +- (void)testItShouldInheritBasePublishDefaultsWhenFileMessageRequestCreated { + PNPublishFileMessageRequest *request = [PNPublishFileMessageRequest requestWithChannel:@"ch" + fileIdentifier:@"id" + name:@"file.txt"]; + + XCTAssertTrue(request.shouldReplicate, @"Replicate should default to YES from base"); + XCTAssertTrue(request.shouldStore, @"Store should default to YES from base"); + XCTAssertEqual(request.ttl, 0, @"TTL should default to 0 from base"); +} + +- (void)testItShouldIncludeStoreDisabledInFileMessageQuery { + PNPublishFileMessageRequest *request = [PNPublishFileMessageRequest requestWithChannel:@"ch" + fileIdentifier:@"id" + name:@"file.txt"]; + request.store = NO; + + XCTAssertEqualObjects(request.query[@"store"], @"0"); +} + +- (void)testItShouldIncludeCustomMessageTypeInFileMessageQuery { + PNPublishFileMessageRequest *request = [PNPublishFileMessageRequest requestWithChannel:@"ch" + fileIdentifier:@"id" + name:@"file.txt"]; + request.customMessageType = @"file-share"; + + XCTAssertEqualObjects(request.query[@"custom_message_type"], @"file-share"); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Core/Requests/Push/PNPushRequestTest.m b/Tests/Tests/Unit/Core/Requests/Push/PNPushRequestTest.m new file mode 100644 index 000000000..38adb0e7f --- /dev/null +++ b/Tests/Tests/Unit/Core/Requests/Push/PNPushRequestTest.m @@ -0,0 +1,347 @@ +#import +#import +#import +#import +#import +#import "PNBaseRequest+Private.h" + + +#pragma mark Interface declaration + +@interface PNPushRequestTest : XCTestCase + +@end + + +#pragma mark - Tests + +@implementation PNPushRequestTest + + +#pragma mark - Helper + +- (NSData *)sampleDeviceToken { + unsigned char bytes[] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20 + }; + return [NSData dataWithBytes:bytes length:sizeof(bytes)]; +} + + +#pragma mark - PNPushNotificationManageRequest :: Add channels (APNS) + +- (void)testItShouldCreateAddChannelsRequestWhenAPNSTokenProvided { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToAddChannels:@[@"ch1", @"ch2"] + toDeviceWithToken:token + pushType:PNAPNSPush]; + + XCTAssertNotNil(request); + XCTAssertEqual(request.pushType, PNAPNSPush); + XCTAssertEqualObjects(request.query[@"type"], @"apns"); +} + +- (void)testItShouldIncludeChannelsInAddQuery { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToAddChannels:@[@"ch1", @"ch2"] + toDeviceWithToken:token + pushType:PNAPNSPush]; + + NSString *addChannels = request.query[@"add"]; + + XCTAssertNotNil(addChannels); + XCTAssertTrue([addChannels containsString:@"ch1"]); + XCTAssertTrue([addChannels containsString:@"ch2"]); +} + +- (void)testItShouldRetainPushTokenWhenAPNSRequestCreated { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToAddChannels:@[@"ch"] + toDeviceWithToken:token + pushType:PNAPNSPush]; + + XCTAssertNotNil(request.pushToken); + XCTAssertTrue([request.pushToken isKindOfClass:[NSData class]]); +} + + +#pragma mark - PNPushNotificationManageRequest :: Add channels (APNS2) + +- (void)testItShouldCreateAddChannelsRequestWhenAPNS2TokenProvided { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToAddChannels:@[@"ch1"] + toDeviceWithToken:token + pushType:PNAPNS2Push]; + + XCTAssertNotNil(request); + XCTAssertEqual(request.pushType, PNAPNS2Push); +} + +- (void)testItShouldHaveDefaultEnvironmentInQueryWhenAPNS2RequestCreated { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToAddChannels:@[@"ch"] + toDeviceWithToken:token + pushType:PNAPNS2Push]; + + XCTAssertEqualObjects(request.query[@"environment"], @"development", + @"Default environment should be development"); +} + +- (void)testItShouldIncludeEnvironmentInQueryWhenAPNS2Configured { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToAddChannels:@[@"ch"] + toDeviceWithToken:token + pushType:PNAPNS2Push]; + request.environment = PNAPNSProduction; + + XCTAssertEqualObjects(request.query[@"environment"], @"production"); +} + +- (void)testItShouldIncludeTopicInQueryWhenAPNS2Configured { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToAddChannels:@[@"ch"] + toDeviceWithToken:token + pushType:PNAPNS2Push]; + request.topic = @"com.example.myapp"; + + XCTAssertEqualObjects(request.query[@"topic"], @"com.example.myapp"); +} + + +#pragma mark - PNPushNotificationManageRequest :: Add channels (FCM) + +- (void)testItShouldCreateAddChannelsRequestWhenFCMTokenProvided { + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToAddChannels:@[@"ch1"] + toDeviceWithToken:@"fcm-device-token" + pushType:PNFCMPush]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.query[@"type"], @"gcm"); +} + +- (void)testItShouldRetainStringTokenWhenFCMRequestCreated { + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToAddChannels:@[@"ch"] + toDeviceWithToken:@"fcm-token-123" + pushType:PNFCMPush]; + + XCTAssertNotNil(request.pushToken); + XCTAssertTrue([request.pushToken isKindOfClass:[NSString class]]); +} + + +#pragma mark - PNPushNotificationManageRequest :: Remove channels + +- (void)testItShouldIncludeChannelsInRemoveQuery { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToRemoveChannels:@[@"ch1"] + fromDeviceWithToken:token + pushType:PNAPNSPush]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.query[@"remove"], @"ch1"); +} + + +#pragma mark - PNPushNotificationManageRequest :: Remove all (device) + +- (void)testItShouldCreateRemoveDeviceRequestWhenTokenProvided { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToRemoveDeviceWithToken:token + pushType:PNAPNSPush]; + + XCTAssertNotNil(request); + XCTAssertNil(request.channels, @"channels should be nil for remove device request"); +} + + +#pragma mark - PNPushNotificationManageRequest :: Arbitrary query params + +- (void)testItShouldIncludeArbitraryParametersInManageQuery { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToAddChannels:@[@"ch"] + toDeviceWithToken:token + pushType:PNAPNSPush]; + request.arbitraryQueryParameters = @{ @"key": @"value" }; + + XCTAssertEqualObjects(request.query[@"key"], @"value"); +} + + +#pragma mark - PNPushNotificationManageRequest :: Validation + +- (void)testItShouldPassValidationWhenAddChannelsWithAPNSToken { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToAddChannels:@[@"ch"] + toDeviceWithToken:token + pushType:PNAPNSPush]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldPassValidationWhenAddChannelsWithFCMToken { + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToAddChannels:@[@"ch"] + toDeviceWithToken:@"fcm-token" + pushType:PNFCMPush]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldPassValidationWhenRemoveDeviceWithToken { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToRemoveDeviceWithToken:token + pushType:PNAPNSPush]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldFailValidationWhenAPNSTokenIsNotNSData { + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToAddChannels:@[@"ch"] + toDeviceWithToken:@"wrong-type" + pushType:PNAPNSPush]; + + XCTAssertNotNil([request validate], @"Validation should fail when APNS token is not NSData"); +} + +- (void)testItShouldFailValidationWhenFCMTokenIsNotNSString { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToAddChannels:@[@"ch"] + toDeviceWithToken:token + pushType:PNFCMPush]; + + XCTAssertNotNil([request validate], @"Validation should fail when FCM token is not NSString"); +} + +- (void)testItShouldFailValidationWhenAddChannelsListIsEmpty { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToAddChannels:@[] + toDeviceWithToken:token + pushType:PNAPNSPush]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty channels list for add"); +} + +- (void)testItShouldFailValidationWhenAPNS2TopicIsEmptyAndBundleIdNil { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationManageRequest *request = [PNPushNotificationManageRequest requestToAddChannels:@[@"ch"] + toDeviceWithToken:token + pushType:PNAPNS2Push]; + request.topic = @""; + + // Validate returns an error when topic is empty (even though query falls back to bundle ID). + PNError *error = [request validate]; + XCTAssertNotNil(error, @"Validation should fail when APNS2 topic is empty"); +} + + +#pragma mark - PNPushNotificationFetchRequest :: Construction + +- (void)testItShouldCreateFetchRequestWhenAPNSTokenProvided { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationFetchRequest *request = [PNPushNotificationFetchRequest requestWithDevicePushToken:token + pushType:PNAPNSPush]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.query[@"type"], @"apns"); +} + +- (void)testItShouldCreateFetchRequestWhenAPNS2TokenProvided { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationFetchRequest *request = [PNPushNotificationFetchRequest requestWithDevicePushToken:token + pushType:PNAPNS2Push]; + + XCTAssertNotNil(request); + XCTAssertEqual(request.pushType, PNAPNS2Push); +} + +- (void)testItShouldCreateFetchRequestWhenFCMTokenProvided { + PNPushNotificationFetchRequest *request = [PNPushNotificationFetchRequest requestWithDevicePushToken:@"fcm-token" + pushType:PNFCMPush]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.query[@"type"], @"gcm"); +} + +- (void)testItShouldHaveDefaultEnvironmentInQueryWhenFetchPushCreated { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationFetchRequest *request = [PNPushNotificationFetchRequest requestWithDevicePushToken:token + pushType:PNAPNS2Push]; + + XCTAssertEqualObjects(request.query[@"environment"], @"development", + @"Default environment should be development"); +} + +- (void)testItShouldIncludeArbitraryParametersInFetchPushQuery { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationFetchRequest *request = [PNPushNotificationFetchRequest requestWithDevicePushToken:token + pushType:PNAPNSPush]; + request.arbitraryQueryParameters = @{ @"custom": @"param" }; + + XCTAssertEqualObjects(request.query[@"custom"], @"param"); +} + +- (void)testItShouldIncludeEnvironmentAndTopicInQueryWhenFetchAPNS2Configured { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationFetchRequest *request = [PNPushNotificationFetchRequest requestWithDevicePushToken:token + pushType:PNAPNS2Push]; + request.environment = PNAPNSProduction; + request.topic = @"com.example.app"; + + XCTAssertEqualObjects(request.query[@"environment"], @"production"); + XCTAssertEqualObjects(request.query[@"topic"], @"com.example.app"); +} + + +#pragma mark - PNPushNotificationFetchRequest :: Validation + +- (void)testItShouldPassValidationWhenFetchWithAPNSToken { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationFetchRequest *request = [PNPushNotificationFetchRequest requestWithDevicePushToken:token + pushType:PNAPNSPush]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldPassValidationWhenFetchWithFCMToken { + PNPushNotificationFetchRequest *request = [PNPushNotificationFetchRequest requestWithDevicePushToken:@"fcm-token" + pushType:PNFCMPush]; + + XCTAssertNil([request validate]); +} + +- (void)testItShouldFailValidationWhenFetchAPNSTokenIsNotNSData { + PNPushNotificationFetchRequest *request = [PNPushNotificationFetchRequest requestWithDevicePushToken:@"wrong-type" + pushType:PNAPNSPush]; + + XCTAssertNotNil([request validate], @"Validation should fail when APNS token is not NSData"); +} + +- (void)testItShouldFailValidationWhenFetchFCMTokenIsNotNSString { + NSData *token = [self sampleDeviceToken]; + PNPushNotificationFetchRequest *request = [PNPushNotificationFetchRequest requestWithDevicePushToken:token + pushType:PNFCMPush]; + + XCTAssertNotNil([request validate], @"Validation should fail when FCM token is not NSString"); +} + +- (void)testItShouldFailValidationWhenFetchTokenIsEmptyData { + NSData *emptyToken = [NSData data]; + PNPushNotificationFetchRequest *request = [PNPushNotificationFetchRequest requestWithDevicePushToken:emptyToken + pushType:PNAPNSPush]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty NSData token"); +} + +- (void)testItShouldFailValidationWhenFetchTokenIsEmptyString { + PNPushNotificationFetchRequest *request = [PNPushNotificationFetchRequest requestWithDevicePushToken:@"" + pushType:PNFCMPush]; + + XCTAssertNotNil([request validate], @"Validation should fail with empty string token"); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Core/Requests/Subscribe/PNSubscribeRequestTest.m b/Tests/Tests/Unit/Core/Requests/Subscribe/PNSubscribeRequestTest.m new file mode 100644 index 000000000..2ff92b939 --- /dev/null +++ b/Tests/Tests/Unit/Core/Requests/Subscribe/PNSubscribeRequestTest.m @@ -0,0 +1,187 @@ +#import +#import +#import +#import "PNBaseRequest+Private.h" + + +#pragma mark Interface declaration + +@interface PNSubscribeRequestTest : XCTestCase + +@end + + +#pragma mark - Tests + +@implementation PNSubscribeRequestTest + + +#pragma mark - PNSubscribeRequest :: Construction with channels + +- (void)testItShouldCreateSubscribeRequestWhenChannelsProvided { + NSArray *channels = @[@"channel1", @"channel2"]; + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:channels channelGroups:nil]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects([NSSet setWithArray:request.channels], [NSSet setWithArray:channels]); + XCTAssertNil(request.channelGroups); +} + +- (void)testItShouldCreateSubscribeRequestWhenChannelGroupsProvided { + NSArray *groups = @[@"group1", @"group2"]; + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:nil channelGroups:groups]; + + XCTAssertNotNil(request); + XCTAssertNil(request.channels); + XCTAssertEqualObjects([NSSet setWithArray:request.channelGroups], [NSSet setWithArray:groups]); +} + +- (void)testItShouldCreateSubscribeRequestWhenBothChannelsAndGroupsProvided { + NSArray *channels = @[@"ch1"]; + NSArray *groups = @[@"grp1"]; + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:channels channelGroups:groups]; + + XCTAssertEqualObjects(request.channels, channels); + XCTAssertEqualObjects(request.channelGroups, groups); +} + + +#pragma mark - PNSubscribeRequest :: Default values + +- (void)testItShouldHaveDefaultValuesWhenSubscribeRequestCreated { + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:@[@"ch"] channelGroups:nil]; + + XCTAssertFalse(request.shouldObservePresence, @"observePresence should default to NO for regular subscribe"); + XCTAssertNil(request.state); + XCTAssertNil(request.timetoken); + XCTAssertNil(request.region); +} + + +#pragma mark - PNSubscribeRequest :: Query parameters + +- (void)testItShouldIncludeTimetokenInQuery { + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:@[@"ch"] channelGroups:nil]; + request.timetoken = @(16000000000000000); + + XCTAssertEqualObjects(request.query[@"tt"], @(16000000000000000).stringValue); +} + +- (void)testItShouldIncludeRegionInQuery { + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:@[@"ch"] channelGroups:nil]; + request.region = @42; + + XCTAssertEqualObjects(request.query[@"tr"], @(42).stringValue); +} + +- (void)testItShouldIncludeStateInQuery { + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:@[@"ch"] channelGroups:nil]; + request.state = @{ @"ch": @{ @"mood": @"happy" } }; + + NSString *stateJSON = request.query[@"state"]; + + XCTAssertNotNil(stateJSON); + NSData *stateData = [stateJSON dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *state = [NSJSONSerialization JSONObjectWithData:stateData options:0 error:nil]; + XCTAssertEqualObjects(state[@"ch"][@"mood"], @"happy"); +} + +- (void)testItShouldIncludeChannelGroupsInQuery { + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:@[@"ch"] channelGroups:@[@"grp1", @"grp2"]]; + + NSString *channelGroups = request.query[@"channel-group"]; + + XCTAssertNotNil(channelGroups); + XCTAssertTrue([channelGroups containsString:@"grp1"]); + XCTAssertTrue([channelGroups containsString:@"grp2"]); +} + + +#pragma mark - PNSubscribeRequest :: Presence channels + +- (void)testItShouldCreatePresenceSubscribeRequestWhenPresenceChannelsProvided { + NSArray *channels = @[@"ch1", @"ch2"]; + PNSubscribeRequest *request = [PNSubscribeRequest requestWithPresenceChannels:channels channelGroups:nil]; + + XCTAssertNotNil(request); + XCTAssertTrue(request.shouldObservePresence, @"observePresence should be YES for presence subscribe"); +} + +- (void)testItShouldAppendPresenceSuffixWhenPresenceChannelsCreated { + NSArray *channels = @[@"ch1"]; + PNSubscribeRequest *request = [PNSubscribeRequest requestWithPresenceChannels:channels channelGroups:nil]; + + XCTAssertTrue([request.channels.firstObject hasSuffix:@"-pnpres"], + @"Channels should have -pnpres suffix"); +} + +- (void)testItShouldNotDuplicatePresenceSuffixWhenAlreadyPresent { + NSArray *channels = @[@"ch1-pnpres"]; + PNSubscribeRequest *request = [PNSubscribeRequest requestWithPresenceChannels:channels channelGroups:nil]; + + XCTAssertEqualObjects(request.channels.firstObject, @"ch1-pnpres", + @"Should not double the -pnpres suffix"); +} + +- (void)testItShouldAppendPresenceSuffixToChannelGroupsWhenPresenceRequest { + NSArray *groups = @[@"group1"]; + PNSubscribeRequest *request = [PNSubscribeRequest requestWithPresenceChannels:nil channelGroups:groups]; + + XCTAssertTrue([request.channelGroups.firstObject hasSuffix:@"-pnpres"], + @"Channel groups should have -pnpres suffix"); +} + + +#pragma mark - PNSubscribeRequest :: Validation + +- (void)testItShouldFailValidationWhenNoChannelsOrGroupsProvided { + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:@[] channelGroups:@[]]; + + XCTAssertNotNil([request validate], @"Validation should fail with no channels or groups"); +} + +- (void)testItShouldPassValidationWhenChannelsProvided { + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:@[@"ch1"] channelGroups:nil]; + + XCTAssertNil([request validate], @"Validation should pass with channels"); +} + +- (void)testItShouldPassValidationWhenChannelGroupsProvided { + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:nil channelGroups:@[@"grp1"]]; + + XCTAssertNil([request validate], @"Validation should pass with channel groups"); +} + + +#pragma mark - PNPresenceLeaveRequest :: Construction + +- (void)testItShouldCreateLeaveRequestWhenChannelsProvided { + NSArray *channels = @[@"ch1", @"ch2"]; + PNPresenceLeaveRequest *request = [PNPresenceLeaveRequest requestWithChannels:channels channelGroups:nil]; + + XCTAssertNotNil(request); + XCTAssertEqualObjects([NSSet setWithArray:request.channels], [NSSet setWithArray:channels]); + XCTAssertNil(request.channelGroups); +} + +- (void)testItShouldCreateLeaveRequestWhenChannelGroupsProvided { + NSArray *groups = @[@"grp1"]; + PNPresenceLeaveRequest *request = [PNPresenceLeaveRequest requestWithChannels:nil channelGroups:groups]; + + XCTAssertNotNil(request); + XCTAssertNil(request.channels); + XCTAssertEqualObjects(request.channelGroups, groups); +} + +- (void)testItShouldCreatePresenceLeaveRequestWhenPresenceChannelsProvided { + PNPresenceLeaveRequest *request = [PNPresenceLeaveRequest requestWithPresenceChannels:@[@"ch1"] + channelGroups:nil]; + + XCTAssertNotNil(request); + XCTAssertTrue([request.channels.firstObject hasSuffix:@"-pnpres"]); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Core/Requests/Time/PNTimeRequestTest.m b/Tests/Tests/Unit/Core/Requests/Time/PNTimeRequestTest.m new file mode 100644 index 000000000..03b508f82 --- /dev/null +++ b/Tests/Tests/Unit/Core/Requests/Time/PNTimeRequestTest.m @@ -0,0 +1,77 @@ +#import +#import +#import "PNBaseRequest+Private.h" + + +#pragma mark Interface declaration + +@interface PNTimeRequestTest : XCTestCase + +@end + + +#pragma mark - Tests + +@implementation PNTimeRequestTest + + +#pragma mark - Construction + +- (void)testItShouldCreateTimeRequestWhenInitialized { + PNTimeRequest *request = [PNTimeRequest new]; + + XCTAssertNotNil(request); +} + +- (void)testItShouldHaveNilQueryWhenCreated { + PNTimeRequest *request = [PNTimeRequest new]; + + XCTAssertNil(request.query); +} + +- (void)testItShouldIncludeArbitraryParametersInQuery { + PNTimeRequest *request = [PNTimeRequest new]; + request.arbitraryQueryParameters = @{ @"custom_key": @"custom_value" }; + + XCTAssertEqualObjects(request.query[@"custom_key"], @"custom_value"); +} + +- (void)testItShouldReturnNilQueryWhenEmptyParametersSet { + PNTimeRequest *request = [PNTimeRequest new]; + request.arbitraryQueryParameters = @{}; + + XCTAssertNil(request.query, @"Empty arbitrary parameters should not produce a query"); +} + +- (void)testItShouldIncludeAllArbitraryParametersInQuery { + PNTimeRequest *request = [PNTimeRequest new]; + request.arbitraryQueryParameters = @{ @"key1": @"val1", @"key2": @"val2", @"key3": @"val3" }; + + NSDictionary *query = request.query; + + XCTAssertEqual(query.count, 3); + XCTAssertEqualObjects(query[@"key2"], @"val2"); +} + +- (void)testItShouldPassValidationWhenCreated { + PNTimeRequest *request = [PNTimeRequest new]; + + // Time request has no required parameters, validate should return nil. + XCTAssertNil([request validate]); +} + +- (void)testItShouldOnlyIncludeLatestParametersInQuery { + PNTimeRequest *request = [PNTimeRequest new]; + request.arbitraryQueryParameters = @{ @"key1": @"val1" }; + request.arbitraryQueryParameters = @{ @"key2": @"val2" }; + + NSDictionary *query = request.query; + + XCTAssertNil(query[@"key1"]); + XCTAssertEqualObjects(query[@"key2"], @"val2"); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Core/Subscribe/PNSubscribeErrorTest.m b/Tests/Tests/Unit/Core/Subscribe/PNSubscribeErrorTest.m new file mode 100644 index 000000000..42be28a6d --- /dev/null +++ b/Tests/Tests/Unit/Core/Subscribe/PNSubscribeErrorTest.m @@ -0,0 +1,120 @@ +/** + * @brief Error / negative path tests for Subscribe operations. + * + * @author PubNub Tests + * @copyright (c) 2010-2026 PubNub, Inc. + */ +#import +#import +#import "PNRecordableTestCase.h" +#import "PNBaseRequest+Private.h" +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNSubscribeErrorTest : PNRecordableTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNSubscribeErrorTest + + +#pragma mark - VCR configuration + +- (BOOL)shouldSetupVCR { + return NO; +} + + +#pragma mark - Tests :: Subscribe :: Empty channels and nil channel groups + +- (void)testItShouldReturnValidationErrorWhenSubscribeWithEmptyChannelsAndNilGroups { + NSArray *channelGroups = nil; + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:@[] channelGroups:channelGroups]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channels"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenSubscribeWithNilChannelsAndEmptyGroups { + NSArray *channels = nil; + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:channels channelGroups:@[]]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channels"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenSubscribeWithNilChannelsAndNilGroups { + NSArray *channels = nil; + NSArray *channelGroups = nil; + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:channels channelGroups:channelGroups]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channels"].location, NSNotFound); +} + + +#pragma mark - Tests :: Subscribe :: Valid with at least one channel + +- (void)testItShouldNotReturnValidationErrorWhenSubscribeWithOneChannel { + NSArray *channelGroups = nil; + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:@[@"channel-a"] channelGroups:channelGroups]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + +- (void)testItShouldNotReturnValidationErrorWhenSubscribeWithOneChannelGroup { + NSArray *channels = nil; + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:channels channelGroups:@[@"group-a"]]; + + PNError *error = [request validate]; + + XCTAssertNil(error); +} + + +#pragma mark - Tests :: Presence Subscribe :: Empty channels and groups + +- (void)testItShouldReturnValidationErrorWhenPresenceSubscribeWithEmptyChannelsAndGroups { + PNSubscribeRequest *request = [PNSubscribeRequest requestWithPresenceChannels:@[] channelGroups:@[]]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); + XCTAssertNotEqual([error.localizedFailureReason rangeOfString:@"channels"].location, NSNotFound); +} + +- (void)testItShouldReturnValidationErrorWhenPresenceSubscribeWithNilChannelsAndNilGroups { + NSArray *channels = nil; + NSArray *channelGroups = nil; + PNSubscribeRequest *request = [PNSubscribeRequest requestWithPresenceChannels:channels channelGroups:channelGroups]; + + PNError *error = [request validate]; + + XCTAssertNotNil(error); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Core/Subscribe/PNSubscribeTest.m b/Tests/Tests/Unit/Core/Subscribe/PNSubscribeTest.m index f6b9aae2b..d8f26f0c1 100644 --- a/Tests/Tests/Unit/Core/Subscribe/PNSubscribeTest.m +++ b/Tests/Tests/Unit/Core/Subscribe/PNSubscribeTest.m @@ -1,6 +1,7 @@ #import "PNRecordableTestCase.h" #import #import +#import #import "PNBaseRequest+Private.h" @@ -40,7 +41,6 @@ - (void)testItShouldSetProperRequestValues { withCompletionBlock:[OCMArg any]]) .andDo(^(NSInvocation *invocation) { PNTransportRequest *transportRequest = [self objectForInvocation:invocation argumentAtIndex:1]; - NSLog(@"CALLED"); XCTAssertEqual(transportRequest.timeout, self.client.configuration.subscribeMaximumIdleTime); XCTAssertTrue(transportRequest.cancellable); @@ -52,6 +52,111 @@ - (void)testItShouldSetProperRequestValues { }]; } +#pragma mark - Tests :: Subscribe channel uniqueness + +- (void)testItShouldHaveUniqueChannelsInCompiledSubscribeRequest { + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:@[@"ch1", @"ch1", @"ch2"] + channelGroups:nil]; + + id clientTransportMock = [self mockForObject:self.client.subscriptionNetwork]; + id recorded = OCMExpect([clientTransportMock sendRequest:[OCMArg isKindOfClass:[PNTransportRequest class]] + withCompletionBlock:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + PNTransportRequest *transportRequest = [self objectForInvocation:invocation argumentAtIndex:1]; + NSString *path = transportRequest.path; + NSArray *pathComponents = [path componentsSeparatedByString:@"/"]; + // Path: /v2/subscribe///0 + NSString *channelSegment = pathComponents[4]; + NSString *decoded = [channelSegment stringByRemovingPercentEncoding]; + NSArray *channels = [decoded componentsSeparatedByString:@","]; + + XCTAssertEqual(channels.count, [NSSet setWithArray:channels].count, + @"Subscribe request path should contain unique channels."); + }); + + [self waitForObject:clientTransportMock recordedInvocationCall:recorded afterBlock:^{ + [self.client subscribeWithRequest:request]; + }]; +} + +- (void)testItShouldHaveUniqueChannelGroupsInCompiledSubscribeRequest { + PNSubscribeRequest *request = [PNSubscribeRequest requestWithChannels:@[@"ch1"] + channelGroups:@[@"gr1", @"gr1", @"gr2"]]; + + id clientTransportMock = [self mockForObject:self.client.subscriptionNetwork]; + id recorded = OCMExpect([clientTransportMock sendRequest:[OCMArg isKindOfClass:[PNTransportRequest class]] + withCompletionBlock:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + PNTransportRequest *transportRequest = [self objectForInvocation:invocation argumentAtIndex:1]; + NSString *groupValue = transportRequest.query[@"channel-group"]; + NSArray *groups = [groupValue componentsSeparatedByString:@","]; + + XCTAssertEqual(groups.count, [NSSet setWithArray:groups].count, + @"Subscribe request query should contain unique channel groups."); + }); + + [self waitForObject:clientTransportMock recordedInvocationCall:recorded afterBlock:^{ + [self.client subscribeWithRequest:request]; + }]; +} + +#pragma mark - Tests :: Heartbeat channel uniqueness + +- (void)testItShouldHaveUniqueChannelsInCompiledHeartbeatRequest { + PNConfiguration *configuration = [self defaultConfiguration]; + configuration.managePresenceListManually = YES; + configuration.presenceHeartbeatValue = 20; + PubNub *client = [self createPubNubForUser:@"heartbeat-ch" withConfiguration:configuration]; + + [client.heartbeatManager setConnected:YES forChannels:@[@"ch1", @"ch1", @"ch2"]]; + + id clientTransportMock = [self mockForObject:client.serviceNetwork]; + id recorded = OCMExpect([clientTransportMock sendRequest:[OCMArg isKindOfClass:[PNTransportRequest class]] + withCompletionBlock:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + PNTransportRequest *transportRequest = [self objectForInvocation:invocation argumentAtIndex:1]; + NSString *path = transportRequest.path; + NSArray *pathComponents = [path componentsSeparatedByString:@"/"]; + // Path: /v2/presence/sub-key//channel//heartbeat + NSString *channelSegment = pathComponents[5]; + NSString *decoded = [channelSegment stringByRemovingPercentEncoding]; + NSArray *channels = [decoded componentsSeparatedByString:@","]; + + XCTAssertEqual(channels.count, [NSSet setWithArray:channels].count, + @"Heartbeat request path should contain unique channels."); + }); + + [self waitForObject:clientTransportMock recordedInvocationCall:recorded afterBlock:^{ + [client heartbeatWithCompletion:nil]; + }]; +} + +- (void)testItShouldHaveUniqueChannelGroupsInCompiledHeartbeatRequest { + PNConfiguration *configuration = [self defaultConfiguration]; + configuration.managePresenceListManually = YES; + configuration.presenceHeartbeatValue = 20; + PubNub *client = [self createPubNubForUser:@"heartbeat-cg" withConfiguration:configuration]; + + [client.heartbeatManager setConnected:YES forChannels:@[@"ch1"]]; + [client.heartbeatManager setConnected:YES forChannelGroups:@[@"gr1", @"gr1", @"gr2"]]; + + id clientTransportMock = [self mockForObject:client.serviceNetwork]; + id recorded = OCMExpect([clientTransportMock sendRequest:[OCMArg isKindOfClass:[PNTransportRequest class]] + withCompletionBlock:[OCMArg any]]) + .andDo(^(NSInvocation *invocation) { + PNTransportRequest *transportRequest = [self objectForInvocation:invocation argumentAtIndex:1]; + NSString *groupValue = transportRequest.query[@"channel-group"]; + NSArray *groups = [groupValue componentsSeparatedByString:@","]; + + XCTAssertEqual(groups.count, [NSSet setWithArray:groups].count, + @"Heartbeat request query should contain unique channel groups."); + }); + + [self waitForObject:clientTransportMock recordedInvocationCall:recorded afterBlock:^{ + [client heartbeatWithCompletion:nil]; + }]; +} + #pragma mark - @end diff --git a/Tests/Tests/Unit/Helpers/PNArrayTest.m b/Tests/Tests/Unit/Helpers/PNArrayTest.m new file mode 100644 index 000000000..91064b06d --- /dev/null +++ b/Tests/Tests/Unit/Helpers/PNArrayTest.m @@ -0,0 +1,121 @@ +/** + * @author Serhii Mamontov + * @copyright © 2010-2026 PubNub, Inc. + */ +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNArrayTest : XCTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNArrayTest + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" + + +#pragma mark - Tests :: Data mapping :: mapObjects:usingBlock: + +- (void)testItShouldMapObjectsWhenTransformBlockProvided { + NSArray *objects = @[@1, @2, @3]; + + NSArray *mapped = [PNArray mapObjects:objects usingBlock:^id(NSNumber *object) { + return @(object.integerValue * 2); + }]; + + XCTAssertEqual(mapped.count, 3, @"Mapped array should have same count."); + XCTAssertEqualObjects(mapped[0], @2, @"First element should be doubled."); + XCTAssertEqualObjects(mapped[1], @4, @"Second element should be doubled."); + XCTAssertEqualObjects(mapped[2], @6, @"Third element should be doubled."); +} + +- (void)testItShouldReturnNilWhenEmptyArrayProvided { + NSArray *objects = @[]; + + NSArray *mapped = [PNArray mapObjects:objects usingBlock:^id(id object) { + return object; + }]; + + XCTAssertNil(mapped, @"Mapping empty array should return nil."); +} + +- (void)testItShouldFilterNilValuesWhenBlockReturnsNilForSomeObjects { + NSArray *objects = @[@1, @2, @3, @4, @5]; + + NSArray *mapped = [PNArray mapObjects:objects usingBlock:^id(NSNumber *object) { + // Only keep even numbers + return (object.integerValue % 2 == 0) ? object : nil; + }]; + + XCTAssertEqual(mapped.count, 2, @"Mapped array should only contain even numbers."); + XCTAssertEqualObjects(mapped[0], @2, @"First element should be 2."); + XCTAssertEqualObjects(mapped[1], @4, @"Second element should be 4."); +} + +- (void)testItShouldMapStringsWhenStringTransformProvided { + NSArray *objects = @[@"hello", @"world"]; + + NSArray *mapped = [PNArray mapObjects:objects usingBlock:^id(NSString *object) { + return [object uppercaseString]; + }]; + + XCTAssertEqual(mapped.count, 2, @"Mapped array should have same count."); + XCTAssertEqualObjects(mapped[0], @"HELLO", @"First element should be uppercased."); + XCTAssertEqualObjects(mapped[1], @"WORLD", @"Second element should be uppercased."); +} + +- (void)testItShouldReturnNilWhenAllObjectsFilteredOut { + NSArray *objects = @[@1, @2, @3]; + + NSArray *mapped = [PNArray mapObjects:objects usingBlock:^id(__unused id object) { + return nil; + }]; + + // When all objects map to nil, the mutable array is empty and copy returns empty array. + XCTAssertEqual(mapped.count, 0, @"Mapped array should be empty when all objects filtered."); +} + +- (void)testItShouldChangeObjectTypesWhenBlockReturnsNewType { + NSArray *objects = @[@1, @2, @3]; + + NSArray *mapped = [PNArray mapObjects:objects usingBlock:^id(NSNumber *object) { + return [object stringValue]; + }]; + + XCTAssertEqual(mapped.count, 3, @"Mapped array should have same count."); + XCTAssertTrue([mapped[0] isKindOfClass:[NSString class]], + @"Mapped objects should be strings."); + XCTAssertEqualObjects(mapped[0], @"1", @"First element should be string '1'."); +} + +- (void)testItShouldMapSingleElementWhenSingleElementArrayProvided { + NSArray *objects = @[@42]; + + NSArray *mapped = [PNArray mapObjects:objects usingBlock:^id(NSNumber *object) { + return @(object.integerValue + 8); + }]; + + XCTAssertEqual(mapped.count, 1, @"Mapped array should have one element."); + XCTAssertEqualObjects(mapped[0], @50, @"Single element should be mapped correctly."); +} + + +#pragma mark - + +#pragma clang diagnostic pop + +@end diff --git a/Tests/Tests/Unit/Helpers/PNChannelTest.m b/Tests/Tests/Unit/Helpers/PNChannelTest.m new file mode 100644 index 000000000..d25bacf18 --- /dev/null +++ b/Tests/Tests/Unit/Helpers/PNChannelTest.m @@ -0,0 +1,248 @@ +/** + * @author Serhii Mamontov + * @copyright © 2010-2026 PubNub, Inc. + */ +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNChannelTest : XCTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNChannelTest + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" + + +#pragma mark - Tests :: Lists encoding :: namesForRequest: + +- (void)testItShouldReturnCommaSeparatedStringWhenMultipleNamesProvided { + NSArray *names = @[@"channel1", @"channel2", @"channel3"]; + + NSString *result = [PNChannel namesForRequest:names]; + + XCTAssertNotNil(result, @"Result should not be nil."); + XCTAssertTrue([result containsString:@"channel1"], @"Should contain first channel."); + XCTAssertTrue([result containsString:@"channel2"], @"Should contain second channel."); + XCTAssertTrue([result containsString:@"channel3"], @"Should contain third channel."); + XCTAssertTrue([result containsString:@","], @"Should use comma as separator."); +} + +- (void)testItShouldReturnSingleNameWhenOneNameProvided { + NSArray *names = @[@"my-channel"]; + + NSString *result = [PNChannel namesForRequest:names]; + + XCTAssertNotNil(result, @"Result should not be nil."); + XCTAssertEqualObjects(result, @"my-channel", @"Should return single channel name."); +} + +- (void)testItShouldReturnNilWhenEmptyArrayProvided { + NSArray *names = @[]; + + NSString *result = [PNChannel namesForRequest:names]; + + XCTAssertNil(result, @"Result should be nil for empty array."); +} + +- (void)testItShouldPercentEncodeNamesWhenSpecialCharactersPresent { + NSArray *names = @[@"channel with spaces"]; + + NSString *result = [PNChannel namesForRequest:names]; + + XCTAssertNotNil(result, @"Result should not be nil."); + XCTAssertTrue([result rangeOfString:@" "].location == NSNotFound, + @"Spaces should be percent-encoded."); +} + + +#pragma mark - Tests :: Lists encoding :: namesForRequest:defaultString: + +- (void)testItShouldReturnDefaultStringWhenEmptyArrayAndDefaultProvided { + NSArray *names = @[]; + NSString *defaultString = @","; + + NSString *result = [PNChannel namesForRequest:names defaultString:defaultString]; + + XCTAssertEqualObjects(result, defaultString, + @"Should return default string when names array is empty."); +} + +- (void)testItShouldReturnNamesWhenNonEmptyArrayAndDefaultProvided { + NSArray *names = @[@"channel1"]; + NSString *defaultString = @","; + + NSString *result = [PNChannel namesForRequest:names defaultString:defaultString]; + + XCTAssertEqualObjects(result, @"channel1", + @"Should return encoded names instead of default when array is non-empty."); +} + + +#pragma mark - Tests :: Lists decoding :: namesFromRequest: + +- (void)testItShouldSplitResponseStringWhenCommaSeparatedNamesProvided { + NSString *response = @"channel1,channel2,channel3"; + + NSArray *names = [PNChannel namesFromRequest:response]; + + XCTAssertEqual(names.count, 3, @"Should split into 3 names."); + XCTAssertEqualObjects(names[0], @"channel1", @"First name should match."); + XCTAssertEqualObjects(names[1], @"channel2", @"Second name should match."); + XCTAssertEqualObjects(names[2], @"channel3", @"Third name should match."); +} + +- (void)testItShouldReturnSingleElementArrayWhenNoCommasPresent { + NSString *response = @"channel1"; + + NSArray *names = [PNChannel namesFromRequest:response]; + + XCTAssertEqual(names.count, 1, @"Should return array with single element."); + XCTAssertEqualObjects(names[0], @"channel1", @"Name should match."); +} + + +#pragma mark - Tests :: Subscriber helper :: isPresenceObject: + +- (void)testItShouldReturnYESWhenChannelNameEndsWithPnpres { + XCTAssertTrue([PNChannel isPresenceObject:@"my-channel-pnpres"], + @"Channel ending with -pnpres should be detected as presence."); +} + +- (void)testItShouldReturnNOWhenChannelNameDoesNotEndWithPnpres { + XCTAssertFalse([PNChannel isPresenceObject:@"my-channel"], + @"Regular channel should not be detected as presence."); +} + +- (void)testItShouldReturnNOWhenChannelNameContainsPnpresInMiddle { + XCTAssertFalse([PNChannel isPresenceObject:@"my-pnpres-channel"], + @"Channel with -pnpres in the middle should not be detected as presence."); +} + +- (void)testItShouldReturnYESWhenChannelIsJustPnpresSuffix { + XCTAssertTrue([PNChannel isPresenceObject:@"-pnpres"], + @"Channel that is just '-pnpres' should be detected as presence."); +} + + +#pragma mark - Tests :: Subscriber helper :: channelForPresence: + +- (void)testItShouldRemovePnpresSuffixWhenPresenceChannelProvided { + NSString *result = [PNChannel channelForPresence:@"my-channel-pnpres"]; + + XCTAssertEqualObjects(result, @"my-channel", + @"Should strip -pnpres suffix to get the actual channel name."); +} + +- (void)testItShouldReturnSameStringWhenNonPresenceChannelProvided { + NSString *result = [PNChannel channelForPresence:@"my-channel"]; + + XCTAssertEqualObjects(result, @"my-channel", + @"Should return unchanged string when -pnpres suffix is not present."); +} + +- (void)testItShouldReturnEmptyStringWhenChannelIsJustPnpresSuffix { + NSString *result = [PNChannel channelForPresence:@"-pnpres"]; + + XCTAssertEqualObjects(result, @"", + @"Should return empty string when channel name is just '-pnpres'."); +} + + +#pragma mark - Tests :: Subscriber helper :: presenceChannelsFrom: + +- (void)testItShouldAppendPnpresSuffixWhenRegularChannelsProvided { + NSArray *names = @[@"channel1", @"channel2"]; + + NSArray *presenceNames = [PNChannel presenceChannelsFrom:names]; + + XCTAssertEqual(presenceNames.count, 2, @"Should return same number of channels."); + for (NSString *name in presenceNames) { + XCTAssertTrue([name hasSuffix:@"-pnpres"], + @"Each channel should have -pnpres suffix, got: %@", name); + } +} + +- (void)testItShouldNotDoubleSuffixWhenPresenceChannelsProvided { + NSArray *names = @[@"channel1-pnpres"]; + + NSArray *presenceNames = [PNChannel presenceChannelsFrom:names]; + + XCTAssertEqual(presenceNames.count, 1, @"Should return one channel."); + XCTAssertTrue([presenceNames containsObject:@"channel1-pnpres"], + @"Should not double -pnpres suffix."); +} + +- (void)testItShouldNotAppendSuffixWhenWildcardChannelProvided { + NSArray *names = @[@"channel.*"]; + + NSArray *presenceNames = [PNChannel presenceChannelsFrom:names]; + + XCTAssertEqual(presenceNames.count, 1, @"Should return one channel."); + XCTAssertTrue([presenceNames containsObject:@"channel.*"], + @"Wildcard channels should not get -pnpres suffix."); +} + +- (void)testItShouldReturnEmptyArrayWhenEmptyArrayProvided { + NSArray *presenceNames = [PNChannel presenceChannelsFrom:@[]]; + + XCTAssertEqual(presenceNames.count, 0, @"Should return empty array for empty input."); +} + + +#pragma mark - Tests :: Subscriber helper :: objectsWithOutPresenceFrom: + +- (void)testItShouldRemovePresenceChannelsWhenMixedListProvided { + NSArray *names = @[@"channel1", @"channel1-pnpres", @"channel2", @"channel2-pnpres"]; + + NSArray *filtered = [PNChannel objectsWithOutPresenceFrom:names]; + + XCTAssertEqual(filtered.count, 2, @"Should contain only non-presence channels."); + XCTAssertTrue([filtered containsObject:@"channel1"], @"Should contain channel1."); + XCTAssertTrue([filtered containsObject:@"channel2"], @"Should contain channel2."); +} + +- (void)testItShouldReturnSameListWhenNoPresenceChannelsPresent { + NSArray *names = @[@"channel1", @"channel2"]; + + NSArray *filtered = [PNChannel objectsWithOutPresenceFrom:names]; + + XCTAssertEqual(filtered.count, 2, @"Should contain all channels."); + XCTAssertTrue([filtered containsObject:@"channel1"], @"Should contain channel1."); + XCTAssertTrue([filtered containsObject:@"channel2"], @"Should contain channel2."); +} + +- (void)testItShouldReturnEmptyListWhenAllChannelsArePresence { + NSArray *names = @[@"channel1-pnpres", @"channel2-pnpres"]; + + NSArray *filtered = [PNChannel objectsWithOutPresenceFrom:names]; + + XCTAssertEqual(filtered.count, 0, @"Should return empty list when all channels are presence."); +} + +- (void)testItShouldReturnEmptyArrayWhenEmptyArrayProvidedForFiltering { + NSArray *filtered = [PNChannel objectsWithOutPresenceFrom:@[]]; + + XCTAssertEqual(filtered.count, 0, @"Should return empty array for empty input."); +} + + +#pragma mark - + +#pragma clang diagnostic pop + +@end diff --git a/Tests/Tests/Unit/Helpers/PNDataTest.m b/Tests/Tests/Unit/Helpers/PNDataTest.m new file mode 100644 index 000000000..98af6b376 --- /dev/null +++ b/Tests/Tests/Unit/Helpers/PNDataTest.m @@ -0,0 +1,138 @@ +/** + * @author Serhii Mamontov + * @copyright © 2010-2026 PubNub, Inc. + */ +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNDataTest : XCTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNDataTest + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" + + +#pragma mark - Tests :: HEX conversion :: HEXFrom: + +- (void)testItShouldConvertToHEXWhenValidDataProvided { + unsigned char bytes[] = {0xDE, 0xAD, 0xBE, 0xEF}; + NSData *data = [NSData dataWithBytes:bytes length:sizeof(bytes)]; + + NSString *hex = [PNData HEXFrom:data]; + + // The implementation iterates over data.length * 0.5, so it processes half the bytes. + XCTAssertEqualObjects(hex, @"DEAD", @"HEX string should contain first half of bytes."); +} + +- (void)testItShouldReturnEmptyStringWhenEmptyDataProvidedForHEX { + NSData *data = [NSData data]; + + NSString *hex = [PNData HEXFrom:data]; + + XCTAssertEqual(hex.length, 0, @"HEX string from empty data should be empty."); +} + +- (void)testItShouldReturnUppercaseHEXWhenDataProvided { + unsigned char bytes[] = {0xab, 0xcd}; + NSData *data = [NSData dataWithBytes:bytes length:sizeof(bytes)]; + + NSString *hex = [PNData HEXFrom:data]; + + // The implementation uses %02lX (uppercase) + XCTAssertEqualObjects(hex, @"AB", @"HEX should be uppercase."); +} + + +#pragma mark - Tests :: HEX conversion :: HEXFromDevicePushToken: + +- (void)testItShouldConvertFullTokenToHEXWhenPushTokenProvided { + unsigned char bytes[] = {0xDE, 0xAD, 0xBE, 0xEF}; + NSData *data = [NSData dataWithBytes:bytes length:sizeof(bytes)]; + + NSString *hex = [PNData HEXFromDevicePushToken:data]; + + // Unlike HEXFrom:, this method processes all bytes. + XCTAssertEqualObjects(hex, @"DEADBEEF", @"HEX should contain all bytes of push token."); +} + +- (void)testItShouldReturnEmptyStringWhenEmptyPushTokenProvided { + NSData *data = [NSData data]; + + NSString *hex = [PNData HEXFromDevicePushToken:data]; + + XCTAssertEqual(hex.length, 0, @"HEX from empty push token should be empty."); +} + +- (void)testItShouldConvertLongPushTokenWhenFullTokenProvided { + unsigned char bytes[] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, + 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10}; + NSData *data = [NSData dataWithBytes:bytes length:sizeof(bytes)]; + + NSString *hex = [PNData HEXFromDevicePushToken:data]; + + XCTAssertEqual(hex.length, 32, @"16 bytes should produce 32-char HEX string."); + XCTAssertEqualObjects(hex, @"0123456789ABCDEFFEDCBA9876543210", + @"Full push token should convert correctly."); +} + +- (void)testItShouldPadSingleDigitBytesWhenLowValueBytesPresent { + unsigned char bytes[] = {0x00, 0x01, 0x0F}; + NSData *data = [NSData dataWithBytes:bytes length:sizeof(bytes)]; + + NSString *hex = [PNData HEXFromDevicePushToken:data]; + + XCTAssertEqualObjects(hex, @"00010F", @"Low-value bytes should be zero-padded."); +} + + +#pragma mark - Tests :: Base64 conversion :: base64StringFrom: + +- (void)testItShouldConvertToBase64WhenValidDataProvided { + NSData *data = [@"Hello, World!" dataUsingEncoding:NSUTF8StringEncoding]; + + NSString *base64 = [PNData base64StringFrom:data]; + + XCTAssertEqualObjects(base64, @"SGVsbG8sIFdvcmxkIQ==", + @"Base64 encoding should match expected output."); +} + +- (void)testItShouldReturnEmptyStringWhenEmptyDataProvidedForBase64 { + NSData *data = [NSData data]; + + NSString *base64 = [PNData base64StringFrom:data]; + + XCTAssertEqual(base64.length, 0, @"Base64 from empty data should be empty string."); +} + +- (void)testItShouldRoundTripWhenDataEncodedAndDecoded { + NSData *originalData = [@"PubNub SDK" dataUsingEncoding:NSUTF8StringEncoding]; + + NSString *base64 = [PNData base64StringFrom:originalData]; + NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:base64 options:0]; + + XCTAssertEqualObjects(decodedData, originalData, + @"Base64 round-trip should produce original data."); +} + + +#pragma mark - + +#pragma clang diagnostic pop + +@end diff --git a/Tests/Tests/Unit/Helpers/PNDateTest.m b/Tests/Tests/Unit/Helpers/PNDateTest.m new file mode 100644 index 000000000..884d3829f --- /dev/null +++ b/Tests/Tests/Unit/Helpers/PNDateTest.m @@ -0,0 +1,133 @@ +/** + * @author Serhii Mamontov + * @copyright © 2010-2026 PubNub, Inc. + */ +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNDateTest : XCTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNDateTest + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" + + +#pragma mark - Tests :: RFC3339 Conversion + +- (void)testItShouldFormatDateAsRFC3339WhenUnixEpochProvided { + // Unix epoch: January 1, 1970, 00:00:00 UTC + NSDate *epochDate = [NSDate dateWithTimeIntervalSince1970:0]; + + NSString *result = [PNDate RFC3339StringFromDate:epochDate]; + + XCTAssertEqualObjects(result, @"1970-01-01T00:00:00Z", + @"Unix epoch should format as expected RFC3339 string."); +} + +- (void)testItShouldFormatDateAsRFC3339WhenSpecificDateProvided { + // Create a specific date: 2023-06-15T12:30:45Z + NSDateComponents *components = [[NSDateComponents alloc] init]; + components.year = 2023; + components.month = 6; + components.day = 15; + components.hour = 12; + components.minute = 30; + components.second = 45; + components.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; + NSCalendar *calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian]; + NSDate *date = [calendar dateFromComponents:components]; + + NSString *result = [PNDate RFC3339StringFromDate:date]; + + XCTAssertEqualObjects(result, @"2023-06-15T12:30:45Z", + @"Specific date should format correctly."); +} + +- (void)testItShouldFormatDateInUTCWhenNonUTCDateProvided { + // The formatter should always produce UTC time regardless of input timezone + NSString *result = [PNDate RFC3339StringFromDate:[NSDate dateWithTimeIntervalSince1970:0]]; + + XCTAssertTrue([result hasSuffix:@"Z"], @"RFC3339 string should end with 'Z' (UTC indicator)."); +} + +- (void)testItShouldIncludeDateAndTimeComponentsWhenFormatted { + NSDate *date = [NSDate dateWithTimeIntervalSince1970:1000000000]; // 2001-09-09T01:46:40Z + + NSString *result = [PNDate RFC3339StringFromDate:date]; + + XCTAssertTrue([result containsString:@"T"], + @"RFC3339 string should contain 'T' separator between date and time."); + XCTAssertTrue([result containsString:@"-"], + @"RFC3339 string should contain hyphens in date portion."); + XCTAssertTrue([result containsString:@":"], + @"RFC3339 string should contain colons in time portion."); +} + +- (void)testItShouldFormatFarFutureDateWhenYear2099DateProvided { + NSDateComponents *components = [[NSDateComponents alloc] init]; + components.year = 2099; + components.month = 12; + components.day = 31; + components.hour = 23; + components.minute = 59; + components.second = 59; + components.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; + NSCalendar *calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian]; + NSDate *date = [calendar dateFromComponents:components]; + + NSString *result = [PNDate RFC3339StringFromDate:date]; + + XCTAssertEqualObjects(result, @"2099-12-31T23:59:59Z", + @"Far future date should format correctly."); +} + +- (void)testItShouldFormatDateConsistentlyWhenSameDateFormattedTwice { + NSDate *date = [NSDate dateWithTimeIntervalSince1970:1234567890]; + + NSString *result1 = [PNDate RFC3339StringFromDate:date]; + NSString *result2 = [PNDate RFC3339StringFromDate:date]; + + XCTAssertEqualObjects(result1, result2, + @"Same date should always produce the same RFC3339 string."); +} + +- (void)testItShouldFormatLeapYearDateWhenFebruary29Provided { + NSDateComponents *components = [[NSDateComponents alloc] init]; + components.year = 2024; + components.month = 2; + components.day = 29; + components.hour = 12; + components.minute = 0; + components.second = 0; + components.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; + NSCalendar *calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian]; + NSDate *date = [calendar dateFromComponents:components]; + + NSString *result = [PNDate RFC3339StringFromDate:date]; + + XCTAssertEqualObjects(result, @"2024-02-29T12:00:00Z", + @"Leap year date should format correctly."); +} + + +#pragma mark - + +#pragma clang diagnostic pop + +@end diff --git a/Tests/Tests/Unit/Helpers/PNDictionaryTest.m b/Tests/Tests/Unit/Helpers/PNDictionaryTest.m new file mode 100644 index 000000000..fb5663380 --- /dev/null +++ b/Tests/Tests/Unit/Helpers/PNDictionaryTest.m @@ -0,0 +1,149 @@ +/** + * @author Serhii Mamontov + * @copyright © 2010-2026 PubNub, Inc. + */ +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNDictionaryTest : XCTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNDictionaryTest + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" + + +#pragma mark - Tests :: Validation :: isDictionary:containValueOfClasses: + +- (void)testItShouldReturnYESWhenAllValuesMatchExpectedClasses { + NSDictionary *dictionary = @{@"key1": @"value1", @"key2": @"value2"}; + NSArray *classes = @[[NSString class]]; + + BOOL result = [PNDictionary isDictionary:dictionary containValueOfClasses:classes]; + + XCTAssertTrue(result, @"Should return YES when all values are of expected class."); +} + +- (void)testItShouldReturnNOWhenSomeValuesDoNotMatchExpectedClasses { + NSDictionary *dictionary = @{@"key1": @"value1", @"key2": @(42)}; + NSArray *classes = @[[NSString class]]; + + BOOL result = [PNDictionary isDictionary:dictionary containValueOfClasses:classes]; + + XCTAssertFalse(result, @"Should return NO when some values are not of expected class."); +} + +- (void)testItShouldReturnYESWhenValuesMatchAnyOfMultipleClasses { + NSDictionary *dictionary = @{@"key1": @"value1", @"key2": @(42)}; + NSArray *classes = @[[NSString class], [NSNumber class]]; + + BOOL result = [PNDictionary isDictionary:dictionary containValueOfClasses:classes]; + + XCTAssertTrue(result, @"Should return YES when values match any of the specified classes."); +} + +- (void)testItShouldReturnYESWhenEmptyDictionaryProvided { + NSDictionary *dictionary = @{}; + NSArray *classes = @[[NSString class]]; + + BOOL result = [PNDictionary isDictionary:dictionary containValueOfClasses:classes]; + + XCTAssertTrue(result, @"Should return YES for empty dictionary since there are no non-matching values."); +} + +- (void)testItShouldReturnNOWhenNoValuesMatchAnyExpectedClass { + NSDictionary *dictionary = @{@"key1": @(1), @"key2": @(2)}; + NSArray *classes = @[[NSString class]]; + + BOOL result = [PNDictionary isDictionary:dictionary containValueOfClasses:classes]; + + XCTAssertFalse(result, @"Should return NO when no values match expected classes."); +} + +- (void)testItShouldMatchSubclassesWhenSubclassValuePresent { + NSMutableString *mutableString = [NSMutableString stringWithString:@"mutable"]; + NSDictionary *dictionary = @{@"key1": mutableString}; + NSArray *classes = @[[NSString class]]; + + BOOL result = [PNDictionary isDictionary:dictionary containValueOfClasses:classes]; + + XCTAssertTrue(result, @"Should match subclasses (NSMutableString is subclass of NSString)."); +} + +- (void)testItShouldReturnYESWhenDictionaryContainsNestedDictionaries { + NSDictionary *dictionary = @{@"key1": @{@"nested": @"value"}, @"key2": @{@"nested2": @"value2"}}; + NSArray *classes = @[[NSDictionary class]]; + + BOOL result = [PNDictionary isDictionary:dictionary containValueOfClasses:classes]; + + XCTAssertTrue(result, @"Should return YES when all values are dictionaries as expected."); +} + +- (void)testItShouldReturnYESWhenDictionaryContainsArrayValues { + NSDictionary *dictionary = @{@"key1": @[@1, @2], @"key2": @[@3, @4]}; + NSArray *classes = @[[NSArray class]]; + + BOOL result = [PNDictionary isDictionary:dictionary containValueOfClasses:classes]; + + XCTAssertTrue(result, @"Should return YES when all values are arrays as expected."); +} + + +#pragma mark - Tests :: URL helper :: queryStringFrom: + +- (void)testItShouldCreateQueryStringWhenDictionaryHasSingleEntry { + NSDictionary *dictionary = @{@"key": @"value"}; + + NSString *query = [PNDictionary queryStringFrom:dictionary]; + + XCTAssertEqualObjects(query, @"key=value", @"Should produce correct query string for single entry."); +} + +- (void)testItShouldCreateQueryStringWhenDictionaryHasMultipleEntries { + NSDictionary *dictionary = @{@"a": @"1", @"b": @"2"}; + + NSString *query = [PNDictionary queryStringFrom:dictionary]; + + // Dictionary ordering is not guaranteed, so check both entries are present + XCTAssertTrue([query containsString:@"a=1"], @"Query should contain a=1."); + XCTAssertTrue([query containsString:@"b=2"], @"Query should contain b=2."); + XCTAssertTrue([query containsString:@"&"], @"Query should contain ampersand separator."); +} + +- (void)testItShouldReturnNilWhenEmptyDictionaryProvided { + NSDictionary *dictionary = @{}; + + NSString *query = [PNDictionary queryStringFrom:dictionary]; + + XCTAssertNil(query, @"Query string from empty dictionary should be nil."); +} + +- (void)testItShouldIncludeNumericValuesWhenDictionaryContainsNumbers { + NSDictionary *dictionary = @{@"count": @(42)}; + + NSString *query = [PNDictionary queryStringFrom:dictionary]; + + XCTAssertEqualObjects(query, @"count=42", @"Should correctly format numeric values."); +} + + +#pragma mark - + +#pragma clang diagnostic pop + +@end diff --git a/Tests/Tests/Unit/Helpers/PNFunctionsTest.m b/Tests/Tests/Unit/Helpers/PNFunctionsTest.m new file mode 100644 index 000000000..d7d5634e8 --- /dev/null +++ b/Tests/Tests/Unit/Helpers/PNFunctionsTest.m @@ -0,0 +1,202 @@ +/** + * @author Serhii Mamontov + * @copyright © 2010-2026 PubNub, Inc. + */ +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNFunctionsTest : XCTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNFunctionsTest + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" + + +#pragma mark - Tests :: PNNSObjectIsKindOfAnyClass + +- (void)testItShouldReturnYESWhenObjectMatchesOneOfProvidedClasses { + NSString *object = @"hello"; + NSArray *classes = @[[NSNumber class], [NSString class]]; + + BOOL result = PNNSObjectIsKindOfAnyClass(object, classes); + + XCTAssertTrue(result, @"NSString should match when NSString class is in the list."); +} + +- (void)testItShouldReturnNOWhenObjectMatchesNoneOfProvidedClasses { + NSString *object = @"hello"; + NSArray *classes = @[[NSNumber class], [NSArray class]]; + + BOOL result = PNNSObjectIsKindOfAnyClass(object, classes); + + XCTAssertFalse(result, @"NSString should not match NSNumber or NSArray."); +} + +- (void)testItShouldMatchSubclassWhenSuperclassIsInList { + NSMutableArray *object = [NSMutableArray new]; + NSArray *classes = @[[NSArray class]]; + + BOOL result = PNNSObjectIsKindOfAnyClass(object, classes); + + XCTAssertTrue(result, @"NSMutableArray should match NSArray (superclass)."); +} + + +#pragma mark - Tests :: PNNSObjectIsSubclassOfAnyClass + +- (void)testItShouldReturnYESWhenObjectIsSubclassOfProvidedClass { + NSMutableString *object = [NSMutableString new]; + NSArray *classes = @[[NSString class]]; + + BOOL result = PNNSObjectIsSubclassOfAnyClass(object, classes); + + XCTAssertTrue(result, @"NSMutableString should be subclass of NSString."); +} + +- (void)testItShouldReturnNOWhenObjectIsNotSubclassOfAnyProvidedClass { + NSString *object = @"hello"; + NSArray *classes = @[[NSNumber class], [NSArray class]]; + + BOOL result = PNNSObjectIsSubclassOfAnyClass(object, classes); + + XCTAssertFalse(result, @"NSString should not be subclass of NSNumber or NSArray."); +} + +- (void)testItShouldReturnYESWhenObjectIsSameClassAsProvided { + NSString *object = @"hello"; + NSArray *classes = @[[NSString class]]; + + BOOL result = PNNSObjectIsSubclassOfAnyClass(object, classes); + + XCTAssertTrue(result, @"Object's class should be considered subclass of itself."); +} + + +#pragma mark - Tests :: PNMessageFingerprint + +- (void)testItShouldReturnConsistentFingerprintWhenSameStringProvidedTwice { + NSString *payload = @"Hello, PubNub!"; + + NSString *fingerprint1 = PNMessageFingerprint(payload); + NSString *fingerprint2 = PNMessageFingerprint(payload); + + XCTAssertEqualObjects(fingerprint1, fingerprint2, + @"Same payload should produce same fingerprint."); +} + +- (void)testItShouldReturnDifferentFingerprintsWhenDifferentStringsProvided { + NSString *fingerprint1 = PNMessageFingerprint(@"Hello"); + NSString *fingerprint2 = PNMessageFingerprint(@"World"); + + XCTAssertNotEqualObjects(fingerprint1, fingerprint2, + @"Different payloads should produce different fingerprints."); +} + +- (void)testItShouldReturn8CharacterHexStringWhenStringPayloadProvided { + NSString *fingerprint = PNMessageFingerprint(@"test payload"); + + XCTAssertEqual(fingerprint.length, 8, + @"Fingerprint should be 8-character hex string."); +} + +- (void)testItShouldReturnFingerprintWhenDictionaryPayloadProvided { + NSDictionary *payload = @{@"message": @"hello", @"sender": @"test"}; + + NSString *fingerprint = PNMessageFingerprint(payload); + + XCTAssertEqual(fingerprint.length, 8, @"Fingerprint should be 8 characters."); +} + +- (void)testItShouldReturnFingerprintWhenArrayPayloadProvided { + NSArray *payload = @[@1, @2, @3]; + + NSString *fingerprint = PNMessageFingerprint(payload); + + XCTAssertEqual(fingerprint.length, 8, @"Fingerprint should be 8 characters."); +} + +- (void)testItShouldReturnFingerprintWhenEmptyStringProvided { + NSString *fingerprint = PNMessageFingerprint(@""); + + XCTAssertEqual(fingerprint.length, 8, @"Fingerprint should be 8 characters."); +} + + +#pragma mark - Tests :: PNStringFormat + +- (void)testItShouldFormatStringWhenPlaceholdersProvided { + NSString *result = PNStringFormat(@"Hello, %@!", @"PubNub"); + + XCTAssertEqualObjects(result, @"Hello, PubNub!", + @"Format should substitute placeholders."); +} + +- (void)testItShouldFormatNumericValuesWhenNumberPlaceholderProvided { + NSString *result = PNStringFormat(@"Value: %d", 42); + + XCTAssertEqualObjects(result, @"Value: 42", + @"Format should handle numeric placeholders."); +} + +- (void)testItShouldReturnFormatStringWhenNoArgumentsProvided { + NSString *result = PNStringFormat(@"No args here"); + + XCTAssertEqualObjects(result, @"No args here", + @"Format without arguments should return string as-is."); +} + +- (void)testItShouldHandleMultiplePlaceholdersWhenProvided { + NSString *result = PNStringFormat(@"%@ + %@ = %d", @"1", @"2", 3); + + XCTAssertEqualObjects(result, @"1 + 2 = 3", + @"Format should handle multiple mixed-type placeholders."); +} + + +#pragma mark - Tests :: PNErrorUserInfo + +- (void)testItShouldCreateUserInfoDictionaryWhenAllFieldsProvided { + NSError *underlyingError = [NSError errorWithDomain:@"test" code:1 userInfo:nil]; + + NSDictionary *userInfo = PNErrorUserInfo(@"Description", @"Reason", @"Recovery", underlyingError); + + XCTAssertEqualObjects(userInfo[NSLocalizedDescriptionKey], @"Description", + @"Description should match."); + XCTAssertEqualObjects(userInfo[NSLocalizedFailureReasonErrorKey], @"Reason", + @"Reason should match."); + XCTAssertEqualObjects(userInfo[NSLocalizedRecoverySuggestionErrorKey], @"Recovery", + @"Recovery should match."); + XCTAssertEqualObjects(userInfo[NSUnderlyingErrorKey], underlyingError, + @"Underlying error should match."); +} + +- (void)testItShouldCreateUserInfoDictionaryWhenSomeFieldsAreNil { + NSDictionary *userInfo = PNErrorUserInfo(@"Description", nil, nil, nil); + + XCTAssertEqualObjects(userInfo[NSLocalizedDescriptionKey], @"Description", + @"Description should match."); + // NSMutableDictionary setObject:nil removes the key, so nil values should not be present. +} + + +#pragma mark - + +#pragma clang diagnostic pop + +@end diff --git a/Tests/Tests/Unit/Helpers/PNGZIPTest.m b/Tests/Tests/Unit/Helpers/PNGZIPTest.m new file mode 100644 index 000000000..66193b2c8 --- /dev/null +++ b/Tests/Tests/Unit/Helpers/PNGZIPTest.m @@ -0,0 +1,216 @@ +/** + * @author Serhii Mamontov + * @copyright © 2010-2026 PubNub, Inc. + */ +#import +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNGZIPTest : XCTestCase + +#pragma mark - Helpers + +/// Decompress GZIP deflated data using zlib. +/// +/// - Parameter data: Compressed data which should be inflated. +/// - Returns: Decompressed data or `nil` if decompression fails. +- (nullable NSData *)inflateData:(NSData *)data; + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNGZIPTest + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" + + +#pragma mark - Tests :: Compression :: Happy paths + +- (void)testItShouldCompressDataWhenNonEmptyDataProvided { + NSData *originalData = [@"Hello, PubNub!" dataUsingEncoding:NSUTF8StringEncoding]; + + NSData *compressedData = [PNGZIP GZIPDeflatedData:originalData]; + + XCTAssertNotNil(compressedData, @"Compressed data should not be nil for valid input."); + XCTAssertGreaterThan(compressedData.length, 0, @"Compressed data should have non-zero length."); +} + +- (void)testItShouldProduceDecompressibleDataWhenCompressed { + NSData *originalData = [@"Hello, PubNub! This is a test of GZIP compression." dataUsingEncoding:NSUTF8StringEncoding]; + + NSData *compressedData = [PNGZIP GZIPDeflatedData:originalData]; + XCTAssertNotNil(compressedData, @"Compressed data should not be nil."); + + NSData *decompressedData = [self inflateData:compressedData]; + + XCTAssertNotNil(decompressedData, @"Decompressed data should not be nil."); + XCTAssertEqualObjects(decompressedData, originalData, @"Decompressed data should match original."); +} + +- (void)testItShouldReturnNilWhenEmptyDataProvided { + NSData *emptyData = [NSData data]; + + NSData *compressedData = [PNGZIP GZIPDeflatedData:emptyData]; + + XCTAssertNil(compressedData, @"Compressing empty data should return nil."); +} + +- (void)testItShouldCompressSmallDataWhenFewBytesProvided { + NSData *smallData = [@"Hi" dataUsingEncoding:NSUTF8StringEncoding]; + + NSData *compressedData = [PNGZIP GZIPDeflatedData:smallData]; + + XCTAssertNotNil(compressedData, @"Should compress even very small data."); + + NSData *decompressed = [self inflateData:compressedData]; + XCTAssertEqualObjects(decompressed, smallData, @"Decompressed small data should match original."); +} + +- (void)testItShouldCompressMediumDataWhenMultipleKilobytesProvided { + NSMutableString *mediumString = [NSMutableString new]; + for (int i = 0; i < 500; i++) { + [mediumString appendFormat:@"Line %d: The quick brown fox jumps over the lazy dog.\n", i]; + } + NSData *mediumData = [mediumString dataUsingEncoding:NSUTF8StringEncoding]; + + NSData *compressedData = [PNGZIP GZIPDeflatedData:mediumData]; + + XCTAssertNotNil(compressedData, @"Should compress medium-sized data."); + + NSData *decompressed = [self inflateData:compressedData]; + XCTAssertEqualObjects(decompressed, mediumData, @"Decompressed medium data should match original."); +} + +- (void)testItShouldCompressLargeDataWhenHundredsOfKilobytesProvided { + NSMutableData *largeData = [NSMutableData dataWithCapacity:200000]; + NSData *chunk = [@"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" dataUsingEncoding:NSUTF8StringEncoding]; + for (int i = 0; i < 6000; i++) { + [largeData appendData:chunk]; + } + + NSData *compressedData = [PNGZIP GZIPDeflatedData:largeData]; + + XCTAssertNotNil(compressedData, @"Should compress large data."); + + NSData *decompressed = [self inflateData:compressedData]; + XCTAssertEqualObjects(decompressed, largeData, @"Decompressed large data should match original."); +} + +- (void)testItShouldProduceSmallerOutputWhenCompressibleInputProvided { + NSMutableString *repetitiveString = [NSMutableString new]; + for (int i = 0; i < 1000; i++) { + [repetitiveString appendString:@"AAAAAAAAAA"]; + } + NSData *originalData = [repetitiveString dataUsingEncoding:NSUTF8StringEncoding]; + + NSData *compressedData = [PNGZIP GZIPDeflatedData:originalData]; + + XCTAssertNotNil(compressedData, @"Compressed data should not be nil."); + XCTAssertLessThan(compressedData.length, originalData.length, + @"Compressed output should be smaller than highly compressible input."); +} + +- (void)testItShouldCompressBinaryDataWhenNonTextBytesProvided { + unsigned char bytes[] = {0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD, 0x80, 0x7F}; + NSData *binaryData = [NSData dataWithBytes:bytes length:sizeof(bytes)]; + + NSData *compressedData = [PNGZIP GZIPDeflatedData:binaryData]; + + XCTAssertNotNil(compressedData, @"Should compress binary data."); + + NSData *decompressed = [self inflateData:compressedData]; + XCTAssertEqualObjects(decompressed, binaryData, @"Decompressed binary data should match original."); +} + +- (void)testItShouldProduceValidGZIPHeaderWhenDataCompressed { + NSData *originalData = [@"Test data for GZIP header validation" dataUsingEncoding:NSUTF8StringEncoding]; + + NSData *compressedData = [PNGZIP GZIPDeflatedData:originalData]; + + XCTAssertNotNil(compressedData, @"Compressed data should not be nil."); + // GZIP magic number: first two bytes should be 0x1F 0x8B + XCTAssertGreaterThanOrEqual(compressedData.length, (NSUInteger)2, + @"Compressed data should have at least 2 bytes for header."); + const unsigned char *compressedBytes = compressedData.bytes; + XCTAssertEqual(compressedBytes[0], 0x1F, @"First byte of GZIP header should be 0x1F."); + XCTAssertEqual(compressedBytes[1], 0x8B, @"Second byte of GZIP header should be 0x8B."); +} + +- (void)testItShouldCompressUnicodeDataWhenUTF8StringProvided { + NSData *unicodeData = [@"Hello \u4e16\u754c \U0001F600 \u00E9\u00E0\u00FC" dataUsingEncoding:NSUTF8StringEncoding]; + + NSData *compressedData = [PNGZIP GZIPDeflatedData:unicodeData]; + + XCTAssertNotNil(compressedData, @"Should compress unicode data."); + + NSData *decompressed = [self inflateData:compressedData]; + XCTAssertEqualObjects(decompressed, unicodeData, @"Decompressed unicode data should match original."); +} + +- (void)testItShouldCompressSingleByteWhenOneByteProvided { + unsigned char singleByte = 0x42; + NSData *singleByteData = [NSData dataWithBytes:&singleByte length:1]; + + NSData *compressedData = [PNGZIP GZIPDeflatedData:singleByteData]; + + XCTAssertNotNil(compressedData, @"Should compress single byte data."); + + NSData *decompressed = [self inflateData:compressedData]; + XCTAssertEqualObjects(decompressed, singleByteData, @"Decompressed single byte data should match."); +} + + +#pragma mark - Helpers + +- (NSData *)inflateData:(NSData *)data { + if (data.length == 0) return nil; + + z_stream stream; + bzero(&stream, sizeof(stream)); + stream.next_in = (Bytef *)data.bytes; + stream.avail_in = (uint)data.length; + + // Use MAX_WBITS + 32 to auto-detect gzip or zlib format. + if (inflateInit2(&stream, MAX_WBITS + 32) != Z_OK) return nil; + + NSMutableData *decompressed = [NSMutableData dataWithLength:data.length * 4]; + int status; + + do { + if (stream.total_out >= decompressed.length) { + [decompressed increaseLengthBy:data.length]; + } + + stream.next_out = (Bytef *)decompressed.mutableBytes + stream.total_out; + stream.avail_out = (uInt)(decompressed.length - stream.total_out); + + status = inflate(&stream, Z_NO_FLUSH); + } while (status == Z_OK); + + inflateEnd(&stream); + + if (status != Z_STREAM_END) return nil; + + [decompressed setLength:stream.total_out]; + return [decompressed copy]; +} + + +#pragma mark - + +#pragma clang diagnostic pop + +@end diff --git a/Tests/Tests/Unit/Helpers/PNJSONHelperTest.m b/Tests/Tests/Unit/Helpers/PNJSONHelperTest.m new file mode 100644 index 000000000..b49ac8fab --- /dev/null +++ b/Tests/Tests/Unit/Helpers/PNJSONHelperTest.m @@ -0,0 +1,264 @@ +/** + * @author Serhii Mamontov + * @copyright © 2010-2026 PubNub, Inc. + */ +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNJSONHelperTest : XCTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNJSONHelperTest + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" + + +#pragma mark - Tests :: Serialization :: JSONStringFrom:withError: + +- (void)testItShouldSerializeDictionaryWhenDictionaryProvided { + NSDictionary *object = @{@"key": @"value"}; + NSError *error = nil; + + NSString *jsonString = [PNJSON JSONStringFrom:object withError:&error]; + + XCTAssertNotNil(jsonString, @"JSON string should not be nil."); + XCTAssertNil(error, @"Error should be nil for valid dictionary."); + XCTAssertEqualObjects(jsonString, @"{\"key\":\"value\"}", + @"Dictionary should serialize to expected JSON string."); +} + +- (void)testItShouldSerializeArrayWhenArrayProvided { + NSArray *object = @[@1, @2, @3]; + NSError *error = nil; + + NSString *jsonString = [PNJSON JSONStringFrom:object withError:&error]; + + XCTAssertNotNil(jsonString, @"JSON string should not be nil."); + XCTAssertNil(error, @"Error should be nil for valid array."); + XCTAssertEqualObjects(jsonString, @"[1,2,3]", + @"Array should serialize to expected JSON string."); +} + +- (void)testItShouldWrapStringInQuotesWhenPlainStringProvided { + NSString *object = @"hello"; + NSError *error = nil; + + NSString *jsonString = [PNJSON JSONStringFrom:object withError:&error]; + + XCTAssertNotNil(jsonString, @"JSON string should not be nil."); + XCTAssertEqualObjects(jsonString, @"\"hello\"", + @"Plain string should be wrapped in quotes."); +} + +- (void)testItShouldReturnExistingJSONStringWhenAlreadyJSONFormattedStringProvided { + NSString *object = @"{\"already\":\"json\"}"; + NSError *error = nil; + + NSString *jsonString = [PNJSON JSONStringFrom:object withError:&error]; + + XCTAssertNotNil(jsonString, @"JSON string should not be nil."); + XCTAssertEqualObjects(jsonString, object, + @"Already-JSON-formatted string should pass through unchanged."); +} + +- (void)testItShouldReturnNilWhenNilObjectProvided { + NSError *error = nil; + + NSString *jsonString = [PNJSON JSONStringFrom:nil withError:&error]; + + XCTAssertNil(jsonString, @"JSON string should be nil for nil input."); +} + +- (void)testItShouldSerializeEmptyDictionaryWhenEmptyDictionaryProvided { + NSDictionary *object = @{}; + NSError *error = nil; + + NSString *jsonString = [PNJSON JSONStringFrom:object withError:&error]; + + XCTAssertNotNil(jsonString, @"JSON string should not be nil."); + XCTAssertEqualObjects(jsonString, @"{}", + @"Empty dictionary should serialize to '{}'."); +} + +- (void)testItShouldSerializeEmptyArrayWhenEmptyArrayProvided { + NSArray *object = @[]; + NSError *error = nil; + + NSString *jsonString = [PNJSON JSONStringFrom:object withError:&error]; + + XCTAssertNotNil(jsonString, @"JSON string should not be nil."); + XCTAssertEqualObjects(jsonString, @"[]", + @"Empty array should serialize to '[]'."); +} + +- (void)testItShouldSerializeNestedObjectWhenNestedDictionaryProvided { + NSDictionary *object = @{@"outer": @{@"inner": @"value"}}; + NSError *error = nil; + + NSString *jsonString = [PNJSON JSONStringFrom:object withError:&error]; + + XCTAssertNotNil(jsonString, @"JSON string should not be nil."); + XCTAssertNil(error, @"Error should be nil."); + XCTAssertTrue([jsonString containsString:@"\"outer\""], + @"Should contain outer key."); + XCTAssertTrue([jsonString containsString:@"\"inner\""], + @"Should contain inner key."); +} + + +#pragma mark - Tests :: De-serialization :: JSONObjectFrom:withError: + +- (void)testItShouldDeserializeDictionaryWhenJSONDictionaryStringProvided { + NSString *jsonString = @"{\"key\":\"value\"}"; + NSError *error = nil; + + id result = [PNJSON JSONObjectFrom:jsonString withError:&error]; + + XCTAssertNotNil(result, @"Deserialized object should not be nil."); + XCTAssertNil(error, @"Error should be nil for valid JSON."); + XCTAssertTrue([result isKindOfClass:[NSDictionary class]], + @"Result should be a dictionary."); + XCTAssertEqualObjects(result[@"key"], @"value", + @"Dictionary value should match."); +} + +- (void)testItShouldDeserializeArrayWhenJSONArrayStringProvided { + NSString *jsonString = @"[1,2,3]"; + NSError *error = nil; + + id result = [PNJSON JSONObjectFrom:jsonString withError:&error]; + + XCTAssertNotNil(result, @"Deserialized object should not be nil."); + XCTAssertNil(error, @"Error should be nil for valid JSON."); + XCTAssertTrue([result isKindOfClass:[NSArray class]], + @"Result should be an array."); + XCTAssertEqual([(NSArray *)result count], 3, @"Array should have 3 elements."); +} + +- (void)testItShouldDeserializeStringWhenQuotedStringProvided { + NSString *jsonString = @"\"hello world\""; + NSError *error = nil; + + id result = [PNJSON JSONObjectFrom:jsonString withError:&error]; + + XCTAssertNotNil(result, @"Deserialized object should not be nil."); + XCTAssertTrue([result isKindOfClass:[NSString class]], + @"Result should be a string."); + XCTAssertEqualObjects(result, @"hello world", + @"Deserialized string should match original without quotes."); +} + +- (void)testItShouldReturnNilWhenNilStringProvidedForDeserialization { + NSError *error = nil; + + id result = [PNJSON JSONObjectFrom:nil withError:&error]; + + XCTAssertNil(result, @"Result should be nil for nil input."); +} + +- (void)testItShouldSetErrorWhenInvalidJSONStringProvided { + NSString *jsonString = @"{invalid json}"; + NSError *error = nil; + + id result = [PNJSON JSONObjectFrom:jsonString withError:&error]; + + // The implementation tries to parse and should fail for invalid JSON. + // Depending on whether the braces trigger the JSON path or string path. + // '{invalid json}' starts with { and ends with }, so isJSONString returns YES but + // the NSJSONSerialization should fail. + // Actually the implementation checks first char == '"', so this goes to NSJSONSerialization. + XCTAssertNil(result, @"Result should be nil for invalid JSON."); + XCTAssertNotNil(error, @"Error should be set for invalid JSON."); +} + +- (void)testItShouldDeserializeNumberWhenNumericJSONStringProvided { + NSString *jsonString = @"42"; + NSError *error = nil; + + id result = [PNJSON JSONObjectFrom:jsonString withError:&error]; + + XCTAssertNotNil(result, @"Deserialized object should not be nil."); + XCTAssertNil(error, @"Error should be nil."); + XCTAssertEqualObjects(result, @(42), @"Deserialized number should match."); +} + +- (void)testItShouldDeserializeBooleanWhenBoolJSONStringProvided { + NSString *jsonString = @"true"; + NSError *error = nil; + + id result = [PNJSON JSONObjectFrom:jsonString withError:&error]; + + XCTAssertNotNil(result, @"Deserialized object should not be nil."); + XCTAssertNil(error, @"Error should be nil."); + XCTAssertEqualObjects(result, @YES, @"Deserialized boolean should be YES."); +} + + +#pragma mark - Tests :: Validation :: isJSONString: + +- (void)testItShouldReturnYESWhenStringStartsAndEndsWithCurlyBraces { + XCTAssertTrue([PNJSON isJSONString:@"{\"key\":\"value\"}"], + @"String starting and ending with curly braces should be detected as JSON."); +} + +- (void)testItShouldReturnYESWhenStringStartsAndEndsWithSquareBrackets { + XCTAssertTrue([PNJSON isJSONString:@"[1,2,3]"], + @"String starting and ending with square brackets should be detected as JSON."); +} + +- (void)testItShouldReturnYESWhenStringStartsAndEndsWithQuotes { + XCTAssertTrue([PNJSON isJSONString:@"\"hello\""], + @"String starting and ending with quotes should be detected as JSON."); +} + +- (void)testItShouldReturnNOWhenPlainStringProvided { + XCTAssertFalse([PNJSON isJSONString:@"hello"], + @"Plain string without JSON delimiters should not be detected as JSON."); +} + +- (void)testItShouldReturnNOWhenEmptyStringProvided { + XCTAssertFalse([PNJSON isJSONString:@""], + @"Empty string should not be detected as JSON."); +} + +- (void)testItShouldReturnYESWhenNSNumberProvided { + XCTAssertTrue([PNJSON isJSONString:@(42)], + @"NSNumber should be detected as JSON (numeric literal)."); +} + +- (void)testItShouldReturnNOWhenMismatchedBracesProvided { + XCTAssertFalse([PNJSON isJSONString:@"{hello]"], + @"Mismatched braces should not be detected as JSON."); +} + +- (void)testItShouldReturnYESWhenEmptyJSONObjectProvided { + XCTAssertTrue([PNJSON isJSONString:@"{}"], + @"Empty JSON object should be detected as JSON."); +} + +- (void)testItShouldReturnYESWhenEmptyJSONArrayProvided { + XCTAssertTrue([PNJSON isJSONString:@"[]"], + @"Empty JSON array should be detected as JSON."); +} + + +#pragma mark - + +#pragma clang diagnostic pop + +@end diff --git a/Tests/Tests/Unit/Helpers/PNNotificationPayloadBuilderTest.m b/Tests/Tests/Unit/Helpers/PNNotificationPayloadBuilderTest.m index 528dd5f8c..63674989a 100644 --- a/Tests/Tests/Unit/Helpers/PNNotificationPayloadBuilderTest.m +++ b/Tests/Tests/Unit/Helpers/PNNotificationPayloadBuilderTest.m @@ -61,8 +61,6 @@ - (void)testItShouldPreparePlatformSpecificBuildersWhenCalled { PNNotificationsPayload *builder = [PNNotificationsPayload payloadsWithNotificationTitle:expectedTitle body:expectedBody]; - XCTAssertNotNil(builder.apns); - XCTAssertNotNil(builder.fcm); } - (void)testItShouldPassTitleAndBodyToBuildersWhenValuesPassed { @@ -195,7 +193,6 @@ - (void)testItShouldSetDefaultAPNSStructureWhenCalledAPNSBuilderOnlyWithStorage notificationTitle:nil body:nil]; - XCTAssertNotNil(builder); XCTAssertTrue([self.platformPayloadStorage[@"aps"] isKindOfClass:[NSMutableDictionary class]]); XCTAssertTrue([self.platformPayloadStorage[@"aps"][@"alert"] isKindOfClass:[NSMutableDictionary class]]); XCTAssertEqual(((NSDictionary *)self.platformPayloadStorage[@"aps"]).count, 1); @@ -529,7 +526,6 @@ - (void)testItShouldSetDefaultFCMStructureWhenCalledFCMBuilderOnlyWithStorage { notificationTitle:nil body:nil]; - XCTAssertNotNil(builder); XCTAssertTrue([self.platformPayloadStorage[@"notification"] isKindOfClass:[NSMutableDictionary class]]); XCTAssertTrue([self.platformPayloadStorage[@"data"] isKindOfClass:[NSMutableDictionary class]]); } diff --git a/Tests/Tests/Unit/Helpers/PNNumberTest.m b/Tests/Tests/Unit/Helpers/PNNumberTest.m new file mode 100644 index 000000000..c043c0bf7 --- /dev/null +++ b/Tests/Tests/Unit/Helpers/PNNumberTest.m @@ -0,0 +1,95 @@ +/** + * @author Serhii Mamontov + * @copyright © 2010-2026 PubNub, Inc. + */ +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNNumberTest : XCTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNNumberTest + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" + + +#pragma mark - Tests :: timeTokenFromNumber: + +- (void)testItShouldReturn17DigitTimetokenWhenUnixTimestampProvided { + // Unix timestamp 10 digits: 1609459200 (2021-01-01T00:00:00Z) + NSNumber *unixTimestamp = @(1609459200); + + NSNumber *timetoken = [PNNumber timeTokenFromNumber:unixTimestamp]; + + // 10-digit timestamp needs 7 more digits to reach 17 total = multiply by 10^7 + NSNumber *expected = @(16094592000000000ULL); + XCTAssertEqualObjects(timetoken, expected, + @"Unix timestamp should be converted to 17-digit timetoken."); +} + +- (void)testItShouldReturnSameTimetokenWhen17DigitNumberProvided { + // Already a 17-digit timetoken + NSNumber *fullTimetoken = @(16094592000000000ULL); + + NSNumber *timetoken = [PNNumber timeTokenFromNumber:fullTimetoken]; + + XCTAssertEqualObjects(timetoken, fullTimetoken, + @"Already 17-digit timetoken should remain unchanged."); +} + +- (void)testItShouldReturnNilWhenNilProvided { + NSNumber *timetoken = [PNNumber timeTokenFromNumber:nil]; + + XCTAssertNil(timetoken, @"Timetoken from nil should be nil."); +} + +- (void)testItShouldReturnZeroWhenZeroProvided { + NSNumber *timetoken = [PNNumber timeTokenFromNumber:@0]; + + XCTAssertEqualObjects(timetoken, @0, + @"Timetoken from zero should be zero (0 * any multiplier = 0)."); +} + +- (void)testItShouldConvert13DigitNumberWhenMillisecondTimestampProvided { + // 13-digit millisecond timestamp + NSNumber *millisTimestamp = @(1609459200000ULL); + + NSNumber *timetoken = [PNNumber timeTokenFromNumber:millisTimestamp]; + + // 13 digits + 4 more to reach 17 = multiply by 10000 + NSNumber *expected = @(16094592000000000ULL); + XCTAssertEqualObjects(timetoken, expected, + @"13-digit timestamp should be padded to 17 digits."); +} + +- (void)testItShouldHandleFloatNumberWhenFloatTimestampProvided { + // Float value representing unix timestamp with fractional seconds: 1609459200.123 + NSNumber *floatTimestamp = @(1609459200.123); + + NSNumber *timetoken = [PNNumber timeTokenFromNumber:floatTimestamp]; + + XCTAssertGreaterThan(timetoken.unsignedLongLongValue, 0, + @"Float-based timetoken should produce positive result."); +} + + +#pragma mark - + +#pragma clang diagnostic pop + +@end diff --git a/Tests/Tests/Unit/Helpers/PNStringTest.m b/Tests/Tests/Unit/Helpers/PNStringTest.m new file mode 100644 index 000000000..02375ed77 --- /dev/null +++ b/Tests/Tests/Unit/Helpers/PNStringTest.m @@ -0,0 +1,238 @@ +/** + * @author Serhii Mamontov + * @copyright © 2010-2026 PubNub, Inc. + */ +#import +#import +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNStringTest : XCTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNStringTest + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" + + +#pragma mark - Tests :: Percent Encoding + +- (void)testItShouldPercentEncodeSpecialCharactersWhenStringContainsReservedCharacters { + NSString *input = @"hello world&foo=bar"; + + NSString *encoded = [PNString percentEscapedString:input]; + + XCTAssertTrue([encoded rangeOfString:@"&"].location == NSNotFound, + @"Ampersand should be percent-encoded."); + XCTAssertTrue([encoded rangeOfString:@"="].location == NSNotFound, + @"Equals sign should be percent-encoded."); + XCTAssertTrue([encoded rangeOfString:@" "].location == NSNotFound, + @"Space should be percent-encoded."); +} + +- (void)testItShouldReturnSameStringWhenNoSpecialCharactersPresent { + NSString *input = @"helloworld"; + + NSString *encoded = [PNString percentEscapedString:input]; + + XCTAssertEqualObjects(encoded, input, @"String without special characters should remain unchanged."); +} + +- (void)testItShouldEncodeNewlinesWhenStringContainsCRLF { + NSString *input = @"line1\nline2\rline3"; + + NSString *encoded = [PNString percentEscapedString:input]; + + // The implementation replaces %0A with %5Cn and %0D with %5Cr + XCTAssertTrue([encoded rangeOfString:@"%5Cn"].location != NSNotFound, + @"Newline should be encoded as escaped newline."); + XCTAssertTrue([encoded rangeOfString:@"%5Cr"].location != NSNotFound, + @"Carriage return should be encoded as escaped carriage return."); +} + +- (void)testItShouldEncodeColonAndSlashWhenStringContainsThem { + NSString *input = @"http://example.com:8080/path?q=1"; + + NSString *encoded = [PNString percentEscapedString:input]; + + // Colons, slashes, question marks should be percent-encoded per the implementation + XCTAssertTrue([encoded rangeOfString:@":"].location == NSNotFound, + @"Colons should be percent-encoded."); + XCTAssertTrue([encoded rangeOfString:@"?"].location == NSNotFound, + @"Question marks should be percent-encoded."); +} + +- (void)testItShouldHandleUnicodeCharactersWhenStringContainsNonASCII { + NSString *input = @"\u00E9\u00E0\u00FC\u00F1"; + + NSString *encoded = [PNString percentEscapedString:input]; + + XCTAssertGreaterThan(encoded.length, 0, @"Encoded unicode string should have non-zero length."); +} + +- (void)testItShouldReturnEmptyStringWhenEmptyStringProvided { + NSString *input = @""; + + NSString *encoded = [PNString percentEscapedString:input]; + + XCTAssertEqual(encoded.length, 0, @"Encoded empty string should have zero length."); +} + +- (void)testItShouldEncodeHashAndBracketsWhenStringContainsThem { + NSString *input = @"test#value[0]"; + + NSString *encoded = [PNString percentEscapedString:input]; + + XCTAssertTrue([encoded rangeOfString:@"#"].location == NSNotFound, + @"Hash should be percent-encoded."); + XCTAssertTrue([encoded rangeOfString:@"["].location == NSNotFound, + @"Opening bracket should be percent-encoded."); + XCTAssertTrue([encoded rangeOfString:@"]"].location == NSNotFound, + @"Closing bracket should be percent-encoded."); +} + + +#pragma mark - Tests :: UTF8 Data Conversion + +- (void)testItShouldConvertToUTF8DataWhenValidStringProvided { + NSString *input = @"Hello, PubNub!"; + + NSData *data = [PNString UTF8DataFrom:input]; + + XCTAssertNotNil(data, @"UTF8 data should not be nil."); + NSString *roundTrip = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(roundTrip, input, @"Round-trip conversion should match original string."); +} + +- (void)testItShouldReturnEmptyDataWhenEmptyStringProvided { + NSString *input = @""; + + NSData *data = [PNString UTF8DataFrom:input]; + + XCTAssertNotNil(data, @"UTF8 data from empty string should not be nil."); + XCTAssertEqual(data.length, 0, @"UTF8 data from empty string should have zero length."); +} + +- (void)testItShouldConvertUnicodeToUTF8DataWhenUnicodeStringProvided { + NSString *input = @"\u4e16\u754c \U0001F600"; + + NSData *data = [PNString UTF8DataFrom:input]; + + XCTAssertNotNil(data, @"UTF8 data from unicode string should not be nil."); + XCTAssertGreaterThan(data.length, 0, @"UTF8 data should have non-zero length."); + NSString *roundTrip = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(roundTrip, input, @"Round-trip of unicode should match original."); +} + + +#pragma mark - Tests :: Base64 Data Conversion + +- (void)testItShouldDecodeBase64DataWhenValidBase64StringProvided { + NSString *original = @"Hello, World!"; + NSString *base64String = [[original dataUsingEncoding:NSUTF8StringEncoding] + base64EncodedStringWithOptions:0]; + + NSData *decodedData = [PNString base64DataFrom:base64String]; + + NSString *decoded = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(decoded, original, @"Decoded base64 string should match original."); +} + +- (void)testItShouldReturnEmptyDataWhenEmptyBase64StringProvided { + NSString *base64String = @""; + + NSData *decodedData = [PNString base64DataFrom:base64String]; + + XCTAssertEqual(decodedData.length, 0, @"Decoded base64 from empty string should have zero length."); +} + + +#pragma mark - Tests :: SHA-256 Hashing + +- (void)testItShouldProduceConsistentHashWhenSameStringHashedTwice { + NSString *input = @"Hello, PubNub!"; + + NSData *hash1 = [PNString SHA256DataFrom:input]; + NSData *hash2 = [PNString SHA256DataFrom:input]; + + XCTAssertEqualObjects(hash1, hash2, @"Same input should always produce the same SHA-256 hash."); +} + +- (void)testItShouldReturn32ByteHashWhenStringProvided { + NSString *input = @"test"; + + NSData *hash = [PNString SHA256DataFrom:input]; + + XCTAssertEqual(hash.length, (NSUInteger)CC_SHA256_DIGEST_LENGTH, + @"SHA-256 hash should be 32 bytes (256 bits)."); +} + +- (void)testItShouldProduceDifferentHashesWhenDifferentStringsProvided { + NSData *hash1 = [PNString SHA256DataFrom:@"Hello"]; + NSData *hash2 = [PNString SHA256DataFrom:@"World"]; + + XCTAssertNotEqualObjects(hash1, hash2, @"Different strings should produce different SHA-256 hashes."); +} + +- (void)testItShouldHashEmptyStringWhenEmptyStringProvided { + NSData *hash = [PNString SHA256DataFrom:@""]; + + XCTAssertEqual(hash.length, (NSUInteger)CC_SHA256_DIGEST_LENGTH, + @"SHA-256 hash of empty string should still be 32 bytes."); +} + + +#pragma mark - Tests :: URL-Friendly Base64 + +- (void)testItShouldConvertURLFriendlyBase64WhenDashesAndUnderscoresPresent { + // URL-friendly base64 uses - instead of + and _ instead of / + NSString *urlFriendlyBase64 = @"SGVsbG8-V29ybGQ_"; + + NSString *standardBase64 = [PNString base64StringFromURLFriendlyBase64String:urlFriendlyBase64]; + + XCTAssertTrue([standardBase64 rangeOfString:@"-"].location == NSNotFound, + @"Dashes should be replaced with plus signs."); + XCTAssertTrue([standardBase64 rangeOfString:@"_"].location == NSNotFound, + @"Underscores should be replaced with slashes."); + XCTAssertTrue([standardBase64 rangeOfString:@"+"].location != NSNotFound, + @"Plus signs should be present in standard base64."); + XCTAssertTrue([standardBase64 rangeOfString:@"/"].location != NSNotFound, + @"Slashes should be present in standard base64."); +} + +- (void)testItShouldReturnSameStringWhenNoURLFriendlyCharactersPresent { + NSString *standardBase64 = @"SGVsbG8gV29ybGQ="; + + NSString *result = [PNString base64StringFromURLFriendlyBase64String:standardBase64]; + + XCTAssertEqualObjects(result, standardBase64, + @"String without URL-friendly chars should remain unchanged."); +} + +- (void)testItShouldReturnEmptyStringWhenEmptyBase64StringProvided { + NSString *result = [PNString base64StringFromURLFriendlyBase64String:@""]; + + XCTAssertEqual(result.length, 0, @"Result should be empty for empty input."); +} + + +#pragma mark - + +#pragma clang diagnostic pop + +@end diff --git a/Tests/Tests/Unit/Helpers/PNURLRequestTest.m b/Tests/Tests/Unit/Helpers/PNURLRequestTest.m new file mode 100644 index 000000000..0108ae659 --- /dev/null +++ b/Tests/Tests/Unit/Helpers/PNURLRequestTest.m @@ -0,0 +1,101 @@ +/** + * @author Serhii Mamontov + * @copyright © 2010-2026 PubNub, Inc. + */ +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +@interface PNURLRequestTest : XCTestCase + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNURLRequestTest + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" + + +#pragma mark - Tests :: Packet size :: packetSizeForRequest: + +- (void)testItShouldReturnPositiveSizeWhenValidGETRequestProvided { + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://ps.pndsn.com/v2/subscribe"]]; + request.HTTPMethod = @"GET"; + + NSInteger size = [PNURLRequest packetSizeForRequest:request]; + + XCTAssertGreaterThan(size, 0, @"Packet size should be positive for valid request."); +} + +- (void)testItShouldIncludeBodySizeWhenPOSTRequestWithBodyProvided { + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://ps.pndsn.com/publish"]]; + request.HTTPMethod = @"POST"; + request.HTTPBody = [@"Hello, PubNub!" dataUsingEncoding:NSUTF8StringEncoding]; + + NSInteger sizeWithBody = [PNURLRequest packetSizeForRequest:request]; + + request.HTTPBody = nil; + NSInteger sizeWithoutBody = [PNURLRequest packetSizeForRequest:request]; + + XCTAssertGreaterThan(sizeWithBody, sizeWithoutBody, + @"Request with body should have larger packet size."); +} + +- (void)testItShouldIncludeHeadersSizeWhenCustomHeadersProvided { + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://ps.pndsn.com/v2/subscribe"]]; + request.HTTPMethod = @"GET"; + + NSInteger sizeWithoutHeaders = [PNURLRequest packetSizeForRequest:request]; + + [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + [request setValue:@"PubNub-ObjC/5.0" forHTTPHeaderField:@"User-Agent"]; + + NSInteger sizeWithHeaders = [PNURLRequest packetSizeForRequest:request]; + + XCTAssertGreaterThan(sizeWithHeaders, sizeWithoutHeaders, + @"Request with headers should have larger packet size."); +} + +- (void)testItShouldIncludeHTTPMethodInSizeWhenRequestBuilt { + NSMutableURLRequest *getRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://ps.pndsn.com/v2/subscribe"]]; + getRequest.HTTPMethod = @"GET"; + + NSMutableURLRequest *deleteRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://ps.pndsn.com/v2/subscribe"]]; + deleteRequest.HTTPMethod = @"DELETE"; + + NSInteger getSize = [PNURLRequest packetSizeForRequest:getRequest]; + NSInteger deleteSize = [PNURLRequest packetSizeForRequest:deleteRequest]; + + // DELETE is 3 chars longer than GET + XCTAssertEqual(deleteSize - getSize, 3, + @"DELETE request should be 3 bytes larger than GET (method name difference)."); +} + +- (void)testItShouldIncludeHostHeaderWhenURLHasHost { + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://ps.pndsn.com/v2/subscribe"]]; + request.HTTPMethod = @"GET"; + + NSInteger size = [PNURLRequest packetSizeForRequest:request]; + + // The implementation adds "Host: \r\n" so size should be positive. + XCTAssertGreaterThan(size, 0, @"Packet size should include host header."); +} + + +#pragma mark - + +#pragma clang diagnostic pop + +@end diff --git a/Tests/Tests/Unit/Models/PNEventResultTest.m b/Tests/Tests/Unit/Models/PNEventResultTest.m new file mode 100644 index 000000000..32ab4a390 --- /dev/null +++ b/Tests/Tests/Unit/Models/PNEventResultTest.m @@ -0,0 +1,371 @@ +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import "PNOperationResult+Private.h" +#import "PNSubscribeEventData+Private.h" + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +/// Unit tests for subscribe event result model objects. +/// +/// Tests covering construction and property access for PNMessageResult, PNSignalResult, +/// PNPresenceEventResult, PNFileEventResult, PNObjectEventResult, and PNMessageActionResult. +@interface PNEventResultTest : XCTestCase + +#pragma mark - + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNEventResultTest + + +#pragma mark - Tests :: PNMessageResult + +- (void)testItShouldCreateMessageResult { + PNMessageResult *result = [PNMessageResult objectWithOperation:PNSubscribeOperation response:nil]; + + XCTAssertEqual(result.operation, PNSubscribeOperation, + @"Operation type should be PNSubscribeOperation."); +} + +- (void)testItShouldReturnNilDataWhenMessageResponseIsNil { + PNMessageResult *result = [PNMessageResult objectWithOperation:PNSubscribeOperation response:nil]; + + XCTAssertNil(result.data, @"Message result data should be nil when no response is set."); +} + +- (void)testItShouldBeSubclassOfPNOperationResult { + PNMessageResult *result = [PNMessageResult objectWithOperation:PNSubscribeOperation response:nil]; + + XCTAssertTrue([result isKindOfClass:[PNOperationResult class]], + @"PNMessageResult should be a subclass of PNOperationResult."); +} + +- (void)testItShouldReturnMessageEventDataWhenResponseSet { + PNSubscribeMessageEventData *eventData = [[PNSubscribeMessageEventData alloc] init]; + PNMessageResult *result = [PNMessageResult objectWithOperation:PNSubscribeOperation response:eventData]; + + XCTAssertNotNil(result.data, @"Data should not be nil when response is set."); + XCTAssertTrue([result.data isKindOfClass:[PNSubscribeMessageEventData class]], + @"Data should be PNSubscribeMessageEventData instance."); +} + +- (void)testItShouldCopyMessageResult { + PNSubscribeMessageEventData *eventData = [[PNSubscribeMessageEventData alloc] init]; + PNMessageResult *result = [PNMessageResult objectWithOperation:PNSubscribeOperation response:eventData]; + PNMessageResult *copy = [result copy]; + + XCTAssertNotNil(copy, @"Copied message result should not be nil."); + XCTAssertEqual(copy.operation, PNSubscribeOperation, + @"Copied result should preserve operation type."); +} + + +#pragma mark - Tests :: PNSignalResult + +- (void)testItShouldCreateSignalResult { + PNSignalResult *result = [PNSignalResult objectWithOperation:PNSubscribeOperation response:nil]; + + XCTAssertEqual(result.operation, PNSubscribeOperation, + @"Operation type should be PNSubscribeOperation."); +} + +- (void)testItShouldReturnNilDataWhenSignalResponseIsNil { + PNSignalResult *result = [PNSignalResult objectWithOperation:PNSubscribeOperation response:nil]; + + XCTAssertNil(result.data, @"Signal result data should be nil when no response is set."); +} + +- (void)testItShouldReturnSignalEventDataWhenResponseSet { + PNSubscribeSignalEventData *eventData = [[PNSubscribeSignalEventData alloc] init]; + PNSignalResult *result = [PNSignalResult objectWithOperation:PNSubscribeOperation response:eventData]; + + XCTAssertNotNil(result.data, @"Data should not be nil when response is set."); + XCTAssertTrue([result.data isKindOfClass:[PNSubscribeSignalEventData class]], + @"Data should be PNSubscribeSignalEventData instance."); +} + +- (void)testItShouldBeSignalEventDataSubclassOfMessageEventData { + PNSubscribeSignalEventData *signalData = [[PNSubscribeSignalEventData alloc] init]; + + XCTAssertTrue([signalData isKindOfClass:[PNSubscribeMessageEventData class]], + @"PNSubscribeSignalEventData should be a subclass of PNSubscribeMessageEventData."); +} + + +#pragma mark - Tests :: PNPresenceEventResult + +- (void)testItShouldCreatePresenceEventResult { + PNPresenceEventResult *result = [PNPresenceEventResult objectWithOperation:PNSubscribeOperation response:nil]; + + XCTAssertEqual(result.operation, PNSubscribeOperation, + @"Operation type should be PNSubscribeOperation."); +} + +- (void)testItShouldReturnNilDataWhenPresenceResponseIsNil { + PNPresenceEventResult *result = [PNPresenceEventResult objectWithOperation:PNSubscribeOperation response:nil]; + + XCTAssertNil(result.data, @"Presence event result data should be nil when no response is set."); +} + +- (void)testItShouldReturnPresenceEventDataWhenResponseSet { + PNSubscribePresenceEventData *eventData = [[PNSubscribePresenceEventData alloc] init]; + PNPresenceEventResult *result = [PNPresenceEventResult objectWithOperation:PNSubscribeOperation + response:eventData]; + + XCTAssertNotNil(result.data, @"Data should not be nil when response is set."); + XCTAssertTrue([result.data isKindOfClass:[PNSubscribePresenceEventData class]], + @"Data should be PNSubscribePresenceEventData instance."); +} + +- (void)testPresenceEventDataShouldBeSubclassOfSubscribeEventData { + PNSubscribePresenceEventData *presenceData = [[PNSubscribePresenceEventData alloc] init]; + + XCTAssertTrue([presenceData isKindOfClass:[PNSubscribeEventData class]], + @"PNSubscribePresenceEventData should be a subclass of PNSubscribeEventData."); +} + + +#pragma mark - Tests :: PNFileEventResult + +- (void)testItShouldCreateFileEventResult { + PNFileEventResult *result = [PNFileEventResult objectWithOperation:PNSubscribeOperation response:nil]; + + XCTAssertEqual(result.operation, PNSubscribeOperation, + @"Operation type should be PNSubscribeOperation."); +} + +- (void)testItShouldReturnNilDataWhenFileResponseIsNil { + PNFileEventResult *result = [PNFileEventResult objectWithOperation:PNSubscribeOperation response:nil]; + + XCTAssertNil(result.data, @"File event result data should be nil when no response is set."); +} + +- (void)testItShouldReturnFileEventDataWhenResponseSet { + PNSubscribeFileEventData *eventData = [[PNSubscribeFileEventData alloc] init]; + PNFileEventResult *result = [PNFileEventResult objectWithOperation:PNSubscribeOperation response:eventData]; + + XCTAssertNotNil(result.data, @"Data should not be nil when response is set."); + XCTAssertTrue([result.data isKindOfClass:[PNSubscribeFileEventData class]], + @"Data should be PNSubscribeFileEventData instance."); +} + +- (void)testFileEventDataShouldBeSubclassOfSubscribeEventData { + PNSubscribeFileEventData *fileData = [[PNSubscribeFileEventData alloc] init]; + + XCTAssertTrue([fileData isKindOfClass:[PNSubscribeEventData class]], + @"PNSubscribeFileEventData should be a subclass of PNSubscribeEventData."); +} + + +#pragma mark - Tests :: PNObjectEventResult + +- (void)testItShouldCreateObjectEventResult { + PNObjectEventResult *result = [PNObjectEventResult objectWithOperation:PNSubscribeOperation response:nil]; + + XCTAssertEqual(result.operation, PNSubscribeOperation, + @"Operation type should be PNSubscribeOperation."); +} + +- (void)testItShouldReturnNilDataWhenObjectResponseIsNil { + PNObjectEventResult *result = [PNObjectEventResult objectWithOperation:PNSubscribeOperation response:nil]; + + XCTAssertNil(result.data, @"Object event result data should be nil when no response is set."); +} + +- (void)testItShouldReturnObjectEventDataWhenResponseSet { + PNSubscribeObjectEventData *eventData = [[PNSubscribeObjectEventData alloc] init]; + PNObjectEventResult *result = [PNObjectEventResult objectWithOperation:PNSubscribeOperation response:eventData]; + + XCTAssertNotNil(result.data, @"Data should not be nil when response is set."); + XCTAssertTrue([result.data isKindOfClass:[PNSubscribeObjectEventData class]], + @"Data should be PNSubscribeObjectEventData instance."); +} + +- (void)testObjectEventDataShouldBeSubclassOfSubscribeEventData { + PNSubscribeObjectEventData *objectData = [[PNSubscribeObjectEventData alloc] init]; + + XCTAssertTrue([objectData isKindOfClass:[PNSubscribeEventData class]], + @"PNSubscribeObjectEventData should be a subclass of PNSubscribeEventData."); +} + + +#pragma mark - Tests :: PNMessageActionResult + +- (void)testItShouldCreateMessageActionResult { + PNMessageActionResult *result = [PNMessageActionResult objectWithOperation:PNSubscribeOperation response:nil]; + + XCTAssertEqual(result.operation, PNSubscribeOperation, + @"Operation type should be PNSubscribeOperation."); +} + +- (void)testItShouldReturnNilDataWhenMessageActionResponseIsNil { + PNMessageActionResult *result = [PNMessageActionResult objectWithOperation:PNSubscribeOperation response:nil]; + + XCTAssertNil(result.data, @"Message action result data should be nil when no response is set."); +} + +- (void)testItShouldReturnMessageActionEventDataWhenResponseSet { + PNSubscribeMessageActionEventData *eventData = [[PNSubscribeMessageActionEventData alloc] init]; + PNMessageActionResult *result = [PNMessageActionResult objectWithOperation:PNSubscribeOperation response:eventData]; + + XCTAssertNotNil(result.data, @"Data should not be nil when response is set."); + XCTAssertTrue([result.data isKindOfClass:[PNSubscribeMessageActionEventData class]], + @"Data should be PNSubscribeMessageActionEventData instance."); +} + +- (void)testMessageActionEventDataShouldBeSubclassOfSubscribeEventData { + PNSubscribeMessageActionEventData *actionData = [[PNSubscribeMessageActionEventData alloc] init]; + + XCTAssertTrue([actionData isKindOfClass:[PNSubscribeEventData class]], + @"PNSubscribeMessageActionEventData should be a subclass of PNSubscribeEventData."); +} + + +#pragma mark - Tests :: PNSubscribeEventData base properties + +- (void)testSubscribeEventDataShouldBeSubclassOfPNBaseOperationData { + PNSubscribeEventData *eventData = [[PNSubscribeEventData alloc] init]; + + XCTAssertTrue([eventData isKindOfClass:[PNBaseOperationData class]], + @"PNSubscribeEventData should be a subclass of PNBaseOperationData."); +} + + +#pragma mark - Tests :: Event result types independence + +- (void)testDifferentEventResultTypesShouldBeIndependent { + PNSubscribeMessageEventData *messageData = [[PNSubscribeMessageEventData alloc] init]; + PNSubscribePresenceEventData *presenceData = [[PNSubscribePresenceEventData alloc] init]; + + PNMessageResult *messageResult = [PNMessageResult objectWithOperation:PNSubscribeOperation + response:messageData]; + PNPresenceEventResult *presenceResult = [PNPresenceEventResult objectWithOperation:PNSubscribeOperation + response:presenceData]; + + XCTAssertTrue([messageResult.data isKindOfClass:[PNSubscribeMessageEventData class]], + @"Message result data should be message event data."); + XCTAssertTrue([presenceResult.data isKindOfClass:[PNSubscribePresenceEventData class]], + @"Presence result data should be presence event data."); + XCTAssertFalse([messageResult.data isKindOfClass:[PNSubscribePresenceEventData class]], + @"Message result data should not be presence event data."); +} + + +#pragma mark - Tests :: All event result types subclass PNOperationResult + +- (void)testAllEventResultTypesShouldSubclassPNOperationResult { + PNMessageResult *messageResult = [PNMessageResult objectWithOperation:PNSubscribeOperation response:nil]; + PNSignalResult *signalResult = [PNSignalResult objectWithOperation:PNSubscribeOperation response:nil]; + PNPresenceEventResult *presenceResult = [PNPresenceEventResult objectWithOperation:PNSubscribeOperation response:nil]; + PNFileEventResult *fileResult = [PNFileEventResult objectWithOperation:PNSubscribeOperation response:nil]; + PNObjectEventResult *objectResult = [PNObjectEventResult objectWithOperation:PNSubscribeOperation response:nil]; + PNMessageActionResult *actionResult = [PNMessageActionResult objectWithOperation:PNSubscribeOperation response:nil]; + + XCTAssertTrue([messageResult isKindOfClass:[PNOperationResult class]], + @"PNMessageResult should be PNOperationResult subclass."); + XCTAssertTrue([signalResult isKindOfClass:[PNOperationResult class]], + @"PNSignalResult should be PNOperationResult subclass."); + XCTAssertTrue([presenceResult isKindOfClass:[PNOperationResult class]], + @"PNPresenceEventResult should be PNOperationResult subclass."); + XCTAssertTrue([fileResult isKindOfClass:[PNOperationResult class]], + @"PNFileEventResult should be PNOperationResult subclass."); + XCTAssertTrue([objectResult isKindOfClass:[PNOperationResult class]], + @"PNObjectEventResult should be PNOperationResult subclass."); + XCTAssertTrue([actionResult isKindOfClass:[PNOperationResult class]], + @"PNMessageActionResult should be PNOperationResult subclass."); +} + + +#pragma mark - Tests :: Event data class inheritance + +- (void)testMessageEventDataShouldInheritFromSubscribeEventData { + XCTAssertTrue([PNSubscribeMessageEventData isSubclassOfClass:[PNSubscribeEventData class]], + @"PNSubscribeMessageEventData should be a subclass of PNSubscribeEventData."); +} + +- (void)testSignalEventDataShouldInheritFromMessageEventData { + XCTAssertTrue([PNSubscribeSignalEventData isSubclassOfClass:[PNSubscribeMessageEventData class]], + @"PNSubscribeSignalEventData should be a subclass of PNSubscribeMessageEventData."); +} + +- (void)testPresenceEventDataShouldInheritFromSubscribeEventData { + XCTAssertTrue([PNSubscribePresenceEventData isSubclassOfClass:[PNSubscribeEventData class]], + @"PNSubscribePresenceEventData should be a subclass of PNSubscribeEventData."); +} + +- (void)testFileEventDataShouldInheritFromSubscribeEventData { + XCTAssertTrue([PNSubscribeFileEventData isSubclassOfClass:[PNSubscribeEventData class]], + @"PNSubscribeFileEventData should be a subclass of PNSubscribeEventData."); +} + +- (void)testObjectEventDataShouldInheritFromSubscribeEventData { + XCTAssertTrue([PNSubscribeObjectEventData isSubclassOfClass:[PNSubscribeEventData class]], + @"PNSubscribeObjectEventData should be a subclass of PNSubscribeEventData."); +} + +- (void)testMessageActionEventDataShouldInheritFromSubscribeEventData { + XCTAssertTrue([PNSubscribeMessageActionEventData isSubclassOfClass:[PNSubscribeEventData class]], + @"PNSubscribeMessageActionEventData should be a subclass of PNSubscribeEventData."); +} + + +#pragma mark - Tests :: Copy event results + +- (void)testItShouldCopyMessageEventResult { + PNSubscribeMessageEventData *eventData = [[PNSubscribeMessageEventData alloc] init]; + PNMessageResult *result = [PNMessageResult objectWithOperation:PNSubscribeOperation response:eventData]; + PNMessageResult *copy = [result copy]; + + XCTAssertNotNil(copy, @"Copied message result should not be nil."); + XCTAssertEqual(copy.operation, PNSubscribeOperation, + @"Copied result should preserve operation."); + XCTAssertNotNil(copy.data, @"Copied result should preserve data."); +} + +- (void)testItShouldCopyPresenceEventResult { + PNSubscribePresenceEventData *eventData = [[PNSubscribePresenceEventData alloc] init]; + PNPresenceEventResult *result = [PNPresenceEventResult objectWithOperation:PNSubscribeOperation + response:eventData]; + PNPresenceEventResult *copy = [result copy]; + + XCTAssertNotNil(copy, @"Copied presence result should not be nil."); + XCTAssertEqual(copy.operation, PNSubscribeOperation, + @"Copied result should preserve operation."); +} + +- (void)testItShouldCopyFileEventResult { + PNSubscribeFileEventData *eventData = [[PNSubscribeFileEventData alloc] init]; + PNFileEventResult *result = [PNFileEventResult objectWithOperation:PNSubscribeOperation response:eventData]; + PNFileEventResult *copy = [result copy]; + + XCTAssertNotNil(copy, @"Copied file event result should not be nil."); + XCTAssertEqual(copy.operation, PNSubscribeOperation, + @"Copied result should preserve operation."); +} + +#pragma mark - + + +@end diff --git a/Tests/Tests/Unit/Models/PNResultTest.m b/Tests/Tests/Unit/Models/PNResultTest.m new file mode 100644 index 000000000..741eb1292 --- /dev/null +++ b/Tests/Tests/Unit/Models/PNResultTest.m @@ -0,0 +1,326 @@ +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import "PNOperationResult+Private.h" + + +#pragma mark - Private interface exposure + +@interface PNTimeData (TestAccess) + +- (instancetype)initWithTimetoken:(NSNumber *)timetoken; + +@end + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +/// Unit tests for PNOperationResult subclasses and their associated data objects. +/// +/// Tests covering construction and property access for result model objects used by the SDK. +@interface PNResultTest : XCTestCase + +#pragma mark - + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNResultTest + + +#pragma mark - Tests :: PNTimeResult + +- (void)testItShouldCreateTimeResult { + PNTimeResult *result = [PNTimeResult objectWithOperation:PNTimeOperation response:nil]; + + XCTAssertEqual(result.operation, PNTimeOperation, @"Operation type should be PNTimeOperation."); +} + +- (void)testItShouldReturnTimeResultDataWhenResponseIsSet { + PNTimeData *timeData = [[PNTimeData alloc] initWithTimetoken:@(16000000000000000)]; + PNTimeResult *result = [PNTimeResult objectWithOperation:PNTimeOperation response:timeData]; + + XCTAssertNotNil(result.data, @"Time result data should not be nil."); + XCTAssertEqualObjects(result.data.timetoken, @(16000000000000000), + @"Timetoken should match the provided value."); +} + +- (void)testItShouldReturnNilDataWhenTimeResponseIsNil { + PNTimeResult *result = [PNTimeResult objectWithOperation:PNTimeOperation response:nil]; + + XCTAssertNil(result.data, @"Time result data should be nil when no response is set."); +} + +- (void)testItShouldReturnStringifiedTimeOperation { + PNTimeResult *result = [PNTimeResult objectWithOperation:PNTimeOperation response:nil]; + + XCTAssertEqualObjects(result.stringifiedOperation, @"Time", + @"Stringified operation should be 'Time'."); +} + +- (void)testItShouldBeSubclassOfPNOperationResult { + PNTimeResult *result = [PNTimeResult objectWithOperation:PNTimeOperation response:nil]; + + XCTAssertTrue([result isKindOfClass:[PNOperationResult class]], + @"PNTimeResult should be a subclass of PNOperationResult."); +} + + +#pragma mark - Tests :: PNHistoryResult + +- (void)testItShouldCreateHistoryResult { + PNHistoryResult *result = [PNHistoryResult objectWithOperation:PNHistoryOperation response:nil]; + + XCTAssertEqual(result.operation, PNHistoryOperation, @"Operation type should be PNHistoryOperation."); +} + +- (void)testItShouldReturnNilDataWhenHistoryResponseIsNil { + PNHistoryResult *result = [PNHistoryResult objectWithOperation:PNHistoryOperation response:nil]; + + XCTAssertNil(result.data, @"History result data should be nil when no response is set."); +} + +- (void)testItShouldSupportHistoryForChannelsOperation { + PNHistoryResult *result = [PNHistoryResult objectWithOperation:PNHistoryForChannelsOperation response:nil]; + + XCTAssertEqual(result.operation, PNHistoryForChannelsOperation, + @"Operation type should be PNHistoryForChannelsOperation."); + XCTAssertEqualObjects(result.stringifiedOperation, @"History for Channels", + @"Stringified operation should match."); +} + +- (void)testItShouldSupportHistoryWithActionsOperation { + PNHistoryResult *result = [PNHistoryResult objectWithOperation:PNHistoryWithActionsOperation response:nil]; + + XCTAssertEqualObjects(result.stringifiedOperation, @"History with Actions", + @"Stringified operation should match."); +} + + +#pragma mark - Tests :: PNMessageCountResult + +- (void)testItShouldCreateMessageCountResult { + PNMessageCountResult *result = [PNMessageCountResult objectWithOperation:PNMessageCountOperation response:nil]; + + XCTAssertEqual(result.operation, PNMessageCountOperation, + @"Operation type should be PNMessageCountOperation."); +} + +- (void)testItShouldReturnNilDataWhenMessageCountResponseIsNil { + PNMessageCountResult *result = [PNMessageCountResult objectWithOperation:PNMessageCountOperation response:nil]; + + XCTAssertNil(result.data, @"Message count result data should be nil when no response is set."); +} + +- (void)testItShouldReturnStringifiedMessageCountOperation { + PNMessageCountResult *result = [PNMessageCountResult objectWithOperation:PNMessageCountOperation response:nil]; + + XCTAssertEqualObjects(result.stringifiedOperation, @"Message count for Channels", + @"Stringified operation should match."); +} + + +#pragma mark - Tests :: PNPresenceHereNowResult + +- (void)testItShouldCreateHereNowResult { + PNPresenceHereNowResult *result = [PNPresenceHereNowResult objectWithOperation:PNHereNowForChannelOperation + response:nil]; + + XCTAssertEqual(result.operation, PNHereNowForChannelOperation, + @"Operation type should be PNHereNowForChannelOperation."); +} + +- (void)testItShouldReturnNilDataWhenHereNowResponseIsNil { + PNPresenceHereNowResult *result = [PNPresenceHereNowResult objectWithOperation:PNHereNowForChannelOperation + response:nil]; + + XCTAssertNil(result.data, @"Here now result data should be nil when no response is set."); +} + +- (void)testItShouldSupportGlobalHereNowOperation { + PNPresenceHereNowResult *result = [PNPresenceHereNowResult objectWithOperation:PNHereNowGlobalOperation + response:nil]; + + XCTAssertEqualObjects(result.stringifiedOperation, @"Global Here Now", + @"Stringified operation should match."); +} + + +#pragma mark - Tests :: PNPresenceWhereNowResult + +- (void)testItShouldCreateWhereNowResult { + PNPresenceWhereNowResult *result = [PNPresenceWhereNowResult objectWithOperation:PNWhereNowOperation + response:nil]; + + XCTAssertEqual(result.operation, PNWhereNowOperation, + @"Operation type should be PNWhereNowOperation."); +} + +- (void)testItShouldReturnNilDataWhenWhereNowResponseIsNil { + PNPresenceWhereNowResult *result = [PNPresenceWhereNowResult objectWithOperation:PNWhereNowOperation + response:nil]; + + XCTAssertNil(result.data, @"Where now result data should be nil when no response is set."); +} + +- (void)testItShouldReturnStringifiedWhereNowOperation { + PNPresenceWhereNowResult *result = [PNPresenceWhereNowResult objectWithOperation:PNWhereNowOperation + response:nil]; + + XCTAssertEqualObjects(result.stringifiedOperation, @"Where Now", + @"Stringified operation should be 'Where Now'."); +} + + +#pragma mark - Tests :: PNChannelGroupChannelsResult + +- (void)testItShouldCreateChannelGroupChannelsResult { + PNChannelGroupChannelsResult *result = [PNChannelGroupChannelsResult objectWithOperation:PNChannelsForGroupOperation + response:nil]; + + XCTAssertEqual(result.operation, PNChannelsForGroupOperation, + @"Operation type should be PNChannelsForGroupOperation."); +} + +- (void)testItShouldReturnNilDataWhenChannelGroupResponseIsNil { + PNChannelGroupChannelsResult *result = [PNChannelGroupChannelsResult objectWithOperation:PNChannelsForGroupOperation + response:nil]; + + XCTAssertNil(result.data, @"Channel group result data should be nil when no response is set."); +} + +- (void)testItShouldReturnStringifiedChannelsForGroupOperation { + PNChannelGroupChannelsResult *result = [PNChannelGroupChannelsResult objectWithOperation:PNChannelsForGroupOperation + response:nil]; + + XCTAssertEqualObjects(result.stringifiedOperation, @"Get Channels For Group", + @"Stringified operation should match."); +} + + +#pragma mark - Tests :: Result with mock response data + +- (void)testItShouldReturnTimeDataWhenSetDirectly { + PNTimeData *timeData = [[PNTimeData alloc] initWithTimetoken:@(16123456789012345)]; + PNTimeResult *result = [PNTimeResult objectWithOperation:PNTimeOperation response:timeData]; + + XCTAssertNotNil(result.data, @"Data should not be nil when response is set."); + XCTAssertTrue([result.data isKindOfClass:[PNTimeData class]], @"Data should be PNTimeData instance."); + XCTAssertEqualObjects(result.data.timetoken, @(16123456789012345), + @"Timetoken should be preserved correctly."); +} + +- (void)testItShouldPreserveLargeTimetoken { + PNTimeData *timeData = [[PNTimeData alloc] initWithTimetoken:@(17094336000000000)]; + PNTimeResult *result = [PNTimeResult objectWithOperation:PNTimeOperation response:timeData]; + + XCTAssertEqualObjects(result.data.timetoken, @(17094336000000000), + @"Large timetoken should be preserved without precision loss."); +} + + +#pragma mark - Tests :: Result copy + +- (void)testItShouldCopyTimeResult { + PNTimeData *timeData = [[PNTimeData alloc] initWithTimetoken:@(16000000000000000)]; + PNTimeResult *result = [PNTimeResult objectWithOperation:PNTimeOperation response:timeData]; + PNTimeResult *copy = [result copy]; + + XCTAssertNotNil(copy, @"Copied result should not be nil."); + XCTAssertEqual(copy.operation, PNTimeOperation, @"Copied result should preserve operation."); + XCTAssertEqualObjects(copy.data.timetoken, @(16000000000000000), + @"Copied result should preserve data."); +} + +- (void)testItShouldCopyHistoryResult { + PNHistoryResult *result = [PNHistoryResult objectWithOperation:PNHistoryOperation response:nil]; + PNHistoryResult *copy = [result copy]; + + XCTAssertNotNil(copy, @"Copied history result should not be nil."); + XCTAssertEqual(copy.operation, PNHistoryOperation, @"Copied result should preserve operation."); +} + + +#pragma mark - Tests :: Multiple result types independence + +- (void)testMultipleResultTypesShouldBeIndependent { + PNTimeData *timeData = [[PNTimeData alloc] initWithTimetoken:@(16000000000000000)]; + PNTimeResult *timeResult = [PNTimeResult objectWithOperation:PNTimeOperation response:timeData]; + PNHistoryResult *historyResult = [PNHistoryResult objectWithOperation:PNHistoryOperation response:nil]; + PNMessageCountResult *countResult = [PNMessageCountResult objectWithOperation:PNMessageCountOperation response:nil]; + + XCTAssertEqual(timeResult.operation, PNTimeOperation, @"Time result operation should be independent."); + XCTAssertEqual(historyResult.operation, PNHistoryOperation, @"History result operation should be independent."); + XCTAssertEqual(countResult.operation, PNMessageCountOperation, @"Count result operation should be independent."); + XCTAssertNotNil(timeResult.data, @"Time result should have data."); + XCTAssertNil(historyResult.data, @"History result should have nil data."); + XCTAssertNil(countResult.data, @"Count result should have nil data."); +} + + +#pragma mark - Tests :: All operation types have string representation + +- (void)testAllMainOperationTypesShouldHaveStringRepresentation { + NSArray *operations = @[ + @(PNSubscribeOperation), + @(PNUnsubscribeOperation), + @(PNPublishOperation), + @(PNSignalOperation), + @(PNHistoryOperation), + @(PNHistoryForChannelsOperation), + @(PNHistoryWithActionsOperation), + @(PNDeleteMessageOperation), + @(PNMessageCountOperation), + @(PNWhereNowOperation), + @(PNHereNowGlobalOperation), + @(PNHereNowForChannelOperation), + @(PNHereNowForChannelGroupOperation), + @(PNTimeOperation), + @(PNChannelsForGroupOperation), + @(PNChannelGroupsOperation), + @(PNSetStateOperation), + @(PNGetStateOperation), + @(PNFetchUUIDMetadataOperation), + @(PNFetchAllUUIDMetadataOperation), + @(PNFetchChannelMetadataOperation), + @(PNFetchAllChannelsMetadataOperation), + @(PNFetchMembershipsOperation), + @(PNFetchChannelMembersOperation), + @(PNFetchMessagesActionsOperation), + @(PNListFilesOperation), + @(PNDownloadFileOperation), + ]; + + for (NSNumber *opNumber in operations) { + PNOperationType op = (PNOperationType)opNumber.integerValue; + PNOperationResult *result = [PNOperationResult objectWithOperation:op response:nil]; + + XCTAssertNotNil(result.stringifiedOperation, + @"Operation %ld should have a string representation.", (long)op); + XCTAssertFalse([result.stringifiedOperation isEqualToString:@"Unknown"], + @"Operation %ld should not return 'Unknown' as string representation.", (long)op); + } +} + +#pragma mark - + + +@end diff --git a/Tests/Tests/Unit/Models/PNStatusTest.m b/Tests/Tests/Unit/Models/PNStatusTest.m new file mode 100644 index 000000000..87c47a321 --- /dev/null +++ b/Tests/Tests/Unit/Models/PNStatusTest.m @@ -0,0 +1,581 @@ +#import +#import +#import +#import +#import +#import +#import +#import +#import "PNStatus+Private.h" +#import "PNErrorStatus+Private.h" +#import "PNOperationResult+Private.h" +#import "PNSubscribeStatus+Private.h" +#import "PNErrorData+Private.h" + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +/// Unit tests for PNStatus, PNErrorStatus, PNAcknowledgmentStatus, and related status model objects. +@interface PNStatusTest : XCTestCase + +#pragma mark - + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNStatusTest + + +#pragma mark - Tests :: PNOperationResult base + +- (void)testItShouldCreateOperationResultWithOperationAndResponse { + PNOperationResult *result = [PNOperationResult objectWithOperation:PNTimeOperation response:nil]; + + XCTAssertEqual(result.operation, PNTimeOperation, @"Operation type should match the provided value."); +} + +- (void)testItShouldReturnStringifiedOperationForKnownOperationType { + PNOperationResult *result = [PNOperationResult objectWithOperation:PNPublishOperation response:nil]; + + XCTAssertEqualObjects(result.stringifiedOperation, @"Publish", + @"Stringified operation should return 'Publish' for PNPublishOperation."); +} + +- (void)testItShouldReturnStringifiedSubscribeOperation { + PNOperationResult *result = [PNOperationResult objectWithOperation:PNSubscribeOperation response:nil]; + + XCTAssertEqualObjects(result.stringifiedOperation, @"Subscribe", + @"Stringified operation should return 'Subscribe' for PNSubscribeOperation."); +} + +- (void)testItShouldReturnStringifiedTimeOperation { + PNOperationResult *result = [PNOperationResult objectWithOperation:PNTimeOperation response:nil]; + + XCTAssertEqualObjects(result.stringifiedOperation, @"Time", + @"Stringified operation should return 'Time' for PNTimeOperation."); +} + +- (void)testItShouldCopyOperationResult { + NSString *responseData = @"test-response"; + PNOperationResult *result = [PNOperationResult objectWithOperation:PNHistoryOperation response:responseData]; + PNOperationResult *copy = [result copy]; + + XCTAssertNotNil(copy, @"Copy should not be nil."); + XCTAssertEqual(copy.operation, PNHistoryOperation, @"Copied object should preserve operation type."); + XCTAssertEqualObjects(copy.responseData, responseData, @"Copied object should preserve response data."); +} + +- (void)testItShouldStoreResponseData { + NSDictionary *response = @{ @"key": @"value" }; + PNOperationResult *result = [PNOperationResult objectWithOperation:PNTimeOperation response:response]; + + XCTAssertEqualObjects(result.responseData, response, @"Response data should match the provided response."); +} + +- (void)testItShouldHandleNilResponse { + PNOperationResult *result = [PNOperationResult objectWithOperation:PNTimeOperation response:nil]; + + XCTAssertNil(result.responseData, @"Response data should be nil when nil is provided."); +} + + +#pragma mark - Tests :: PNStatus base + +- (void)testItShouldCreateStatusWithOperationAndCategory { + PNStatus *status = [PNStatus objectWithOperation:PNSubscribeOperation + category:PNConnectedCategory + response:nil]; + + XCTAssertEqual(status.operation, PNSubscribeOperation, @"Operation type should match."); + XCTAssertEqual(status.category, PNConnectedCategory, @"Category should match."); +} + +- (void)testItShouldNotBeErrorForConnectedCategory { + PNStatus *status = [PNStatus objectWithOperation:PNSubscribeOperation + category:PNConnectedCategory + response:nil]; + + XCTAssertFalse(status.isError, @"Connected category should not be an error."); +} + +- (void)testItShouldNotBeErrorForReconnectedCategory { + PNStatus *status = [PNStatus objectWithOperation:PNSubscribeOperation + category:PNReconnectedCategory + response:nil]; + + XCTAssertFalse(status.isError, @"Reconnected category should not be an error."); +} + +- (void)testItShouldNotBeErrorForDisconnectedCategory { + PNStatus *status = [PNStatus objectWithOperation:PNUnsubscribeOperation + category:PNDisconnectedCategory + response:nil]; + + XCTAssertFalse(status.isError, @"Disconnected category should not be an error."); +} + +- (void)testItShouldNotBeErrorForUnexpectedDisconnectCategory { + PNStatus *status = [PNStatus objectWithOperation:PNSubscribeOperation + category:PNUnexpectedDisconnectCategory + response:nil]; + + XCTAssertFalse(status.isError, @"Unexpected disconnect category should not be an error."); +} + +- (void)testItShouldNotBeErrorForCancelledCategory { + PNStatus *status = [PNStatus objectWithOperation:PNSubscribeOperation + category:PNCancelledCategory + response:nil]; + + XCTAssertFalse(status.isError, @"Cancelled category should not be an error."); +} + +- (void)testItShouldNotBeErrorForAcknowledgmentCategory { + PNStatus *status = [PNStatus objectWithOperation:PNPublishOperation + category:PNAcknowledgmentCategory + response:nil]; + + XCTAssertFalse(status.isError, @"Acknowledgment category should not be an error."); +} + +- (void)testItShouldNotBeErrorForUnknownCategory { + PNStatus *status = [PNStatus objectWithOperation:PNPublishOperation + category:PNUnknownCategory + response:nil]; + + XCTAssertFalse(status.isError, @"Unknown category should not be an error by default."); +} + +- (void)testItShouldBeErrorForAccessDeniedCategory { + PNStatus *status = [PNStatus objectWithOperation:PNPublishOperation + category:PNAccessDeniedCategory + response:nil]; + + XCTAssertTrue(status.isError, @"Access denied category should be an error."); +} + +- (void)testItShouldBeErrorForTimeoutCategory { + PNStatus *status = [PNStatus objectWithOperation:PNPublishOperation + category:PNTimeoutCategory + response:nil]; + + XCTAssertTrue(status.isError, @"Timeout category should be an error."); +} + +- (void)testItShouldBeErrorForNetworkIssuesCategory { + PNStatus *status = [PNStatus objectWithOperation:PNPublishOperation + category:PNNetworkIssuesCategory + response:nil]; + + XCTAssertTrue(status.isError, @"Network issues category should be an error."); +} + +- (void)testItShouldBeErrorForBadRequestCategory { + PNStatus *status = [PNStatus objectWithOperation:PNPublishOperation + category:PNBadRequestCategory + response:nil]; + + XCTAssertTrue(status.isError, @"Bad request category should be an error."); +} + +- (void)testItShouldBeErrorForDecryptionErrorCategory { + PNStatus *status = [PNStatus objectWithOperation:PNSubscribeOperation + category:PNDecryptionErrorCategory + response:nil]; + + XCTAssertTrue(status.isError, @"Decryption error category should be an error."); +} + +- (void)testItShouldBeErrorForMalformedResponseCategory { + PNStatus *status = [PNStatus objectWithOperation:PNHistoryOperation + category:PNMalformedResponseCategory + response:nil]; + + XCTAssertTrue(status.isError, @"Malformed response category should be an error."); +} + +- (void)testItShouldBeErrorForRequestURITooLongCategory { + PNStatus *status = [PNStatus objectWithOperation:PNSubscribeOperation + category:PNRequestURITooLongCategory + response:nil]; + + XCTAssertTrue(status.isError, @"Request URI too long category should be an error."); +} + +- (void)testItShouldBeErrorForTLSConnectionFailedCategory { + PNStatus *status = [PNStatus objectWithOperation:PNSubscribeOperation + category:PNTLSConnectionFailedCategory + response:nil]; + + XCTAssertTrue(status.isError, @"TLS connection failed category should be an error."); +} + +- (void)testItShouldReturnStringifiedCategory { + PNStatus *status = [PNStatus objectWithOperation:PNSubscribeOperation + category:PNConnectedCategory + response:nil]; + + XCTAssertEqualObjects(status.stringifiedCategory, @"Connected", + @"Stringified category should return 'Connected' for PNConnectedCategory."); +} + +- (void)testItShouldReturnStringifiedCategoryForAccessDenied { + PNStatus *status = [PNStatus objectWithOperation:PNPublishOperation + category:PNAccessDeniedCategory + response:nil]; + + XCTAssertEqualObjects(status.stringifiedCategory, @"Access Denied", + @"Stringified category should return 'Access Denied'."); +} + +- (void)testItShouldReturnStringifiedCategoryForTimeout { + PNStatus *status = [PNStatus objectWithOperation:PNPublishOperation + category:PNTimeoutCategory + response:nil]; + + XCTAssertEqualObjects(status.stringifiedCategory, @"Timeout", + @"Stringified category should return 'Timeout'."); +} + +- (void)testItShouldUpdateCategory { + PNStatus *status = [PNStatus objectWithOperation:PNSubscribeOperation + category:PNConnectedCategory + response:nil]; + + XCTAssertFalse(status.isError, @"Should not be error initially."); + + [status updateCategory:PNAccessDeniedCategory]; + + XCTAssertEqual(status.category, PNAccessDeniedCategory, @"Category should be updated."); +} + +- (void)testItShouldSetErrorToYesWhenCategoryUpdatedToDecryptionError { + PNStatus *status = [PNStatus objectWithOperation:PNSubscribeOperation + category:PNAcknowledgmentCategory + response:nil]; + + XCTAssertFalse(status.isError, @"Should not be error initially."); + + [status updateCategory:PNDecryptionErrorCategory]; + + XCTAssertTrue(status.isError, @"Should be error after updating category to decryption error."); +} + +- (void)testItShouldSetErrorToYesWhenCategoryUpdatedToBadRequest { + PNStatus *status = [PNStatus objectWithOperation:PNSubscribeOperation + category:PNAcknowledgmentCategory + response:nil]; + + [status updateCategory:PNBadRequestCategory]; + + XCTAssertTrue(status.isError, @"Should be error after updating category to bad request."); +} + +- (void)testItShouldSetErrorToNoWhenCategoryUpdatedToConnected { + PNStatus *status = [PNStatus objectWithOperation:PNSubscribeOperation + category:PNAccessDeniedCategory + response:nil]; + + XCTAssertTrue(status.isError, @"Should be error initially."); + + [status updateCategory:PNConnectedCategory]; + + XCTAssertFalse(status.isError, @"Should not be error after updating category to connected."); +} + +- (void)testItShouldCopyStatusWithAllProperties { + PNStatus *status = [PNStatus objectWithOperation:PNSubscribeOperation + category:PNConnectedCategory + response:nil]; + status.subscribedChannels = @[@"channel1", @"channel2"]; + status.subscribedChannelGroups = @[@"group1"]; + status.currentTimetoken = @(16000000000000000); + status.lastTimeToken = @(15000000000000000); + status.currentTimeTokenRegion = @(1); + status.lastTimeTokenRegion = @(2); + + PNStatus *copy = [status copy]; + + XCTAssertEqual(copy.operation, PNSubscribeOperation, @"Copied status should preserve operation."); + XCTAssertEqual(copy.category, PNConnectedCategory, @"Copied status should preserve category."); + XCTAssertFalse(copy.isError, @"Copied status should preserve error flag."); + XCTAssertEqualObjects(copy.subscribedChannels, (@[@"channel1", @"channel2"]), + @"Copied status should preserve subscribed channels."); + XCTAssertEqualObjects(copy.subscribedChannelGroups, @[@"group1"], + @"Copied status should preserve subscribed channel groups."); + XCTAssertEqualObjects(copy.currentTimetoken, @(16000000000000000), + @"Copied status should preserve current timetoken."); + XCTAssertEqualObjects(copy.lastTimeToken, @(15000000000000000), + @"Copied status should preserve last time token."); + XCTAssertEqualObjects(copy.currentTimeTokenRegion, @(1), + @"Copied status should preserve current time token region."); + XCTAssertEqualObjects(copy.lastTimeTokenRegion, @(2), + @"Copied status should preserve last time token region."); +} + + +#pragma mark - Tests :: PNErrorStatus + +- (void)testItShouldCreateErrorStatusObject { + PNErrorStatus *errorStatus = [PNErrorStatus objectWithOperation:PNPublishOperation + category:PNAccessDeniedCategory + response:nil]; + + XCTAssertTrue(errorStatus.isError, @"Error status should indicate error."); + XCTAssertEqual(errorStatus.category, PNAccessDeniedCategory, @"Category should match."); +} + +- (void)testItShouldCreateErrorStatusFromErrorObject { + NSError *error = [NSError errorWithDomain:@"PNTransportErrorDomain" + code:3001 // PNTransportErrorRequestTimeout + userInfo:@{NSLocalizedFailureReasonErrorKey: @"Request timed out"}]; + PNErrorData *errorData = [PNErrorData dataWithError:error]; + + PNErrorStatus *errorStatus = [PNErrorStatus objectWithOperation:PNPublishOperation + category:PNUnknownCategory + response:errorData]; + + XCTAssertNotNil(errorStatus.errorData, @"Error data should be set."); +} + +- (void)testItShouldSetAssociatedObjectOnErrorStatus { + PNErrorStatus *errorStatus = [PNErrorStatus objectWithOperation:PNSubscribeOperation + category:PNDecryptionErrorCategory + response:nil]; + NSDictionary *associatedObject = @{ @"original": @"message" }; + errorStatus.associatedObject = associatedObject; + + XCTAssertEqualObjects(errorStatus.associatedObject, associatedObject, + @"Associated object should match the assigned value."); +} + +- (void)testItShouldCopyErrorStatusWithAssociatedObject { + PNErrorStatus *errorStatus = [PNErrorStatus objectWithOperation:PNSubscribeOperation + category:PNDecryptionErrorCategory + response:nil]; + errorStatus.associatedObject = @{ @"original": @"message" }; + + PNErrorStatus *copy = [errorStatus copy]; + + XCTAssertEqualObjects(copy.associatedObject, errorStatus.associatedObject, + @"Copied error status should preserve associated object."); +} + +- (void)testItShouldFallbackToAcknowledgmentCategoryWhenNotErrorAndUnknown { + PNErrorStatus *errorStatus = [PNErrorStatus objectWithOperation:PNPublishOperation + category:PNAcknowledgmentCategory + response:nil]; + + XCTAssertEqual(errorStatus.category, PNAcknowledgmentCategory, + @"Non-error status with acknowledgment category should stay acknowledgment."); + XCTAssertFalse(errorStatus.isError, @"Acknowledgment status should not be error."); +} + + +#pragma mark - Tests :: PNAcknowledgmentStatus + +- (void)testItShouldCreateAcknowledgmentStatus { + PNAcknowledgmentStatus *ackStatus = [PNAcknowledgmentStatus objectWithOperation:PNDeleteMessageOperation + category:PNAcknowledgmentCategory + response:nil]; + + XCTAssertFalse(ackStatus.isError, @"Acknowledgment status should not be error."); + XCTAssertEqual(ackStatus.category, PNAcknowledgmentCategory, + @"Category should be acknowledgment."); +} + +- (void)testItShouldBeSubclassOfPNErrorStatus { + PNAcknowledgmentStatus *ackStatus = [PNAcknowledgmentStatus objectWithOperation:PNDeleteMessageOperation + category:PNAcknowledgmentCategory + response:nil]; + + XCTAssertTrue([ackStatus isKindOfClass:[PNErrorStatus class]], + @"PNAcknowledgmentStatus should be a subclass of PNErrorStatus."); + XCTAssertTrue([ackStatus isKindOfClass:[PNStatus class]], + @"PNAcknowledgmentStatus should be a subclass of PNStatus."); +} + + +#pragma mark - Tests :: PNPublishStatus + +- (void)testItShouldCreatePublishStatus { + PNPublishStatus *publishStatus = [PNPublishStatus objectWithOperation:PNPublishOperation + category:PNAcknowledgmentCategory + response:nil]; + + XCTAssertEqual(publishStatus.operation, PNPublishOperation, @"Operation should be publish."); + XCTAssertFalse(publishStatus.isError, @"Successful publish should not be error."); +} + +- (void)testItShouldReturnNilDataWhenPublishStatusIsError { + PNPublishStatus *publishStatus = [PNPublishStatus objectWithOperation:PNPublishOperation + category:PNAccessDeniedCategory + response:nil]; + + XCTAssertTrue(publishStatus.isError, @"Publish status with access denied should be error."); + XCTAssertNil(publishStatus.data, @"Data should be nil when status is error."); +} + +- (void)testItShouldBeSubclassOfPNAcknowledgmentStatus { + PNPublishStatus *publishStatus = [PNPublishStatus objectWithOperation:PNPublishOperation + category:PNAcknowledgmentCategory + response:nil]; + + XCTAssertTrue([publishStatus isKindOfClass:[PNAcknowledgmentStatus class]], + @"PNPublishStatus should be a subclass of PNAcknowledgmentStatus."); +} + + +#pragma mark - Tests :: PNSignalStatus + +- (void)testItShouldCreateSignalStatus { + PNSignalStatus *signalStatus = [PNSignalStatus objectWithOperation:PNSignalOperation + category:PNAcknowledgmentCategory + response:nil]; + + XCTAssertEqual(signalStatus.operation, PNSignalOperation, @"Operation should be signal."); + XCTAssertFalse(signalStatus.isError, @"Successful signal should not be error."); +} + +- (void)testItShouldReturnNilDataWhenSignalStatusIsError { + PNSignalStatus *signalStatus = [PNSignalStatus objectWithOperation:PNSignalOperation + category:PNBadRequestCategory + response:nil]; + + XCTAssertTrue(signalStatus.isError, @"Signal status with bad request should be error."); + XCTAssertNil(signalStatus.data, @"Data should be nil when status is error."); +} + + +#pragma mark - Tests :: PNSubscribeStatus + +- (void)testItShouldCreateSubscribeStatus { + PNSubscribeStatus *subStatus = [PNSubscribeStatus objectWithOperation:PNSubscribeOperation + category:PNConnectedCategory + response:nil]; + + XCTAssertEqual(subStatus.operation, PNSubscribeOperation, @"Operation should be subscribe."); + XCTAssertFalse(subStatus.isError, @"Connected subscribe status should not be error."); +} + +- (void)testItShouldExposeSubscriptionProperties { + PNSubscribeStatus *subStatus = [PNSubscribeStatus objectWithOperation:PNSubscribeOperation + category:PNConnectedCategory + response:nil]; + subStatus.subscribedChannels = @[@"ch1", @"ch2"]; + subStatus.subscribedChannelGroups = @[@"cg1"]; + subStatus.currentTimetoken = @(16500000000000000); + subStatus.lastTimeToken = @(16400000000000000); + + XCTAssertEqualObjects(subStatus.subscribedChannels, (@[@"ch1", @"ch2"]), + @"Subscribed channels should be accessible."); + XCTAssertEqualObjects(subStatus.subscribedChannelGroups, @[@"cg1"], + @"Subscribed channel groups should be accessible."); + XCTAssertEqualObjects(subStatus.currentTimetoken, @(16500000000000000), + @"Current timetoken should be accessible."); + XCTAssertEqualObjects(subStatus.lastTimeToken, @(16400000000000000), + @"Last time token should be accessible."); +} + +- (void)testSubscribeStatusShouldBeSubclassOfPNErrorStatus { + PNSubscribeStatus *subStatus = [PNSubscribeStatus objectWithOperation:PNSubscribeOperation + category:PNConnectedCategory + response:nil]; + + XCTAssertTrue([subStatus isKindOfClass:[PNErrorStatus class]], + @"PNSubscribeStatus should be a subclass of PNErrorStatus."); +} + +- (void)testItShouldBeErrorForSubscribeWithAccessDenied { + PNSubscribeStatus *subStatus = [PNSubscribeStatus objectWithOperation:PNSubscribeOperation + category:PNAccessDeniedCategory + response:nil]; + + XCTAssertTrue(subStatus.isError, @"Subscribe status with access denied should be error."); +} + + +#pragma mark - Tests :: Category error flag mapping comprehensive + +- (void)testAllNonErrorCategoriesShouldNotSetErrorFlag { + NSArray *nonErrorCategories = @[ + @(PNConnectedCategory), + @(PNReconnectedCategory), + @(PNDisconnectedCategory), + @(PNUnexpectedDisconnectCategory), + @(PNCancelledCategory), + @(PNAcknowledgmentCategory), + ]; + + for (NSNumber *categoryNumber in nonErrorCategories) { + PNStatusCategory category = (PNStatusCategory)categoryNumber.integerValue; + PNStatus *status = [PNStatus objectWithOperation:PNSubscribeOperation + category:category + response:nil]; + + XCTAssertFalse(status.isError, @"Category %@ should not be an error.", + status.stringifiedCategory); + } +} + +- (void)testAllErrorCategoriesShouldSetErrorFlag { + NSArray *errorCategories = @[ + @(PNAccessDeniedCategory), + @(PNTimeoutCategory), + @(PNNetworkIssuesCategory), + @(PNRequestMessageCountExceededCategory), + @(PNBadRequestCategory), + @(PNRequestURITooLongCategory), + @(PNMalformedFilterExpressionCategory), + @(PNMalformedResponseCategory), + @(PNDecryptionErrorCategory), + @(PNTLSConnectionFailedCategory), + @(PNTLSUntrustedCertificateCategory), + @(PNSendFileErrorCategory), + @(PNPublishFileMessageErrorCategory), + @(PNDownloadErrorCategory), + @(PNResourceNotFoundCategory), + ]; + + for (NSNumber *categoryNumber in errorCategories) { + PNStatusCategory category = (PNStatusCategory)categoryNumber.integerValue; + PNStatus *status = [PNStatus objectWithOperation:PNPublishOperation + category:category + response:nil]; + + XCTAssertTrue(status.isError, @"Category %@ should be an error.", + status.stringifiedCategory); + } +} + + +#pragma mark - Tests :: PNStatus inheritance chain + +- (void)testStatusShouldBeSubclassOfOperationResult { + PNStatus *status = [PNStatus objectWithOperation:PNPublishOperation + category:PNAcknowledgmentCategory + response:nil]; + + XCTAssertTrue([status isKindOfClass:[PNOperationResult class]], + @"PNStatus should be a subclass of PNOperationResult."); +} + +- (void)testErrorStatusShouldBeSubclassOfStatus { + PNErrorStatus *errorStatus = [PNErrorStatus objectWithOperation:PNPublishOperation + category:PNAccessDeniedCategory + response:nil]; + + XCTAssertTrue([errorStatus isKindOfClass:[PNStatus class]], + @"PNErrorStatus should be a subclass of PNStatus."); +} + +#pragma mark - + + +@end diff --git a/Tests/Tests/Unit/Modules/Crypto/PNAESCBCCryptorTest.m b/Tests/Tests/Unit/Modules/Crypto/PNAESCBCCryptorTest.m new file mode 100644 index 000000000..35a460d49 --- /dev/null +++ b/Tests/Tests/Unit/Modules/Crypto/PNAESCBCCryptorTest.m @@ -0,0 +1,299 @@ +#import +#import +#import +#import +#import +#import "PNAESCBCCryptor+Private.h" + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +/// `PNAESCBCCryptor` unit tests. +/// +/// Tests covering AES-256-CBC cryptor encrypt/decrypt operations, data sizes, randomness, and error paths. +@interface PNAESCBCCryptorTest : XCTestCase + +#pragma mark - + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNAESCBCCryptorTest + + +#pragma mark - Tests :: Initialization + +- (void)testItShouldHaveACRHIdentifier { + PNAESCBCCryptor *cryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"testKey"]; + NSData *expectedIdentifier = [@"ACRH" dataUsingEncoding:NSUTF8StringEncoding]; + + XCTAssertEqualObjects([cryptor identifier], expectedIdentifier, + @"AES-CBC cryptor identifier should be 'ACRH'."); +} + + +#pragma mark - Tests :: Encrypt and decrypt round-trip + +- (void)testItShouldEncryptAndDecryptDataRoundTrip { + PNAESCBCCryptor *cryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"enigma"]; + NSData *originalData = [@"Hello, AES-CBC!" dataUsingEncoding:NSUTF8StringEncoding]; + + PNResult *encryptResult = [cryptor encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Encryption should succeed."); + XCTAssertNotNil(encryptResult.data, @"Encrypted data object should not be nil."); + XCTAssertNotNil(encryptResult.data.data, @"Encrypted data payload should not be nil."); + XCTAssertNotNil(encryptResult.data.metadata, @"Metadata (IV) should not be nil for random IV mode."); + XCTAssertEqual(encryptResult.data.metadata.length, 16u, @"IV should be 16 bytes (AES block size)."); + + PNResult *decryptResult = [cryptor decryptData:encryptResult.data]; + XCTAssertFalse(decryptResult.isError, @"Decryption should succeed."); + XCTAssertEqualObjects(decryptResult.data, originalData, @"Decrypted data should match original."); +} + + +#pragma mark - Tests :: Different data sizes + +- (void)testItShouldEncryptAndDecryptSingleByte { + PNAESCBCCryptor *cryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"singleByteKey"]; + uint8_t byte = 0x42; + NSData *originalData = [NSData dataWithBytes:&byte length:1]; + + PNResult *encryptResult = [cryptor encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Single byte encryption should succeed."); + + PNResult *decryptResult = [cryptor decryptData:encryptResult.data]; + XCTAssertFalse(decryptResult.isError, @"Single byte decryption should succeed."); + XCTAssertEqualObjects(decryptResult.data, originalData, @"Single byte round-trip should preserve data."); +} + +- (void)testItShouldEncryptAndDecryptExactBlockSize { + PNAESCBCCryptor *cryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"blockSizeKey"]; + // AES block size is 16 bytes. + NSMutableData *originalData = [NSMutableData dataWithLength:16]; + memset(originalData.mutableBytes, 0xAB, 16); + + PNResult *encryptResult = [cryptor encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Block-aligned data encryption should succeed."); + + PNResult *decryptResult = [cryptor decryptData:encryptResult.data]; + XCTAssertFalse(decryptResult.isError, @"Block-aligned data decryption should succeed."); + XCTAssertEqualObjects(decryptResult.data, originalData, + @"Block-aligned data round-trip should preserve data."); +} + +- (void)testItShouldEncryptAndDecryptMultipleBlockSize { + PNAESCBCCryptor *cryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"multiBlockKey"]; + // Multiple of block size: 3 * 16 = 48 bytes. + NSMutableData *originalData = [NSMutableData dataWithLength:48]; + memset(originalData.mutableBytes, 0xCD, 48); + + PNResult *encryptResult = [cryptor encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Multi-block data encryption should succeed."); + + PNResult *decryptResult = [cryptor decryptData:encryptResult.data]; + XCTAssertFalse(decryptResult.isError, @"Multi-block data decryption should succeed."); + XCTAssertEqualObjects(decryptResult.data, originalData, + @"Multi-block data round-trip should preserve data."); +} + +- (void)testItShouldEncryptAndDecryptLargeData { + PNAESCBCCryptor *cryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"largeDataKey"]; + NSMutableData *originalData = [NSMutableData dataWithLength:65536]; + SecRandomCopyBytes(kSecRandomDefault, originalData.length, originalData.mutableBytes); + + PNResult *encryptResult = [cryptor encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Large data encryption should succeed."); + + PNResult *decryptResult = [cryptor decryptData:encryptResult.data]; + XCTAssertFalse(decryptResult.isError, @"Large data decryption should succeed."); + XCTAssertEqualObjects(decryptResult.data, originalData, + @"Large data round-trip should preserve data."); +} + + +#pragma mark - Tests :: Random IV verification + +- (void)testItShouldProduceDifferentCiphertextForSameInput { + PNAESCBCCryptor *cryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"randomIVKey"]; + NSData *originalData = [@"Deterministic input" dataUsingEncoding:NSUTF8StringEncoding]; + + PNResult *encryptResult1 = [cryptor encryptData:originalData]; + PNResult *encryptResult2 = [cryptor encryptData:originalData]; + + XCTAssertFalse(encryptResult1.isError, @"First encryption should succeed."); + XCTAssertFalse(encryptResult2.isError, @"Second encryption should succeed."); + + // With random IV, both the metadata (IV) and the ciphertext should differ. + XCTAssertFalse([encryptResult1.data.metadata isEqualToData:encryptResult2.data.metadata], + @"IVs should differ between two encryptions of the same data."); + XCTAssertFalse([encryptResult1.data.data isEqualToData:encryptResult2.data.data], + @"Ciphertext should differ between two encryptions due to random IV."); +} + +- (void)testItShouldDecryptBothEncryptionsCorrectly { + PNAESCBCCryptor *cryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"multiEncKey"]; + NSData *originalData = [@"Same data, multiple encryptions" dataUsingEncoding:NSUTF8StringEncoding]; + + PNResult *encryptResult1 = [cryptor encryptData:originalData]; + PNResult *encryptResult2 = [cryptor encryptData:originalData]; + + PNResult *decryptResult1 = [cryptor decryptData:encryptResult1.data]; + PNResult *decryptResult2 = [cryptor decryptData:encryptResult2.data]; + + XCTAssertFalse(decryptResult1.isError, @"First decryption should succeed."); + XCTAssertFalse(decryptResult2.isError, @"Second decryption should succeed."); + XCTAssertEqualObjects(decryptResult1.data, originalData, @"First decryption should match original."); + XCTAssertEqualObjects(decryptResult2.data, originalData, @"Second decryption should match original."); +} + + +#pragma mark - Tests :: Encrypted data structure + +- (void)testItShouldProduceEncryptedDataWithMetadata { + PNAESCBCCryptor *cryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"structureKey"]; + NSData *originalData = [@"Structure test" dataUsingEncoding:NSUTF8StringEncoding]; + + PNResult *encryptResult = [cryptor encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Encryption should succeed."); + XCTAssertNotNil(encryptResult.data.data, @"Encrypted payload should be present."); + XCTAssertNotNil(encryptResult.data.metadata, @"Metadata should be present (contains IV)."); + XCTAssertGreaterThan(encryptResult.data.data.length, 0u, + @"Encrypted payload should have non-zero length."); +} + +- (void)testEncryptedDataLengthShouldBeBlockAligned { + PNAESCBCCryptor *cryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"alignKey"]; + NSData *originalData = [@"Test alignment" dataUsingEncoding:NSUTF8StringEncoding]; + + PNResult *encryptResult = [cryptor encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Encryption should succeed."); + // AES-CBC with PKCS7 padding always produces output that is a multiple of block size. + XCTAssertEqual(encryptResult.data.data.length % 16, 0u, + @"Encrypted data should be block-aligned (multiple of 16)."); +} + + +#pragma mark - Tests :: Error paths + +- (void)testItShouldReturnErrorWhenEncryptingEmptyData { + PNAESCBCCryptor *cryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"emptyKey"]; + NSData *emptyData = [NSData data]; + + PNResult *encryptResult = [cryptor encryptData:emptyData]; + XCTAssertTrue(encryptResult.isError, @"Encrypting empty data should return an error."); + XCTAssertEqual(encryptResult.error.code, PNCryptorErrorEncryption, + @"Error code should indicate encryption failure."); +} + +- (void)testItShouldNotDecryptOriginalDataWithWrongKey { + PNAESCBCCryptor *encryptCryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"correctKey"]; + NSString *originalString = @"Secret data"; + NSData *originalData = [originalString dataUsingEncoding:NSUTF8StringEncoding]; + + PNResult *encryptResult = [encryptCryptor encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Encryption should succeed."); + + PNAESCBCCryptor *decryptCryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"wrongKey"]; + PNResult *decryptResult = [decryptCryptor decryptData:encryptResult.data]; + + if (!decryptResult.isError) { + // In rare cases (~0.4%) random garbage may have valid PKCS7 padding; verify content mismatch. + NSString *decryptedString = [[NSString alloc] initWithData:decryptResult.data + encoding:NSUTF8StringEncoding]; + XCTAssertFalse([originalString isEqualToString:decryptedString], + @"Decrypted data with wrong key should not match the original."); + } +} + +- (void)testItShouldNotDecryptOriginalDataFromTruncatedCiphertext { + PNAESCBCCryptor *cryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"truncKey"]; + NSString *originalString = @"Data for truncation test"; + NSData *originalData = [originalString dataUsingEncoding:NSUTF8StringEncoding]; + + PNResult *encryptResult = [cryptor encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Encryption should succeed."); + + // Truncate the encrypted data to half its length. + NSData *truncatedData = [encryptResult.data.data subdataWithRange:NSMakeRange(0, encryptResult.data.data.length / 2)]; + PNEncryptedData *truncatedEncrypted = [PNEncryptedData encryptedDataWithData:truncatedData + metadata:encryptResult.data.metadata]; + + PNResult *decryptResult = [cryptor decryptData:truncatedEncrypted]; + + if (!decryptResult.isError) { + NSString *decryptedString = [[NSString alloc] initWithData:decryptResult.data + encoding:NSUTF8StringEncoding]; + XCTAssertFalse([originalString isEqualToString:decryptedString], + @"Decrypted truncated ciphertext should not match the original."); + } +} + +- (void)testItShouldReturnErrorWhenDecryptingDataShorterThanIVSize { + PNAESCBCCryptor *cryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"shortDataKey"]; + + // Create encrypted data with very short payload and no metadata (simulating missing IV). + NSData *shortData = [NSData dataWithBytes:"AB" length:2]; + PNEncryptedData *encryptedData = [PNEncryptedData encryptedDataWithData:shortData metadata:nil]; + + PNResult *decryptResult = [cryptor decryptData:encryptedData]; + // With random IV mode and no metadata, cryptor tries to extract IV from data. + // Data shorter than block size should produce an error. + XCTAssertTrue(decryptResult.isError, + @"Decrypting data shorter than IV size without metadata should return an error."); + XCTAssertEqual(decryptResult.error.code, PNCryptorErrorDecryption, + @"Error code should indicate decryption failure."); +} + +- (void)testItShouldReturnErrorWhenDecryptingEmptyEncryptedData { + PNAESCBCCryptor *cryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"emptyDecKey"]; + + // Create encrypted data with empty payload but valid metadata. + NSMutableData *iv = [NSMutableData dataWithLength:16]; + SecRandomCopyBytes(kSecRandomDefault, iv.length, iv.mutableBytes); + PNEncryptedData *encryptedData = [PNEncryptedData encryptedDataWithData:[NSData data] metadata:iv]; + + PNResult *decryptResult = [cryptor decryptData:encryptedData]; + XCTAssertTrue(decryptResult.isError, @"Decrypting empty encrypted data should return an error."); + XCTAssertEqual(decryptResult.error.code, PNCryptorErrorDecryption, + @"Error code should indicate decryption failure."); +} + + +#pragma mark - Tests :: Key digest + +- (void)testItShouldProduceSHA256DigestForCipherKey { + PNAESCBCCryptor *cryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"testKey"]; + NSData *digest = [cryptor digestForKey:@"testKey"]; + + XCTAssertNotNil(digest, @"Digest should not be nil."); + XCTAssertEqual(digest.length, 32u, @"SHA-256 digest should be 32 bytes long."); +} + +- (void)testItShouldProduceDifferentDigestsForDifferentKeys { + PNAESCBCCryptor *cryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"key1"]; + NSData *digest1 = [cryptor digestForKey:@"key1"]; + NSData *digest2 = [cryptor digestForKey:@"key2"]; + + XCTAssertFalse([digest1 isEqualToData:digest2], + @"Different keys should produce different SHA-256 digests."); +} + +- (void)testItShouldProduceConsistentDigestForSameKey { + PNAESCBCCryptor *cryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"consistentKey"]; + NSData *digest1 = [cryptor digestForKey:@"consistentKey"]; + NSData *digest2 = [cryptor digestForKey:@"consistentKey"]; + + XCTAssertEqualObjects(digest1, digest2, + @"Same key should always produce the same SHA-256 digest."); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Modules/Crypto/PNCryptoModuleTest.m b/Tests/Tests/Unit/Modules/Crypto/PNCryptoModuleTest.m new file mode 100644 index 000000000..54048623a --- /dev/null +++ b/Tests/Tests/Unit/Modules/Crypto/PNCryptoModuleTest.m @@ -0,0 +1,297 @@ +#import +#import +#import +#import +#import +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +/// `PNCryptoModule` unit tests. +/// +/// Tests covering crypto module encrypt/decrypt round-trips, multi-cryptor fallback, and error paths. +@interface PNCryptoModuleTest : XCTestCase + +#pragma mark - + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNCryptoModuleTest + + +#pragma mark - Tests :: NSData encrypt/decrypt round-trip + +- (void)testItShouldEncryptAndDecryptNSDataWithAESCBC { + PNCryptoModule *module = [PNCryptoModule AESCBCCryptoModuleWithCipherKey:@"enigma" + randomInitializationVector:YES]; + NSData *originalData = [@"Hello, PubNub!" dataUsingEncoding:NSUTF8StringEncoding]; + + PNResult *encryptResult = [module encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Encryption should succeed."); + XCTAssertNotNil(encryptResult.data, @"Encrypted data should not be nil."); + XCTAssertFalse([encryptResult.data isEqualToData:originalData], @"Encrypted data should differ from original."); + + PNResult *decryptResult = [module decryptData:encryptResult.data]; + XCTAssertFalse(decryptResult.isError, @"Decryption should succeed."); + XCTAssertEqualObjects(decryptResult.data, originalData, @"Decrypted data should match original."); +} + +- (void)testItShouldEncryptAndDecryptNSDataWithLegacyCryptor { + PNCryptoModule *module = [PNCryptoModule legacyCryptoModuleWithCipherKey:@"enigma" + randomInitializationVector:YES]; + NSData *originalData = [@"Hello, Legacy!" dataUsingEncoding:NSUTF8StringEncoding]; + + PNResult *encryptResult = [module encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Encryption should succeed."); + XCTAssertNotNil(encryptResult.data, @"Encrypted data should not be nil."); + + PNResult *decryptResult = [module decryptData:encryptResult.data]; + XCTAssertFalse(decryptResult.isError, @"Decryption should succeed."); + XCTAssertEqualObjects(decryptResult.data, originalData, @"Decrypted data should match original."); +} + +- (void)testItShouldEncryptAndDecryptLargeData { + PNCryptoModule *module = [PNCryptoModule AESCBCCryptoModuleWithCipherKey:@"testKey123" + randomInitializationVector:YES]; + NSMutableData *originalData = [NSMutableData dataWithLength:100000]; + SecRandomCopyBytes(kSecRandomDefault, originalData.length, originalData.mutableBytes); + + PNResult *encryptResult = [module encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Large data encryption should succeed."); + + PNResult *decryptResult = [module decryptData:encryptResult.data]; + XCTAssertFalse(decryptResult.isError, @"Large data decryption should succeed."); + XCTAssertEqualObjects(decryptResult.data, originalData, @"Decrypted large data should match original."); +} + + +#pragma mark - Tests :: NSString encrypt/decrypt round-trip + +- (void)testItShouldEncryptAndDecryptStringContentRoundTrip { + PNCryptoModule *module = [PNCryptoModule AESCBCCryptoModuleWithCipherKey:@"myCipherKey" + randomInitializationVector:YES]; + NSString *originalString = @"The quick brown fox jumps over the lazy dog"; + NSData *originalData = [originalString dataUsingEncoding:NSUTF8StringEncoding]; + + PNResult *encryptResult = [module encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"String encryption should succeed."); + + PNResult *decryptResult = [module decryptData:encryptResult.data]; + XCTAssertFalse(decryptResult.isError, @"String decryption should succeed."); + + NSString *decryptedString = [[NSString alloc] initWithData:decryptResult.data encoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(decryptedString, originalString, @"Decrypted string should match original."); +} + + +#pragma mark - Tests :: Multi-cryptor fallback + +- (void)testItShouldDecryptDataEncryptedByFallbackCryptor { + NSString *cipherKey = @"sharedKey"; + + // Encrypt with legacy cryptor as default. + PNCryptoModule *legacyModule = [PNCryptoModule legacyCryptoModuleWithCipherKey:cipherKey + randomInitializationVector:YES]; + NSData *originalData = [@"Fallback test data" dataUsingEncoding:NSUTF8StringEncoding]; + PNResult *encryptResult = [legacyModule encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Encryption with legacy module should succeed."); + + // Decrypt with AES-CBC as default, legacy as fallback. + PNCryptoModule *aesCBCModule = [PNCryptoModule AESCBCCryptoModuleWithCipherKey:cipherKey + randomInitializationVector:YES]; + PNResult *decryptResult = [aesCBCModule decryptData:encryptResult.data]; + XCTAssertFalse(decryptResult.isError, @"Decryption should succeed using fallback legacy cryptor."); + XCTAssertEqualObjects(decryptResult.data, originalData, + @"Decrypted data should match original when using fallback cryptor."); +} + +- (void)testItShouldDecryptAESCBCEncryptedDataUsingLegacyModuleWithFallback { + NSString *cipherKey = @"sharedKey"; + + // Encrypt with AES-CBC module. + PNCryptoModule *aesCBCModule = [PNCryptoModule AESCBCCryptoModuleWithCipherKey:cipherKey + randomInitializationVector:YES]; + NSData *originalData = [@"AES-CBC encrypted data" dataUsingEncoding:NSUTF8StringEncoding]; + PNResult *encryptResult = [aesCBCModule encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"AES-CBC encryption should succeed."); + + // Decrypt with legacy module (which has AES-CBC as fallback). + PNCryptoModule *legacyModule = [PNCryptoModule legacyCryptoModuleWithCipherKey:cipherKey + randomInitializationVector:YES]; + PNResult *decryptResult = [legacyModule decryptData:encryptResult.data]; + XCTAssertFalse(decryptResult.isError, @"Decryption should succeed using AES-CBC fallback from legacy module."); + XCTAssertEqualObjects(decryptResult.data, originalData, + @"Decrypted data should match original."); +} + + +#pragma mark - Tests :: Encrypt produces different ciphertext each time (random IV) + +- (void)testItShouldProduceDifferentCiphertextForSameDataDueToRandomIV { + PNCryptoModule *module = [PNCryptoModule AESCBCCryptoModuleWithCipherKey:@"randomIVKey" + randomInitializationVector:YES]; + NSData *originalData = [@"Same data, different ciphertext" dataUsingEncoding:NSUTF8StringEncoding]; + + PNResult *encryptResult1 = [module encryptData:originalData]; + PNResult *encryptResult2 = [module encryptData:originalData]; + + XCTAssertFalse(encryptResult1.isError, @"First encryption should succeed."); + XCTAssertFalse(encryptResult2.isError, @"Second encryption should succeed."); + XCTAssertFalse([encryptResult1.data isEqualToData:encryptResult2.data], + @"Two encryptions of the same data should produce different ciphertext due to random IV."); +} + + +#pragma mark - Tests :: Error paths + +- (void)testItShouldNotDecryptOriginalDataWithWrongKey { + PNCryptoModule *encryptModule = [PNCryptoModule AESCBCCryptoModuleWithCipherKey:@"correctKey" + randomInitializationVector:YES]; + NSString *originalString = @"Secret message"; + NSData *originalData = [originalString dataUsingEncoding:NSUTF8StringEncoding]; + PNResult *encryptResult = [encryptModule encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Encryption should succeed."); + + PNCryptoModule *decryptModule = [PNCryptoModule AESCBCCryptoModuleWithCipherKey:@"wrongKey" + randomInitializationVector:YES]; + PNResult *decryptResult = [decryptModule decryptData:encryptResult.data]; + + if (decryptResult.isError) { + // Wrong key typically causes PKCS7 padding validation to fail. + XCTAssertEqualObjects(decryptResult.error.domain, PNCryptorErrorDomain, + @"Error domain should be PNCryptorErrorDomain."); + } else { + // In rare cases (~0.4%) random garbage may have valid PKCS7 padding; verify content mismatch. + NSString *decryptedString = [[NSString alloc] initWithData:decryptResult.data + encoding:NSUTF8StringEncoding]; + XCTAssertFalse([originalString isEqualToString:decryptedString], + @"Decrypted data with wrong key should not match the original."); + } +} + +- (void)testItShouldNotDecryptMeaningfulDataFromCorruptPayload { + PNCryptoModule *module = [PNCryptoModule AESCBCCryptoModuleWithCipherKey:@"testKey" + randomInitializationVector:YES]; + + // Encrypt known data so we can verify corrupt payload doesn't produce it. + NSString *originalString = @"Known plaintext for comparison"; + NSData *originalData = [originalString dataUsingEncoding:NSUTF8StringEncoding]; + PNResult *validEncryptResult = [module encryptData:originalData]; + XCTAssertFalse(validEncryptResult.isError, @"Encryption should succeed."); + + // Create data that starts with PNED sentinel but has corrupt payload. + NSMutableData *corruptData = [NSMutableData dataWithData:[@"PNED" dataUsingEncoding:NSUTF8StringEncoding]]; + uint8_t version = 1; + [corruptData appendBytes:&version length:1]; + [corruptData appendData:[@"ACRH" dataUsingEncoding:NSUTF8StringEncoding]]; + uint8_t metadataLen = 16; + [corruptData appendBytes:&metadataLen length:1]; + // Append random bytes as "metadata" and "encrypted data". + NSMutableData *randomBytes = [NSMutableData dataWithLength:48]; + SecRandomCopyBytes(kSecRandomDefault, randomBytes.length, randomBytes.mutableBytes); + [corruptData appendData:randomBytes]; + + PNResult *decryptResult = [module decryptData:corruptData]; + + if (!decryptResult.isError) { + // If padding happened to be valid, the result must still be garbage. + NSString *decryptedString = [[NSString alloc] initWithData:decryptResult.data + encoding:NSUTF8StringEncoding]; + XCTAssertFalse([originalString isEqualToString:decryptedString], + @"Decrypted corrupt data should not match any known plaintext."); + } +} + +- (void)testItShouldReturnErrorWhenDecryptingEmptyData { + PNCryptoModule *module = [PNCryptoModule AESCBCCryptoModuleWithCipherKey:@"testKey" + randomInitializationVector:YES]; + NSData *emptyData = [NSData data]; + + PNResult *decryptResult = [module decryptData:emptyData]; + XCTAssertTrue(decryptResult.isError, @"Decrypting empty data should return an error."); + XCTAssertEqual(decryptResult.error.code, PNCryptorErrorDecryption, + @"Error code should indicate decryption failure."); +} + +- (void)testItShouldReturnErrorWhenEncryptingEmptyData { + PNCryptoModule *module = [PNCryptoModule AESCBCCryptoModuleWithCipherKey:@"testKey" + randomInitializationVector:YES]; + NSData *emptyData = [NSData data]; + + PNResult *encryptResult = [module encryptData:emptyData]; + // The underlying AES-CBC cryptor returns an error for empty data. + XCTAssertTrue(encryptResult.isError, @"Encrypting empty data should return an error."); + XCTAssertEqual(encryptResult.error.code, PNCryptorErrorEncryption, + @"Error code should indicate encryption failure."); +} + +- (void)testItShouldReturnErrorWhenDecryptingDataWithUnknownCryptor { + // Create a module with only legacy cryptor and an explicit empty fallback list. + // Using @[] (not nil) ensures that `cryptorWithIdentifier:` checks the identifier instead of + // short-circuiting to the default cryptor when `self.cryptors` is nil. + id legacyCryptor = [PNLegacyCryptor cryptorWithCipherKey:@"testKey" randomInitializationVector:YES]; + PNCryptoModule *legacyOnlyModule = [PNCryptoModule moduleWithDefaultCryptor:legacyCryptor cryptors:@[]]; + + // Encrypt using AES-CBC module (which produces PNED header with ACRH identifier). + PNCryptoModule *aesModule = [PNCryptoModule AESCBCCryptoModuleWithCipherKey:@"testKey" + randomInitializationVector:YES]; + NSData *originalData = [@"Test data" dataUsingEncoding:NSUTF8StringEncoding]; + PNResult *encryptResult = [aesModule encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Encryption should succeed."); + + // Decrypt with legacy-only module -- should fail because ACRH identifier is unknown. + PNResult *decryptResult = [legacyOnlyModule decryptData:encryptResult.data]; + XCTAssertTrue(decryptResult.isError, @"Decryption with unknown cryptor should return an error."); + XCTAssertEqual(decryptResult.error.code, PNCryptorErrorUnknownCryptor, + @"Error code should indicate unknown cryptor."); +} + + +#pragma mark - Tests :: Cross-module compatibility + +- (void)testItShouldDecryptDataFromLegacyModuleWithConstantIV { + NSString *cipherKey = @"myCipherKey"; + + // Encrypt with legacy module using constant IV. + PNCryptoModule *legacyConstIV = [PNCryptoModule legacyCryptoModuleWithCipherKey:cipherKey + randomInitializationVector:NO]; + NSData *originalData = [@"Constant IV test" dataUsingEncoding:NSUTF8StringEncoding]; + PNResult *encryptResult = [legacyConstIV encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Legacy constant IV encryption should succeed."); + + // Decrypt with the same configuration. + PNResult *decryptResult = [legacyConstIV decryptData:encryptResult.data]; + XCTAssertFalse(decryptResult.isError, @"Legacy constant IV decryption should succeed."); + XCTAssertEqualObjects(decryptResult.data, originalData, + @"Round-trip with legacy constant IV should preserve original data."); +} + +- (void)testItShouldEncryptAndDecryptWithLegacyModuleUsingRandomIV { + NSString *cipherKey = @"myCipherKey"; + + PNCryptoModule *module = [PNCryptoModule legacyCryptoModuleWithCipherKey:cipherKey + randomInitializationVector:YES]; + NSData *originalData = [@"Random IV legacy test" dataUsingEncoding:NSUTF8StringEncoding]; + + PNResult *encryptResult = [module encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Legacy random IV encryption should succeed."); + + PNResult *decryptResult = [module decryptData:encryptResult.data]; + XCTAssertFalse(decryptResult.isError, @"Legacy random IV decryption should succeed."); + XCTAssertEqualObjects(decryptResult.data, originalData, + @"Round-trip with legacy random IV should preserve original data."); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Modules/Crypto/PNEncryptedDataTest.m b/Tests/Tests/Unit/Modules/Crypto/PNEncryptedDataTest.m new file mode 100644 index 000000000..3100c7248 --- /dev/null +++ b/Tests/Tests/Unit/Modules/Crypto/PNEncryptedDataTest.m @@ -0,0 +1,121 @@ +#import +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +/// `PNEncryptedData` and `PNEncryptedStream` model unit tests. +/// +/// Tests covering construction and property access for cryptor data model objects. +@interface PNEncryptedDataTest : XCTestCase + +#pragma mark - + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNEncryptedDataTest + + +#pragma mark - Tests :: PNEncryptedData construction + +- (void)testItShouldCreateEncryptedDataWithDataAndMetadata { + NSData *data = [@"encrypted payload" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *metadata = [@"initialization vector" dataUsingEncoding:NSUTF8StringEncoding]; + + PNEncryptedData *encryptedData = [PNEncryptedData encryptedDataWithData:data metadata:metadata]; + + XCTAssertEqualObjects(encryptedData.data, data, @"Data property should match provided data."); + XCTAssertEqualObjects(encryptedData.metadata, metadata, @"Metadata property should match provided metadata."); +} + +- (void)testItShouldCreateEncryptedDataWithNilMetadata { + NSData *data = [@"encrypted payload" dataUsingEncoding:NSUTF8StringEncoding]; + + PNEncryptedData *encryptedData = [PNEncryptedData encryptedDataWithData:data metadata:nil]; + + XCTAssertEqualObjects(encryptedData.data, data, @"Data property should match provided data."); + XCTAssertNil(encryptedData.metadata, @"Metadata should be nil when not provided."); +} + +- (void)testItShouldPreserveDataIntegrity { + NSMutableData *data = [NSMutableData dataWithLength:256]; + SecRandomCopyBytes(kSecRandomDefault, data.length, data.mutableBytes); + NSMutableData *metadata = [NSMutableData dataWithLength:16]; + SecRandomCopyBytes(kSecRandomDefault, metadata.length, metadata.mutableBytes); + + PNEncryptedData *encryptedData = [PNEncryptedData encryptedDataWithData:data metadata:metadata]; + + XCTAssertEqualObjects(encryptedData.data, data, + @"Data property should preserve the exact binary content."); + XCTAssertEqualObjects(encryptedData.metadata, metadata, + @"Metadata property should preserve the exact binary content."); +} + +- (void)testItShouldCreateEncryptedDataWithEmptyData { + NSData *data = [NSData data]; + NSData *metadata = [NSData data]; + + PNEncryptedData *encryptedData = [PNEncryptedData encryptedDataWithData:data metadata:metadata]; + + XCTAssertEqual(encryptedData.data.length, 0u, @"Data length should be zero for empty data."); + XCTAssertEqual(encryptedData.metadata.length, 0u, @"Metadata length should be zero for empty metadata."); +} + + +#pragma mark - Tests :: PNEncryptedData readonly properties + +- (void)testDataPropertyShouldBeReadonly { + NSData *data = [@"test" dataUsingEncoding:NSUTF8StringEncoding]; + PNEncryptedData *encryptedData = [PNEncryptedData encryptedDataWithData:data metadata:nil]; + + // Verify the property value is stable (readonly). + NSData *firstAccess = encryptedData.data; + NSData *secondAccess = encryptedData.data; + + XCTAssertEqualObjects(firstAccess, secondAccess, + @"Multiple accesses to data property should return the same value."); +} + +- (void)testMetadataPropertyShouldBeReadonly { + NSData *metadata = [@"meta" dataUsingEncoding:NSUTF8StringEncoding]; + PNEncryptedData *encryptedData = [PNEncryptedData encryptedDataWithData:[NSData data] metadata:metadata]; + + NSData *firstAccess = encryptedData.metadata; + NSData *secondAccess = encryptedData.metadata; + + XCTAssertEqualObjects(firstAccess, secondAccess, + @"Multiple accesses to metadata property should return the same value."); +} + + +#pragma mark - Tests :: Multiple instances independence + +- (void)testMultipleInstancesShouldBeIndependent { + NSData *data1 = [@"payload1" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *meta1 = [@"meta1" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *data2 = [@"payload2" dataUsingEncoding:NSUTF8StringEncoding]; + NSData *meta2 = [@"meta2" dataUsingEncoding:NSUTF8StringEncoding]; + + PNEncryptedData *encrypted1 = [PNEncryptedData encryptedDataWithData:data1 metadata:meta1]; + PNEncryptedData *encrypted2 = [PNEncryptedData encryptedDataWithData:data2 metadata:meta2]; + + XCTAssertEqualObjects(encrypted1.data, data1, @"First instance data should remain unchanged."); + XCTAssertEqualObjects(encrypted1.metadata, meta1, @"First instance metadata should remain unchanged."); + XCTAssertEqualObjects(encrypted2.data, data2, @"Second instance data should be independent."); + XCTAssertEqualObjects(encrypted2.metadata, meta2, @"Second instance metadata should be independent."); + XCTAssertFalse([encrypted1.data isEqualToData:encrypted2.data], + @"Different instances should hold different data."); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Modules/Crypto/PNLegacyCryptorTest.m b/Tests/Tests/Unit/Modules/Crypto/PNLegacyCryptorTest.m new file mode 100644 index 000000000..840d404a0 --- /dev/null +++ b/Tests/Tests/Unit/Modules/Crypto/PNLegacyCryptorTest.m @@ -0,0 +1,281 @@ +#import +#import +#import +#import +#import +#import +#import "PNAESCBCCryptor+Private.h" + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +/// `PNLegacyCryptor` unit tests. +/// +/// Tests covering the legacy AES-256-CBC cryptor: encrypt/decrypt round-trips, compatibility, and error paths. +@interface PNLegacyCryptorTest : XCTestCase + +#pragma mark - + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNLegacyCryptorTest + + +#pragma mark - Tests :: Initialization + +- (void)testItShouldHaveLegacyIdentifier { + PNLegacyCryptor *cryptor = [PNLegacyCryptor cryptorWithCipherKey:@"testKey" + randomInitializationVector:YES]; + NSData *expectedIdentifier = [[NSMutableData dataWithLength:4] copy]; + + XCTAssertEqualObjects([cryptor identifier], expectedIdentifier, + @"Legacy cryptor identifier should be 4 zero bytes."); +} + +- (void)testItShouldHaveDifferentIdentifierThanAESCBC { + PNLegacyCryptor *legacyCryptor = [PNLegacyCryptor cryptorWithCipherKey:@"testKey" + randomInitializationVector:YES]; + PNAESCBCCryptor *aesCryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"testKey"]; + + XCTAssertFalse([[legacyCryptor identifier] isEqualToData:[aesCryptor identifier]], + @"Legacy and AES-CBC cryptors should have different identifiers."); +} + + +#pragma mark - Tests :: Encrypt and decrypt round-trip with random IV + +- (void)testItShouldEncryptAndDecryptWithRandomIV { + PNLegacyCryptor *cryptor = [PNLegacyCryptor cryptorWithCipherKey:@"enigma" + randomInitializationVector:YES]; + NSData *originalData = [@"Hello, Legacy Cryptor!" dataUsingEncoding:NSUTF8StringEncoding]; + + PNResult *encryptResult = [cryptor encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Encryption with random IV should succeed."); + XCTAssertNotNil(encryptResult.data, @"Encrypted data should not be nil."); + XCTAssertNotNil(encryptResult.data.metadata, @"Metadata (IV) should be present for random IV mode."); + + PNResult *decryptResult = [cryptor decryptData:encryptResult.data]; + XCTAssertFalse(decryptResult.isError, @"Decryption with random IV should succeed."); + XCTAssertEqualObjects(decryptResult.data, originalData, + @"Decrypted data should match original."); +} + + +#pragma mark - Tests :: Encrypt and decrypt round-trip with constant IV + +- (void)testItShouldEncryptAndDecryptWithConstantIV { + PNLegacyCryptor *cryptor = [PNLegacyCryptor cryptorWithCipherKey:@"enigma" + randomInitializationVector:NO]; + NSData *originalData = [@"Hello, Constant IV!" dataUsingEncoding:NSUTF8StringEncoding]; + + PNResult *encryptResult = [cryptor encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Encryption with constant IV should succeed."); + XCTAssertNotNil(encryptResult.data, @"Encrypted data should not be nil."); + + PNResult *decryptResult = [cryptor decryptData:encryptResult.data]; + XCTAssertFalse(decryptResult.isError, @"Decryption with constant IV should succeed."); + XCTAssertEqualObjects(decryptResult.data, originalData, + @"Decrypted data should match original."); +} + +- (void)testItShouldProduceSameCiphertextWithConstantIV { + PNLegacyCryptor *cryptor = [PNLegacyCryptor cryptorWithCipherKey:@"constantKey" + randomInitializationVector:NO]; + NSData *originalData = [@"Deterministic" dataUsingEncoding:NSUTF8StringEncoding]; + + PNResult *encryptResult1 = [cryptor encryptData:originalData]; + PNResult *encryptResult2 = [cryptor encryptData:originalData]; + + XCTAssertFalse(encryptResult1.isError, @"First encryption should succeed."); + XCTAssertFalse(encryptResult2.isError, @"Second encryption should succeed."); + XCTAssertEqualObjects(encryptResult1.data.data, encryptResult2.data.data, + @"Constant IV should produce identical ciphertext for same input."); +} + +- (void)testItShouldProduceDifferentCiphertextWithRandomIV { + PNLegacyCryptor *cryptor = [PNLegacyCryptor cryptorWithCipherKey:@"randomKey" + randomInitializationVector:YES]; + NSData *originalData = [@"Non-deterministic" dataUsingEncoding:NSUTF8StringEncoding]; + + PNResult *encryptResult1 = [cryptor encryptData:originalData]; + PNResult *encryptResult2 = [cryptor encryptData:originalData]; + + XCTAssertFalse(encryptResult1.isError, @"First encryption should succeed."); + XCTAssertFalse(encryptResult2.isError, @"Second encryption should succeed."); + XCTAssertFalse([encryptResult1.data.data isEqualToData:encryptResult2.data.data], + @"Random IV should produce different ciphertext for same input."); +} + + +#pragma mark - Tests :: Different data sizes + +- (void)testItShouldHandleSingleByteData { + PNLegacyCryptor *cryptor = [PNLegacyCryptor cryptorWithCipherKey:@"singleByte" + randomInitializationVector:YES]; + uint8_t byte = 0xFF; + NSData *originalData = [NSData dataWithBytes:&byte length:1]; + + PNResult *encryptResult = [cryptor encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Single byte encryption should succeed."); + + PNResult *decryptResult = [cryptor decryptData:encryptResult.data]; + XCTAssertFalse(decryptResult.isError, @"Single byte decryption should succeed."); + XCTAssertEqualObjects(decryptResult.data, originalData, + @"Single byte round-trip should preserve data."); +} + +- (void)testItShouldHandleBlockAlignedData { + PNLegacyCryptor *cryptor = [PNLegacyCryptor cryptorWithCipherKey:@"blockAligned" + randomInitializationVector:YES]; + NSMutableData *originalData = [NSMutableData dataWithLength:32]; + memset(originalData.mutableBytes, 0xBB, 32); + + PNResult *encryptResult = [cryptor encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Block-aligned data encryption should succeed."); + + PNResult *decryptResult = [cryptor decryptData:encryptResult.data]; + XCTAssertFalse(decryptResult.isError, @"Block-aligned data decryption should succeed."); + XCTAssertEqualObjects(decryptResult.data, originalData, + @"Block-aligned data round-trip should preserve data."); +} + +- (void)testItShouldHandleLargeData { + PNLegacyCryptor *cryptor = [PNLegacyCryptor cryptorWithCipherKey:@"largeData" + randomInitializationVector:YES]; + NSMutableData *originalData = [NSMutableData dataWithLength:50000]; + SecRandomCopyBytes(kSecRandomDefault, originalData.length, originalData.mutableBytes); + + PNResult *encryptResult = [cryptor encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Large data encryption should succeed."); + + PNResult *decryptResult = [cryptor decryptData:encryptResult.data]; + XCTAssertFalse(decryptResult.isError, @"Large data decryption should succeed."); + XCTAssertEqualObjects(decryptResult.data, originalData, + @"Large data round-trip should preserve data."); +} + + +#pragma mark - Tests :: Legacy key digest + +- (void)testItShouldUseLegacyKeyDigest { + PNLegacyCryptor *legacyCryptor = [PNLegacyCryptor cryptorWithCipherKey:@"testKey" + randomInitializationVector:YES]; + PNAESCBCCryptor *aesCryptor = [PNAESCBCCryptor cryptorWithCipherKey:@"testKey"]; + + NSData *legacyDigest = [legacyCryptor digestForKey:@"testKey"]; + NSData *aesDigest = [aesCryptor digestForKey:@"testKey"]; + + // Legacy uses hex string of first 16 bytes of SHA-256, while AES-CBC uses raw SHA-256. + XCTAssertFalse([legacyDigest isEqualToData:aesDigest], + @"Legacy key digest should differ from standard AES-CBC key digest."); + // Legacy digest is hex encoded half of SHA-256 = 16 hex chars * 2 = 32 bytes as string. + XCTAssertEqual(legacyDigest.length, 32u, + @"Legacy key digest should be 32 bytes (hex encoded half of SHA-256)."); +} + + +#pragma mark - Tests :: Error paths + +- (void)testItShouldReturnErrorWhenEncryptingEmptyData { + PNLegacyCryptor *cryptor = [PNLegacyCryptor cryptorWithCipherKey:@"emptyKey" + randomInitializationVector:YES]; + NSData *emptyData = [NSData data]; + + PNResult *encryptResult = [cryptor encryptData:emptyData]; + XCTAssertTrue(encryptResult.isError, @"Encrypting empty data should return an error."); + XCTAssertEqual(encryptResult.error.code, PNCryptorErrorEncryption, + @"Error code should indicate encryption failure."); +} + +- (void)testItShouldNotDecryptOriginalDataWithWrongKey { + PNLegacyCryptor *encryptCryptor = [PNLegacyCryptor cryptorWithCipherKey:@"rightKey" + randomInitializationVector:YES]; + NSString *originalString = @"Secret message"; + NSData *originalData = [originalString dataUsingEncoding:NSUTF8StringEncoding]; + PNResult *encryptResult = [encryptCryptor encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Encryption should succeed."); + + PNLegacyCryptor *decryptCryptor = [PNLegacyCryptor cryptorWithCipherKey:@"wrongKey" + randomInitializationVector:YES]; + PNResult *decryptResult = [decryptCryptor decryptData:encryptResult.data]; + + if (!decryptResult.isError) { + // In rare cases (~0.4%) random garbage may have valid PKCS7 padding; verify content mismatch. + NSString *decryptedString = [[NSString alloc] initWithData:decryptResult.data + encoding:NSUTF8StringEncoding]; + XCTAssertFalse([originalString isEqualToString:decryptedString], + @"Decrypted data with wrong key should not match the original."); + } +} + +- (void)testItShouldNotDecryptOriginalDataFromInvalidData { + PNLegacyCryptor *cryptor = [PNLegacyCryptor cryptorWithCipherKey:@"invalidKey" + randomInitializationVector:YES]; + + // Create encrypted data with random garbage. + NSMutableData *garbageData = [NSMutableData dataWithLength:64]; + SecRandomCopyBytes(kSecRandomDefault, garbageData.length, garbageData.mutableBytes); + NSMutableData *fakeIV = [NSMutableData dataWithLength:16]; + SecRandomCopyBytes(kSecRandomDefault, fakeIV.length, fakeIV.mutableBytes); + PNEncryptedData *encryptedData = [PNEncryptedData encryptedDataWithData:garbageData metadata:fakeIV]; + + PNResult *decryptResult = [cryptor decryptData:encryptedData]; + + if (!decryptResult.isError) { + // If padding happened to be valid, the result must still be garbage (not meaningful data). + XCTAssertFalse([decryptResult.data isEqualToData:garbageData], + @"Decrypted data should not match the original garbage input."); + } +} + +- (void)testItShouldReturnErrorWhenDecryptingDataShorterThanIVWithoutMetadata { + PNLegacyCryptor *cryptor = [PNLegacyCryptor cryptorWithCipherKey:@"shortKey" + randomInitializationVector:YES]; + + // 10 bytes - shorter than AES block size with no metadata. + NSData *shortData = [NSData dataWithBytes:"0123456789" length:10]; + PNEncryptedData *encryptedData = [PNEncryptedData encryptedDataWithData:shortData metadata:nil]; + + PNResult *decryptResult = [cryptor decryptData:encryptedData]; + XCTAssertTrue(decryptResult.isError, + @"Decrypting data shorter than IV size without metadata should return an error."); +} + + +#pragma mark - Tests :: Interop between random and constant IV configurations + +- (void)testItShouldNotDecryptConstantIVDataWithRandomIVCryptor { + NSString *cipherKey = @"interopKey"; + + PNLegacyCryptor *constantCryptor = [PNLegacyCryptor cryptorWithCipherKey:cipherKey + randomInitializationVector:NO]; + PNLegacyCryptor *randomCryptor = [PNLegacyCryptor cryptorWithCipherKey:cipherKey + randomInitializationVector:YES]; + + NSData *originalData = [@"Interop test data" dataUsingEncoding:NSUTF8StringEncoding]; + + PNResult *encryptResult = [constantCryptor encryptData:originalData]; + XCTAssertFalse(encryptResult.isError, @"Constant IV encryption should succeed."); + + // The constant IV cryptor does not put IV in metadata (metadata is nil). + // A random IV cryptor will try to extract IV from the data bytes, which will be misinterpreted. + // The result may be an error or garbled data depending on padding. + PNResult *decryptResult = [randomCryptor decryptData:encryptResult.data]; + // Either error or data mismatch is acceptable here. + if (!decryptResult.isError) { + XCTAssertFalse([decryptResult.data isEqualToData:originalData], + @"Constant IV data decrypted with random IV config should not match original."); + } +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Modules/Logger/PNConsoleLoggerTest.m b/Tests/Tests/Unit/Modules/Logger/PNConsoleLoggerTest.m new file mode 100644 index 000000000..1064985e8 --- /dev/null +++ b/Tests/Tests/Unit/Modules/Logger/PNConsoleLoggerTest.m @@ -0,0 +1,651 @@ +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import "PNNetworkResponseLogEntry+Private.h" +#import "PNNetworkRequestLogEntry+Private.h" +#import "PNDictionaryLogEntry+Private.h" +#import "PNStringLogEntry+Private.h" +#import "PNErrorLogEntry+Private.h" +#import "PNTransportRequest+Private.h" +#import "PNConsoleLogger.h" +#import "PNLogEntry+Private.h" + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +/// Console logger unit tests. +@interface PNConsoleLoggerTest : XCTestCase + +#pragma mark - + +@end + +/// Minimal PNTransportResponse conforming object for testing purposes. +@interface PNConsoleTestTransportResponse : NSObject + +@property(strong, nonatomic) NSDictionary *headers; +@property(strong, nonatomic) NSInputStream *bodyStream; +@property(strong, nonatomic) NSString *MIMEType; +@property(assign, nonatomic) BOOL bodyStreamAvailable; +@property(strong, nonatomic) NSData *body; +@property(assign, nonatomic) NSUInteger statusCode; +@property(strong, nonatomic) NSString *url; + +- (instancetype)initWithURL:(NSString *)url + statusCode:(NSUInteger)statusCode + body:(nullable NSData *)body + headers:(nullable NSDictionary *)headers; + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNConsoleLoggerTest + + +#pragma mark - Tests :: Creation + +- (void)testItShouldCreateConsoleLoggerInstance { + PNConsoleLogger *logger = [PNConsoleLogger new]; + +} + +- (void)testItShouldConformToPNLoggerProtocol { + PNConsoleLogger *logger = [PNConsoleLogger new]; + + XCTAssertTrue([logger conformsToProtocol:@protocol(PNLogger)], + @"Console logger should conform to PNLogger protocol."); +} + + +#pragma mark - Tests :: PNLogger protocol methods + +- (void)testItShouldRespondToTraceWithMessage { + PNConsoleLogger *logger = [PNConsoleLogger new]; + + XCTAssertTrue([logger respondsToSelector:@selector(traceWithMessage:)], + @"Console logger should respond to traceWithMessage:."); +} + +- (void)testItShouldRespondToDebugWithMessage { + PNConsoleLogger *logger = [PNConsoleLogger new]; + + XCTAssertTrue([logger respondsToSelector:@selector(debugWithMessage:)], + @"Console logger should respond to debugWithMessage:."); +} + +- (void)testItShouldRespondToInfoWithMessage { + PNConsoleLogger *logger = [PNConsoleLogger new]; + + XCTAssertTrue([logger respondsToSelector:@selector(infoWithMessage:)], + @"Console logger should respond to infoWithMessage:."); +} + +- (void)testItShouldRespondToWarnWithMessage { + PNConsoleLogger *logger = [PNConsoleLogger new]; + + XCTAssertTrue([logger respondsToSelector:@selector(warnWithMessage:)], + @"Console logger should respond to warnWithMessage:."); +} + +- (void)testItShouldRespondToErrorWithMessage { + PNConsoleLogger *logger = [PNConsoleLogger new]; + + XCTAssertTrue([logger respondsToSelector:@selector(errorWithMessage:)], + @"Console logger should respond to errorWithMessage:."); +} + + +#pragma mark - Tests :: Stringification :: String entries + +- (void)testItShouldStringifyStringLogEntry { + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"Hello, World!"]; + entry.pubNubId = @"client-123"; + entry.location = @"PubNub+Core.m:42"; + entry.logLevel = PNInfoLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"PubNub-client-123"], + @"Output should contain the PubNub client identifier."); + XCTAssertTrue([result containsString:@"PubNub+Core.m:42"], + @"Output should contain the location."); + XCTAssertTrue([result containsString:@"Hello, World!"], + @"Output should contain the original message text."); + XCTAssertTrue([result containsString:@"INFO"], + @"Output should contain the INFO log level label."); +} + +- (void)testItShouldStringifyStringLogEntryWithTraceLevel { + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"Trace test"]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNTraceLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"TRACE"], + @"Output should contain the TRACE log level label."); +} + +- (void)testItShouldStringifyStringLogEntryWithDebugLevel { + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"Debug test"]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNDebugLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"DEBUG"], + @"Output should contain the DEBUG log level label."); +} + +- (void)testItShouldStringifyStringLogEntryWithWarnLevel { + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"Warn test"]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNWarnLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"WARN"], + @"Output should contain the WARN log level label."); +} + +- (void)testItShouldStringifyStringLogEntryWithErrorLevel { + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"Error test"]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNErrorLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"ERROR"], + @"Output should contain the ERROR log level label."); +} + +- (void)testItShouldStringifyNoneLogLevelAsUnknown { + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"None test"]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNNoneLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"UNKNW"], + @"Output should contain UNKNW for PNNoneLogLevel."); +} + + +#pragma mark - Tests :: Stringification :: Dictionary entries + +- (void)testItShouldStringifyDictionaryLogEntry { + NSDictionary *dict = @{ @"key": @"value" }; + PNDictionaryLogEntry *entry = [PNDictionaryLogEntry entryWithMessage:dict details:@"Test context"]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNDebugLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"Test context"], + @"Output should contain the details string."); + XCTAssertTrue([result containsString:@"key"], + @"Output should contain the dictionary key."); + XCTAssertTrue([result containsString:@"value"], + @"Output should contain the dictionary value."); +} + +- (void)testItShouldStringifyDictionaryLogEntryWithNilDetails { + NSDictionary *dict = @{ @"status": @"ok" }; + PNDictionaryLogEntry *entry = [PNDictionaryLogEntry entryWithMessage:dict details:nil]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNDebugLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"status"], + @"Output should contain the dictionary key."); +} + +- (void)testItShouldStringifyEmptyDictionary { + NSDictionary *dict = @{}; + PNDictionaryLogEntry *entry = [PNDictionaryLogEntry entryWithMessage:dict details:nil]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNInfoLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"{}"], + @"Output should contain '{}' for an empty dictionary."); +} + +- (void)testItShouldStringifyDictionaryWithBooleanValues { + NSDictionary *dict = @{ @"enabled": @YES, @"disabled": @NO }; + PNDictionaryLogEntry *entry = [PNDictionaryLogEntry entryWithMessage:dict details:nil]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNInfoLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"YES"] || [result containsString:@"NO"], + @"Boolean values should be stringified as YES/NO."); +} + +- (void)testItShouldStringifyNestedDictionary { + NSDictionary *dict = @{ @"outer": @{ @"inner": @"value" } }; + PNDictionaryLogEntry *entry = [PNDictionaryLogEntry entryWithMessage:dict details:nil]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNInfoLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"inner"], + @"Output should contain nested dictionary key."); + XCTAssertTrue([result containsString:@"value"], + @"Output should contain nested dictionary value."); +} + + +#pragma mark - Tests :: Stringification :: Error entries + +- (void)testItShouldStringifyErrorLogEntry { + NSError *error = [NSError errorWithDomain:@"com.pubnub.test" code:42 userInfo:@{ + NSLocalizedDescriptionKey: @"Something went wrong" + }]; + PNErrorLogEntry *entry = [PNErrorLogEntry entryWithMessage:error]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNErrorLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"com.pubnub.test"], + @"Output should contain the error domain."); + XCTAssertTrue([result containsString:@"42"], + @"Output should contain the error code."); + XCTAssertTrue([result containsString:@"Something went wrong"], + @"Output should contain the error description."); +} + +- (void)testItShouldStringifyErrorWithFailureReason { + NSError *error = [NSError errorWithDomain:@"com.pubnub.test" code:1 userInfo:@{ + NSLocalizedDescriptionKey: @"Request failed", + NSLocalizedFailureReasonErrorKey: @"Network timeout" + }]; + PNErrorLogEntry *entry = [PNErrorLogEntry entryWithMessage:error]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNErrorLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"Network timeout"], + @"Output should contain the failure reason."); +} + +- (void)testItShouldStringifyErrorWithRecoverySuggestion { + NSError *error = [NSError errorWithDomain:@"com.pubnub.test" code:1 userInfo:@{ + NSLocalizedDescriptionKey: @"Auth failed", + NSLocalizedRecoverySuggestionErrorKey: @"Check your credentials" + }]; + PNErrorLogEntry *entry = [PNErrorLogEntry entryWithMessage:error]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNErrorLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"Check your credentials"], + @"Output should contain the recovery suggestion."); +} + +- (void)testItShouldStringifyErrorWithUnderlyingError { + NSError *underlying = [NSError errorWithDomain:@"com.pubnub.underlying" code:99 userInfo:@{ + NSLocalizedDescriptionKey: @"Root cause" + }]; + NSError *error = [NSError errorWithDomain:@"com.pubnub.test" code:1 userInfo:@{ + NSLocalizedDescriptionKey: @"Outer error", + NSUnderlyingErrorKey: underlying + }]; + PNErrorLogEntry *entry = [PNErrorLogEntry entryWithMessage:error]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNErrorLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"com.pubnub.underlying"], + @"Output should contain the underlying error domain."); +} + +- (void)testItShouldStringifyErrorWithNoUserInfo { + NSError *error = [NSError errorWithDomain:@"com.pubnub.test" code:0 userInfo:nil]; + PNErrorLogEntry *entry = [PNErrorLogEntry entryWithMessage:error]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNErrorLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"com.pubnub.test"], + @"Output should contain the error domain."); +} + + +#pragma mark - Tests :: Stringification :: Network request entries + +- (void)testItShouldStringifyNetworkRequestLogEntry { + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/v2/subscribe/sub-key/channel/0"; + request.origin = @"ps.pndsn.com"; + request.method = TransportGETMethod; + + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request + details:@"Subscribe"]; + entry.pubNubId = @"client-1"; + entry.location = @"Transport.m:1"; + entry.logLevel = PNDebugLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"Sending"], + @"Output should contain 'Sending' prefix for normal requests."); + XCTAssertTrue([result containsString:@"Subscribe"], + @"Output should contain the request details."); + XCTAssertTrue([result containsString:@"ps.pndsn.com"], + @"Output should contain the request origin."); + XCTAssertTrue([result containsString:@"/v2/subscribe"], + @"Output should contain the request path."); +} + +- (void)testItShouldStringifyCancelledNetworkRequestLogEntry { + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/publish/key"; + request.origin = @"ps.pndsn.com"; + request.cancelled = YES; + + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request + details:nil + canceled:YES + failed:NO]; + entry.pubNubId = @"client-1"; + entry.location = @"Transport.m:1"; + entry.logLevel = PNDebugLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"Canceled"], + @"Output should contain 'Canceled' for cancelled requests."); +} + +- (void)testItShouldStringifyFailedNetworkRequestLogEntry { + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/publish/key"; + request.origin = @"ps.pndsn.com"; + request.failed = YES; + + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request + details:nil + canceled:NO + failed:YES]; + entry.pubNubId = @"client-1"; + entry.location = @"Transport.m:1"; + entry.logLevel = PNDebugLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"Failed"], + @"Output should contain 'Failed' for failed requests."); +} + +- (void)testItShouldStringifyNetworkRequestWithQueryParameters { + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/v2/subscribe/sub-key/channel/0"; + request.origin = @"ps.pndsn.com"; + request.query = @{ @"tt": @"0", @"tr": @"1" }; + + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:nil]; + entry.pubNubId = @"client-1"; + entry.location = @"Transport.m:1"; + entry.logLevel = PNDebugLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"tt=0"] || [result containsString:@"tr=1"], + @"Output should contain query parameters."); +} + + +#pragma mark - Tests :: Stringification :: Network response entries + +- (void)testItShouldStringifyNetworkResponseLogEntry { + PNConsoleTestTransportResponse *response = + [[PNConsoleTestTransportResponse alloc] initWithURL:@"https://ps.pndsn.com/v2/subscribe?tt=0" + statusCode:200 + body:nil + headers:nil]; + + PNNetworkResponseLogEntry *entry = [PNNetworkResponseLogEntry entryWithMessage:response]; + entry.pubNubId = @"client-1"; + entry.location = @"Transport.m:1"; + entry.logLevel = PNDebugLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"Received HTTP response"], + @"Output should contain 'Received HTTP response' prefix."); + XCTAssertTrue([result containsString:@"ps.pndsn.com"], + @"Output should contain the response URL."); +} + +- (void)testItShouldStringifyNetworkResponseWithJSONBody { + NSData *body = [@"{\"status\":200}" dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *headers = @{ @"content-type": @"application/json" }; + + PNConsoleTestTransportResponse *response = + [[PNConsoleTestTransportResponse alloc] initWithURL:@"https://ps.pndsn.com/publish" + statusCode:200 + body:body + headers:headers]; + + PNNetworkResponseLogEntry *entry = [PNNetworkResponseLogEntry entryWithMessage:response]; + entry.pubNubId = @"client-1"; + entry.location = @"Transport.m:1"; + entry.logLevel = PNDebugLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"{\"status\":200}"], + @"Output should contain the JSON body content."); +} + +- (void)testItShouldStringifyNetworkResponseWithBinaryBody { + NSData *body = [@"binary data" dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *headers = @{ @"content-type": @"application/octet-stream" }; + + PNConsoleTestTransportResponse *response = + [[PNConsoleTestTransportResponse alloc] initWithURL:@"https://ps.pndsn.com/files" + statusCode:200 + body:body + headers:headers]; + + PNNetworkResponseLogEntry *entry = [PNNetworkResponseLogEntry entryWithMessage:response]; + entry.pubNubId = @"client-1"; + entry.location = @"Transport.m:1"; + entry.logLevel = PNDebugLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"NSData (length:"], + @"Output should show binary body as NSData with length."); +} + + +#pragma mark - Tests :: Stringification :: Headers at trace level + +- (void)testItShouldIncludeRequestHeadersAtTraceLevel { + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/v2/subscribe/sub-key/channel/0"; + request.origin = @"ps.pndsn.com"; + request.headers = @{ @"Authorization": @"Bearer token123" }; + + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:nil]; + entry.pubNubId = @"client-1"; + entry.location = @"Transport.m:1"; + entry.logLevel = PNDebugLogLevel; + entry.minimumLogLevel = PNTraceLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"Authorization"], + @"Output should include request headers at trace level."); +} + +- (void)testItShouldNotIncludeRequestHeadersAboveTraceLevel { + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/v2/subscribe/sub-key/channel/0"; + request.origin = @"ps.pndsn.com"; + request.headers = @{ @"Authorization": @"Bearer token123" }; + + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:nil]; + entry.pubNubId = @"client-1"; + entry.location = @"Transport.m:1"; + entry.logLevel = PNDebugLogLevel; + entry.minimumLogLevel = PNDebugLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertFalse([result containsString:@"Authorization"], + @"Output should NOT include request headers above trace level."); +} + +- (void)testItShouldIncludeResponseHeadersAtTraceLevel { + NSDictionary *headers = @{ @"content-type": @"application/json", @"x-custom": @"custom-value" }; + + PNConsoleTestTransportResponse *response = + [[PNConsoleTestTransportResponse alloc] initWithURL:@"https://ps.pndsn.com/subscribe" + statusCode:200 + body:nil + headers:headers]; + + PNNetworkResponseLogEntry *entry = [PNNetworkResponseLogEntry entryWithMessage:response]; + entry.pubNubId = @"client-1"; + entry.location = @"Transport.m:1"; + entry.logLevel = PNDebugLogLevel; + entry.minimumLogLevel = PNTraceLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + XCTAssertTrue([result containsString:@"x-custom"], + @"Output should include response headers at trace level."); +} + + +#pragma mark - Tests :: Pre-processed string caching + +- (void)testItShouldNotCallLogMessageWhenLogEntryAlreadyLogged { + PNConsoleLogger *logger = [PNConsoleLogger new]; + + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"test"]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNInfoLogLevel; + + // First log sets preProcessedString. + [logger infoWithMessage:entry]; + NSString *cached = entry.preProcessedString; + + XCTAssertNotNil(cached, @"Pre-processed string should be set after first log call."); + + // Second log should use cached version. + [logger infoWithMessage:entry]; + XCTAssertEqualObjects(entry.preProcessedString, cached, + @"Pre-processed string should remain unchanged on second log call."); +} + + +#pragma mark - Tests :: Log method dispatching + +- (void)testItShouldNotCrashWhenCallingAllLogMethods { + PNConsoleLogger *logger = [PNConsoleLogger new]; + + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"stability test"]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + + entry.logLevel = PNTraceLogLevel; + XCTAssertNoThrow([logger traceWithMessage:entry], @"traceWithMessage: should not throw."); + + entry.logLevel = PNDebugLogLevel; + XCTAssertNoThrow([logger debugWithMessage:entry], @"debugWithMessage: should not throw."); + + entry.logLevel = PNInfoLogLevel; + XCTAssertNoThrow([logger infoWithMessage:entry], @"infoWithMessage: should not throw."); + + entry.logLevel = PNWarnLogLevel; + XCTAssertNoThrow([logger warnWithMessage:entry], @"warnWithMessage: should not throw."); + + entry.logLevel = PNErrorLogLevel; + XCTAssertNoThrow([logger errorWithMessage:entry], @"errorWithMessage: should not throw."); +} + + +#pragma mark - Tests :: ISO8601 timestamp format + +- (void)testItShouldIncludeISO8601TimestampInOutput { + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"timestamp test"]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNInfoLogLevel; + + NSString *result = [PNConsoleLogger stringifiedLogEntry:entry]; + + // ISO8601 timestamps contain T separator and end with Z (for UTC) or timezone offset. + NSString *pattern = @"\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}"; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil]; + NSUInteger matches = [regex numberOfMatchesInString:result options:0 range:NSMakeRange(0, result.length)]; + + XCTAssertGreaterThan(matches, 0, @"Output should contain an ISO8601-formatted timestamp."); +} + + +#pragma mark - + +@end + + +#pragma mark - Test transport response helper + +@implementation PNConsoleTestTransportResponse + +- (instancetype)initWithURL:(NSString *)url + statusCode:(NSUInteger)statusCode + body:(NSData *)body + headers:(NSDictionary *)headers { + if ((self = [super init])) { + _statusCode = statusCode; + _headers = headers; + _body = body; + _url = url; + } + return self; +} + +@end diff --git a/Tests/Tests/Unit/Modules/Logger/PNFileLoggerTest.m b/Tests/Tests/Unit/Modules/Logger/PNFileLoggerTest.m new file mode 100644 index 000000000..18517c642 --- /dev/null +++ b/Tests/Tests/Unit/Modules/Logger/PNFileLoggerTest.m @@ -0,0 +1,477 @@ +#import +#import +#import +#import +#import +#import +#import +#import "PNStringLogEntry+Private.h" +#import "PNErrorLogEntry+Private.h" +#import "PNTransportRequest+Private.h" +#import "PNLogEntry+Private.h" +#import "PNFileLogger.h" +#import "PNFileLoggerFileInformation.h" + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +/// File logger unit tests. +@interface PNFileLoggerTest : XCTestCase + +/// Temporary directory used for log files during tests. +@property(copy, nonatomic) NSString *testLogsDirectory; + +#pragma mark - + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNFileLoggerTest + + +#pragma mark - Setup / Teardown + +- (void)setUp { + [super setUp]; + + NSString *uniqueDir = [NSString stringWithFormat:@"com.pubnub.test.logger.%@", + [[NSUUID UUID] UUIDString]]; + self.testLogsDirectory = [NSTemporaryDirectory() stringByAppendingPathComponent:uniqueDir]; +} + +- (void)tearDown { + // Clean up test log files. + if ([[NSFileManager defaultManager] fileExistsAtPath:self.testLogsDirectory]) { + [[NSFileManager defaultManager] removeItemAtPath:self.testLogsDirectory error:nil]; + } + + [super tearDown]; +} + + +#pragma mark - Tests :: Creation + +- (void)testItShouldCreateFileLoggerWithValidPath { + PNFileLogger *logger = [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + +} + +- (void)testItShouldConformToPNLoggerProtocol { + PNFileLogger *logger = [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + + XCTAssertTrue([logger conformsToProtocol:@protocol(PNLogger)], + @"File logger should conform to PNLogger protocol."); +} + +- (void)testItShouldCreateLogsDirectoryOnInit { + [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:self.testLogsDirectory]; + + XCTAssertTrue(exists, @"Logs directory should be created upon initialization."); +} + + +#pragma mark - Tests :: Default configuration + +- (void)testItShouldHaveDefaultMaximumNumberOfLogFiles { + PNFileLogger *logger = [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + + XCTAssertEqual(logger.maximumNumberOfLogFiles, 5, + @"Default maximum number of log files should be 5."); +} + +- (void)testItShouldHaveDefaultMaximumLogFileSize { + PNFileLogger *logger = [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + + XCTAssertEqual(logger.maximumLogFileSize, (1 * 1024 * 1024), + @"Default maximum log file size should be 1 MB."); +} + +- (void)testItShouldHaveDefaultLogFilesDiskQuota { + PNFileLogger *logger = [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + + XCTAssertEqual(logger.logFilesDiskQuota, (20 * 1024 * 1024), + @"Default log files disk quota should be 20 MB."); +} + + +#pragma mark - Tests :: Configuration changes + +- (void)testItShouldAllowChangingMaximumNumberOfLogFiles { + PNFileLogger *logger = [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + logger.maximumNumberOfLogFiles = 10; + + XCTAssertEqual(logger.maximumNumberOfLogFiles, 10, + @"Maximum number of log files should be updated."); +} + +- (void)testItShouldAllowChangingMaximumLogFileSize { + PNFileLogger *logger = [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + logger.maximumLogFileSize = (5 * 1024 * 1024); + + XCTAssertEqual(logger.maximumLogFileSize, (5 * 1024 * 1024), + @"Maximum log file size should be updated."); +} + +- (void)testItShouldAllowChangingLogFilesDiskQuota { + PNFileLogger *logger = [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + logger.logFilesDiskQuota = (50 * 1024 * 1024); + + XCTAssertEqual(logger.logFilesDiskQuota, (50 * 1024 * 1024), + @"Log files disk quota should be updated."); +} + + +#pragma mark - Tests :: PNLogger protocol methods + +- (void)testItShouldRespondToTraceWithMessage { + PNFileLogger *logger = [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + + XCTAssertTrue([logger respondsToSelector:@selector(traceWithMessage:)], + @"File logger should respond to traceWithMessage:."); +} + +- (void)testItShouldRespondToDebugWithMessage { + PNFileLogger *logger = [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + + XCTAssertTrue([logger respondsToSelector:@selector(debugWithMessage:)], + @"File logger should respond to debugWithMessage:."); +} + +- (void)testItShouldRespondToInfoWithMessage { + PNFileLogger *logger = [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + + XCTAssertTrue([logger respondsToSelector:@selector(infoWithMessage:)], + @"File logger should respond to infoWithMessage:."); +} + +- (void)testItShouldRespondToWarnWithMessage { + PNFileLogger *logger = [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + + XCTAssertTrue([logger respondsToSelector:@selector(warnWithMessage:)], + @"File logger should respond to warnWithMessage:."); +} + +- (void)testItShouldRespondToErrorWithMessage { + PNFileLogger *logger = [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + + XCTAssertTrue([logger respondsToSelector:@selector(errorWithMessage:)], + @"File logger should respond to errorWithMessage:."); +} + + +#pragma mark - Tests :: Writing log entries to file + +- (void)testItShouldCreateLogFileWhenLoggingMessage { + PNFileLogger *logger = [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"File logger test message"]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNInfoLogLevel; + + [logger infoWithMessage:entry]; + + // Wait for async write to complete. + XCTestExpectation *expectation = [self expectationWithDescription:@"Log file created"]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.testLogsDirectory + error:nil]; + // Filter for .txt files (log files). + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self ENDSWITH '.txt'"]; + NSArray *logFiles = [files filteredArrayUsingPredicate:predicate]; + + XCTAssertGreaterThan(logFiles.count, 0, @"At least one log file should be created."); + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:3 handler:nil]; +} + +- (void)testItShouldWriteContentToLogFile { + PNFileLogger *logger = [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"Verify written content"]; + entry.pubNubId = @"client-write-test"; + entry.location = @"Test.m:42"; + entry.logLevel = PNDebugLogLevel; + + [logger debugWithMessage:entry]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Content written to file"]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.testLogsDirectory + error:nil]; + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self ENDSWITH '.txt'"]; + NSArray *logFiles = [files filteredArrayUsingPredicate:predicate]; + + if (logFiles.count > 0) { + NSString *filePath = [self.testLogsDirectory stringByAppendingPathComponent:logFiles.firstObject]; + NSString *content = [NSString stringWithContentsOfFile:filePath + encoding:NSUTF8StringEncoding + error:nil]; + XCTAssertTrue([content containsString:@"Verify written content"], + @"Log file should contain the message text."); + XCTAssertTrue([content containsString:@"client-write-test"], + @"Log file should contain the PubNub client ID."); + } + + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:3 handler:nil]; +} + +- (void)testItShouldCreateLogFileWithCorrectNamingPattern { + PNFileLogger *logger = [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"naming test"]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + entry.logLevel = PNInfoLogLevel; + + [logger infoWithMessage:entry]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"File name pattern verified"]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.testLogsDirectory + error:nil]; + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self ENDSWITH '.txt'"]; + NSArray *logFiles = [files filteredArrayUsingPredicate:predicate]; + + if (logFiles.count > 0) { + NSString *fileName = logFiles.firstObject; + XCTAssertTrue([fileName hasPrefix:@"pubnub-"], + @"Log file should start with 'pubnub-' prefix."); + XCTAssertTrue([fileName hasSuffix:@".txt"], + @"Log file should have .txt extension."); + } + + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:3 handler:nil]; +} + + +#pragma mark - Tests :: Multiple writes to same file + +- (void)testItShouldWriteMultipleEntriesToSameLogFile { + PNFileLogger *logger = [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + + for (NSUInteger i = 0; i < 5; i++) { + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage: + [NSString stringWithFormat:@"Message number %lu", (unsigned long)i]]; + entry.pubNubId = @"client-multi"; + entry.location = @"Test.m:1"; + entry.logLevel = PNInfoLogLevel; + [logger infoWithMessage:entry]; + } + + XCTestExpectation *expectation = [self expectationWithDescription:@"Multiple entries written"]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.testLogsDirectory + error:nil]; + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self ENDSWITH '.txt'"]; + NSArray *logFiles = [files filteredArrayUsingPredicate:predicate]; + + if (logFiles.count > 0) { + NSString *filePath = [self.testLogsDirectory stringByAppendingPathComponent:logFiles.firstObject]; + NSString *content = [NSString stringWithContentsOfFile:filePath + encoding:NSUTF8StringEncoding + error:nil]; + XCTAssertTrue([content containsString:@"Message number 0"], + @"Log file should contain first message."); + XCTAssertTrue([content containsString:@"Message number 4"], + @"Log file should contain last message."); + } + + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:5 handler:nil]; +} + + +#pragma mark - Tests :: All log level methods write to file + +- (void)testItShouldNotCrashWhenCallingAllLogMethods { + PNFileLogger *logger = [PNFileLogger loggerWithLogsDirectoryPath:self.testLogsDirectory]; + + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"stability"]; + entry.pubNubId = @"client-1"; + entry.location = @"Test.m:1"; + + entry.logLevel = PNTraceLogLevel; + XCTAssertNoThrow([logger traceWithMessage:entry], @"traceWithMessage: should not crash."); + + entry.logLevel = PNDebugLogLevel; + XCTAssertNoThrow([logger debugWithMessage:entry], @"debugWithMessage: should not crash."); + + entry.logLevel = PNInfoLogLevel; + XCTAssertNoThrow([logger infoWithMessage:entry], @"infoWithMessage: should not crash."); + + entry.logLevel = PNWarnLogLevel; + XCTAssertNoThrow([logger warnWithMessage:entry], @"warnWithMessage: should not crash."); + + entry.logLevel = PNErrorLogLevel; + XCTAssertNoThrow([logger errorWithMessage:entry], @"errorWithMessage: should not crash."); +} + + +#pragma mark - Tests :: PNFileLoggerFileInformation + +- (void)testItShouldCreateFileInformationForExistingFile { + // Create a temporary file. + NSString *filePath = [self.testLogsDirectory stringByAppendingPathComponent:@"test-log.txt"]; + [[NSFileManager defaultManager] createDirectoryAtPath:self.testLogsDirectory + withIntermediateDirectories:YES + attributes:nil + error:nil]; + [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]; + + PNFileLoggerFileInformation *info = [PNFileLoggerFileInformation informationForFileAtPath:filePath]; + + XCTAssertEqualObjects(info.path, filePath, @"Path should match the provided file path."); + XCTAssertEqualObjects(info.name, @"test-log.txt", @"Name should be the file name component."); +} + +- (void)testItShouldReportCreationDateForExistingFile { + NSString *filePath = [self.testLogsDirectory stringByAppendingPathComponent:@"dated-log.txt"]; + [[NSFileManager defaultManager] createDirectoryAtPath:self.testLogsDirectory + withIntermediateDirectories:YES + attributes:nil + error:nil]; + [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]; + + PNFileLoggerFileInformation *info = [PNFileLoggerFileInformation informationForFileAtPath:filePath]; + +} + +- (void)testItShouldReportModificationDateForExistingFile { + NSString *filePath = [self.testLogsDirectory stringByAppendingPathComponent:@"modified-log.txt"]; + [[NSFileManager defaultManager] createDirectoryAtPath:self.testLogsDirectory + withIntermediateDirectories:YES + attributes:nil + error:nil]; + [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]; + + PNFileLoggerFileInformation *info = [PNFileLoggerFileInformation informationForFileAtPath:filePath]; + +} + +- (void)testItShouldReportZeroSizeForEmptyFile { + NSString *filePath = [self.testLogsDirectory stringByAppendingPathComponent:@"empty-log.txt"]; + [[NSFileManager defaultManager] createDirectoryAtPath:self.testLogsDirectory + withIntermediateDirectories:YES + attributes:nil + error:nil]; + [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]; + + PNFileLoggerFileInformation *info = [PNFileLoggerFileInformation informationForFileAtPath:filePath]; + + XCTAssertEqual(info.size, 0, @"Size should be 0 for an empty file."); +} + +- (void)testItShouldReportCorrectSizeForNonEmptyFile { + NSString *filePath = [self.testLogsDirectory stringByAppendingPathComponent:@"sized-log.txt"]; + [[NSFileManager defaultManager] createDirectoryAtPath:self.testLogsDirectory + withIntermediateDirectories:YES + attributes:nil + error:nil]; + NSData *content = [@"Hello, World!" dataUsingEncoding:NSUTF8StringEncoding]; + [[NSFileManager defaultManager] createFileAtPath:filePath contents:content attributes:nil]; + + PNFileLoggerFileInformation *info = [PNFileLoggerFileInformation informationForFileAtPath:filePath]; + + XCTAssertEqual(info.size, content.length, @"Size should match the written content length."); +} + +- (void)testItShouldNotBeArchivedByDefault { + NSString *filePath = [self.testLogsDirectory stringByAppendingPathComponent:@"fresh-log.txt"]; + [[NSFileManager defaultManager] createDirectoryAtPath:self.testLogsDirectory + withIntermediateDirectories:YES + attributes:nil + error:nil]; + [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]; + + PNFileLoggerFileInformation *info = [PNFileLoggerFileInformation informationForFileAtPath:filePath]; + + XCTAssertFalse(info.isArchived, @"File should not be marked as archived by default."); +} + +- (void)testItShouldAllowSettingArchivedFlag { + NSString *filePath = [self.testLogsDirectory stringByAppendingPathComponent:@"archive-log.txt"]; + [[NSFileManager defaultManager] createDirectoryAtPath:self.testLogsDirectory + withIntermediateDirectories:YES + attributes:nil + error:nil]; + [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]; + + PNFileLoggerFileInformation *info = [PNFileLoggerFileInformation informationForFileAtPath:filePath]; + info.archived = YES; + + XCTAssertTrue(info.isArchived, @"File should be marked as archived after setting the flag."); +} + +- (void)testItShouldAllowUnsettingArchivedFlag { + NSString *filePath = [self.testLogsDirectory stringByAppendingPathComponent:@"unarchive-log.txt"]; + [[NSFileManager defaultManager] createDirectoryAtPath:self.testLogsDirectory + withIntermediateDirectories:YES + attributes:nil + error:nil]; + [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]; + + PNFileLoggerFileInformation *info = [PNFileLoggerFileInformation informationForFileAtPath:filePath]; + info.archived = YES; + info.archived = NO; + + XCTAssertFalse(info.isArchived, @"File should not be archived after unsetting the flag."); +} + +- (void)testItShouldConsiderTwoFileInformationsEqualWhenPathsMatch { + NSString *filePath = [self.testLogsDirectory stringByAppendingPathComponent:@"equal-log.txt"]; + [[NSFileManager defaultManager] createDirectoryAtPath:self.testLogsDirectory + withIntermediateDirectories:YES + attributes:nil + error:nil]; + [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]; + + PNFileLoggerFileInformation *info1 = [PNFileLoggerFileInformation informationForFileAtPath:filePath]; + PNFileLoggerFileInformation *info2 = [PNFileLoggerFileInformation informationForFileAtPath:filePath]; + + XCTAssertEqualObjects(info1, info2, + @"Two file information objects with the same path should be equal."); +} + +- (void)testItShouldConsiderTwoFileInformationsNotEqualWhenPathsDiffer { + [[NSFileManager defaultManager] createDirectoryAtPath:self.testLogsDirectory + withIntermediateDirectories:YES + attributes:nil + error:nil]; + NSString *path1 = [self.testLogsDirectory stringByAppendingPathComponent:@"log-a.txt"]; + NSString *path2 = [self.testLogsDirectory stringByAppendingPathComponent:@"log-b.txt"]; + [[NSFileManager defaultManager] createFileAtPath:path1 contents:nil attributes:nil]; + [[NSFileManager defaultManager] createFileAtPath:path2 contents:nil attributes:nil]; + + PNFileLoggerFileInformation *info1 = [PNFileLoggerFileInformation informationForFileAtPath:path1]; + PNFileLoggerFileInformation *info2 = [PNFileLoggerFileInformation informationForFileAtPath:path2]; + + XCTAssertNotEqualObjects(info1, info2, + @"Two file information objects with different paths should not be equal."); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Modules/Logger/PNLogEntryTest.m b/Tests/Tests/Unit/Modules/Logger/PNLogEntryTest.m new file mode 100644 index 000000000..d8e7d0623 --- /dev/null +++ b/Tests/Tests/Unit/Modules/Logger/PNLogEntryTest.m @@ -0,0 +1,357 @@ +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import "PNNetworkResponseLogEntry+Private.h" +#import "PNNetworkRequestLogEntry+Private.h" +#import "PNDictionaryLogEntry+Private.h" +#import "PNStringLogEntry+Private.h" +#import "PNErrorLogEntry+Private.h" +#import "PNTransportRequest+Private.h" +#import "PNLogEntry+Private.h" + + +/// Minimal PNTransportResponse conforming object for testing purposes. +@interface PNTestTransportResponse : NSObject + +@property(strong, nonatomic) NSDictionary *headers; +@property(strong, nonatomic) NSInputStream *bodyStream; +@property(strong, nonatomic) NSString *MIMEType; +@property(assign, nonatomic) BOOL bodyStreamAvailable; +@property(strong, nonatomic) NSData *body; +@property(assign, nonatomic) NSUInteger statusCode; +@property(strong, nonatomic) NSString *url; + +- (instancetype)initWithURL:(NSString *)url + statusCode:(NSUInteger)statusCode + body:(nullable NSData *)body + headers:(nullable NSDictionary *)headers; + +@end + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +/// Log entry data model unit tests. +@interface PNLogEntryTest : XCTestCase + +#pragma mark - + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNLogEntryTest + + +#pragma mark - Tests :: PNStringLogEntry :: Creation + +- (void)testItShouldCreateStringLogEntryWithMessage { + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"Test log message"]; + + XCTAssertEqualObjects(entry.message, @"Test log message", @"Message should match the provided string."); + XCTAssertEqual(entry.messageType, PNTextLogMessageType, @"Message type should be PNTextLogMessageType."); + XCTAssertEqual(entry.operation, PNUnknownLogMessageOperation, + @"Default operation should be PNUnknownLogMessageOperation."); +} + +- (void)testItShouldCreateStringLogEntryWithTimestamp { + NSDate *beforeCreation = [NSDate date]; + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"Test"]; + NSDate *afterCreation = [NSDate date]; + + XCTAssertTrue([entry.timestamp compare:beforeCreation] != NSOrderedAscending, + @"Timestamp should not be before creation time."); + XCTAssertTrue([entry.timestamp compare:afterCreation] != NSOrderedDescending, + @"Timestamp should not be after creation time."); +} + +- (void)testItShouldCreateStringLogEntryWithEmptyMessage { + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@""]; + + XCTAssertEqualObjects(entry.message, @"", @"Message should be an empty string."); + XCTAssertEqual(entry.messageType, PNTextLogMessageType, @"Message type should be PNTextLogMessageType."); +} + + +#pragma mark - Tests :: PNStringLogEntry :: Properties set by manager + +- (void)testItShouldAcceptLocationSetByManager { + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"Test"]; + entry.location = @"PubNub+Core.m:123"; + + XCTAssertEqualObjects(entry.location, @"PubNub+Core.m:123", + @"Location should match the value set externally."); +} + +- (void)testItShouldAcceptPubNubIdSetByManager { + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"Test"]; + entry.pubNubId = @"client-abc-123"; + + XCTAssertEqualObjects(entry.pubNubId, @"client-abc-123", + @"PubNub ID should match the value set externally."); +} + +- (void)testItShouldAcceptLogLevelSetByManager { + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"Test"]; + entry.logLevel = PNInfoLogLevel; + + XCTAssertEqual(entry.logLevel, PNInfoLogLevel, @"Log level should be PNInfoLogLevel after assignment."); +} + +- (void)testItShouldAcceptMinimumLogLevelSetByManager { + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"Test"]; + entry.minimumLogLevel = PNDebugLogLevel; + + XCTAssertEqual(entry.minimumLogLevel, PNDebugLogLevel, + @"Minimum log level should be PNDebugLogLevel after assignment."); +} + + +#pragma mark - Tests :: PNDictionaryLogEntry :: Creation + +- (void)testItShouldCreateDictionaryLogEntryWithMessageAndDetails { + NSDictionary *dict = @{ @"key": @"value", @"count": @42 }; + PNDictionaryLogEntry *entry = [PNDictionaryLogEntry entryWithMessage:dict details:@"Some context"]; + + XCTAssertEqualObjects(entry.message, dict, @"Message should match the provided dictionary."); + XCTAssertEqualObjects(entry.details, @"Some context", @"Details should match the provided string."); + XCTAssertEqual(entry.messageType, PNObjectLogMessageType, @"Message type should be PNObjectLogMessageType."); + XCTAssertEqual(entry.operation, PNUnknownLogMessageOperation, + @"Default operation should be PNUnknownLogMessageOperation."); +} + +- (void)testItShouldCreateDictionaryLogEntryWithNilDetails { + NSDictionary *dict = @{ @"key": @"value" }; + PNDictionaryLogEntry *entry = [PNDictionaryLogEntry entryWithMessage:dict details:nil]; + + XCTAssertEqualObjects(entry.message, dict, @"Message should match the provided dictionary."); + XCTAssertNil(entry.details, @"Details should be nil when nil was provided."); +} + +- (void)testItShouldCreateDictionaryLogEntryWithEmptyDictionary { + NSDictionary *dict = @{}; + PNDictionaryLogEntry *entry = [PNDictionaryLogEntry entryWithMessage:dict details:@"empty dict"]; + + XCTAssertEqualObjects(entry.message, dict, @"Message should be an empty dictionary."); +} + +- (void)testItShouldCreateDictionaryLogEntryWithNestedDictionary { + NSDictionary *dict = @{ @"outer": @{ @"inner": @"value" } }; + PNDictionaryLogEntry *entry = [PNDictionaryLogEntry entryWithMessage:dict details:nil]; + + XCTAssertEqualObjects(entry.message[@"outer"], (@{ @"inner": @"value" }), + @"Nested dictionary should be preserved."); +} + + +#pragma mark - Tests :: PNErrorLogEntry :: Creation + +- (void)testItShouldCreateErrorLogEntryWithError { + NSError *error = [NSError errorWithDomain:@"com.pubnub.test" code:42 userInfo:@{ + NSLocalizedDescriptionKey: @"Test error occurred" + }]; + PNErrorLogEntry *entry = [PNErrorLogEntry entryWithMessage:error]; + + XCTAssertEqualObjects(entry.message, error, @"Message should match the provided error."); + XCTAssertEqual(entry.messageType, PNErrorLogMessageType, @"Message type should be PNErrorLogMessageType."); + XCTAssertEqual(entry.operation, PNUnknownLogMessageOperation, + @"Default operation should be PNUnknownLogMessageOperation."); +} + +- (void)testItShouldCreateErrorLogEntryWithErrorDomainAndCode { + NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:nil]; + PNErrorLogEntry *entry = [PNErrorLogEntry entryWithMessage:error]; + + XCTAssertEqualObjects(entry.message.domain, NSURLErrorDomain, @"Error domain should be preserved."); + XCTAssertEqual(entry.message.code, NSURLErrorTimedOut, @"Error code should be preserved."); +} + +- (void)testItShouldCreateErrorLogEntryWithUnderlyingError { + NSError *underlying = [NSError errorWithDomain:@"com.pubnub.underlying" code:1 userInfo:nil]; + NSError *error = [NSError errorWithDomain:@"com.pubnub.test" code:42 userInfo:@{ + NSUnderlyingErrorKey: underlying + }]; + PNErrorLogEntry *entry = [PNErrorLogEntry entryWithMessage:error]; + + XCTAssertNotNil(entry.message.userInfo[NSUnderlyingErrorKey], + @"Underlying error should be preserved in userInfo."); +} + + +#pragma mark - Tests :: PNNetworkRequestLogEntry :: Creation + +- (void)testItShouldCreateNetworkRequestLogEntryWithTransportRequest { + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/v2/subscribe"; + request.origin = @"ps.pndsn.com"; + + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:@"Subscribe request"]; + + XCTAssertEqualObjects(entry.message, request, @"Message should match the provided transport request."); + XCTAssertEqualObjects(entry.details, @"Subscribe request", @"Details should match the provided string."); + XCTAssertEqual(entry.messageType, PNNetworkRequestLogMessageType, + @"Message type should be PNNetworkRequestLogMessageType."); + XCTAssertFalse(entry.isCanceled, @"Canceled flag should be NO by default."); + XCTAssertFalse(entry.isFailed, @"Failed flag should be NO by default."); +} + +- (void)testItShouldCreateNetworkRequestLogEntryWithCancelledFlag { + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/publish/key"; + request.origin = @"ps.pndsn.com"; + + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request + details:nil + canceled:YES + failed:NO]; + + XCTAssertTrue(entry.isCanceled, @"Canceled flag should be YES when set."); + XCTAssertFalse(entry.isFailed, @"Failed flag should be NO when not set."); +} + +- (void)testItShouldCreateNetworkRequestLogEntryWithFailedFlag { + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/publish/key"; + request.origin = @"ps.pndsn.com"; + + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request + details:nil + canceled:NO + failed:YES]; + + XCTAssertFalse(entry.isCanceled, @"Canceled flag should be NO when not set."); + XCTAssertTrue(entry.isFailed, @"Failed flag should be YES when set."); +} + +- (void)testItShouldCreateNetworkRequestLogEntryWithNilDetails { + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/v2/presence"; + request.origin = @"ps.pndsn.com"; + + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:nil]; + + XCTAssertNil(entry.details, @"Details should be nil when nil was provided."); +} + + +#pragma mark - Tests :: PNNetworkResponseLogEntry :: Creation + +- (void)testItShouldCreateNetworkResponseLogEntryWithResponse { + id mockResponse = [self mockTransportResponseWithURL:@"https://ps.pndsn.com/v2/subscribe" + statusCode:200 + body:nil + headers:nil]; + + PNNetworkResponseLogEntry *entry = [PNNetworkResponseLogEntry entryWithMessage:mockResponse]; + + XCTAssertEqual(entry.messageType, PNNetworkResponseLogMessageType, + @"Message type should be PNNetworkResponseLogMessageType."); +} + +- (void)testItShouldCreateNetworkResponseLogEntryPreservingResponseURL { + NSString *expectedURL = @"https://ps.pndsn.com/v2/subscribe?tt=0"; + id mockResponse = [self mockTransportResponseWithURL:expectedURL + statusCode:200 + body:nil + headers:nil]; + + PNNetworkResponseLogEntry *entry = [PNNetworkResponseLogEntry entryWithMessage:mockResponse]; + + XCTAssertEqualObjects([entry.message url], expectedURL, + @"Response URL should be preserved in the log entry."); +} + + +#pragma mark - Tests :: PNLogEntry :: Operation assignment + +- (void)testItShouldAllowOperationToBeSetOnStringEntry { + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"subscribe test"]; + entry.operation = PNSubscribeLogMessageOperation; + + XCTAssertEqual(entry.operation, PNSubscribeLogMessageOperation, + @"Operation should be PNSubscribeLogMessageOperation after assignment."); +} + +- (void)testItShouldAllowOperationToBeSetOnDictionaryEntry { + PNDictionaryLogEntry *entry = [PNDictionaryLogEntry entryWithMessage:@{} details:nil]; + entry.operation = PNMessageSendLogMessageOperation; + + XCTAssertEqual(entry.operation, PNMessageSendLogMessageOperation, + @"Operation should be PNMessageSendLogMessageOperation after assignment."); +} + + +#pragma mark - Tests :: PNLogEntry :: PreProcessedString caching + +- (void)testItShouldStorePreProcessedString { + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"Test"]; + + XCTAssertNil(entry.preProcessedString, @"Pre-processed string should be nil initially."); + + entry.preProcessedString = @"Formatted output"; + XCTAssertEqualObjects(entry.preProcessedString, @"Formatted output", + @"Pre-processed string should be stored after assignment."); +} + + +#pragma mark - Tests :: PNLogEntry :: Immutability of timestamp + +- (void)testItShouldNotChangeTimestampAfterCreation { + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"Test"]; + NSDate *firstTimestamp = entry.timestamp; + + // Small delay to ensure time has passed. + [NSThread sleepForTimeInterval:0.01]; + + NSDate *secondTimestamp = entry.timestamp; + XCTAssertEqualObjects(firstTimestamp, secondTimestamp, + @"Timestamp should remain the same after creation."); +} + + +#pragma mark - Helpers + +- (id)mockTransportResponseWithURL:(NSString *)url + statusCode:(NSUInteger)statusCode + body:(nullable NSData *)body + headers:(nullable NSDictionary *)headers { + // Create a simple object conforming to PNTransportResponse using NSProxy or a test double. + // Since PNTransportResponse is a protocol, we use a simple class that implements it. + return [[PNTestTransportResponse alloc] initWithURL:url statusCode:statusCode body:body headers:headers]; +} + +#pragma mark - + +@end + + +#pragma mark - Test transport response helper + +@implementation PNTestTransportResponse + +- (instancetype)initWithURL:(NSString *)url + statusCode:(NSUInteger)statusCode + body:(NSData *)body + headers:(NSDictionary *)headers { + if ((self = [super init])) { + _statusCode = statusCode; + _headers = headers; + _body = body; + _url = url; + } + return self; +} + +@end diff --git a/Tests/Tests/Unit/Modules/Logger/PNLoggerManagerTest.m b/Tests/Tests/Unit/Modules/Logger/PNLoggerManagerTest.m new file mode 100644 index 000000000..268140ed4 --- /dev/null +++ b/Tests/Tests/Unit/Modules/Logger/PNLoggerManagerTest.m @@ -0,0 +1,893 @@ +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import "PNNetworkResponseLogEntry+Private.h" +#import "PNNetworkRequestLogEntry+Private.h" +#import "PNDictionaryLogEntry+Private.h" +#import "PNStringLogEntry+Private.h" +#import "PNErrorLogEntry+Private.h" +#import "PNTransportRequest+Private.h" +#import "PNLoggerManager+Private.h" +#import "PNLogEntry+Private.h" + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - Mock logger + +/// Mock logger that records received messages for verification. +@interface PNMockLogger : NSObject + +/// All messages received across all log level methods. +@property(strong, nonatomic, readonly) NSMutableArray *receivedMessages; + +/// Messages received specifically via `traceWithMessage:`. +@property(strong, nonatomic, readonly) NSMutableArray *traceMessages; + +/// Messages received specifically via `debugWithMessage:`. +@property(strong, nonatomic, readonly) NSMutableArray *debugMessages; + +/// Messages received specifically via `infoWithMessage:`. +@property(strong, nonatomic, readonly) NSMutableArray *infoMessages; + +/// Messages received specifically via `warnWithMessage:`. +@property(strong, nonatomic, readonly) NSMutableArray *warnMessages; + +/// Messages received specifically via `errorWithMessage:`. +@property(strong, nonatomic, readonly) NSMutableArray *errorMessages; + +@end + + +@implementation PNMockLogger + +- (instancetype)init { + if ((self = [super init])) { + _receivedMessages = [NSMutableArray new]; + _traceMessages = [NSMutableArray new]; + _debugMessages = [NSMutableArray new]; + _infoMessages = [NSMutableArray new]; + _warnMessages = [NSMutableArray new]; + _errorMessages = [NSMutableArray new]; + } + return self; +} + +- (void)traceWithMessage:(PNLogEntry *)message { + [self.receivedMessages addObject:message]; + [self.traceMessages addObject:message]; +} + +- (void)debugWithMessage:(PNLogEntry *)message { + [self.receivedMessages addObject:message]; + [self.debugMessages addObject:message]; +} + +- (void)infoWithMessage:(PNLogEntry *)message { + [self.receivedMessages addObject:message]; + [self.infoMessages addObject:message]; +} + +- (void)warnWithMessage:(PNLogEntry *)message { + [self.receivedMessages addObject:message]; + [self.warnMessages addObject:message]; +} + +- (void)errorWithMessage:(PNLogEntry *)message { + [self.receivedMessages addObject:message]; + [self.errorMessages addObject:message]; +} + +@end + + +#pragma mark - Interface declaration + +/// Logger manager module unit tests. +@interface PNLoggerManagerTest : XCTestCase + +#pragma mark - + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNLoggerManagerTest + + +#pragma mark - Tests :: Initialization + +- (void)testItShouldCreateManagerWithDefaultConfiguration { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNDebugLogLevel + andLoggers:@[logger]]; + + XCTAssertEqual(manager.logLevel, PNDebugLogLevel, @"Log level should match the configured value."); +} + +- (void)testItShouldCreateManagerWithTraceLogLevel { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + + XCTAssertEqual(manager.logLevel, PNTraceLogLevel, @"Log level should be PNTraceLogLevel."); +} + +- (void)testItShouldCreateManagerWithNoneLogLevel { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNNoneLogLevel + andLoggers:@[logger]]; + + XCTAssertEqual(manager.logLevel, PNNoneLogLevel, @"Log level should be PNNoneLogLevel."); +} + +- (void)testItShouldCreateManagerWithEmptyLoggersArray { + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNInfoLogLevel + andLoggers:@[]]; + +} + +- (void)testItShouldAllowLogLevelToBeChanged { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNDebugLogLevel + andLoggers:@[logger]]; + + manager.logLevel = PNErrorLogLevel; + XCTAssertEqual(manager.logLevel, PNErrorLogLevel, @"Log level should be updated to PNErrorLogLevel."); +} + + +#pragma mark - Tests :: Trace level logging + +- (void)testItShouldForwardTraceMessageToLoggerWhenLogLevelIsTrace { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"trace message"]; + + [manager traceWithLocation:@"TestClass.m:10" andMessage:entry]; + + XCTAssertEqual(logger.traceMessages.count, 1, @"Trace logger should receive exactly 1 message."); + XCTAssertEqualObjects(logger.traceMessages.firstObject, entry, + @"The forwarded message should be the same entry."); +} + +- (void)testItShouldNotForwardTraceMessageWhenLogLevelIsDebug { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNDebugLogLevel + andLoggers:@[logger]]; + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"trace message"]; + + [manager traceWithLocation:@"TestClass.m:10" andMessage:entry]; + + XCTAssertEqual(logger.receivedMessages.count, 0, + @"No messages should be forwarded when log level is higher than trace."); +} + + +#pragma mark - Tests :: Debug level logging + +- (void)testItShouldForwardDebugMessageWhenLogLevelIsDebug { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNDebugLogLevel + andLoggers:@[logger]]; + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"debug message"]; + + [manager debugWithLocation:@"TestClass.m:20" andMessage:entry]; + + XCTAssertEqual(logger.debugMessages.count, 1, @"Debug logger should receive exactly 1 message."); +} + +- (void)testItShouldForwardDebugMessageWhenLogLevelIsTrace { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"debug message"]; + + [manager debugWithLocation:@"TestClass.m:20" andMessage:entry]; + + XCTAssertEqual(logger.debugMessages.count, 1, + @"Debug message should be forwarded when log level is lower (trace)."); +} + +- (void)testItShouldNotForwardDebugMessageWhenLogLevelIsInfo { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNInfoLogLevel + andLoggers:@[logger]]; + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"debug message"]; + + [manager debugWithLocation:@"TestClass.m:20" andMessage:entry]; + + XCTAssertEqual(logger.receivedMessages.count, 0, + @"Debug message should not be forwarded when log level is info."); +} + + +#pragma mark - Tests :: Info level logging + +- (void)testItShouldForwardInfoMessageWhenLogLevelIsInfo { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNInfoLogLevel + andLoggers:@[logger]]; + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"info message"]; + + [manager infoWithLocation:@"TestClass.m:30" andMessage:entry]; + + XCTAssertEqual(logger.infoMessages.count, 1, @"Info logger should receive exactly 1 message."); +} + +- (void)testItShouldNotForwardInfoMessageWhenLogLevelIsWarn { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNWarnLogLevel + andLoggers:@[logger]]; + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"info message"]; + + [manager infoWithLocation:@"TestClass.m:30" andMessage:entry]; + + XCTAssertEqual(logger.receivedMessages.count, 0, + @"Info message should not be forwarded when log level is warn."); +} + + +#pragma mark - Tests :: Warn level logging + +- (void)testItShouldForwardWarnMessageWhenLogLevelIsWarn { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNWarnLogLevel + andLoggers:@[logger]]; + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"warn message"]; + + [manager warnWithLocation:@"TestClass.m:40" andMessage:entry]; + + XCTAssertEqual(logger.warnMessages.count, 1, @"Warn logger should receive exactly 1 message."); +} + +- (void)testItShouldNotForwardWarnMessageWhenLogLevelIsError { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNErrorLogLevel + andLoggers:@[logger]]; + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"warn message"]; + + [manager warnWithLocation:@"TestClass.m:40" andMessage:entry]; + + XCTAssertEqual(logger.receivedMessages.count, 0, + @"Warn message should not be forwarded when log level is error."); +} + + +#pragma mark - Tests :: Error level logging + +- (void)testItShouldForwardErrorMessageWhenLogLevelIsError { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNErrorLogLevel + andLoggers:@[logger]]; + NSError *error = [NSError errorWithDomain:@"com.pubnub.test" code:1 userInfo:nil]; + PNErrorLogEntry *entry = [PNErrorLogEntry entryWithMessage:error]; + + [manager errorWithLocation:@"TestClass.m:50" andMessage:entry]; + + XCTAssertEqual(logger.errorMessages.count, 1, @"Error logger should receive exactly 1 message."); +} + +- (void)testItShouldForwardErrorMessageWhenLogLevelIsTrace { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + NSError *error = [NSError errorWithDomain:@"com.pubnub.test" code:1 userInfo:nil]; + PNErrorLogEntry *entry = [PNErrorLogEntry entryWithMessage:error]; + + [manager errorWithLocation:@"TestClass.m:50" andMessage:entry]; + + XCTAssertEqual(logger.errorMessages.count, 1, + @"Error message should be forwarded when log level is lower (trace)."); +} + + +#pragma mark - Tests :: Multiple loggers + +- (void)testItShouldForwardMessageToAllRegisteredLoggers { + PNMockLogger *logger1 = [PNMockLogger new]; + PNMockLogger *logger2 = [PNMockLogger new]; + PNMockLogger *logger3 = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNInfoLogLevel + andLoggers:@[logger1, logger2, logger3]]; + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"broadcast message"]; + + [manager infoWithLocation:@"TestClass.m:60" andMessage:entry]; + + XCTAssertEqual(logger1.infoMessages.count, 1, @"Logger 1 should receive the info message."); + XCTAssertEqual(logger2.infoMessages.count, 1, @"Logger 2 should receive the info message."); + XCTAssertEqual(logger3.infoMessages.count, 1, @"Logger 3 should receive the info message."); +} + +- (void)testItShouldForwardSameEntryInstanceToAllLoggers { + PNMockLogger *logger1 = [PNMockLogger new]; + PNMockLogger *logger2 = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNDebugLogLevel + andLoggers:@[logger1, logger2]]; + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"shared entry"]; + + [manager debugWithLocation:@"TestClass.m:70" andMessage:entry]; + + XCTAssertEqual(logger1.debugMessages.firstObject, logger2.debugMessages.firstObject, + @"Both loggers should receive the same entry object."); +} + + +#pragma mark - Tests :: Message enrichment + +- (void)testItShouldSetPubNubIdOnMessageWhenLogging { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"client-xyz" + logLevel:PNInfoLogLevel + andLoggers:@[logger]]; + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"test"]; + + [manager infoWithLocation:@"TestClass.m:80" andMessage:entry]; + + XCTAssertEqualObjects(entry.pubNubId, @"client-xyz", + @"PubNub ID should be set to the client identifier from the manager."); +} + +- (void)testItShouldSetLocationOnMessageWhenLogging { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNInfoLogLevel + andLoggers:@[logger]]; + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"test"]; + + [manager infoWithLocation:@"PubNub+Core.m:42" andMessage:entry]; + + XCTAssertEqualObjects(entry.location, @"PubNub+Core.m:42", + @"Location should be set to the value provided to the log method."); +} + +- (void)testItShouldSetLogLevelOnMessageWhenLogging { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"test"]; + + [manager warnWithLocation:@"TestClass.m:90" andMessage:entry]; + + XCTAssertEqual(entry.logLevel, PNWarnLogLevel, + @"Log level on the entry should match the method used (warn)."); +} + +- (void)testItShouldSetMinimumLogLevelOnMessageWhenLogging { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNDebugLogLevel + andLoggers:@[logger]]; + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"test"]; + + [manager debugWithLocation:@"TestClass.m:100" andMessage:entry]; + + XCTAssertEqual(entry.minimumLogLevel, PNDebugLogLevel, + @"Minimum log level should be set from the manager's configured level."); +} + + +#pragma mark - Tests :: Factory block logging + +- (void)testItShouldCallFactoryBlockWhenLogLevelPermitsTrace { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + __block BOOL factoryCalled = NO; + + [manager traceWithLocation:@"TestClass.m:110" andMessageFactory:^PNLogEntry * _Nullable{ + factoryCalled = YES; + return [PNStringLogEntry entryWithMessage:@"lazy trace"]; + }]; + + XCTAssertTrue(factoryCalled, @"Factory block should be called when log level permits trace."); + XCTAssertEqual(logger.traceMessages.count, 1, @"Trace message from factory should be forwarded."); +} + +- (void)testItShouldNotCallFactoryBlockWhenLogLevelDoesNotPermitTrace { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNErrorLogLevel + andLoggers:@[logger]]; + __block BOOL factoryCalled = NO; + + [manager traceWithLocation:@"TestClass.m:120" andMessageFactory:^PNLogEntry * _Nullable{ + factoryCalled = YES; + return [PNStringLogEntry entryWithMessage:@"lazy trace"]; + }]; + + XCTAssertFalse(factoryCalled, + @"Factory block should NOT be called when log level is higher than trace."); +} + +- (void)testItShouldCallDebugFactoryBlockWhenLogLevelPermits { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNDebugLogLevel + andLoggers:@[logger]]; + __block BOOL factoryCalled = NO; + + [manager debugWithLocation:@"TestClass.m:130" andMessageFactory:^PNLogEntry * _Nullable{ + factoryCalled = YES; + return [PNStringLogEntry entryWithMessage:@"lazy debug"]; + }]; + + XCTAssertTrue(factoryCalled, @"Debug factory block should be called when log level permits."); +} + +- (void)testItShouldNotCallDebugFactoryBlockWhenLogLevelDoesNotPermit { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNInfoLogLevel + andLoggers:@[logger]]; + __block BOOL factoryCalled = NO; + + [manager debugWithLocation:@"TestClass.m:140" andMessageFactory:^PNLogEntry * _Nullable{ + factoryCalled = YES; + return [PNStringLogEntry entryWithMessage:@"lazy debug"]; + }]; + + XCTAssertFalse(factoryCalled, + @"Debug factory block should NOT be called when log level is higher than debug."); +} + +- (void)testItShouldCallInfoFactoryBlockWhenLogLevelPermits { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNInfoLogLevel + andLoggers:@[logger]]; + __block BOOL factoryCalled = NO; + + [manager infoWithLocation:@"TestClass.m:150" andMessageFactory:^PNLogEntry * _Nullable{ + factoryCalled = YES; + return [PNStringLogEntry entryWithMessage:@"lazy info"]; + }]; + + XCTAssertTrue(factoryCalled, @"Info factory block should be called when log level permits."); +} + +- (void)testItShouldNotCallInfoFactoryBlockWhenLogLevelDoesNotPermit { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNWarnLogLevel + andLoggers:@[logger]]; + __block BOOL factoryCalled = NO; + + [manager infoWithLocation:@"TestClass.m:160" andMessageFactory:^PNLogEntry * _Nullable{ + factoryCalled = YES; + return [PNStringLogEntry entryWithMessage:@"lazy info"]; + }]; + + XCTAssertFalse(factoryCalled, + @"Info factory block should NOT be called when log level is higher than info."); +} + +- (void)testItShouldCallWarnFactoryBlockWhenLogLevelPermits { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNWarnLogLevel + andLoggers:@[logger]]; + __block BOOL factoryCalled = NO; + + [manager warnWithLocation:@"TestClass.m:170" andMessageFactory:^PNLogEntry * _Nullable{ + factoryCalled = YES; + return [PNStringLogEntry entryWithMessage:@"lazy warn"]; + }]; + + XCTAssertTrue(factoryCalled, @"Warn factory block should be called when log level permits."); +} + +- (void)testItShouldNotCallWarnFactoryBlockWhenLogLevelDoesNotPermit { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNErrorLogLevel + andLoggers:@[logger]]; + __block BOOL factoryCalled = NO; + + [manager warnWithLocation:@"TestClass.m:180" andMessageFactory:^PNLogEntry * _Nullable{ + factoryCalled = YES; + return [PNStringLogEntry entryWithMessage:@"lazy warn"]; + }]; + + XCTAssertFalse(factoryCalled, + @"Warn factory block should NOT be called when log level is higher than warn."); +} + +- (void)testItShouldCallErrorFactoryBlockWhenLogLevelPermits { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNErrorLogLevel + andLoggers:@[logger]]; + __block BOOL factoryCalled = NO; + + [manager errorWithLocation:@"TestClass.m:190" andMessageFactory:^PNLogEntry * _Nullable{ + factoryCalled = YES; + NSError *error = [NSError errorWithDomain:@"test" code:1 userInfo:nil]; + return [PNErrorLogEntry entryWithMessage:error]; + }]; + + XCTAssertTrue(factoryCalled, @"Error factory block should be called when log level permits."); +} + + +#pragma mark - Tests :: Nil message handling + +- (void)testItShouldNotForwardNilMessageToLoggers { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + + [manager infoWithLocation:@"TestClass.m:200" andMessage:nil]; + + XCTAssertEqual(logger.receivedMessages.count, 0, + @"No messages should be forwarded when the message is nil."); +} + +- (void)testItShouldNotForwardWhenFactoryReturnsNil { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + + [manager traceWithLocation:@"TestClass.m:210" andMessageFactory:^PNLogEntry * _Nullable{ + return nil; + }]; + + XCTAssertEqual(logger.receivedMessages.count, 0, + @"No messages should be forwarded when the factory block returns nil."); +} + + +#pragma mark - Tests :: No loggers registered + +- (void)testItShouldNotCrashWhenLoggingWithNoLoggers { + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[]]; + PNStringLogEntry *entry = [PNStringLogEntry entryWithMessage:@"no loggers"]; + + XCTAssertNoThrow([manager infoWithLocation:@"TestClass.m:220" andMessage:entry], + @"Logging with no loggers should not throw or crash."); +} + + +#pragma mark - Tests :: None log level + +- (void)testItShouldNotForwardAnyMessageWhenLogLevelIsNone { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNNoneLogLevel + andLoggers:@[logger]]; + + [manager traceWithLocation:@"T" andMessage:[PNStringLogEntry entryWithMessage:@"t"]]; + [manager debugWithLocation:@"T" andMessage:[PNStringLogEntry entryWithMessage:@"d"]]; + [manager infoWithLocation:@"T" andMessage:[PNStringLogEntry entryWithMessage:@"i"]]; + [manager warnWithLocation:@"T" andMessage:[PNStringLogEntry entryWithMessage:@"w"]]; + [manager errorWithLocation:@"T" andMessage:[PNErrorLogEntry entryWithMessage: + [NSError errorWithDomain:@"test" code:1 userInfo:nil]]]; + + XCTAssertEqual(logger.receivedMessages.count, 0, + @"No messages should be forwarded when log level is PNNoneLogLevel."); +} + + +#pragma mark - Tests :: Operation detection from path + +- (void)testItShouldDetectSubscribeOperationForSubscribePath { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/v2/subscribe/sub-key/channel/0"; + request.origin = @"ps.pndsn.com"; + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:nil]; + + [manager debugWithLocation:@"Transport" andMessage:entry]; + + XCTAssertEqual(entry.operation, PNSubscribeLogMessageOperation, + @"Operation should be detected as subscribe based on the path."); +} + +- (void)testItShouldDetectPublishOperationForPublishPath { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/publish/pub-key/sub-key/0/channel/0"; + request.origin = @"ps.pndsn.com"; + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:nil]; + + [manager debugWithLocation:@"Transport" andMessage:entry]; + + XCTAssertEqual(entry.operation, PNMessageSendLogMessageOperation, + @"Operation should be detected as message send for publish path."); +} + +- (void)testItShouldDetectSignalOperationForSignalPath { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/signal/pub-key/sub-key/0/channel/0"; + request.origin = @"ps.pndsn.com"; + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:nil]; + + [manager debugWithLocation:@"Transport" andMessage:entry]; + + XCTAssertEqual(entry.operation, PNMessageSendLogMessageOperation, + @"Operation should be detected as message send for signal path."); +} + +- (void)testItShouldDetectPresenceOperationForPresencePath { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/v2/presence/sub-key/channel"; + request.origin = @"ps.pndsn.com"; + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:nil]; + + [manager debugWithLocation:@"Transport" andMessage:entry]; + + XCTAssertEqual(entry.operation, PNPresenceLogMessageOperation, + @"Operation should be detected as presence for presence path."); +} + +- (void)testItShouldDetectHistoryOperationForV2HistoryPath { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/v2/history/sub-key/channel"; + request.origin = @"ps.pndsn.com"; + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:nil]; + + [manager debugWithLocation:@"Transport" andMessage:entry]; + + XCTAssertEqual(entry.operation, PNMessageStorageLogMessageOperation, + @"Operation should be detected as message storage for v2 history path."); +} + +- (void)testItShouldDetectHistoryOperationForV3HistoryPath { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/v3/history/sub-key/channel"; + request.origin = @"ps.pndsn.com"; + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:nil]; + + [manager debugWithLocation:@"Transport" andMessage:entry]; + + XCTAssertEqual(entry.operation, PNMessageStorageLogMessageOperation, + @"Operation should be detected as message storage for v3 history path."); +} + +- (void)testItShouldDetectMessageActionsOperationForPath { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/v1/message-actions/sub-key/channel"; + request.origin = @"ps.pndsn.com"; + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:nil]; + + [manager debugWithLocation:@"Transport" andMessage:entry]; + + XCTAssertEqual(entry.operation, PNMessageReactionsLogMessageOperation, + @"Operation should be detected as message reactions for message-actions path."); +} + +- (void)testItShouldDetectChannelGroupsOperationForPath { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/v1/channel-registration/sub-key/channel-group"; + request.origin = @"ps.pndsn.com"; + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:nil]; + + [manager debugWithLocation:@"Transport" andMessage:entry]; + + XCTAssertEqual(entry.operation, PNChannelGroupsLogMessageOperation, + @"Operation should be detected as channel groups for channel-registration path."); +} + +- (void)testItShouldDetectAppContextOperationForPath { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/v2/objects/sub-key/uuid"; + request.origin = @"ps.pndsn.com"; + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:nil]; + + [manager debugWithLocation:@"Transport" andMessage:entry]; + + XCTAssertEqual(entry.operation, PNAppContextLogMessageOperation, + @"Operation should be detected as app context for objects path."); +} + +- (void)testItShouldDetectPushOperationForV1PushPath { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/v1/push/sub-key/devices/token"; + request.origin = @"ps.pndsn.com"; + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:nil]; + + [manager debugWithLocation:@"Transport" andMessage:entry]; + + XCTAssertEqual(entry.operation, PNDevicePushNotificationsLogMessageOperation, + @"Operation should be detected as push notifications for v1 push path."); +} + +- (void)testItShouldDetectPushOperationForV2PushPath { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/v2/push/sub-key/devices/token"; + request.origin = @"ps.pndsn.com"; + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:nil]; + + [manager debugWithLocation:@"Transport" andMessage:entry]; + + XCTAssertEqual(entry.operation, PNDevicePushNotificationsLogMessageOperation, + @"Operation should be detected as push notifications for v2 push path."); +} + +- (void)testItShouldDetectFilesOperationForPath { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/v1/files/sub-key/channels/channel/files"; + request.origin = @"ps.pndsn.com"; + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:nil]; + + [manager debugWithLocation:@"Transport" andMessage:entry]; + + XCTAssertEqual(entry.operation, PNFilesLogMessageOperation, + @"Operation should be detected as files for files path."); +} + +- (void)testItShouldReturnUnknownOperationForUnrecognizedPath { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/v99/unknown/endpoint"; + request.origin = @"ps.pndsn.com"; + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:nil]; + + [manager debugWithLocation:@"Transport" andMessage:entry]; + + XCTAssertEqual(entry.operation, PNUnknownLogMessageOperation, + @"Operation should remain unknown for unrecognized paths."); +} + +- (void)testItShouldNotOverrideExplicitlySetOperation { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + + PNTransportRequest *request = [PNTransportRequest new]; + request.path = @"/v2/subscribe/sub-key/channel/0"; + request.origin = @"ps.pndsn.com"; + PNNetworkRequestLogEntry *entry = [PNNetworkRequestLogEntry entryWithMessage:request details:nil]; + entry.operation = PNMessageSendLogMessageOperation; + + [manager debugWithLocation:@"Transport" andMessage:entry]; + + XCTAssertEqual(entry.operation, PNMessageSendLogMessageOperation, + @"Explicitly set operation should not be overridden by path-based detection."); +} + + +#pragma mark - Tests :: Log level hierarchy + +- (void)testItShouldForwardAllLevelsWhenLogLevelIsTrace { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNTraceLogLevel + andLoggers:@[logger]]; + NSError *error = [NSError errorWithDomain:@"test" code:1 userInfo:nil]; + + [manager traceWithLocation:@"T" andMessage:[PNStringLogEntry entryWithMessage:@"t"]]; + [manager debugWithLocation:@"T" andMessage:[PNStringLogEntry entryWithMessage:@"d"]]; + [manager infoWithLocation:@"T" andMessage:[PNStringLogEntry entryWithMessage:@"i"]]; + [manager warnWithLocation:@"T" andMessage:[PNStringLogEntry entryWithMessage:@"w"]]; + [manager errorWithLocation:@"T" andMessage:[PNErrorLogEntry entryWithMessage:error]]; + + XCTAssertEqual(logger.receivedMessages.count, 5, + @"All 5 messages should be forwarded when log level is trace."); + XCTAssertEqual(logger.traceMessages.count, 1, @"Exactly 1 trace message should be received."); + XCTAssertEqual(logger.debugMessages.count, 1, @"Exactly 1 debug message should be received."); + XCTAssertEqual(logger.infoMessages.count, 1, @"Exactly 1 info message should be received."); + XCTAssertEqual(logger.warnMessages.count, 1, @"Exactly 1 warn message should be received."); + XCTAssertEqual(logger.errorMessages.count, 1, @"Exactly 1 error message should be received."); +} + +- (void)testItShouldOnlyForwardErrorWhenLogLevelIsError { + PNMockLogger *logger = [PNMockLogger new]; + PNLoggerManager *manager = [PNLoggerManager managerWithClientIdentifier:@"test-client" + logLevel:PNErrorLogLevel + andLoggers:@[logger]]; + NSError *error = [NSError errorWithDomain:@"test" code:1 userInfo:nil]; + + [manager traceWithLocation:@"T" andMessage:[PNStringLogEntry entryWithMessage:@"t"]]; + [manager debugWithLocation:@"T" andMessage:[PNStringLogEntry entryWithMessage:@"d"]]; + [manager infoWithLocation:@"T" andMessage:[PNStringLogEntry entryWithMessage:@"i"]]; + [manager warnWithLocation:@"T" andMessage:[PNStringLogEntry entryWithMessage:@"w"]]; + [manager errorWithLocation:@"T" andMessage:[PNErrorLogEntry entryWithMessage:error]]; + + XCTAssertEqual(logger.receivedMessages.count, 1, + @"Only 1 message should be forwarded when log level is error."); + XCTAssertEqual(logger.errorMessages.count, 1, @"Only the error message should be received."); +} + + +#pragma mark - + +@end diff --git a/Tests/Tests/Unit/Network/Reachability/PNMockTransport.h b/Tests/Tests/Unit/Network/Reachability/PNMockTransport.h new file mode 100644 index 000000000..7b437a2f0 --- /dev/null +++ b/Tests/Tests/Unit/Network/Reachability/PNMockTransport.h @@ -0,0 +1,148 @@ +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Types + +/// Describes the simulated network state of the mock transport. +typedef NS_ENUM(NSUInteger, PNMockTransportNetworkState) { + /// Transport is connected and will deliver responses normally. + PNMockTransportConnected, + + /// Transport is disconnected and will return network errors for all requests. + PNMockTransportDisconnected, + + /// Transport will simulate request timeouts. + PNMockTransportTimingOut +}; + + +#pragma mark - Structures + +/// A recorded request entry captured by the mock transport. +@interface PNMockTransportRequestRecord : NSObject + +/// The transport request that was sent. +@property(strong, nonatomic, readonly) PNTransportRequest *request; + +/// Timestamp when the request was recorded. +@property(strong, nonatomic, readonly) NSDate *timestamp; + +/// Initialise a request record. +/// +/// - Parameter request: The transport request to record. +/// - Returns: Initialised request record instance. +- (instancetype)initWithRequest:(PNTransportRequest *)request; + +@end + + +/// A pre-configured response that the mock transport should return. +@interface PNMockTransportResponse : NSObject + +/// HTTP status code to return. +@property(assign, nonatomic) NSUInteger statusCode; + +/// Response body data. +@property(strong, nullable, nonatomic) NSData *body; + +/// Response headers (keys will be lowercased by the mock response object). +@property(strong, nullable, nonatomic) NSDictionary *headers; + +/// Simulated delay before delivering the response (in seconds). +@property(assign, nonatomic) NSTimeInterval delay; + +/// Transport error to return instead of a response (simulates connection failure). +/// +/// When set, `statusCode`, `body`, and `headers` are ignored and this error is delivered. +@property(strong, nullable, nonatomic) NSError *error; + +/// Create a successful response with the given status code and optional body. +/// +/// - Parameters: +/// - statusCode: HTTP status code. +/// - body: Optional response body data. +/// - Returns: Configured mock response. ++ (instancetype)responseWithStatusCode:(NSUInteger)statusCode body:(nullable NSData *)body; + +/// Create a successful JSON response with the given status code and dictionary payload. +/// +/// - Parameters: +/// - statusCode: HTTP status code. +/// - json: Dictionary to serialise as JSON for the response body. +/// - Returns: Configured mock response with `application/json` content type. ++ (instancetype)responseWithStatusCode:(NSUInteger)statusCode json:(nullable NSDictionary *)json; + +/// Create an error response that simulates a transport-level failure. +/// +/// - Parameter error: The `NSError` to deliver. +/// - Returns: Configured mock response that delivers an error. ++ (instancetype)responseWithError:(NSError *)error; + +@end + + +#pragma mark - Interface declaration + +/// Mock transport for testing SDK behaviour under network failure conditions. +/// +/// This transport implements the `PNTransport` protocol and can be configured to simulate various network conditions +/// including connectivity loss, timeouts, HTTP errors, and delayed responses. All requests are recorded for +/// verification in tests. +/// +/// - Since: Test support (not shipped in SDK) +@interface PNMockTransport : NSObject + + +#pragma mark - Properties + +/// Current simulated network state. +@property(assign, nonatomic) PNMockTransportNetworkState networkState; + +/// List of all requests that have been sent through this transport. +/// +/// Includes requests from retry attempts. Access is thread-safe via copy. +@property(copy, nonatomic, readonly) NSArray *recordedRequests; + +/// Number of requests recorded (convenience accessor). +@property(assign, nonatomic, readonly) NSUInteger recordedRequestCount; + + +#pragma mark - Configuration + +/// Enqueue a response to be returned for the next matching request. +/// +/// Responses are consumed in FIFO order. When the queue is empty, the transport falls back to the default response +/// (200 OK with empty body) or returns a network error if `networkState` is not `PNMockTransportConnected`. +/// +/// - Parameter response: The mock response to enqueue. +- (void)enqueueResponse:(PNMockTransportResponse *)response; + +/// Enqueue multiple responses to be returned in order. +/// +/// - Parameter responses: Array of mock responses to enqueue. +- (void)enqueueResponses:(NSArray *)responses; + +/// Set a default response that is returned when the response queue is empty and the transport is connected. +/// +/// - Parameter response: The default mock response (nil resets to built-in 200 OK). +- (void)setDefaultResponse:(nullable PNMockTransportResponse *)response; + +/// Remove all enqueued responses and reset to default state. +- (void)resetResponses; + +/// Remove all recorded requests. +- (void)resetRecordedRequests; + +/// Fully reset the transport: clear responses, recorded requests, and set state to connected. +- (void)reset; + + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END diff --git a/Tests/Tests/Unit/Network/Reachability/PNMockTransport.m b/Tests/Tests/Unit/Network/Reachability/PNMockTransport.m new file mode 100644 index 000000000..042365e82 --- /dev/null +++ b/Tests/Tests/Unit/Network/Reachability/PNMockTransport.m @@ -0,0 +1,410 @@ +#import "PNMockTransport.h" +#import "PNTransportRequest+Private.h" +#import + + +#pragma mark - PNMockTransportRequestRecord implementation + +@implementation PNMockTransportRequestRecord + +- (instancetype)initWithRequest:(PNTransportRequest *)request { + if ((self = [super init])) { + _request = request; + _timestamp = [NSDate date]; + } + return self; +} + +@end + + +#pragma mark - PNMockTransportResponse implementation + +@interface PNMockTransportResponse () + +/// Internal mutable headers storage. +@property(strong, nonatomic) NSMutableDictionary *mutableHeaders; + +@end + + +@implementation PNMockTransportResponse + +- (instancetype)init { + if ((self = [super init])) { + _statusCode = 200; + _mutableHeaders = [NSMutableDictionary new]; + } + return self; +} + +- (NSDictionary *)headers { + return [self.mutableHeaders copy]; +} + +- (void)setHeaders:(NSDictionary *)headers { + [self.mutableHeaders removeAllObjects]; + [headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop) { + self.mutableHeaders[key.lowercaseString] = value; + }]; +} + ++ (instancetype)responseWithStatusCode:(NSUInteger)statusCode body:(NSData *)body { + PNMockTransportResponse *response = [[self alloc] init]; + response.statusCode = statusCode; + response.body = body; + return response; +} + ++ (instancetype)responseWithStatusCode:(NSUInteger)statusCode json:(NSDictionary *)json { + PNMockTransportResponse *response = [[self alloc] init]; + response.statusCode = statusCode; + response.mutableHeaders[@"content-type"] = @"application/json"; + + if (json) { + response.body = [NSJSONSerialization dataWithJSONObject:json options:(NSJSONWritingOptions)0 error:nil]; + } + + return response; +} + ++ (instancetype)responseWithError:(NSError *)error { + PNMockTransportResponse *response = [[self alloc] init]; + response.error = error; + return response; +} + +@end + + +#pragma mark - PNMockTransportInternalResponse + +/// Internal response object that conforms to PNTransportResponse protocol. +@interface PNMockTransportInternalResponse : NSObject + +@property(strong, nullable, nonatomic) NSDictionary *headers; +@property(strong, nullable, nonatomic) NSData *body; +@property(assign, nonatomic) NSUInteger statusCode; +@property(strong, nonatomic) NSString *url; +@property(strong, nullable, nonatomic) NSString *MIMEType; + ++ (instancetype)responseFromMockResponse:(PNMockTransportResponse *)mockResponse requestURL:(NSString *)url; + +@end + + +@implementation PNMockTransportInternalResponse + +- (NSInputStream *)bodyStream { + return nil; +} + +- (BOOL)bodyStreamAvailable { + return NO; +} + ++ (instancetype)responseFromMockResponse:(PNMockTransportResponse *)mockResponse requestURL:(NSString *)url { + PNMockTransportInternalResponse *response = [[self alloc] init]; + response.statusCode = mockResponse.statusCode; + response.headers = mockResponse.headers; + response.body = mockResponse.body; + response.url = url ?: @""; + response.MIMEType = mockResponse.headers[@"content-type"]; + return response; +} + +@end + + +#pragma mark - PNMockTransport private interface + +@interface PNMockTransport () + +/// Queue of responses to return in FIFO order. +@property(strong, nonatomic) NSMutableArray *responseQueue; + +/// Default response when queue is empty. +@property(strong, nullable, nonatomic) PNMockTransportResponse *defaultMockResponse; + +/// Mutable array of recorded requests. +@property(strong, nonatomic) NSMutableArray *mutableRecordedRequests; + +/// Serial queue for thread-safe access to transport state. +@property(strong, nonatomic) dispatch_queue_t syncQueue; + +/// Active transport requests. +@property(strong, nonatomic) NSMutableArray *activeRequests; + +/// Transport configuration (stored from setupWithConfiguration:). +@property(strong, nullable, nonatomic) PNTransportConfiguration *configuration; + +/// Whether the transport has been invalidated. +@property(assign, nonatomic) BOOL invalidated; + +/// Whether the transport is suspended. +@property(assign, nonatomic) BOOL suspended; + +@end + + +#pragma mark - PNMockTransport implementation + +@implementation PNMockTransport + + +#pragma mark - Initialisation + +- (instancetype)init { + if ((self = [super init])) { + _networkState = PNMockTransportConnected; + _responseQueue = [NSMutableArray new]; + _mutableRecordedRequests = [NSMutableArray new]; + _activeRequests = [NSMutableArray new]; + _syncQueue = dispatch_queue_create("com.pubnub.test.mock-transport", DISPATCH_QUEUE_SERIAL); + } + return self; +} + + +#pragma mark - Properties + +- (NSArray *)recordedRequests { + __block NSArray *copy; + dispatch_sync(self.syncQueue, ^{ + copy = [self.mutableRecordedRequests copy]; + }); + return copy; +} + +- (NSUInteger)recordedRequestCount { + __block NSUInteger count; + dispatch_sync(self.syncQueue, ^{ + count = self.mutableRecordedRequests.count; + }); + return count; +} + + +#pragma mark - Configuration + +- (void)enqueueResponse:(PNMockTransportResponse *)response { + dispatch_sync(self.syncQueue, ^{ + [self.responseQueue addObject:response]; + }); +} + +- (void)enqueueResponses:(NSArray *)responses { + dispatch_sync(self.syncQueue, ^{ + [self.responseQueue addObjectsFromArray:responses]; + }); +} + +- (void)setDefaultResponse:(PNMockTransportResponse *)response { + dispatch_sync(self.syncQueue, ^{ + self.defaultMockResponse = response; + }); +} + +- (void)resetResponses { + dispatch_sync(self.syncQueue, ^{ + [self.responseQueue removeAllObjects]; + self.defaultMockResponse = nil; + }); +} + +- (void)resetRecordedRequests { + dispatch_sync(self.syncQueue, ^{ + [self.mutableRecordedRequests removeAllObjects]; + }); +} + +- (void)reset { + dispatch_sync(self.syncQueue, ^{ + [self.responseQueue removeAllObjects]; + [self.mutableRecordedRequests removeAllObjects]; + [self.activeRequests removeAllObjects]; + self.defaultMockResponse = nil; + self.networkState = PNMockTransportConnected; + self.invalidated = NO; + self.suspended = NO; + }); +} + + +#pragma mark - PNTransport :: Initialization and Configuration + +- (void)setupWithConfiguration:(PNTransportConfiguration *)configuration { + dispatch_sync(self.syncQueue, ^{ + self.configuration = configuration; + }); +} + + +#pragma mark - PNTransport :: Information + +- (void)requestsWithBlock:(void (^)(NSArray *))block { + if (!block) return; + + dispatch_sync(self.syncQueue, ^{ + block(self.activeRequests); + }); +} + + +#pragma mark - PNTransport :: Request processing + +- (void)sendRequest:(PNTransportRequest *)request withCompletionBlock:(PNRequestCompletionBlock)block { + block = [block copy]; + + dispatch_sync(self.syncQueue, ^{ + [self.mutableRecordedRequests addObject:[[PNMockTransportRequestRecord alloc] initWithRequest:request]]; + + if (request.cancellable) { + [self.activeRequests addObject:request]; + } + }); + + __block PNMockTransportResponse *mockResponse = nil; + __block PNMockTransportNetworkState state; + + dispatch_sync(self.syncQueue, ^{ + state = self.networkState; + + if (state == PNMockTransportConnected) { + if (self.responseQueue.count > 0) { + mockResponse = self.responseQueue.firstObject; + [self.responseQueue removeObjectAtIndex:0]; + } else { + mockResponse = self.defaultMockResponse ?: [PNMockTransportResponse responseWithStatusCode:200 body:nil]; + } + } + }); + + dispatch_block_t deliveryBlock = ^{ + dispatch_sync(self.syncQueue, ^{ + [self.activeRequests removeObject:request]; + }); + + if (state == PNMockTransportDisconnected) { + NSError *networkError = [NSError errorWithDomain:NSURLErrorDomain + code:NSURLErrorNotConnectedToInternet + userInfo:@{ + NSLocalizedDescriptionKey: @"The Internet connection appears to be offline." + }]; + PNError *error = [PNError errorWithDomain:PNTransportErrorDomain + code:PNTransportErrorNetworkIssues + userInfo:@{ + NSLocalizedDescriptionKey: @"Network issues.", + NSLocalizedFailureReasonErrorKey: @"Request processing failed because of network issues.", + NSUnderlyingErrorKey: networkError + }]; + block(request, nil, error); + return; + } + + if (state == PNMockTransportTimingOut) { + NSError *timeoutError = [NSError errorWithDomain:NSURLErrorDomain + code:NSURLErrorTimedOut + userInfo:@{ + NSLocalizedDescriptionKey: @"The request timed out." + }]; + PNError *error = [PNError errorWithDomain:PNTransportErrorDomain + code:PNTransportErrorRequestTimeout + userInfo:@{ + NSLocalizedDescriptionKey: @"The request time out.", + NSLocalizedFailureReasonErrorKey: @"The server didn't respond with data in time.", + NSUnderlyingErrorKey: timeoutError + }]; + block(request, nil, error); + return; + } + + // Connected state: deliver the mock response. + if (mockResponse.error) { + PNError *error = [PNError errorWithDomain:PNTransportErrorDomain + code:PNTransportErrorNetworkIssues + userInfo:@{ + NSLocalizedDescriptionKey: @"Network issues.", + NSLocalizedFailureReasonErrorKey: @"Request processing failed because of network issues.", + NSUnderlyingErrorKey: mockResponse.error + }]; + block(request, nil, error); + return; + } + + NSString *requestURL = [self urlStringFromRequest:request]; + id response = [PNMockTransportInternalResponse responseFromMockResponse:mockResponse + requestURL:requestURL]; + block(request, response, nil); + }; + + NSTimeInterval delay = mockResponse.delay; + if (delay > 0) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + deliveryBlock); + } else { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), deliveryBlock); + } +} + +- (void)sendDownloadRequest:(PNTransportRequest *)request + withCompletionBlock:(PNDownloadRequestCompletionBlock)block { + block = [block copy]; + + // Reuse the same response resolution logic via sendRequest, then adapt the callback. + [self sendRequest:request withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + block(req, response, nil, error); + }]; +} + +- (PNTransportRequest *)transportRequestFromTransportRequest:(PNTransportRequest *)request { + // Return the request as-is; no transformations needed for mock transport. + return request; +} + + +#pragma mark - PNTransport :: State + +- (void)suspend { + dispatch_sync(self.syncQueue, ^{ + self.suspended = YES; + }); +} + +- (void)resume { + dispatch_sync(self.syncQueue, ^{ + self.suspended = NO; + }); +} + +- (void)invalidate { + dispatch_sync(self.syncQueue, ^{ + self.invalidated = YES; + [self.activeRequests removeAllObjects]; + }); +} + + +#pragma mark - Helpers + +- (NSString *)urlStringFromRequest:(PNTransportRequest *)request { + NSMutableArray *queryPairs = [NSMutableArray new]; + [request.query enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + [queryPairs addObject:[NSString stringWithFormat:@"%@=%@", key, value]]; + }]; + + NSString *queryString = queryPairs.count > 0 + ? [NSString stringWithFormat:@"?%@", [queryPairs componentsJoinedByString:@"&"]] + : @""; + NSString *scheme = request.secure ? @"https" : @"http"; + + return [NSString stringWithFormat:@"%@://%@%@%@", scheme, request.origin ?: @"", request.path ?: @"", queryString]; +} + +#pragma mark - + + +@end diff --git a/Tests/Tests/Unit/Network/Reachability/PNNetworkReachabilityTest.m b/Tests/Tests/Unit/Network/Reachability/PNNetworkReachabilityTest.m new file mode 100644 index 000000000..cfb26de1d --- /dev/null +++ b/Tests/Tests/Unit/Network/Reachability/PNNetworkReachabilityTest.m @@ -0,0 +1,960 @@ +#import +#import +#import +#import +#import "PNTransportRequest+Private.h" +#import "PNTransportConfiguration+Private.h" +#import "PNMockTransport.h" + + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark Interface declaration + +/// Network reachability and failure behaviour unit tests. +/// +/// Tests verify SDK transport layer behaviour under simulated network failure conditions using a mock +/// transport that conforms to the `PNTransport` protocol. +/// +/// - Copyright: 2010-2026 PubNub, Inc. +@interface PNNetworkReachabilityTest : XCTestCase + + +#pragma mark - Properties + +/// Mock transport instance used across test cases. +@property(strong, nonatomic) PNMockTransport *mockTransport; + + +#pragma mark - Helpers + +/// Create a transport request targeting a specific endpoint path. +/// +/// - Parameters: +/// - path: The URL path for the request. +/// - origin: The origin host (defaults to `ps.pndsn.com` if nil). +/// - Returns: Configured transport request. +- (PNTransportRequest *)transportRequestWithPath:(NSString *)path origin:(nullable NSString *)origin; + +/// Create a transport request for the subscribe endpoint. +/// +/// - Returns: Configured transport request for `/v2/subscribe/...`. +- (PNTransportRequest *)subscribeRequest; + +/// Create a transport request for the publish endpoint. +/// +/// - Returns: Configured transport request for `/publish/...`. +- (PNTransportRequest *)publishRequest; + +/// Create a retriable transport request for the publish endpoint. +/// +/// - Returns: Configured transport request with `retriable = YES`. +- (PNTransportRequest *)retriablePublishRequest; + +/// Create a non-retriable transport request. +/// +/// - Returns: Configured transport request with `retriable = NO`. +- (PNTransportRequest *)nonRetriableRequest; + +/// Create a `PNTransportConfiguration` with the given retry configuration. +/// +/// - Parameter retryConfiguration: Optional retry configuration. +/// - Returns: Configured transport configuration. +- (PNTransportConfiguration *)transportConfigurationWithRetry:(nullable PNRequestRetryConfiguration *)retryConfiguration; + + +#pragma mark - + + +@end + +NS_ASSUME_NONNULL_END + + +#pragma mark - Tests + +@implementation PNNetworkReachabilityTest + + +#pragma mark - Setup + +- (void)setUp { + [super setUp]; + self.mockTransport = [[PNMockTransport alloc] init]; +} + +- (void)tearDown { + [self.mockTransport reset]; + self.mockTransport = nil; + [super tearDown]; +} + + +#pragma mark - Tests :: Happy Path :: Connected state delivers response + +- (void)testItShouldDeliverSuccessResponseWhenConnected { + XCTestExpectation *expectation = [self expectationWithDescription:@"Request completes successfully"]; + PNTransportRequest *request = [self publishRequest]; + + NSDictionary *json = @{@"status": @200, @"message": @"OK"}; + [self.mockTransport enqueueResponse:[PNMockTransportResponse responseWithStatusCode:200 json:json]]; + + [self.mockTransport sendRequest:request withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + XCTAssertNil(error); + XCTAssertNotNil(response); + XCTAssertEqual(response.statusCode, 200); + XCTAssertNotNil(response.body); + + NSError *jsonError = nil; + NSDictionary *parsed = [NSJSONSerialization JSONObjectWithData:response.body + options:(NSJSONReadingOptions)0 + error:&jsonError]; + XCTAssertNil(jsonError); + XCTAssertEqualObjects(parsed[@"message"], @"OK"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testItShouldDeliverDefaultResponseWhenQueueIsEmpty { + XCTestExpectation *expectation = [self expectationWithDescription:@"Default response delivered"]; + PNTransportRequest *request = [self publishRequest]; + + // No enqueued responses; default is 200 OK with nil body. + [self.mockTransport sendRequest:request withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + XCTAssertNil(error); + XCTAssertNotNil(response); + XCTAssertEqual(response.statusCode, 200); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testItShouldDeliverCustomDefaultResponseWhenQueueIsEmpty { + XCTestExpectation *expectation = [self expectationWithDescription:@"Custom default response delivered"]; + PNTransportRequest *request = [self publishRequest]; + + PNMockTransportResponse *defaultResponse = [PNMockTransportResponse responseWithStatusCode:204 body:nil]; + [self.mockTransport setDefaultResponse:defaultResponse]; + + [self.mockTransport sendRequest:request withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + XCTAssertNil(error); + XCTAssertEqual(response.statusCode, 204); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + + +#pragma mark - Tests :: Error Path :: Disconnected state returns network error + +- (void)testItShouldReturnNetworkErrorWhenDisconnected { + XCTestExpectation *expectation = [self expectationWithDescription:@"Network error on disconnect"]; + PNTransportRequest *request = [self publishRequest]; + + self.mockTransport.networkState = PNMockTransportDisconnected; + + [self.mockTransport sendRequest:request withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + XCTAssertNotNil(error); + XCTAssertNil(response); + XCTAssertEqualObjects(error.domain, PNTransportErrorDomain); + XCTAssertEqual(error.code, PNTransportErrorNetworkIssues); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + + +#pragma mark - Tests :: Error Path :: Timeout state returns timeout error + +- (void)testItShouldReturnTimeoutErrorWhenTimingOut { + XCTestExpectation *expectation = [self expectationWithDescription:@"Timeout error"]; + PNTransportRequest *request = [self publishRequest]; + + self.mockTransport.networkState = PNMockTransportTimingOut; + + [self.mockTransport sendRequest:request withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + XCTAssertNotNil(error); + XCTAssertNil(response); + XCTAssertEqualObjects(error.domain, PNTransportErrorDomain); + XCTAssertEqual(error.code, PNTransportErrorRequestTimeout); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + + +#pragma mark - Tests :: Error Path :: Enqueued error response + +- (void)testItShouldReturnNetworkErrorWhenResponseHasError { + XCTestExpectation *expectation = [self expectationWithDescription:@"Enqueued error delivered"]; + PNTransportRequest *request = [self publishRequest]; + + NSError *simulatedError = [NSError errorWithDomain:NSURLErrorDomain + code:NSURLErrorNetworkConnectionLost + userInfo:nil]; + [self.mockTransport enqueueResponse:[PNMockTransportResponse responseWithError:simulatedError]]; + + [self.mockTransport sendRequest:request withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + XCTAssertNotNil(error); + XCTAssertNil(response); + XCTAssertEqualObjects(error.domain, PNTransportErrorDomain); + XCTAssertEqual(error.code, PNTransportErrorNetworkIssues); + + NSError *underlying = error.userInfo[NSUnderlyingErrorKey]; + XCTAssertNotNil(underlying); + XCTAssertEqual(underlying.code, NSURLErrorNetworkConnectionLost); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + + +#pragma mark - Tests :: Error Path :: HTTP 503 response + +- (void)testItShouldReturnHTTP503Response { + XCTestExpectation *expectation = [self expectationWithDescription:@"503 response delivered"]; + PNTransportRequest *request = [self publishRequest]; + + NSDictionary *json = @{@"error": @YES, @"message": @"Service Unavailable"}; + [self.mockTransport enqueueResponse:[PNMockTransportResponse responseWithStatusCode:503 json:json]]; + + [self.mockTransport sendRequest:request withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + XCTAssertNil(error); + XCTAssertNotNil(response); + XCTAssertEqual(response.statusCode, 503); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + + +#pragma mark - Tests :: Error Path :: HTTP 403 response + +- (void)testItShouldReturnHTTP403Response { + XCTestExpectation *expectation = [self expectationWithDescription:@"403 response delivered"]; + PNTransportRequest *request = [self publishRequest]; + + NSDictionary *json = @{@"error": @YES, @"message": @"Forbidden"}; + [self.mockTransport enqueueResponse:[PNMockTransportResponse responseWithStatusCode:403 json:json]]; + + [self.mockTransport sendRequest:request withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + XCTAssertNil(error); + XCTAssertNotNil(response); + XCTAssertEqual(response.statusCode, 403); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + + +#pragma mark - Tests :: Request Recording + +- (void)testItShouldRecordAllSentRequests { + XCTestExpectation *expectation = [self expectationWithDescription:@"Multiple requests recorded"]; + PNTransportRequest *request1 = [self publishRequest]; + PNTransportRequest *request2 = [self subscribeRequest]; + + __block NSUInteger completionCount = 0; + + void (^checkCompletion)(void) = ^{ + completionCount++; + if (completionCount == 2) { + XCTAssertEqual(self.mockTransport.recordedRequestCount, 2); + NSArray *records = self.mockTransport.recordedRequests; + XCTAssertEqualObjects(records[0].request, request1); + XCTAssertEqualObjects(records[1].request, request2); + [expectation fulfill]; + } + }; + + [self.mockTransport sendRequest:request1 withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + // Send second request after first completes to guarantee ordering. + [self.mockTransport sendRequest:request2 withCompletionBlock:^(PNTransportRequest *req2, + id response2, + PNError *error2) { + checkCompletion(); + }]; + checkCompletion(); + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testItShouldRecordRequestTimestamps { + XCTestExpectation *expectation = [self expectationWithDescription:@"Timestamp recorded"]; + PNTransportRequest *request = [self publishRequest]; + NSDate *beforeSend = [NSDate date]; + + [self.mockTransport sendRequest:request withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + NSDate *afterSend = [NSDate date]; + PNMockTransportRequestRecord *record = self.mockTransport.recordedRequests.firstObject; + XCTAssertNotNil(record.timestamp); + XCTAssertTrue([record.timestamp compare:beforeSend] != NSOrderedAscending); + XCTAssertTrue([record.timestamp compare:afterSend] != NSOrderedDescending); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testItShouldClearRecordedRequestsOnReset { + XCTestExpectation *expectation = [self expectationWithDescription:@"Records cleared"]; + PNTransportRequest *request = [self publishRequest]; + + [self.mockTransport sendRequest:request withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + XCTAssertEqual(self.mockTransport.recordedRequestCount, 1); + [self.mockTransport resetRecordedRequests]; + XCTAssertEqual(self.mockTransport.recordedRequestCount, 0); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + + +#pragma mark - Tests :: Response Queue :: FIFO ordering + +- (void)testItShouldDeliverEnqueuedResponsesInFIFOOrder { + XCTestExpectation *expectation = [self expectationWithDescription:@"FIFO order maintained"]; + + [self.mockTransport enqueueResponse:[PNMockTransportResponse responseWithStatusCode:200 body:nil]]; + [self.mockTransport enqueueResponse:[PNMockTransportResponse responseWithStatusCode:503 body:nil]]; + [self.mockTransport enqueueResponse:[PNMockTransportResponse responseWithStatusCode:429 body:nil]]; + + PNTransportRequest *request1 = [self publishRequest]; + PNTransportRequest *request2 = [self publishRequest]; + PNTransportRequest *request3 = [self publishRequest]; + + [self.mockTransport sendRequest:request1 withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + XCTAssertEqual(response.statusCode, 200); + + [self.mockTransport sendRequest:request2 withCompletionBlock:^(PNTransportRequest *req2, + id response2, + PNError *error2) { + XCTAssertEqual(response2.statusCode, 503); + + [self.mockTransport sendRequest:request3 withCompletionBlock:^(PNTransportRequest *req3, + id response3, + PNError *error3) { + XCTAssertEqual(response3.statusCode, 429); + [expectation fulfill]; + }]; + }]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + + +#pragma mark - Tests :: Response Delay + +- (void)testItShouldDelayResponseDeliveryWhenConfigured { + XCTestExpectation *expectation = [self expectationWithDescription:@"Response delayed"]; + PNTransportRequest *request = [self publishRequest]; + + PNMockTransportResponse *delayedResponse = [PNMockTransportResponse responseWithStatusCode:200 body:nil]; + delayedResponse.delay = 0.5; + [self.mockTransport enqueueResponse:delayedResponse]; + + NSDate *startTime = [NSDate date]; + + [self.mockTransport sendRequest:request withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + NSTimeInterval elapsed = [[NSDate date] timeIntervalSinceDate:startTime]; + XCTAssertGreaterThanOrEqual(elapsed, 0.4); + XCTAssertNil(error); + XCTAssertEqual(response.statusCode, 200); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + + +#pragma mark - Tests :: Recovery :: Disconnect then reconnect + +- (void)testItShouldRecoverAfterDisconnectAndReconnect { + XCTestExpectation *expectation = [self expectationWithDescription:@"Recovery after reconnect"]; + PNTransportRequest *request1 = [self publishRequest]; + PNTransportRequest *request2 = [self publishRequest]; + + // Step 1: Disconnect. + self.mockTransport.networkState = PNMockTransportDisconnected; + + [self.mockTransport sendRequest:request1 withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + XCTAssertNotNil(error); + XCTAssertEqual(error.code, PNTransportErrorNetworkIssues); + + // Step 2: Reconnect. + self.mockTransport.networkState = PNMockTransportConnected; + + NSDictionary *json = @{@"status": @200, @"message": @"Recovered"}; + [self.mockTransport enqueueResponse:[PNMockTransportResponse responseWithStatusCode:200 json:json]]; + + [self.mockTransport sendRequest:request2 withCompletionBlock:^(PNTransportRequest *req2, + id response2, + PNError *error2) { + XCTAssertNil(error2); + XCTAssertNotNil(response2); + XCTAssertEqual(response2.statusCode, 200); + [expectation fulfill]; + }]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testItShouldRecoverAfterMultipleConsecutiveFailures { + XCTestExpectation *expectation = [self expectationWithDescription:@"Recovery after N failures"]; + NSUInteger failureCount = 3; + + self.mockTransport.networkState = PNMockTransportDisconnected; + + __block NSUInteger failedRequests = 0; + __block void (^sendNextFailedRequest)(void); + + sendNextFailedRequest = ^{ + if (failedRequests >= failureCount) { + // Now reconnect and verify recovery. + self.mockTransport.networkState = PNMockTransportConnected; + [self.mockTransport enqueueResponse:[PNMockTransportResponse responseWithStatusCode:200 body:nil]]; + + PNTransportRequest *recoveryRequest = [self publishRequest]; + [self.mockTransport sendRequest:recoveryRequest + withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + XCTAssertNil(error); + XCTAssertEqual(response.statusCode, 200); + XCTAssertEqual(self.mockTransport.recordedRequestCount, failureCount + 1); + [expectation fulfill]; + }]; + return; + } + + PNTransportRequest *failedReq = [self publishRequest]; + [self.mockTransport sendRequest:failedReq + withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + XCTAssertNotNil(error); + failedRequests++; + sendNextFailedRequest(); + }]; + }; + + sendNextFailedRequest(); + + [self waitForExpectationsWithTimeout:10.0 handler:nil]; +} + + +#pragma mark - Tests :: Retry Configuration :: Linear policy does not retry HTTP 403 + +- (void)testItShouldNotRetryHTTP403WithLinearPolicy { + PNRequestRetryConfiguration *retryConfig = [PNRequestRetryConfiguration configurationWithLinearDelay]; + NSURLRequest *urlRequest = [NSURLRequest requestWithURL: + [NSURL URLWithString:@"https://ps.pndsn.com/publish/demo/demo/0/test-channel/0/message"]]; + NSURLResponse *response403 = [[NSHTTPURLResponse alloc] initWithURL:urlRequest.URL + statusCode:403 + HTTPVersion:@"1.1" + headerFields:nil]; + + NSTimeInterval delay = [retryConfig retryDelayForFailedRequest:urlRequest + withResponse:response403 + retryAttempt:1]; + XCTAssertEqual(delay, -1.f, @"HTTP 403 should not be retriable."); +} + + +#pragma mark - Tests :: Retry Configuration :: Linear policy retries HTTP 503 + +- (void)testItShouldRetryHTTP503WithLinearPolicy { + PNRequestRetryConfiguration *retryConfig = [PNRequestRetryConfiguration configurationWithLinearDelay]; + NSURLRequest *urlRequest = [NSURLRequest requestWithURL: + [NSURL URLWithString:@"https://ps.pndsn.com/publish/demo/demo/0/test-channel/0/message"]]; + NSURLResponse *response503 = [[NSHTTPURLResponse alloc] initWithURL:urlRequest.URL + statusCode:503 + HTTPVersion:@"1.1" + headerFields:nil]; + + NSTimeInterval delay = [retryConfig retryDelayForFailedRequest:urlRequest + withResponse:response503 + retryAttempt:1]; + XCTAssertGreaterThan(delay, 0.f, @"HTTP 503 should be retriable."); + // Linear policy default delay is 2.0 seconds (plus jitter up to 1.0). + XCTAssertEqualWithAccuracy(delay, 2.f, 1.f); +} + + +#pragma mark - Tests :: Retry Configuration :: Linear delay is constant + +- (void)testItShouldUseConstantDelayWithLinearPolicy { + NSTimeInterval configuredDelay = 3.5; + PNRequestRetryConfiguration *retryConfig = [PNRequestRetryConfiguration configurationWithLinearDelay:configuredDelay + maximumRetry:5 + excludedEndpoints:0]; + NSURLRequest *urlRequest = [NSURLRequest requestWithURL: + [NSURL URLWithString:@"https://ps.pndsn.com/publish/demo/demo/0/test-channel/0/message"]]; + NSURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:urlRequest.URL + statusCode:500 + HTTPVersion:@"1.1" + headerFields:nil]; + + NSTimeInterval delay1 = [retryConfig retryDelayForFailedRequest:urlRequest withResponse:response retryAttempt:1]; + NSTimeInterval delay2 = [retryConfig retryDelayForFailedRequest:urlRequest withResponse:response retryAttempt:2]; + NSTimeInterval delay3 = [retryConfig retryDelayForFailedRequest:urlRequest withResponse:response retryAttempt:3]; + + // All delays should be approximately equal (within jitter). + XCTAssertEqualWithAccuracy(delay1, configuredDelay, 1.f); + XCTAssertEqualWithAccuracy(delay2, configuredDelay, 1.f); + XCTAssertEqualWithAccuracy(delay3, configuredDelay, 1.f); +} + + +#pragma mark - Tests :: Retry Configuration :: Exponential delay increases + +- (void)testItShouldIncreaseDelayWithExponentialPolicy { + PNRequestRetryConfiguration *retryConfig = [PNRequestRetryConfiguration configurationWithExponentialDelay:2.f + maximumDelay:150.f + maximumRetry:6 + excludedEndpoints:0]; + NSURLRequest *urlRequest = [NSURLRequest requestWithURL: + [NSURL URLWithString:@"https://ps.pndsn.com/publish/demo/demo/0/test-channel/0/message"]]; + NSURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:urlRequest.URL + statusCode:500 + HTTPVersion:@"1.1" + headerFields:nil]; + + // Expected delays (before jitter): 2*2^0=2, 2*2^1=4, 2*2^2=8, 2*2^3=16 + NSTimeInterval delay1 = [retryConfig retryDelayForFailedRequest:urlRequest withResponse:response retryAttempt:1]; + NSTimeInterval delay2 = [retryConfig retryDelayForFailedRequest:urlRequest withResponse:response retryAttempt:2]; + NSTimeInterval delay3 = [retryConfig retryDelayForFailedRequest:urlRequest withResponse:response retryAttempt:3]; + NSTimeInterval delay4 = [retryConfig retryDelayForFailedRequest:urlRequest withResponse:response retryAttempt:4]; + + // Each subsequent delay should be approximately double (within jitter tolerance). + XCTAssertEqualWithAccuracy(delay1, 2.f, 1.f); + XCTAssertEqualWithAccuracy(delay2, 4.f, 1.f); + XCTAssertEqualWithAccuracy(delay3, 8.f, 1.5f); + XCTAssertEqualWithAccuracy(delay4, 16.f, 1.5f); + + // Verify ordering (delays increase, accounting for jitter by comparing base values). + XCTAssertLessThan(delay1, delay3, @"Exponential delay should increase over attempts."); +} + + +#pragma mark - Tests :: Retry Configuration :: Exponential delay respects maximum + +- (void)testItShouldCapExponentialDelayAtMaximum { + PNRequestRetryConfiguration *retryConfig = [PNRequestRetryConfiguration configurationWithExponentialDelay:2.f + maximumDelay:10.f + maximumRetry:10 + excludedEndpoints:0]; + NSURLRequest *urlRequest = [NSURLRequest requestWithURL: + [NSURL URLWithString:@"https://ps.pndsn.com/publish/demo/demo/0/test-channel/0/message"]]; + NSURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:urlRequest.URL + statusCode:500 + HTTPVersion:@"1.1" + headerFields:nil]; + + // At attempt 5: 2*2^4 = 32 which exceeds max of 10, so should be capped. + NSTimeInterval delay = [retryConfig retryDelayForFailedRequest:urlRequest withResponse:response retryAttempt:5]; + XCTAssertEqualWithAccuracy(delay, 10.f, 1.f, @"Delay should be capped at maximum."); +} + + +#pragma mark - Tests :: Retry Configuration :: Maximum retry count respected + +- (void)testItShouldStopRetryingAfterMaximumAttempts { + PNRequestRetryConfiguration *retryConfig = [PNRequestRetryConfiguration configurationWithLinearDelay:2.f + maximumRetry:3 + excludedEndpoints:0]; + NSURLRequest *urlRequest = [NSURLRequest requestWithURL: + [NSURL URLWithString:@"https://ps.pndsn.com/publish/demo/demo/0/test-channel/0/message"]]; + NSURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:urlRequest.URL + statusCode:500 + HTTPVersion:@"1.1" + headerFields:nil]; + + // Attempts 1-3 should be retriable. + XCTAssertGreaterThan([retryConfig retryDelayForFailedRequest:urlRequest withResponse:response retryAttempt:1], 0.f); + XCTAssertGreaterThan([retryConfig retryDelayForFailedRequest:urlRequest withResponse:response retryAttempt:2], 0.f); + XCTAssertGreaterThan([retryConfig retryDelayForFailedRequest:urlRequest withResponse:response retryAttempt:3], 0.f); + + // Attempt 4 should exceed maximum. + XCTAssertEqual([retryConfig retryDelayForFailedRequest:urlRequest withResponse:response retryAttempt:4], -1.f, + @"Should return -1 (no retry) after exceeding maximum retry attempts."); +} + + +#pragma mark - Tests :: Retry Configuration :: HTTP 429 with Retry-After header + +- (void)testItShouldUseRetryAfterHeaderValueForHTTP429 { + PNRequestRetryConfiguration *retryConfig = [PNRequestRetryConfiguration configurationWithLinearDelay:2.f + maximumRetry:5 + excludedEndpoints:0]; + NSURLRequest *urlRequest = [NSURLRequest requestWithURL: + [NSURL URLWithString:@"https://ps.pndsn.com/publish/demo/demo/0/test-channel/0/message"]]; + NSURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:urlRequest.URL + statusCode:429 + HTTPVersion:@"1.1" + headerFields:@{@"Retry-After": @"10"}]; + + NSTimeInterval delay = [retryConfig retryDelayForFailedRequest:urlRequest withResponse:response retryAttempt:1]; + XCTAssertEqualWithAccuracy(delay, 10.f, 1.f, @"Should use Retry-After header value."); +} + +- (void)testItShouldUseConfiguredDelayForHTTP429WithoutRetryAfterHeader { + PNRequestRetryConfiguration *retryConfig = [PNRequestRetryConfiguration configurationWithLinearDelay:4.f + maximumRetry:5 + excludedEndpoints:0]; + NSURLRequest *urlRequest = [NSURLRequest requestWithURL: + [NSURL URLWithString:@"https://ps.pndsn.com/publish/demo/demo/0/test-channel/0/message"]]; + NSURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:urlRequest.URL + statusCode:429 + HTTPVersion:@"1.1" + headerFields:nil]; + + NSTimeInterval delay = [retryConfig retryDelayForFailedRequest:urlRequest withResponse:response retryAttempt:1]; + XCTAssertEqualWithAccuracy(delay, 4.f, 1.f, @"Should use configured delay when Retry-After is absent."); +} + + +#pragma mark - Tests :: Retry Configuration :: Excluded endpoints + +- (void)testItShouldNotRetryExcludedEndpoint { + PNRequestRetryConfiguration *retryConfig = [PNRequestRetryConfiguration configurationWithLinearDelay:2.f + maximumRetry:5 + excludedEndpoints:PNMessageSendEndpoint, 0]; + // Publish endpoint is in the excluded list. + NSURLRequest *publishReq = [NSURLRequest requestWithURL: + [NSURL URLWithString:@"https://ps.pndsn.com/publish/demo/demo/0/test-channel/0/message"]]; + NSURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:publishReq.URL + statusCode:500 + HTTPVersion:@"1.1" + headerFields:nil]; + + XCTAssertEqual([retryConfig retryDelayForFailedRequest:publishReq withResponse:response retryAttempt:1], -1.f, + @"Excluded endpoint should not be retried."); + + // Subscribe endpoint is NOT excluded. + NSURLRequest *subscribeReq = [NSURLRequest requestWithURL: + [NSURL URLWithString:@"https://ps.pndsn.com/v2/subscribe/demo/test-channel/0"]]; + NSURLResponse *subResponse = [[NSHTTPURLResponse alloc] initWithURL:subscribeReq.URL + statusCode:500 + HTTPVersion:@"1.1" + headerFields:nil]; + + XCTAssertGreaterThan([retryConfig retryDelayForFailedRequest:subscribeReq withResponse:subResponse retryAttempt:1], + 0.f, @"Non-excluded endpoint should be retried."); +} + + +#pragma mark - Tests :: Transport State :: Suspend and Resume + +- (void)testItShouldTrackSuspendAndResumeState { + [self.mockTransport suspend]; + // Verify transport is suspended (via internal state check through a request). + // The mock transport records requests even when suspended. + XCTestExpectation *expectation = [self expectationWithDescription:@"Request while suspended"]; + PNTransportRequest *request = [self publishRequest]; + + [self.mockTransport sendRequest:request withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + XCTAssertEqual(self.mockTransport.recordedRequestCount, 1); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + + [self.mockTransport resume]; +} + + +#pragma mark - Tests :: Transport State :: Invalidate + +- (void)testItShouldClearActiveRequestsOnInvalidate { + XCTestExpectation *expectation = [self expectationWithDescription:@"Invalidation clears active requests"]; + PNTransportRequest *request = [self publishRequest]; + request.cancellable = YES; + + [self.mockTransport sendRequest:request withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + [self.mockTransport invalidate]; + + [self.mockTransport requestsWithBlock:^(NSArray *requests) { + XCTAssertEqual(requests.count, 0, @"Active requests should be cleared after invalidation."); + }]; + + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + + +#pragma mark - Tests :: Download Request + +- (void)testItShouldHandleDownloadRequestSuccessfully { + XCTestExpectation *expectation = [self expectationWithDescription:@"Download request succeeds"]; + PNTransportRequest *request = [self publishRequest]; + request.responseAsFile = YES; + + NSData *fileData = [@"file content" dataUsingEncoding:NSUTF8StringEncoding]; + [self.mockTransport enqueueResponse:[PNMockTransportResponse responseWithStatusCode:200 body:fileData]]; + + [self.mockTransport sendDownloadRequest:request + withCompletionBlock:^(PNTransportRequest *req, + id response, + NSURL *path, + PNError *error) { + XCTAssertNil(error); + XCTAssertNotNil(response); + XCTAssertEqual(response.statusCode, 200); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testItShouldHandleDownloadRequestNetworkError { + XCTestExpectation *expectation = [self expectationWithDescription:@"Download request fails"]; + PNTransportRequest *request = [self publishRequest]; + request.responseAsFile = YES; + + self.mockTransport.networkState = PNMockTransportDisconnected; + + [self.mockTransport sendDownloadRequest:request + withCompletionBlock:^(PNTransportRequest *req, + id response, + NSURL *path, + PNError *error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, PNTransportErrorDomain); + XCTAssertEqual(error.code, PNTransportErrorNetworkIssues); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + + +#pragma mark - Tests :: Response Queue Exhaustion + +- (void)testItShouldFallBackToDefaultAfterQueueExhausted { + XCTestExpectation *expectation = [self expectationWithDescription:@"Fallback to default"]; + + [self.mockTransport enqueueResponse:[PNMockTransportResponse responseWithStatusCode:503 body:nil]]; + [self.mockTransport setDefaultResponse:[PNMockTransportResponse responseWithStatusCode:200 body:nil]]; + + PNTransportRequest *request1 = [self publishRequest]; + PNTransportRequest *request2 = [self publishRequest]; + + [self.mockTransport sendRequest:request1 withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + XCTAssertEqual(response.statusCode, 503, @"First request should use enqueued response."); + + [self.mockTransport sendRequest:request2 withCompletionBlock:^(PNTransportRequest *req2, + id response2, + PNError *error2) { + XCTAssertEqual(response2.statusCode, 200, @"Second request should use default response."); + [expectation fulfill]; + }]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + + +#pragma mark - Tests :: Full Reset + +- (void)testItShouldFullyResetTransportState { + self.mockTransport.networkState = PNMockTransportDisconnected; + [self.mockTransport enqueueResponse:[PNMockTransportResponse responseWithStatusCode:503 body:nil]]; + + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Request before reset"]; + PNTransportRequest *req1 = [self publishRequest]; + [self.mockTransport sendRequest:req1 withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + XCTAssertNotNil(error); + [expectation1 fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + + [self.mockTransport reset]; + + // After reset: connected state, no enqueued responses, no recorded requests. + XCTAssertEqual(self.mockTransport.networkState, PNMockTransportConnected); + XCTAssertEqual(self.mockTransport.recordedRequestCount, 0); + + XCTestExpectation *expectation2 = [self expectationWithDescription:@"Request after reset"]; + PNTransportRequest *req2 = [self publishRequest]; + [self.mockTransport sendRequest:req2 withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + XCTAssertNil(error); + XCTAssertEqual(response.statusCode, 200); + [expectation2 fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + + +#pragma mark - Tests :: Response Headers + +- (void)testItShouldDeliverResponseWithHeaders { + XCTestExpectation *expectation = [self expectationWithDescription:@"Headers delivered"]; + PNTransportRequest *request = [self publishRequest]; + + PNMockTransportResponse *mockResp = [PNMockTransportResponse responseWithStatusCode:200 body:nil]; + mockResp.headers = @{@"X-Custom-Header": @"test-value", @"Content-Type": @"text/plain"}; + [self.mockTransport enqueueResponse:mockResp]; + + [self.mockTransport sendRequest:request withCompletionBlock:^(PNTransportRequest *req, + id response, + PNError *error) { + XCTAssertNil(error); + XCTAssertEqualObjects(response.headers[@"x-custom-header"], @"test-value"); + XCTAssertEqualObjects(response.headers[@"content-type"], @"text/plain"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + + +#pragma mark - Tests :: Transport Request Passthrough + +- (void)testItShouldReturnRequestUnmodifiedFromTransportRequestMethod { + PNTransportRequest *request = [self publishRequest]; + PNTransportRequest *result = [self.mockTransport transportRequestFromTransportRequest:request]; + XCTAssertEqualObjects(request, result, @"Mock transport should return request as-is."); +} + + +#pragma mark - Tests :: Retry Configuration :: Non-retriable status codes + +- (void)testItShouldNotRetryNon5xxStatusCodes { + PNRequestRetryConfiguration *retryConfig = [PNRequestRetryConfiguration configurationWithLinearDelay]; + NSURLRequest *urlRequest = [NSURLRequest requestWithURL: + [NSURL URLWithString:@"https://ps.pndsn.com/publish/demo/demo/0/test-channel/0/message"]]; + + // Test various non-retriable status codes. + NSArray *nonRetriableCodes = @[@(200), @(201), @(204), @(301), @(302), @(403)]; + + for (NSNumber *code in nonRetriableCodes) { + NSURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:urlRequest.URL + statusCode:code.integerValue + HTTPVersion:@"1.1" + headerFields:nil]; + NSTimeInterval delay = [retryConfig retryDelayForFailedRequest:urlRequest + withResponse:response + retryAttempt:1]; + XCTAssertEqual(delay, -1.f, @"HTTP %@ should not be retriable.", code); + } +} + +- (void)testItShouldRetryRetriableStatusCodes { + PNRequestRetryConfiguration *retryConfig = [PNRequestRetryConfiguration configurationWithLinearDelay]; + NSURLRequest *urlRequest = [NSURLRequest requestWithURL: + [NSURL URLWithString:@"https://ps.pndsn.com/publish/demo/demo/0/test-channel/0/message"]]; + + NSArray *retriableCodes = @[@(429), @(500), @(502), @(503), @(504)]; + + for (NSNumber *code in retriableCodes) { + NSURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:urlRequest.URL + statusCode:code.integerValue + HTTPVersion:@"1.1" + headerFields:nil]; + NSTimeInterval delay = [retryConfig retryDelayForFailedRequest:urlRequest + withResponse:response + retryAttempt:1]; + XCTAssertGreaterThan(delay, 0.f, @"HTTP %@ should be retriable.", code); + } +} + + +#pragma mark - Helpers + +- (PNTransportRequest *)transportRequestWithPath:(NSString *)path origin:(NSString *)origin { + PNTransportRequest *request = [[PNTransportRequest alloc] init]; + request.origin = origin ?: @"ps.pndsn.com"; + request.path = path; + request.method = TransportGETMethod; + request.timeout = 10.0; + request.secure = YES; + request.retriable = YES; + return request; +} + +- (PNTransportRequest *)subscribeRequest { + return [self transportRequestWithPath:@"/v2/subscribe/demo/test-channel/0" origin:nil]; +} + +- (PNTransportRequest *)publishRequest { + return [self transportRequestWithPath:@"/publish/demo/demo/0/test-channel/0/message" origin:nil]; +} + +- (PNTransportRequest *)retriablePublishRequest { + PNTransportRequest *request = [self publishRequest]; + request.retriable = YES; + return request; +} + +- (PNTransportRequest *)nonRetriableRequest { + PNTransportRequest *request = [self publishRequest]; + request.retriable = NO; + return request; +} + +- (PNTransportConfiguration *)transportConfigurationWithRetry:(PNRequestRetryConfiguration *)retryConfiguration { + PNTransportConfiguration *config = [[PNTransportConfiguration alloc] init]; + config.retryConfiguration = retryConfiguration; + config.maximumConnections = 2; + return config; +} + +#pragma mark - + + +@end From 8581fc8aed11a83c46e784bb6a08d992ad0c1bee Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Tue, 3 Mar 2026 11:07:40 +0200 Subject: [PATCH 2/3] test(fixtures): fix fixtures causing test process to fail on exit --- ...annelWithActionsAndReceiveResultWithExpectedOperation.json | 4 ++-- ...udeCustomMessageTypeAndIncludeMessageActionsFlagIsSet.json | 4 ++-- ...edMessagesAndFailToDecryptWhenDifferentCipherKeyIsSet.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/Support Files/Fixtures/PNHistoryIntegrationTests.bundle/ItShouldFetchHistoryForChannelWithActionsAndReceiveResultWithExpectedOperation.json b/Tests/Support Files/Fixtures/PNHistoryIntegrationTests.bundle/ItShouldFetchHistoryForChannelWithActionsAndReceiveResultWithExpectedOperation.json index 895b227c1..cbe0dc69c 100644 --- a/Tests/Support Files/Fixtures/PNHistoryIntegrationTests.bundle/ItShouldFetchHistoryForChannelWithActionsAndReceiveResultWithExpectedOperation.json +++ b/Tests/Support Files/Fixtures/PNHistoryIntegrationTests.bundle/ItShouldFetchHistoryForChannelWithActionsAndReceiveResultWithExpectedOperation.json @@ -598,7 +598,7 @@ }, "pipeline" : false, "network" : 0, - "url" : "https:\/\/ps.pndsn.com\/v3\/history-with-actions\/sub-key\/demo\/channel\/test-channel?include_uuid=1&pnsdk=PubNub-ObjC-iOS\/4.x.x&uuid=Serhii&include_custom_message_type=0&include_message_type=1" + "url" : "https:\/\/ps.pndsn.com\/v3\/history-with-actions\/sub-key\/demo\/channel\/test-channel?include_uuid=1&pnsdk=PubNub-ObjC-iOS\/4.x.x&uuid=Serhii&max=25&include_custom_message_type=0&include_message_type=1" }, "type" : 0 }, @@ -607,7 +607,7 @@ "data" : { "status" : 200, "cls" : "NSHTTPURLResponse", - "url" : "https:\/\/ps.pndsn.com\/v3\/history-with-actions\/sub-key\/demo\/channel\/test-channel?include_uuid=1&pnsdk=PubNub-ObjC-iOS\/4.x.x&uuid=Serhii&include_custom_message_type=0&include_message_type=1", + "url" : "https:\/\/ps.pndsn.com\/v3\/history-with-actions\/sub-key\/demo\/channel\/test-channel?include_uuid=1&pnsdk=PubNub-ObjC-iOS\/4.x.x&uuid=Serhii&max=25&include_custom_message_type=0&include_message_type=1", "headers" : { "Access-Control-Allow-Credentials" : "true", "Content-Type" : "text\/javascript; charset=\"UTF-8\"", diff --git a/Tests/Support Files/Fixtures/PNHistoryIntegrationTests.bundle/ItShouldFetchHistoryForChannelWithCustomMessageTypeWhenIncludeCustomMessageTypeAndIncludeMessageActionsFlagIsSet.json b/Tests/Support Files/Fixtures/PNHistoryIntegrationTests.bundle/ItShouldFetchHistoryForChannelWithCustomMessageTypeWhenIncludeCustomMessageTypeAndIncludeMessageActionsFlagIsSet.json index e073571db..44649106d 100644 --- a/Tests/Support Files/Fixtures/PNHistoryIntegrationTests.bundle/ItShouldFetchHistoryForChannelWithCustomMessageTypeWhenIncludeCustomMessageTypeAndIncludeMessageActionsFlagIsSet.json +++ b/Tests/Support Files/Fixtures/PNHistoryIntegrationTests.bundle/ItShouldFetchHistoryForChannelWithCustomMessageTypeWhenIncludeCustomMessageTypeAndIncludeMessageActionsFlagIsSet.json @@ -1184,7 +1184,7 @@ }, "pipeline" : false, "network" : 0, - "url" : "https:\/\/ps.pndsn.com\/v3\/history-with-actions\/sub-key\/demo\/channel\/test-channel?include_uuid=1&uuid=Serhii&pnsdk=PubNub-ObjC-iOS\/4.x.x&include_custom_message_type=1&include_message_type=1" + "url" : "https:\/\/ps.pndsn.com\/v3\/history-with-actions\/sub-key\/demo\/channel\/test-channel?include_uuid=1&uuid=Serhii&pnsdk=PubNub-ObjC-iOS\/4.x.x&max=25&include_custom_message_type=1&include_message_type=1" }, "type" : 0 }, @@ -1193,7 +1193,7 @@ "data" : { "status" : 200, "cls" : "NSHTTPURLResponse", - "url" : "https:\/\/ps.pndsn.com\/v3\/history-with-actions\/sub-key\/demo\/channel\/test-channel?include_uuid=1&uuid=Serhii&pnsdk=PubNub-ObjC-iOS\/4.x.x&include_custom_message_type=1&include_message_type=1", + "url" : "https:\/\/ps.pndsn.com\/v3\/history-with-actions\/sub-key\/demo\/channel\/test-channel?include_uuid=1&uuid=Serhii&pnsdk=PubNub-ObjC-iOS\/4.x.x&max=25&include_custom_message_type=1&include_message_type=1", "headers" : { "Access-Control-Allow-Credentials" : "true", "Content-Type" : "text\/javascript; charset=\"UTF-8\"", diff --git a/Tests/Support Files/Fixtures/PNHistoryIntegrationTests.bundle/ItShouldFetchHistoryForChannelWithEncryptedMessagesAndFailToDecryptWhenDifferentCipherKeyIsSet.json b/Tests/Support Files/Fixtures/PNHistoryIntegrationTests.bundle/ItShouldFetchHistoryForChannelWithEncryptedMessagesAndFailToDecryptWhenDifferentCipherKeyIsSet.json index a9804c771..2e8be86e5 100644 --- a/Tests/Support Files/Fixtures/PNHistoryIntegrationTests.bundle/ItShouldFetchHistoryForChannelWithEncryptedMessagesAndFailToDecryptWhenDifferentCipherKeyIsSet.json +++ b/Tests/Support Files/Fixtures/PNHistoryIntegrationTests.bundle/ItShouldFetchHistoryForChannelWithEncryptedMessagesAndFailToDecryptWhenDifferentCipherKeyIsSet.json @@ -220,7 +220,7 @@ }, "pipeline" : false, "network" : 0, - "url" : "https:\/\/ps.pndsn.com\/v3\/history-with-actions\/sub-key\/demo\/channel\/test-channel?include_uuid=1&pnsdk=PubNub-ObjC-iOS\/4.x.x&uuid=Kim&include_custom_message_type=0&include_message_type=1" + "url" : "https:\/\/ps.pndsn.com\/v3\/history-with-actions\/sub-key\/demo\/channel\/test-channel?include_uuid=1&pnsdk=PubNub-ObjC-iOS\/4.x.x&uuid=Kim&max=25&include_custom_message_type=0&include_message_type=1" }, "type" : 0 }, @@ -229,7 +229,7 @@ "data" : { "status" : 200, "cls" : "NSHTTPURLResponse", - "url" : "https:\/\/ps.pndsn.com\/v3\/history-with-actions\/sub-key\/demo\/channel\/test-channel?include_uuid=1&pnsdk=PubNub-ObjC-iOS\/4.x.x&uuid=Kim&include_custom_message_type=0&include_message_type=1", + "url" : "https:\/\/ps.pndsn.com\/v3\/history-with-actions\/sub-key\/demo\/channel\/test-channel?include_uuid=1&pnsdk=PubNub-ObjC-iOS\/4.x.x&uuid=Kim&max=25&include_custom_message_type=0&include_message_type=1", "headers" : { "Access-Control-Allow-Credentials" : "true", "Content-Type" : "text\/javascript; charset=\"UTF-8\"", From 94fd02d5c011f21d337fc5e48113e1955aa06f5c Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Tue, 3 Mar 2026 12:32:32 +0200 Subject: [PATCH 3/3] build(cocoapods): remove CocoaPods because the service is sunsetting --- .github/workflows/release.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 23fce9f3d..980f68067 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,13 +55,6 @@ jobs: with: ruby-version: 3.2.2 bundler-cache: true - - name: Setup CocoaPods - run: gem install cocoapods - - name: Publish to CocoaPods Trunk - uses: ./.github/.release/actions/actions/services/cocoapods - with: - token: ${{ secrets.GH_TOKEN }} - cocoapods-token: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} - name: Create Release uses: ./.github/.release/actions/actions/services/github-release with: