diff --git a/Ably.xcodeproj/project.pbxproj b/Ably.xcodeproj/project.pbxproj index 7dc9c3b49..f7a55984c 100644 --- a/Ably.xcodeproj/project.pbxproj +++ b/Ably.xcodeproj/project.pbxproj @@ -103,6 +103,7 @@ D746AE501BBD84E7003ECEF8 /* ARTChannelOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = D746AE4E1BBD84E7003ECEF8 /* ARTChannelOptions.m */; }; D746AE531BBD85C5003ECEF8 /* ARTChannels.h in Headers */ = {isa = PBXBuildFile; fileRef = D746AE511BBD85C5003ECEF8 /* ARTChannels.h */; settings = {ATTRIBUTES = (Public, ); }; }; D746AE541BBD85C5003ECEF8 /* ARTChannels.m in Sources */ = {isa = PBXBuildFile; fileRef = D746AE521BBD85C5003ECEF8 /* ARTChannels.m */; }; + D74A17B81FA0D9A3006D27B5 /* PushAdmin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74A17B61FA0D81A006D27B5 /* PushAdmin.swift */; }; D74EFAEB1C4D09B500CFF98E /* RealtimeClientChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EFAEA1C4D09B500CFF98E /* RealtimeClientChannel.swift */; }; D7534C321D79E5C20054C182 /* Ably.h in Headers */ = {isa = PBXBuildFile; fileRef = D7534C311D79E5C20054C182 /* Ably.h */; settings = {ATTRIBUTES = (Public, ); }; }; D7588AF31BFF91B800BB8279 /* ARTURLSessionServerTrust.h in Headers */ = {isa = PBXBuildFile; fileRef = D7588AF11BFF91B800BB8279 /* ARTURLSessionServerTrust.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -336,6 +337,7 @@ D746AE511BBD85C5003ECEF8 /* ARTChannels.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTChannels.h; sourceTree = ""; }; D746AE521BBD85C5003ECEF8 /* ARTChannels.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTChannels.m; sourceTree = ""; }; D746AE551BBD8622003ECEF8 /* ARTChannels+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ARTChannels+Private.h"; sourceTree = ""; }; + D74A17B61FA0D81A006D27B5 /* PushAdmin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushAdmin.swift; sourceTree = ""; }; D74EFAEA1C4D09B500CFF98E /* RealtimeClientChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealtimeClientChannel.swift; sourceTree = ""; }; D7534C311D79E5C20054C182 /* Ably.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Ably.h; sourceTree = ""; }; D7588AF11BFF91B800BB8279 /* ARTURLSessionServerTrust.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTURLSessionServerTrust.h; sourceTree = ""; }; @@ -524,6 +526,7 @@ 851674EE1B7BA5CD00D35169 /* Stats.swift */, EB1AE0CD1C5C3A4900D62250 /* Utilities.swift */, EB7913A71C6E54C3000ABF9B /* Crypto.swift */, + D74A17B61FA0D81A006D27B5 /* PushAdmin.swift */, ); path = Spec; sourceTree = ""; @@ -1163,6 +1166,7 @@ 851674EF1B7BA5CD00D35169 /* Stats.swift in Sources */, EB1AE0CE1C5C3A4900D62250 /* Utilities.swift in Sources */, D72304701BB72CED00F1ABDA /* RealtimeClient.swift in Sources */, + D74A17B81FA0D9A3006D27B5 /* PushAdmin.swift in Sources */, EB7913A81C6E54C3000ABF9B /* Crypto.swift in Sources */, D746AE2D1BBB625E003ECEF8 /* RestClientChannels.swift in Sources */, EBAB9A6F1C69702800AF036B /* ReadmeExamples.swift in Sources */, diff --git a/Examples/Tests/Podfile b/Examples/Tests/Podfile index 7f4228b4d..76c9436f2 100644 --- a/Examples/Tests/Podfile +++ b/Examples/Tests/Podfile @@ -1,6 +1,6 @@ use_frameworks! -pod 'Ably', '1.0.4' +pod 'Ably', :path => '../..' target 'Tests' do diff --git a/Examples/Tests/Podfile.lock b/Examples/Tests/Podfile.lock index 4dd0aac51..b9b45eddd 100644 --- a/Examples/Tests/Podfile.lock +++ b/Examples/Tests/Podfile.lock @@ -1,8 +1,9 @@ PODS: - - Ably (1.0.6): + - Ably (1.0.9): - KSCrashAblyFork (= 1.15.8-ably-1) - msgpack (= 0.1.8) - SocketRocket (= 0.5.1) + - ULID (= 1.0.2) - KSCrashAblyFork (1.15.8-ably-1): - KSCrashAblyFork/Installations (= 1.15.8-ably-1) - KSCrashAblyFork/Installations (1.15.8-ably-1): @@ -68,16 +69,22 @@ PODS: - KSCrashAblyFork/Recording - msgpack (0.1.8) - SocketRocket (0.5.1) + - ULID (1.0.2) DEPENDENCIES: - - Ably (= 1.0.4) + - Ably (from `../..`) + +EXTERNAL SOURCES: + Ably: + :path: ../.. SPEC CHECKSUMS: - Ably: bd558748b327967343d47c7499239ebce3c7944c + Ably: a649faf9d3e27ede0ce10cbd83c8fd413e84d55b KSCrashAblyFork: 6d0dd5b033710109a8fdde28103eeb0e7f9ba1f7 msgpack: 97491d2ea799408f4694f2c7d7fd79baf77853dd SocketRocket: d57c7159b83c3c6655745cd15302aa24b6bae531 + ULID: fcabaa95746b670beb80c029beb3372da2f729bd -PODFILE CHECKSUM: b0e88f668991e1f477a344039c876de0a56ea710 +PODFILE CHECKSUM: 6af34bf7f91045b23539816c1d0cfe253bec5ea5 -COCOAPODS: 1.2.1 +COCOAPODS: 1.3.1 diff --git a/Examples/Tests/Tests.xcodeproj/project.pbxproj b/Examples/Tests/Tests.xcodeproj/project.pbxproj index 2758c75d3..6ed910677 100644 --- a/Examples/Tests/Tests.xcodeproj/project.pbxproj +++ b/Examples/Tests/Tests.xcodeproj/project.pbxproj @@ -241,13 +241,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-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"; + 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; }; 4B4F9EED98BC61874DAB2F62 /* [CP] Check Pods Manifest.lock */ = { @@ -256,13 +259,16 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-TestsTests-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"; + 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; }; 751C5C44401069CC5CAAEE9B /* [CP] Embed Pods Frameworks */ = { @@ -271,9 +277,20 @@ files = ( ); inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Tests/Pods-Tests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Ably/Ably.framework", + "${BUILT_PRODUCTS_DIR}/KSCrashAblyFork/KSCrashAblyFork.framework", + "${BUILT_PRODUCTS_DIR}/SocketRocket/SocketRocket.framework", + "${BUILT_PRODUCTS_DIR}/ULID/ULID.framework", + "${BUILT_PRODUCTS_DIR}/msgpack/msgpack.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Ably.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KSCrashAblyFork.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SocketRocket.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ULID.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/msgpack.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -316,9 +333,20 @@ files = ( ); inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-TestsTests/Pods-TestsTests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Ably/Ably.framework", + "${BUILT_PRODUCTS_DIR}/KSCrashAblyFork/KSCrashAblyFork.framework", + "${BUILT_PRODUCTS_DIR}/SocketRocket/SocketRocket.framework", + "${BUILT_PRODUCTS_DIR}/ULID/ULID.framework", + "${BUILT_PRODUCTS_DIR}/msgpack/msgpack.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Ably.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KSCrashAblyFork.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SocketRocket.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ULID.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/msgpack.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/Examples/Tests/TestsTests/TestsTests.swift b/Examples/Tests/TestsTests/TestsTests.swift index 5d72340b7..e6309067b 100644 --- a/Examples/Tests/TestsTests/TestsTests.swift +++ b/Examples/Tests/TestsTests/TestsTests.swift @@ -10,9 +10,8 @@ import XCTest import Ably @testable import Tests - - class TestsTests: XCTestCase { + let options: ARTClientOptions! = nil func testAblyWorks() { @@ -26,14 +25,14 @@ class TestsTests: XCTestCase { "Accept" : "application/json", "Content-Type" : "application/json" ] - URLSession.shared.dataTask(with: request, completionHandler: { data, _, error in + URLSession.shared.dataTask(with: request as URLRequest) { data, _, error in defer { postAppExpectation.fulfill() } if let e = error { XCTFail("Error setting up sandbox app: \(e)") return } responseData = data - }) .resume() + }.resume() self.waitForExpectations(timeout: 10, handler: nil) guard let key = responseData @@ -65,22 +64,23 @@ class TestsTests: XCTestCase { let backgroundRealtimeExpectation = self.expectation(description: "Realtime in a Background Queue") var realtime: ARTRealtime! //strong reference - URLSession.shared.dataTask(with: URL(string:"https://ably.io")!, completionHandler: { _ in + URLSession.shared.dataTask(with: URL(string: "https://ably.io")!) { _ in realtime = ARTRealtime(key: key as String) realtime.channels.get("foo").attach { _ in defer { backgroundRealtimeExpectation.fulfill() } } - }) .resume() + } .resume() self.waitForExpectations(timeout: 10, handler: nil) let backgroundRestExpectation = self.expectation(description: "Rest in a Background Queue") var rest: ARTRest! //strong reference - URLSession.shared.dataTask(with: URL(string:"https://ably.io")!, completionHandler: { _ in + URLSession.shared.dataTask(with: URL(string: "https://ably.io")!) { _ in rest = ARTRest(key: key as String) rest.channels.get("foo").history { _ in defer { backgroundRestExpectation.fulfill() } } - }) .resume() + }.resume() self.waitForExpectations(timeout: 10, handler: nil) } + } diff --git a/Source/ARTClientOptions.h b/Source/ARTClientOptions.h index 749a3fd8a..b3f258c1a 100644 --- a/Source/ARTClientOptions.h +++ b/Source/ARTClientOptions.h @@ -29,6 +29,7 @@ ART_ASSUME_NONNULL_BEGIN @property (readwrite, assign, nonatomic) BOOL useBinaryProtocol; @property (readwrite, assign, nonatomic) BOOL autoConnect; @property (art_nullable, readwrite, copy, nonatomic) NSString *recover; +@property (readwrite, assign, nonatomic) BOOL pushFullWait; /** The id of the client represented by this instance. diff --git a/Source/ARTClientOptions.m b/Source/ARTClientOptions.m index f95945d35..54fce868e 100644 --- a/Source/ARTClientOptions.m +++ b/Source/ARTClientOptions.m @@ -48,6 +48,7 @@ - (instancetype)initDefaults { _logExceptionReportingUrl = @"https://765e1fcaba404d7598d2fd5a2a43c4f0:8d469b2b0fb34c01a12ae217931c4aed@errors.ably.io/3"; _dispatchQueue = dispatch_get_main_queue(); _internalDispatchQueue = dispatch_queue_create("io.ably.main", DISPATCH_QUEUE_SERIAL); + _pushFullWait = false; return self; } @@ -125,7 +126,8 @@ - (id)copyWithZone:(NSZone *)zone { options.logExceptionReportingUrl = self.logExceptionReportingUrl; options.dispatchQueue = self.dispatchQueue; options.internalDispatchQueue = self.internalDispatchQueue; - + options.pushFullWait = self.pushFullWait; + return options; } diff --git a/Source/ARTDeviceDetails.m b/Source/ARTDeviceDetails.m index a915d8464..a1a2a77df 100644 --- a/Source/ARTDeviceDetails.m +++ b/Source/ARTDeviceDetails.m @@ -26,4 +26,53 @@ - (instancetype)initWithId:(ARTDeviceId *)deviceId { return self; } +- (id)copyWithZone:(NSZone *)zone { + ARTDeviceDetails *device = [[[self class] allocWithZone:zone] init]; + + device.id = self.id; + device.clientId = self.clientId; + device.platform = self.platform; + device.formFactor = self.formFactor; + device.metadata = [self.metadata copy]; + device.push = [self.push copy]; + device.updateToken = self.updateToken; + + return device; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ - \n\t id: %@; \n\t clientId: %@; \n\t platform: %@; \n\t formFactor: %@; \n\t updateToken: %@;", [super description], self.id, self.clientId, self.formFactor, self.platform, self.updateToken]; +} + +- (BOOL)isEqualToDeviceDetail:(ARTDeviceDetails *)device { + if (!device) { + return NO; + } + + BOOL haveEqualDeviceId = (!self.id && !device.id) || [self.id isEqualToString:device.id]; + BOOL haveEqualCliendId = (!self.clientId && !device.clientId) || [self.clientId isEqualToString:device.clientId]; + BOOL haveEqualPlatform = (!self.platform && !device.platform) || [self.platform isEqualToString:device.platform]; + BOOL haveEqualFormFactor = (!self.formFactor && !device.formFactor) || [self.formFactor isEqualToString:device.formFactor]; + + return haveEqualDeviceId && haveEqualCliendId && haveEqualPlatform && haveEqualFormFactor; +} + +#pragma mark - NSObject + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[ARTDeviceDetails class]]) { + return NO; + } + + return [self isEqualToDeviceDetail:(ARTDeviceDetails *)object]; +} + +- (NSUInteger)hash { + return [self.id hash] ^ [self.clientId hash] ^ [self.formFactor hash] ^ [self.platform hash]; +} + @end diff --git a/Source/ARTDevicePushDetails.m b/Source/ARTDevicePushDetails.m index a0860ec4f..a72917b64 100644 --- a/Source/ARTDevicePushDetails.m +++ b/Source/ARTDevicePushDetails.m @@ -18,4 +18,18 @@ - (instancetype)init { return self; } +- (id)copyWithZone:(NSZone *)zone { + ARTDevicePushDetails *push = [[[self class] allocWithZone:zone] init]; + + push.recipient = [self.recipient copy]; + push.state = self.state; + push.errorReason = [self.errorReason copy]; + + return push; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ - \n\t recipient: %@; \n\t state: %@; \n\t errorReason: %@;", [super description], self.recipient, self.state, self.errorReason]; +} + @end diff --git a/Source/ARTJsonLikeEncoder.m b/Source/ARTJsonLikeEncoder.m index 4ecf2d61d..43c25e80f 100644 --- a/Source/ARTJsonLikeEncoder.m +++ b/Source/ARTJsonLikeEncoder.m @@ -624,7 +624,7 @@ - (NSDictionary *)deviceDetailsToDictionary:(ARTDeviceDetails *)deviceDetails { dictionary[@"formFactor"] = deviceDetails.formFactor; if (deviceDetails.clientId) { - dictionary[@"cliendId"] = deviceDetails.clientId; + dictionary[@"clientId"] = deviceDetails.clientId; } dictionary[@"push"] = [self devicePushDetailsToDictionary:deviceDetails.push]; diff --git a/Source/ARTPush.h b/Source/ARTPush.h index eb6226d0f..383723f24 100644 --- a/Source/ARTPush.h +++ b/Source/ARTPush.h @@ -14,12 +14,6 @@ @class ARTRealtime; @class ARTDeviceDetails; -// More context -typedef NSString ARTDeviceId; -typedef NSData ARTDeviceToken; -typedef NSString ARTUpdateToken; -typedef ARTJsonObject ARTPushRecipient; - #pragma mark ARTPushRegisterer interface #ifdef TARGET_OS_IOS @@ -51,9 +45,6 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; -/// Publish a push notification. -- (void)publish:(ARTPushRecipient *)recipient notification:(ARTJsonObject *)notification callback:(art_nullable void (^)(ARTErrorInfo *__art_nullable error))callback; - #ifdef TARGET_OS_IOS /// Push Registration token diff --git a/Source/ARTPush.m b/Source/ARTPush.m index b56fa7024..e7d6a0994 100644 --- a/Source/ARTPush.m +++ b/Source/ARTPush.m @@ -30,8 +30,6 @@ @implementation ARTPush { ARTRest *_rest; __weak ARTLog *_logger; - dispatch_queue_t _queue; - dispatch_queue_t _userQueue; } - (instancetype)init:(ARTRest *)rest { @@ -39,46 +37,10 @@ - (instancetype)init:(ARTRest *)rest { _rest = rest; _logger = [rest logger]; _admin = [[ARTPushAdmin alloc] init:rest]; - _queue = rest.queue; - _userQueue = rest.userQueue; } return self; } -- (void)publish:(ARTPushRecipient *)recipient notification:(ARTJsonObject *)notification callback:(art_nullable void (^)(ARTErrorInfo *__art_nullable error))callback { - if (callback) { - void (^userCallback)(ARTErrorInfo *error) = callback; - callback = ^(ARTErrorInfo *error) { - ART_EXITING_ABLY_CODE(_rest); - dispatch_async(_userQueue, ^{ - userCallback(error); - }); - }; - } - -dispatch_async(_queue, ^{ -ART_TRY_OR_REPORT_CRASH_START(_rest) { - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/push/publish"]]; - request.HTTPMethod = @"POST"; - NSMutableDictionary *body = [NSMutableDictionary dictionary]; - [body setObject:recipient forKey:@"recipient"]; - [body addEntriesFromDictionary:notification]; - request.HTTPBody = [[_rest defaultEncoder] encode:body error:nil]; - [request setValue:[[_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"]; - - [_logger debug:__FILE__ line:__LINE__ message:@"push notification to a single device %@", request]; - [_rest executeRequest:request withAuthOption:ARTAuthenticationOn completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { - if (error) { - [_logger error:@"%@: push notification to a single device failed (%@)", NSStringFromClass(self.class), error.localizedDescription]; - if (callback) callback([ARTErrorInfo createFromNSError:error]); - return; - } - if (callback) callback(nil); - }]; -} ART_TRY_OR_REPORT_CRASH_END -}); -} - #ifdef TARGET_OS_IOS - (ARTPushActivationStateMachine *)activationMachine { diff --git a/Source/ARTPushAdmin.h b/Source/ARTPushAdmin.h index c47040521..bda43757d 100644 --- a/Source/ARTPushAdmin.h +++ b/Source/ARTPushAdmin.h @@ -7,6 +7,7 @@ // #import +#import "ARTTypes.h" @class ARTPushDeviceRegistrations; @class ARTPushChannelSubscriptions; @@ -17,6 +18,9 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; +/// Publish a push notification. +- (void)publish:(ARTPushRecipient *)recipient data:(ARTJsonObject *)data callback:(nullable void (^)(ARTErrorInfo *_Nullable error))callback; + @property (nonatomic, readonly) ARTPushDeviceRegistrations* deviceRegistrations; @property (nonatomic, readonly) ARTPushChannelSubscriptions* channelSubscriptions; diff --git a/Source/ARTPushAdmin.m b/Source/ARTPushAdmin.m index 21574fa60..52e148d17 100644 --- a/Source/ARTPushAdmin.m +++ b/Source/ARTPushAdmin.m @@ -8,17 +8,74 @@ #import "ARTPushAdmin.h" #import "ARTHttp.h" +#import "ARTRest+Private.h" #import "ARTPushDeviceRegistrations.h" #import "ARTPushChannelSubscriptions.h" +#import "ARTLog.h" +#import "ARTJsonEncoder.h" +#import "ARTJsonLikeEncoder.h" -@implementation ARTPushAdmin; +@implementation ARTPushAdmin { + ARTRest *_rest; + __weak ARTLog *_logger; + dispatch_queue_t _userQueue; + dispatch_queue_t _queue; +} - (instancetype)init:(ARTRest *)rest { if (self = [super init]) { + _rest = rest; + _logger = [rest logger]; _deviceRegistrations = [[ARTPushDeviceRegistrations alloc] init:rest]; _channelSubscriptions = [[ARTPushChannelSubscriptions alloc] init:rest]; + _userQueue = rest.userQueue; + _queue = rest.queue; } return self; } +- (void)publish:(ARTPushRecipient *)recipient data:(ARTJsonObject *)data callback:(nullable void (^)(ARTErrorInfo *_Nullable error))callback { + if (callback) { + void (^userCallback)(ARTErrorInfo *error) = callback; + callback = ^(ARTErrorInfo *error) { + ART_EXITING_ABLY_CODE(_rest); + dispatch_async(_userQueue, ^{ + userCallback(error); + }); + }; + } + + dispatch_async(_queue, ^{ + ART_TRY_OR_REPORT_CRASH_START(_rest) { + if (![[recipient allKeys] count]) { + if (callback) callback([ARTErrorInfo createWithCode:0 message:@"Recipient is missing"]); + return; + } + + if (![[data allKeys] count]) { + if (callback) callback([ARTErrorInfo createWithCode:0 message:@"Data payload is missing"]); + return; + } + + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/push/publish"]]; + request.HTTPMethod = @"POST"; + NSMutableDictionary *body = [NSMutableDictionary dictionary]; + [body setObject:recipient forKey:@"recipient"]; + [body addEntriesFromDictionary:data]; + request.HTTPBody = [[_rest defaultEncoder] encode:body error:nil]; + [request setValue:[[_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"]; + + [_logger debug:__FILE__ line:__LINE__ message:@"push notification to a single device %@", request]; + [_rest executeRequest:request withAuthOption:ARTAuthenticationOn completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { + if (error) { + [_logger error:@"%@: push notification to a single device failed (%@)", NSStringFromClass(self.class), error.localizedDescription]; + if (callback) callback([ARTErrorInfo createFromNSError:error]); + return; + } + if (callback) callback(nil); + }]; + } ART_TRY_OR_REPORT_CRASH_END + }); +} + @end diff --git a/Source/ARTPushChannel.m b/Source/ARTPushChannel.m index b7a826c3d..6838e607b 100644 --- a/Source/ARTPushChannel.m +++ b/Source/ARTPushChannel.m @@ -172,7 +172,7 @@ - (void)listSubscriptions:(NSDictionary *)params callbac request.HTTPMethod = @"GET"; ARTPaginatedResultResponseProcessor responseProcessor = ^(NSHTTPURLResponse *response, NSData *data, NSError **error) { - return [[_rest defaultEncoder] decodePushChannelSubscriptions:data error:error]; + return [_rest.encoders[response.MIMEType] decodePushChannelSubscriptions:data error:error]; }; [ARTPaginatedResult executePaginated:_rest withRequest:request andResponseProcessor:responseProcessor callback:callback]; diff --git a/Source/ARTPushChannelSubscription.m b/Source/ARTPushChannelSubscription.m index 9481f1b3e..15289bda8 100644 --- a/Source/ARTPushChannelSubscription.m +++ b/Source/ARTPushChannelSubscription.m @@ -26,4 +26,48 @@ - (instancetype)initWithClientId:(NSString *)clientId channel:(NSString *)channe return self; } +- (id)copyWithZone:(NSZone *)zone { + ARTPushChannelSubscription *subscription = [[[self class] allocWithZone:zone] init]; + + subscription->_deviceId = self.deviceId; + subscription->_clientId = self.clientId; + subscription->_channel = self.channel; + + return subscription; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ - \n\t deviceId: %@; clientId: %@; \n\t channel: %@;", [super description], self.deviceId, self.clientId, self.channel]; +} + +- (BOOL)isEqualToChannelSubscription:(ARTPushChannelSubscription *)subscription { + if (!subscription) { + return NO; + } + + BOOL haveEqualDeviceId = (!self.clientId && !subscription.clientId) || [self.clientId isEqualToString:subscription.clientId]; + BOOL haveEqualCliendId = (!self.clientId && !subscription.clientId) || [self.clientId isEqualToString:subscription.clientId]; + BOOL haveEqualChannel = (!self.channel && !subscription.channel) || [self.channel isEqualToString:subscription.channel]; + + return haveEqualDeviceId && haveEqualCliendId && haveEqualChannel; +} + +#pragma mark - NSObject + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[ARTPushChannelSubscription class]]) { + return NO; + } + + return [self isEqualToChannelSubscription:(ARTPushChannelSubscription *)object]; +} + +- (NSUInteger)hash { + return [self.deviceId hash] ^ [self.clientId hash] ^ [self.channel hash]; +} + @end diff --git a/Source/ARTPushChannelSubscriptions.m b/Source/ARTPushChannelSubscriptions.m index 4bca0fe80..76717bb45 100644 --- a/Source/ARTPushChannelSubscriptions.m +++ b/Source/ARTPushChannelSubscriptions.m @@ -15,6 +15,7 @@ #import "ARTEncoder.h" #import "ARTNSArray+ARTFunctional.h" #import "ARTRest+Private.h" +#import "ARTTypes.h" @implementation ARTPushChannelSubscriptions { __weak ARTRest *_rest; @@ -46,21 +47,29 @@ - (void)save:(ARTPushChannelSubscription *)channelSubscription callback:(void (^ dispatch_async(_queue, ^{ ART_TRY_OR_REPORT_CRASH_START(_rest) { - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/push/channelSubscriptions"]]; - request.HTTPMethod = @"PUT"; + NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:@"/push/channelSubscriptions"] resolvingAgainstBaseURL:NO]; + if (_rest.options.pushFullWait) { + components.queryItems = @[[NSURLQueryItem queryItemWithName:@"fullWait" value:@"true"]]; + } + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; + request.HTTPMethod = @"POST"; request.HTTPBody = [[_rest defaultEncoder] encodePushChannelSubscription:channelSubscription error:nil]; [request setValue:[[_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"]; [_logger debug:__FILE__ line:__LINE__ message:@"save channel subscription with request %@", request]; [_rest executeRequest:request withAuthOption:ARTAuthenticationOn completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { - if (response.statusCode == 200 /*OK*/) { + if (response.statusCode == 200 /*Ok*/ || response.statusCode == 201 /*Created*/) { [_logger debug:__FILE__ line:__LINE__ message:@"channel subscription saved successfully"]; + callback(nil); } else if (error) { [_logger error:@"%@: save channel subscription failed (%@)", NSStringFromClass(self.class), error.localizedDescription]; + callback([ARTErrorInfo createFromNSError:error]); } else { [_logger error:@"%@: save channel subscription failed with status code %ld", NSStringFromClass(self.class), (long)response.statusCode]; + NSString *plain = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + callback([ARTErrorInfo createWithCode:response.statusCode*100 status:response.statusCode message:[plain shortString]]); } }]; } ART_TRY_OR_REPORT_CRASH_END @@ -80,14 +89,12 @@ - (void)listChannels:(void (^)(ARTPaginatedResult * _Nullable, ARTEr dispatch_async(_queue, ^{ ART_TRY_OR_REPORT_CRASH_START(_rest) { - NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:@"/push/channelSubscriptions"] resolvingAgainstBaseURL:NO]; + NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:@"/push/channels"] resolvingAgainstBaseURL:NO]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; request.HTTPMethod = @"GET"; ARTPaginatedResultResponseProcessor responseProcessor = ^(NSHTTPURLResponse *response, NSData *data, NSError **error) { - return [[[_rest defaultEncoder] decodePushChannelSubscriptions:data error:error] artMap:^NSString *(ARTPushChannelSubscription *item) { - return ((ARTPushChannelSubscription *)item).channel; - }]; + return [_rest.encoders[response.MIMEType] decode:data error:error]; }; [ARTPaginatedResult executePaginated:_rest withRequest:request andResponseProcessor:responseProcessor callback:callback]; } ART_TRY_OR_REPORT_CRASH_END @@ -113,7 +120,7 @@ - (void)list:(NSDictionary *)params callback:(void (^)(A request.HTTPMethod = @"GET"; ARTPaginatedResultResponseProcessor responseProcessor = ^(NSHTTPURLResponse *response, NSData *data, NSError **error) { - return [[_rest defaultEncoder] decodePushChannelSubscriptions:data error:error]; + return [_rest.encoders[response.MIMEType] decodePushChannelSubscriptions:data error:error]; }; [ARTPaginatedResult executePaginated:_rest withRequest:request andResponseProcessor:responseProcessor callback:callback]; } ART_TRY_OR_REPORT_CRASH_END @@ -171,19 +178,26 @@ - (void)removeWhere:(NSDictionary *)params callback:(voi - (void)_removeWhere:(NSDictionary *)params callback:(void (^)(ARTErrorInfo *error))callback { NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:@"/push/channelSubscriptions"] resolvingAgainstBaseURL:NO]; components.queryItems = [params asURLQueryItems]; + if (_rest.options.pushFullWait) { + components.queryItems = [components.queryItems arrayByAddingObject:[NSURLQueryItem queryItemWithName:@"fullWait" value:@"true"]]; + } NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; request.HTTPMethod = @"DELETE"; [_logger debug:__FILE__ line:__LINE__ message:@"remove channel subscription with request %@", request]; [_rest executeRequest:request withAuthOption:ARTAuthenticationOn completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { - if (response.statusCode == 200 /*OK*/) { + if (response.statusCode == 200 /*Ok*/ || response.statusCode == 204 /*not returning any content*/) { [_logger debug:__FILE__ line:__LINE__ message:@"%@: channel subscription removed successfully", NSStringFromClass(self.class)]; + callback(nil); } else if (error) { [_logger error:@"%@: remove channel subscription failed (%@)", NSStringFromClass(self.class), error.localizedDescription]; + callback([ARTErrorInfo createFromNSError:error]); } else { [_logger error:@"%@: remove channel subscription failed with status code %ld", NSStringFromClass(self.class), (long)response.statusCode]; + NSString *plain = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + callback([ARTErrorInfo createWithCode:response.statusCode*100 status:response.statusCode message:[plain shortString]]); } }]; } diff --git a/Source/ARTPushDeviceRegistrations.h b/Source/ARTPushDeviceRegistrations.h index fd444ea45..42e824013 100644 --- a/Source/ARTPushDeviceRegistrations.h +++ b/Source/ARTPushDeviceRegistrations.h @@ -22,6 +22,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)save:(ARTDeviceDetails *)deviceDetails callback:(void (^)(ARTErrorInfo *_Nullable))callback; +- (void)get:(ARTDeviceId *)deviceId callback:(void (^)(ARTDeviceDetails *_Nullable, ARTErrorInfo *_Nullable))callback; + - (void)list:(NSDictionary *)params callback:(void (^)(ARTPaginatedResult *_Nullable, ARTErrorInfo *_Nullable))callback; - (void)remove:(NSString *)deviceId callback:(void (^)(ARTErrorInfo *_Nullable))callback; diff --git a/Source/ARTPushDeviceRegistrations.m b/Source/ARTPushDeviceRegistrations.m index e56e03056..6a931b20f 100644 --- a/Source/ARTPushDeviceRegistrations.m +++ b/Source/ARTPushDeviceRegistrations.m @@ -46,30 +46,94 @@ - (void)save:(ARTDeviceDetails *)deviceDetails callback:(void (^)(ARTErrorInfo * dispatch_async(_queue, ^{ ART_TRY_OR_REPORT_CRASH_START(_rest) { - if (!deviceDetails.updateToken) { - [_logger error:@"%@: update token is missing", NSStringFromClass(self.class)]; - return; + NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[[NSURL URLWithString:@"/push/deviceRegistrations"] URLByAppendingPathComponent:deviceDetails.id] resolvingAgainstBaseURL:NO]; + if (_rest.options.pushFullWait) { + components.queryItems = @[[NSURLQueryItem queryItemWithName:@"fullWait" value:@"true"]]; } - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[[NSURL URLWithString:@"/push/deviceRegistrations"] URLByAppendingPathComponent:deviceDetails.id]]; - NSData *tokenData = [deviceDetails.updateToken dataUsingEncoding:NSUTF8StringEncoding]; - NSString *tokenBase64 = [tokenData base64EncodedStringWithOptions:0]; - [request setValue:[NSString stringWithFormat:@"Bearer %@", tokenBase64] forHTTPHeaderField:@"Authorization"]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; request.HTTPMethod = @"PUT"; request.HTTPBody = [[_rest defaultEncoder] encodeDeviceDetails:deviceDetails error:nil]; [request setValue:[[_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"]; + ARTAuthentication authentication = ARTAuthenticationOn; + if (deviceDetails.updateToken) { + NSData *tokenData = [deviceDetails.updateToken dataUsingEncoding:NSUTF8StringEncoding]; + NSString *tokenBase64 = [tokenData base64EncodedStringWithOptions:0]; + [request setValue:[NSString stringWithFormat:@"Bearer %@", tokenBase64] forHTTPHeaderField:@"Authorization"]; + authentication = ARTAuthenticationOff; + } + [_logger debug:__FILE__ line:__LINE__ message:@"save device with request %@", request]; - [_rest executeRequest:request completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { + [_rest executeRequest:request withAuthOption:authentication completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { if (response.statusCode == 200 /*OK*/) { - [_logger debug:__FILE__ line:__LINE__ message:@"%@: save device successfully", NSStringFromClass(self.class)]; - ARTDeviceDetails *deviceDetails = [[_rest defaultEncoder] decodeDeviceDetails:data error:nil]; - deviceDetails.updateToken = deviceDetails.updateToken; + NSError *decodeError = nil; + ARTDeviceDetails *deviceDetails = [[_rest defaultEncoder] decodeDeviceDetails:data error:&decodeError]; + if (decodeError) { + [_logger debug:__FILE__ line:__LINE__ message:@"%@: decode device failed (%@)", NSStringFromClass(self.class), error.localizedDescription]; + callback([ARTErrorInfo createFromNSError:decodeError]); + } + else { + [_logger debug:__FILE__ line:__LINE__ message:@"%@: save device successfully", NSStringFromClass(self.class)]; + deviceDetails.updateToken = deviceDetails.updateToken; + callback(nil); + } } else if (error) { [_logger error:@"%@: save device failed (%@)", NSStringFromClass(self.class), error.localizedDescription]; + callback([ARTErrorInfo createFromNSError:error]); } else { [_logger error:@"%@: save device failed with status code %ld", NSStringFromClass(self.class), (long)response.statusCode]; + NSString *plain = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + callback([ARTErrorInfo createWithCode:response.statusCode*100 status:response.statusCode message:[plain shortString]]); + } + }]; +} ART_TRY_OR_REPORT_CRASH_END +}); +} + +- (void)get:(ARTDeviceId *)deviceId callback:(void (^)(ARTDeviceDetails *, ARTErrorInfo *))callback { + if (callback) { + void (^userCallback)(ARTDeviceDetails *, ARTErrorInfo *error) = callback; + callback = ^(ARTDeviceDetails *device, ARTErrorInfo *error) { + ART_EXITING_ABLY_CODE(_rest); + dispatch_async(_userQueue, ^{ + userCallback(device, error); + }); + }; + } + +dispatch_async(_queue, ^{ +ART_TRY_OR_REPORT_CRASH_START(_rest) { + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[[NSURL URLWithString:@"/push/deviceRegistrations"] URLByAppendingPathComponent:deviceId]]; + request.HTTPMethod = @"GET"; + + [_logger debug:__FILE__ line:__LINE__ message:@"get device with request %@", request]; + [_rest executeRequest:request withAuthOption:ARTAuthenticationOn completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { + if (response.statusCode == 200 /*OK*/) { + NSError *decodeError = nil; + ARTDeviceDetails *device = [_rest.encoders[response.MIMEType] decodeDeviceDetails:data error:&decodeError]; + if (decodeError) { + [_logger debug:__FILE__ line:__LINE__ message:@"%@: decode device failed (%@)", NSStringFromClass(self.class), error.localizedDescription]; + callback(nil, [ARTErrorInfo createFromNSError:decodeError]); + } + else if (device) { + [_logger debug:__FILE__ line:__LINE__ message:@"%@: get device successfully", NSStringFromClass(self.class)]; + callback(device, nil); + } + else { + [_logger debug:__FILE__ line:__LINE__ message:@"%@: get device failed with unknown error", NSStringFromClass(self.class)]; + callback(nil, [ARTErrorInfo createUnknownError]); + } + } + else if (error) { + [_logger error:@"%@: get device failed (%@)", NSStringFromClass(self.class), error.localizedDescription]; + callback(nil, [ARTErrorInfo createFromNSError:error]); + } + else { + [_logger error:@"%@: get device failed with status code %ld", NSStringFromClass(self.class), (long)response.statusCode]; + NSString *plain = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + callback(nil, [ARTErrorInfo createWithCode:response.statusCode*100 status:response.statusCode message:[plain shortString]]); } }]; } ART_TRY_OR_REPORT_CRASH_END @@ -95,7 +159,7 @@ - (void)list:(NSDictionary *)params callback:(void (^)(A request.HTTPMethod = @"GET"; ARTPaginatedResultResponseProcessor responseProcessor = ^(NSHTTPURLResponse *response, NSData *data, NSError **error) { - return [[_rest defaultEncoder] decodeDevicesDetails:data error:error]; + return [_rest.encoders[response.MIMEType] decodeDevicesDetails:data error:error]; }; [ARTPaginatedResult executePaginated:_rest withRequest:request andResponseProcessor:responseProcessor callback:callback]; } ART_TRY_OR_REPORT_CRASH_END @@ -103,7 +167,44 @@ - (void)list:(NSDictionary *)params callback:(void (^)(A } - (void)remove:(NSString *)deviceId callback:(void (^)(ARTErrorInfo *error))callback { - [self removeWhere:@{@"deviceId": deviceId} callback:callback]; + if (callback) { + void (^userCallback)(ARTErrorInfo *error) = callback; + callback = ^(ARTErrorInfo *error) { + ART_EXITING_ABLY_CODE(_rest); + dispatch_async(_userQueue, ^{ + userCallback(error); + }); + }; + } + +dispatch_async(_queue, ^{ +ART_TRY_OR_REPORT_CRASH_START(_rest) { + NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[[NSURL URLWithString:@"/push/deviceRegistrations"] URLByAppendingPathComponent:deviceId] resolvingAgainstBaseURL:NO]; + if (_rest.options.pushFullWait) { + components.queryItems = @[[NSURLQueryItem queryItemWithName:@"fullWait" value:@"true"]]; + } + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; + request.HTTPMethod = @"DELETE"; + [request setValue:[[_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"]; + + [_logger debug:__FILE__ line:__LINE__ message:@"remove device with request %@", request]; + [_rest executeRequest:request withAuthOption:ARTAuthenticationOn completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { + if (response.statusCode == 200 /*Ok*/ || response.statusCode == 204 /*not returning any content*/) { + [_logger debug:__FILE__ line:__LINE__ message:@"%@: save device successfully", NSStringFromClass(self.class)]; + callback(nil); + } + else if (error) { + [_logger error:@"%@: remove device failed (%@)", NSStringFromClass(self.class), error.localizedDescription]; + callback([ARTErrorInfo createFromNSError:error]); + } + else { + [_logger error:@"%@: remove device failed with status code %ld", NSStringFromClass(self.class), (long)response.statusCode]; + NSString *plain = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + callback([ARTErrorInfo createWithCode:response.statusCode*100 status:response.statusCode message:[plain shortString]]); + } + }]; +} ART_TRY_OR_REPORT_CRASH_END +}); } - (void)removeWhere:(NSDictionary *)params callback:(void (^)(ARTErrorInfo *error))callback { @@ -121,19 +222,26 @@ - (void)removeWhere:(NSDictionary *)params callback:(voi ART_TRY_OR_REPORT_CRASH_START(_rest) { NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[NSURL URLWithString:@"/push/deviceRegistrations"] resolvingAgainstBaseURL:NO]; components.queryItems = [params asURLQueryItems]; + if (_rest.options.pushFullWait) { + components.queryItems = [components.queryItems arrayByAddingObject:[NSURLQueryItem queryItemWithName:@"fullWait" value:@"true"]]; + } NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[components URL]]; request.HTTPMethod = @"DELETE"; - [_logger debug:__FILE__ line:__LINE__ message:@"remove device with request %@", request]; + [_logger debug:__FILE__ line:__LINE__ message:@"remove devices with request %@", request]; [_rest executeRequest:request withAuthOption:ARTAuthenticationOn completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { - if (response.statusCode == 200 /*OK*/) { - [_logger debug:__FILE__ line:__LINE__ message:@"%@: remove device successfully", NSStringFromClass(self.class)]; + if (response.statusCode == 200 /*Ok*/ || response.statusCode == 204 /*not returning any content*/) { + [_logger debug:__FILE__ line:__LINE__ message:@"%@: remove devices successfully", NSStringFromClass(self.class)]; + callback(nil); } else if (error) { - [_logger error:@"%@: remove device failed (%@)", NSStringFromClass(self.class), error.localizedDescription]; + [_logger error:@"%@: remove devices failed (%@)", NSStringFromClass(self.class), error.localizedDescription]; + callback([ARTErrorInfo createFromNSError:error]); } else { - [_logger error:@"%@: remove device failed with status code %ld", NSStringFromClass(self.class), (long)response.statusCode]; + [_logger error:@"%@: remove devices failed with status code %ld", NSStringFromClass(self.class), (long)response.statusCode]; + NSString *plain = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + callback([ARTErrorInfo createWithCode:response.statusCode*100 status:response.statusCode message:[plain shortString]]); } }]; } ART_TRY_OR_REPORT_CRASH_END diff --git a/Source/ARTRest.m b/Source/ARTRest.m index 4b638dde9..f64bf6bf3 100644 --- a/Source/ARTRest.m +++ b/Source/ARTRest.m @@ -275,12 +275,8 @@ - (void)executeRequest:(NSURLRequest *)request completion:(void (^)(NSHTTPURLRes if (!validContentType) { NSString *plain = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - // Short response data - NSRange stringRange = {0, MIN([plain length], 1000)}; //1KB - stringRange = [plain rangeOfComposedCharacterSequencesForRange:stringRange]; - NSString *shortPlain = [plain substringWithRange:stringRange]; // Construct artificial error - error = [ARTErrorInfo createWithCode:response.statusCode * 100 status:response.statusCode message:shortPlain]; + error = [ARTErrorInfo createWithCode:response.statusCode * 100 status:response.statusCode message:[plain shortString]]; data = nil; // Discard data; format is unreliable. [self.logger error:@"Request %@ failed with %@", request, error]; } @@ -493,8 +489,7 @@ - (BOOL)stats:(ARTStatsQuery *)query callback:(void (^)(__GENERIC(ARTPaginatedRe NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[requestUrl URLRelativeToURL:self.baseUrl]]; ARTPaginatedResultResponseProcessor responseProcessor = ^(NSHTTPURLResponse *response, NSData *data, NSError **errorPtr) { - id encoder = [self.encoders objectForKey:response.MIMEType]; - return [encoder decodeStats:data error:errorPtr]; + return [self.encoders[response.MIMEType] decodeStats:data error:errorPtr]; }; dispatch_async(_queue, ^{ diff --git a/Source/ARTStatus.h b/Source/ARTStatus.h index 944556a25..e5c860d45 100644 --- a/Source/ARTStatus.h +++ b/Source/ARTStatus.h @@ -77,6 +77,7 @@ FOUNDATION_EXPORT NSString *const ARTAblyMessageNoMeansToRenewToken; + (ARTErrorInfo *)createWithCode:(NSInteger)code status:(NSInteger)status message:(NSString *)message; + (ARTErrorInfo *)createFromNSError:(NSError *)error; + (ARTErrorInfo *)createFromNSException:(NSException *)error; ++ (ARTErrorInfo *)createUnknownError; + (ARTErrorInfo *)wrap:(ARTErrorInfo *)error prepend:(NSString *)prepend; - (NSString *)description; diff --git a/Source/ARTStatus.m b/Source/ARTStatus.m index e6696fe27..1cd8e435a 100644 --- a/Source/ARTStatus.m +++ b/Source/ARTStatus.m @@ -51,6 +51,10 @@ + (ARTErrorInfo *)createFromNSException:(NSException *)error { return e; } ++ (ARTErrorInfo *)createUnknownError { + return [ARTErrorInfo createWithCode:0 message:@"Unknown error"]; +} + + (ARTErrorInfo *)wrap:(ARTErrorInfo *)error prepend:(NSString *)prepend { return [ARTErrorInfo createWithCode:error.code status:error.statusCode message:[NSString stringWithFormat:@"%@%@", prepend, error.reason]]; } diff --git a/Source/ARTTypes.h b/Source/ARTTypes.h index d0b56ae33..3a8aaccf2 100644 --- a/Source/ARTTypes.h +++ b/Source/ARTTypes.h @@ -22,7 +22,12 @@ @class __GENERIC(ARTPaginatedResult, ItemType); @class ARTStats; +// More context typedef NSDictionary ARTJsonObject; +typedef NSString ARTDeviceId; +typedef NSData ARTDeviceToken; +typedef NSString ARTUpdateToken; +typedef ARTJsonObject ARTPushRecipient; typedef NS_ENUM(NSUInteger, ARTAuthentication) { ARTAuthenticationOff, @@ -188,6 +193,10 @@ NSString *generateNonce(); @interface NSString (ARTJsonCompatible) @end +@interface NSString (Utilities) +- (NSString *)shortString; +@end + @interface NSDictionary (ARTJsonCompatible) @end diff --git a/Source/ARTTypes.m b/Source/ARTTypes.m index 3e48606e5..e190d69d8 100644 --- a/Source/ARTTypes.m +++ b/Source/ARTTypes.m @@ -269,3 +269,15 @@ - (id)peek { } @end + +#pragma mark - NSString (Utilities) + +@implementation NSString (Utilities) + +- (NSString *)shortString { + NSRange stringRange = {0, MIN([self length], 1000)}; //1KB + stringRange = [self rangeOfComposedCharacterSequencesForRange:stringRange]; + return [self substringWithRange:stringRange]; +} + +@end diff --git a/Source/ARTURLSessionServerTrust.m b/Source/ARTURLSessionServerTrust.m index a2c401c70..3c650169e 100644 --- a/Source/ARTURLSessionServerTrust.m +++ b/Source/ARTURLSessionServerTrust.m @@ -42,9 +42,12 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didRece if (challenge.protectionSpace.serverTrust) { completionHandler(NSURLSessionAuthChallengeUseCredential, [[NSURLCredential alloc] initWithTrust:challenge.protectionSpace.serverTrust]); } - else { + else if ([challenge.sender respondsToSelector:@selector(performDefaultHandlingForAuthenticationChallenge:)]) { [challenge.sender performDefaultHandlingForAuthenticationChallenge:challenge]; } + else { + completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); + } } @end diff --git a/Spec/PushAdmin.swift b/Spec/PushAdmin.swift new file mode 100644 index 000000000..be66aeb85 --- /dev/null +++ b/Spec/PushAdmin.swift @@ -0,0 +1,763 @@ +// +// PushAdmin.swift +// Ably +// +// Created by Ricardo Pereira on 25/10/2017. +// Copyright © 2017 Ably. All rights reserved. +// + +import Ably +import Nimble +import Quick + +class PushAdmin : QuickSpec { + + private static var deviceDetails: ARTDeviceDetails = { + let deviceDetails = ARTDeviceDetails(id: "testDeviceDetails") + deviceDetails.platform = "ios" + deviceDetails.formFactor = "phone" + deviceDetails.metadata = NSMutableDictionary() + deviceDetails.push.recipient = [ + "transportType": "apns", + "deviceToken": "foo" + ] + return deviceDetails + }() + + private static var deviceDetails1ClientA: ARTDeviceDetails = { + let deviceDetails = ARTDeviceDetails(id: "deviceDetails1ClientA") + deviceDetails.platform = "android" + deviceDetails.formFactor = "tablet" + deviceDetails.clientId = "clientA" + deviceDetails.metadata = NSMutableDictionary() + deviceDetails.push.recipient = [ + "transportType": "gcm", + "registrationToken": "qux" + ] + return deviceDetails + }() + + private static var deviceDetails2ClientA: ARTDeviceDetails = { + let deviceDetails = ARTDeviceDetails(id: "deviceDetails2ClientA") + deviceDetails.platform = "android" + deviceDetails.formFactor = "tablet" + deviceDetails.clientId = "clientA" + deviceDetails.metadata = NSMutableDictionary() + deviceDetails.push.recipient = [ + "transportType": "gcm", + "registrationToken": "qux" + ] + return deviceDetails + }() + + private static var deviceDetails3ClientB: ARTDeviceDetails = { + let deviceDetails = ARTDeviceDetails(id: "deviceDetails3ClientB") + deviceDetails.platform = "android" + deviceDetails.formFactor = "tablet" + deviceDetails.clientId = "clientB" + deviceDetails.metadata = NSMutableDictionary() + deviceDetails.push.recipient = [ + "transportType": "gcm", + "registrationToken": "qux" + ] + return deviceDetails + }() + + private static var allDeviceDetails: [ARTDeviceDetails] = [ + deviceDetails, + deviceDetails1ClientA, + deviceDetails2ClientA, + deviceDetails3ClientB, + ] + + private static var subscriptionFooDevice1 = ARTPushChannelSubscription(deviceId: "deviceDetails1ClientA", channel: "pushenabled:foo") + private static var subscriptionFooDevice2 = ARTPushChannelSubscription(deviceId: "deviceDetails2ClientA", channel: "pushenabled:foo") + private static var subscriptionBarDevice2 = ARTPushChannelSubscription(deviceId: "deviceDetails2ClientA", channel: "pushenabled:bar") + private static var subscriptionFooClientA = ARTPushChannelSubscription(clientId: "clientA", channel: "pushenabled:foo") + private static var subscriptionFooClientB = ARTPushChannelSubscription(clientId: "clientB", channel: "pushenabled:foo") + private static var subscriptionBarClientB = ARTPushChannelSubscription(clientId: "clientB", channel: "pushenabled:bar") + + private static var allSubscriptions: [ARTPushChannelSubscription] = [ + subscriptionFooDevice1, + subscriptionFooDevice2, + subscriptionBarDevice2, + subscriptionFooClientA, + subscriptionFooClientB, + subscriptionBarClientB, + ] + + private lazy var deviceDetails: ARTDeviceDetails = PushAdmin.deviceDetails + private lazy var deviceDetails1ClientA: ARTDeviceDetails = PushAdmin.deviceDetails1ClientA + private lazy var deviceDetails2ClientA: ARTDeviceDetails = PushAdmin.deviceDetails2ClientA + private lazy var deviceDetails3ClientB: ARTDeviceDetails = PushAdmin.deviceDetails3ClientB + + private lazy var allDeviceDetails: [ARTDeviceDetails] = PushAdmin.allDeviceDetails + + private lazy var subscriptionFooDevice1: ARTPushChannelSubscription = PushAdmin.subscriptionFooDevice1 + private lazy var subscriptionFooDevice2: ARTPushChannelSubscription = PushAdmin.subscriptionFooDevice2 + private lazy var subscriptionBarDevice2: ARTPushChannelSubscription = PushAdmin.subscriptionBarDevice2 + private lazy var subscriptionFooClientA: ARTPushChannelSubscription = PushAdmin.subscriptionFooClientA + private lazy var subscriptionFooClientB: ARTPushChannelSubscription = PushAdmin.subscriptionFooClientB + private lazy var subscriptionBarClientB: ARTPushChannelSubscription = PushAdmin.subscriptionBarClientB + + private lazy var allSubscriptions: [ARTPushChannelSubscription] = PushAdmin.allSubscriptions + + private lazy var allSubscriptionsChannels: [String] = { + var seen = Set() + return allSubscriptions.filter({ seen.insert($0.channel).inserted }).map({ $0.channel }) + }() + + override class func setUp() { + super.setUp() + let rest = ARTRest(options: AblyTests.commonAppSetup()) + let group = DispatchGroup() + + for device in allDeviceDetails { + group.enter() + rest.push.admin.deviceRegistrations.save(device) { error in + defer { + group.leave() + } + assert(error == nil, error?.message ?? "no message") + } + } + + for subscription in allSubscriptions { + group.enter() + rest.push.admin.channelSubscriptions.save(subscription) { error in + defer { + group.leave() + } + assert(error == nil, error?.message ?? "no message") + } + } + + group.wait() + } + + override class func tearDown() { + let rest = ARTRest(options: AblyTests.commonAppSetup()) + let group = DispatchGroup() + + for device in allDeviceDetails { + group.enter() + rest.push.admin.deviceRegistrations.remove(device.id) { _ in + group.leave() + } + } + + for subscription in allSubscriptions { + group.enter() + rest.push.admin.channelSubscriptions.remove(subscription) { _ in + group.leave() + } + } + + super.tearDown() + } + + override func spec() { + + var rest: ARTRest! + var httpExecutor: MockHTTPExecutor! + + let recipient = [ + "clientId": "bob" + ] + + let payload = [ + "notification": [ + "title": "Welcome" + ] + ] + + beforeEach { + rest = ARTRest(key: "xxxx:xxxx") + httpExecutor = MockHTTPExecutor() + rest.httpExecutor = httpExecutor + } + + // RHS1a + fdescribe("publish") { + + it("should perform an HTTP request to /push/publish") { + waitUntil(timeout: testTimeout) { done in + rest.push.admin.publish(recipient, data: payload) { error in + expect(error).to(beNil()) + done() + } + } + + guard let request = httpExecutor.requests.first else { + fail("Request is missing"); return + } + guard let url = request.url else { + fail("URL is missing"); return + } + + expect(url.absoluteString).to(contain("/push/publish")) + + switch extractBodyAsMsgPack(request) { + case .failure(let error): + XCTFail(error) + case .success(let httpBody): + guard let bodyRecipient = httpBody.unbox["recipient"] as? [String: String] else { + fail("recipient is missing"); return + } + expect(bodyRecipient).to(equal(recipient)) + + guard let bodyPayload = httpBody.unbox["notification"] as? [String: String] else { + fail("notification is missing"); return + } + expect(bodyPayload).to(equal(payload["notification"])) + } + } + + it("should work as expected") { + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + let channel = realtime.channels.get("pushenabled:push_admin_publish-ok") + + waitUntil(timeout: testTimeout) { done in + channel.attach() { error in + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.subscribe("__ably_push__") { message in + guard let data = message.data as? NSDictionary else { + fail("Data is not a JSON Object"); partialDone(); return + } + expect(data).to(equal(["data": ["foo": "bar"]] as NSDictionary)) + partialDone() + } + realtime.push.admin.publish(["ablyChannel": channel.name], data: ["data": ["foo": "bar"]]) { error in + expect(error).to(beNil()) + partialDone() + } + } + } + + it("should fail with a bad recipient") { + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + let channel = realtime.channels.get("pushenabled:push_admin_publish-bad-recipient") + + waitUntil(timeout: testTimeout) { done in + channel.attach() { error in + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + channel.subscribe("__ably_push__") { message in + fail("Should not be called") + } + realtime.push.admin.publish(["foo": "bar"], data: ["data": ["foo": "bar"]]) { error in + guard let error = error else { + fail("Error is missing"); done(); return + } + expect(error.statusCode) == 400 + expect(error.message).to(contain("recipient must contain a deviceId, clientId, or transportType")) + done() + } + } + } + + it("should fail with an empty recipient") { + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + let channel = realtime.channels.get("pushenabled:push_admin_publish-empty-recipient") + + waitUntil(timeout: testTimeout) { done in + channel.attach() { error in + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + channel.subscribe("__ably_push__") { message in + fail("Should not be called") + } + realtime.push.admin.publish([:], data: ["data": ["foo": "bar"]]) { error in + guard let error = error else { + fail("Error is missing"); done(); return + } + expect(error.message.lowercased()).to(contain("recipient is missing")) + done() + } + } + } + + it("should fail with an empty payload") { + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + let channel = realtime.channels.get("pushenabled:push_admin_publish-empty-payload") + + waitUntil(timeout: testTimeout) { done in + channel.attach() { error in + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + channel.subscribe("__ably_push__") { message in + fail("Should not be called") + } + realtime.push.admin.publish(["ablyChannel": channel.name], data: [:]) { error in + guard let error = error else { + fail("Error is missing"); done(); return + } + expect(error.message.lowercased()).to(contain("data payload is missing")) + done() + } + } + } + + } + + fdescribe("Device Registrations") { + + // RHS1b1 + context("get") { + it("should return a device") { + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.deviceRegistrations.get("testDeviceDetails") { device, error in + guard let device = device else { + fail("Device is missing"); done(); return; + } + expect(device).to(equal(self.deviceDetails)) + expect(error).to(beNil()) + done() + } + } + } + + it("should not return a device if it doesnt exist") { + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.deviceRegistrations.get("madeup") { device, error in + expect(device).to(beNil()) + guard let error = error else { + fail("Error should not be empty"); done(); return + } + expect(error.statusCode) == 404 + expect(error.message).to(contain("not found")) + done() + } + } + } + } + + // RHS1b2 + context("list") { + it("should list devices by id") { + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.deviceRegistrations.list(["deviceId": "testDeviceDetails"]) { result, error in + guard let result = result else { + fail("PaginatedResult should not be empty"); done(); return + } + expect(result.items.count) == 1 + expect(error).to(beNil()) + done() + } + } + } + + it("should list devices by client id") { [allDeviceDetails] in + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.deviceRegistrations.list(["clientId": "clientA"]) { result, error in + guard let result = result else { + fail("PaginatedResult should not be empty"); done(); return + } + expect(result.items.count) == allDeviceDetails.filter({ $0.clientId == "clientA" }).count + expect(error).to(beNil()) + done() + } + } + } + + it("should list devices sorted") { [allDeviceDetails] in + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.deviceRegistrations.list(["direction": "forwards"]) { result, error in + guard let result = result else { + fail("PaginatedResult should not be empty"); done(); return + } + expect(result.items.count) == allDeviceDetails.count + expect(error).to(beNil()) + done() + } + } + } + + it("should return an empty list when id does not exist") { + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.deviceRegistrations.list(["deviceId": "madeup"]) { result, error in + guard let result = result else { + fail("PaginatedResult should not be empty"); done(); return + } + expect(result.items.count) == 0 + expect(error).to(beNil()) + done() + } + } + } + } + + // RHS1b4 + context("remove") { + it("should unregister a device") { + let options = AblyTests.commonAppSetup() + options.pushFullWait = true + let realtime = ARTRealtime(options: options) + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.deviceRegistrations.remove(self.deviceDetails.id) { error in + expect(error).to(beNil()) + done() + } + } + } + } + + // RHS1b3 + context("save") { + it("should register a device") { + let options = AblyTests.commonAppSetup() + options.pushFullWait = true + let realtime = ARTRealtime(options: options) + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.deviceRegistrations.save(self.deviceDetails) { error in + expect(error).to(beNil()) + done() + } + } + } + } + + // RHS1b5 + context("removeWhere") { [allDeviceDetails] in + it("should unregister a device") { + let options = AblyTests.commonAppSetup() + options.pushFullWait = true + let realtime = ARTRealtime(options: options) + + let params = [ + "clientId": "clientA" + ] + + let expectedRemoved = allDeviceDetails.filter({ $0.clientId == "clientA" }) + + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.deviceRegistrations.list(params) { result, error in + guard let result = result else { + fail("PaginatedResult should not be empty"); done(); return + } + expect(result.items).to(contain(expectedRemoved)) + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.deviceRegistrations.removeWhere(params) { error in + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.deviceRegistrations.list(params) { result, error in + guard let result = result else { + fail("PaginatedResult should not be empty"); done(); return + } + expect(result.items.count) == 0 + expect(error).to(beNil()) + done() + } + } + + // --- Restore state for next tests --- + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(expectedRemoved.count, done: done) + for removedDevice in expectedRemoved { + realtime.push.admin.deviceRegistrations.save(removedDevice) { error in + expect(error).to(beNil()) + partialDone() + } + } + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + realtime.push.admin.channelSubscriptions.save(self.subscriptionFooDevice2) { error in + expect(error).to(beNil()) + partialDone() + } + realtime.push.admin.channelSubscriptions.save(self.subscriptionBarDevice2) { error in + expect(error).to(beNil()) + partialDone() + } + } + } + } + + } + + fdescribe("Channel Subscriptions") { + + let subscription = ARTPushChannelSubscription(clientId: "newClient", channel: "pushenabled:qux") + + // RHS1c3 + context("save") { + it("should add a subscription") { + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.channelSubscriptions.save(subscription) { error in + expect(error).to(beNil()) + done() + } + } + } + } + + // RHS1c1 + context("list") { + it("should receive a list of subscriptions") { + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.channelSubscriptions.list(["channel": "pushenabled:qux"]) { result, error in + guard let result = result else { + fail("PaginatedResult should not be empty"); done(); return + } + expect(result.items.count) == 1 + expect(error).to(beNil()) + done() + } + } + } + } + + // RHS1c2 + context("listChannels") { [allSubscriptionsChannels] in + it("should receive a list of subscriptions") { + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.channelSubscriptions.listChannels() { result, error in + expect(error).to(beNil()) + guard let result = result else { + fail("PaginatedResult should not be empty"); done(); return + } + expect(result.items as [String]).to(contain(allSubscriptionsChannels + [subscription.channel])) + done() + } + } + } + } + + // RHS1c4 + context("remove") { + it("should remove a subscription") { + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.channelSubscriptions.remove(subscription) { error in + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.channelSubscriptions.list(["channel": "pushenabled:qux"]) { result, error in + guard let result = result else { + fail("PaginatedResult should not be empty"); done(); return + } + expect(result.items.count) == 0 + expect(error).to(beNil()) + done() + } + } + } + } + + // RHS1c5 + context("removeWhere") { + it("should remove by cliendId") { + let options = AblyTests.commonAppSetup() + options.pushFullWait = true + let realtime = ARTRealtime(options: options) + + let params = [ + "clientId": "clientB" + ] + + let expectedRemoved = [ + self.subscriptionFooClientB, + self.subscriptionBarClientB + ] + + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.channelSubscriptions.list(params) { result, error in + guard let result = result else { + fail("PaginatedResult should not be empty"); done(); return + } + expect(result.items).to(contain(expectedRemoved)) + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.channelSubscriptions.removeWhere(params) { error in + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.channelSubscriptions.list(params) { result, error in + guard let result = result else { + fail("PaginatedResult should not be empty"); done(); return + } + expect(result.items.count) == 0 + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(expectedRemoved.count, done: done) + for removedSubscription in expectedRemoved { + realtime.push.admin.channelSubscriptions.save(removedSubscription) { error in + expect(error).to(beNil()) + partialDone() + } + } + } + } + + it("should remove by cliendId and channel") { + let options = AblyTests.commonAppSetup() + options.pushFullWait = true + let realtime = ARTRealtime(options: options) + + let params = [ + "clientId": "clientB", + "channel": "pushenabled:foo" + ] + + let expectedRemoved = [ + self.subscriptionFooClientB, + ] + + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.channelSubscriptions.list(params) { result, error in + guard let result = result else { + fail("PaginatedResult should not be empty"); done(); return + } + expect(result.items).to(contain(expectedRemoved)) + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.channelSubscriptions.removeWhere(params) { error in + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.channelSubscriptions.list(params) { result, error in + guard let result = result else { + fail("PaginatedResult should not be empty"); done(); return + } + expect(result.items.count) == 0 + expect(error).to(beNil()) + done() + } + } + } + + it("should remove by deviceId") { + let options = AblyTests.commonAppSetup() + options.pushFullWait = true + let realtime = ARTRealtime(options: options) + + let params = [ + "deviceId": "deviceDetails2ClientA", + ] + + let expectedRemoved = [ + self.subscriptionFooDevice2, + self.subscriptionBarDevice2, + ] + + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.channelSubscriptions.list(params) { result, error in + guard let result = result else { + fail("PaginatedResult should not be empty"); done(); return + } + expect(result.items).to(contain(expectedRemoved)) + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.channelSubscriptions.removeWhere(params) { error in + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.channelSubscriptions.list(params) { result, error in + guard let result = result else { + fail("PaginatedResult should not be empty"); done(); return + } + expect(result.items.count) == 0 + expect(error).to(beNil()) + done() + } + } + } + + it("should not remove by inexistent deviceId") { + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + + let params = [ + "deviceId": "madeup", + ] + + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.channelSubscriptions.list(params) { result, error in + guard let result = result else { + fail("PaginatedResult should not be empty"); done(); return + } + expect(result.items.count) == 0 + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.channelSubscriptions.removeWhere(params) { error in + expect(error).to(beNil()) + done() + } + } + } + } + + } + + } +} diff --git a/Spec/TestUtilities.swift b/Spec/TestUtilities.swift index da3e3fbdb..d7e33ad25 100644 --- a/Spec/TestUtilities.swift +++ b/Spec/TestUtilities.swift @@ -613,6 +613,37 @@ class MockHTTP: ARTHttp { } +class MockHTTPExecutor: NSObject, ARTHTTPAuthenticatedExecutor { + + var _logger = ARTLog() + var clientOptions = ARTClientOptions() + var encoder = ARTJsonLikeEncoder() + var requests: [URLRequest] = [] + + func logger() -> ARTLog { + return _logger + } + + func options() -> ARTClientOptions { + return self.clientOptions + } + + func defaultEncoder() -> ARTEncoder { + return self.encoder + } + + func execute(_ request: NSMutableURLRequest, withAuthOption authOption: ARTAuthentication, completion callback: @escaping (HTTPURLResponse?, Data?, Error?) -> Void) { + self.requests.append(request as URLRequest) + callback(nil, nil, nil) + } + + func execute(_ request: URLRequest, completion callback: ((HTTPURLResponse?, Data?, Error?) -> Void)? = nil) { + self.requests.append(request) + callback?(nil, nil, nil) + } + +} + /// Records each request and response for test purpose. class TestProxyHTTPExecutor: NSObject, ARTHTTPExecutor { struct ErrorSimulator {