From eb777955456bcd0438aeb3432996e53871050fc9 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 25 Oct 2017 20:18:42 +0100 Subject: [PATCH 01/19] MockHTTPExecutor --- Spec/TestUtilities.swift | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) 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 { From b14d4953223b49a831556624fefe28327e5db337 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 25 Oct 2017 20:19:07 +0100 Subject: [PATCH 02/19] PushAdmin --- Ably.xcodeproj/project.pbxproj | 4 + Spec/PushAdmin.swift | 621 +++++++++++++++++++++++++++++++++ 2 files changed, 625 insertions(+) create mode 100644 Spec/PushAdmin.swift 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/Spec/PushAdmin.swift b/Spec/PushAdmin.swift new file mode 100644 index 000000000..eb15fac40 --- /dev/null +++ b/Spec/PushAdmin.swift @@ -0,0 +1,621 @@ +// +// 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 + + 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) + } + } + + for subscription in allSubscriptions { + group.enter() + rest.push.admin.channelSubscriptions.save(subscription) { error in + defer { + group.leave() + } + assert(error == nil) + } + } + + 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 = [ + "client_id": "bob" + ] + + let payload = [ + "notification": [ + "title": "Welcome" + ] + ] + + beforeEach { + rest = ARTRest(key: "xxxx:xxxx") + httpExecutor = MockHTTPExecutor() + rest.httpExecutor = httpExecutor + } + + // RHS1a + describe("publish") { + + fit("should perform an HTTP request to /push/publish") { + waitUntil(timeout: testTimeout) { done in + rest.push.publish(recipient, notification: payload) { error in + expect(error).to(beNil()) + done() + } + } + + switch extractBodyAsMsgPack(httpExecutor.requests.first) { + 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 reject empty values/data for recipient") { + waitUntil(timeout: testTimeout) { done in + rest.push.admin.publish(["client_id": ""], notification: payload) { error in + expect(error).toNot(beNil()) + done() + } + } + } + + it("should reject empty values/data for payload") { + waitUntil(timeout: testTimeout) { done in + rest.push.admin.publish(recipient, notification: ["notification": ""]) { error in + expect(error).toNot(beNil()) + done() + } + } + } + + it("should reject an invalid recipient") { + waitUntil(timeout: testTimeout) { done in + rest.push.admin.publish(["foo": "bar"], notification: payload) { error in + expect(error).toNot(beNil()) + done() + } + } + } + + it("should reject an invalid notification payload") { + waitUntil(timeout: testTimeout) { done in + rest.push.admin.publish(recipient, notification: ["foo": "bar"]) { error in + expect(error).toNot(beNil()) + done() + } + } + } + + it("should send a notification") { + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + let channel = realtime.channels.get("push-test") + + waitUntil(timeout: testTimeout) { done in + channel.subscribe { message in + guard let data = message.data as? NSDictionary else { + fail("Message data should be a dictionary"); done(); return + } + expect(data).to(equal(payload as NSDictionary)) + done() + } + + realtime.push.publish(["ablyChannel": channel.name], notification: payload) { error in + expect(error).to(beNil()) + } + } + } + + } + + describe("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 + expect(device).to(equal(self.deviceDetails)) + expect(error).to(beNil()) + done() + } + } + } + + it("should not return a device if it does not 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") { + 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) == 2 + expect(error).to(beNil()) + done() + } + } + } + + it("should list devices sorted") { + 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) == 0 + 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 realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.deviceRegistrations.remove(self.deviceDetails) { error in + expect(error).to(beNil()) + done() + } + } + } + } + + // RHS1b3 + context("save") { + it("should register a device") { + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.deviceRegistrations.save(self.deviceDetails) { error in + expect(error).to(beNil()) + done() + } + } + } + } + + // RHS1b5 + context("removeWhere") { + it("should unregister a device") { + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + waitUntil(timeout: testTimeout) { done in + realtime.push.admin.deviceRegistrations.removeWhere(["clientId": "clientA"]) { error in + expect(error).to(beNil()) + done() + } + } + } + } + + } + + describe("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") { + 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 + guard let result = result else { + fail("PaginatedResult should not be empty"); done(); return + } + expect(result.items.count) == 1 + expect(result.items.first) == "pushenabled:qux" + expect(error).to(beNil()) + 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 realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + + let params = [ + "clientId": "clientB" + ] + + let expectedRemoved = [ + self.subscriptionFooClientB, + self.subscriptionBarClientB + ] + + 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 realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + + let params = [ + "clientId": "clientB", + "channel": "pushenabled:foo" + ] + + let expectedRemoved = [ + self.subscriptionFooClientB, + ] + + 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 deviceId") { + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + + let params = [ + "deviceId": "subscriptionBarDevice2.deviceId", + ] + + let expectedRemoved = [ + self.subscriptionFooDevice2, + self.subscriptionBarDevice2, + ] + + 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 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() + } + } + } + } + + } + + } +} From 4b03de9376c3e87e284df49e9b180a5e13676f44 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 2 Nov 2017 16:22:01 +0000 Subject: [PATCH 03/19] Move `publish` method to Admin --- Source/ARTPush.h | 9 -------- Source/ARTPush.m | 38 --------------------------------- Source/ARTPushAdmin.h | 4 ++++ Source/ARTPushAdmin.m | 49 ++++++++++++++++++++++++++++++++++++++++++- Source/ARTTypes.h | 5 +++++ Spec/PushAdmin.swift | 6 +++--- 6 files changed, 60 insertions(+), 51 deletions(-) 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..a4ff4af06 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 notification:(ARTJsonObject *)notification 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..436f00f5f 100644 --- a/Source/ARTPushAdmin.m +++ b/Source/ARTPushAdmin.m @@ -8,17 +8,64 @@ #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 notification:(ARTJsonObject *)notification 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) { + 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 + }); +} + @end diff --git a/Source/ARTTypes.h b/Source/ARTTypes.h index d0b56ae33..84ca7b5df 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, diff --git a/Spec/PushAdmin.swift b/Spec/PushAdmin.swift index eb15fac40..ec6cbf15b 100644 --- a/Spec/PushAdmin.swift +++ b/Spec/PushAdmin.swift @@ -177,7 +177,7 @@ class PushAdmin : QuickSpec { fit("should perform an HTTP request to /push/publish") { waitUntil(timeout: testTimeout) { done in - rest.push.publish(recipient, notification: payload) { error in + rest.push.admin.publish(recipient, notification: payload) { error in expect(error).to(beNil()) done() } @@ -248,7 +248,7 @@ class PushAdmin : QuickSpec { done() } - realtime.push.publish(["ablyChannel": channel.name], notification: payload) { error in + realtime.push.admin.publish(["ablyChannel": channel.name], notification: payload) { error in expect(error).to(beNil()) } } @@ -351,7 +351,7 @@ class PushAdmin : QuickSpec { it("should unregister a device") { let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) waitUntil(timeout: testTimeout) { done in - realtime.push.admin.deviceRegistrations.remove(self.deviceDetails) { error in + realtime.push.admin.deviceRegistrations.remove(self.deviceDetails.id) { error in expect(error).to(beNil()) done() } From 88753363767a90a8a5844dec0350be47fe3059e3 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 2 Nov 2017 16:23:45 +0000 Subject: [PATCH 04/19] Add `get` method in PushDeviceRegistrations --- Source/ARTPushDeviceRegistrations.h | 2 ++ Source/ARTPushDeviceRegistrations.m | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/Source/ARTPushDeviceRegistrations.h b/Source/ARTPushDeviceRegistrations.h index fd444ea45..71b1c6f40 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 (^)(ARTPaginatedResult *_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..179a411ab 100644 --- a/Source/ARTPushDeviceRegistrations.m +++ b/Source/ARTPushDeviceRegistrations.m @@ -76,6 +76,10 @@ - (void)save:(ARTDeviceDetails *)deviceDetails callback:(void (^)(ARTErrorInfo * }); } +- (void)get:(ARTDeviceId *)deviceId callback:(void (^)(ARTPaginatedResult *, ARTErrorInfo *))callback { + [self list:@{@"deviceId": deviceId} callback:callback]; +} + - (void)list:(NSDictionary *)params callback:(void (^)(ARTPaginatedResult *result, ARTErrorInfo *error))callback { if (callback) { void (^userCallback)(ARTPaginatedResult *, ARTErrorInfo *error) = callback; From 7ae7ffbae9946314f2693eecb24c49333b584b09 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 2 Nov 2017 16:25:17 +0000 Subject: [PATCH 05/19] Should use response encoder type --- Source/ARTPushChannel.m | 2 +- Source/ARTPushChannelSubscriptions.m | 4 ++-- Source/ARTPushDeviceRegistrations.m | 2 +- Source/ARTRest.m | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) 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/ARTPushChannelSubscriptions.m b/Source/ARTPushChannelSubscriptions.m index 4bca0fe80..fcdb04f33 100644 --- a/Source/ARTPushChannelSubscriptions.m +++ b/Source/ARTPushChannelSubscriptions.m @@ -85,7 +85,7 @@ - (void)listChannels:(void (^)(ARTPaginatedResult * _Nullable, ARTEr request.HTTPMethod = @"GET"; ARTPaginatedResultResponseProcessor responseProcessor = ^(NSHTTPURLResponse *response, NSData *data, NSError **error) { - return [[[_rest defaultEncoder] decodePushChannelSubscriptions:data error:error] artMap:^NSString *(ARTPushChannelSubscription *item) { + return [[_rest.encoders[response.MIMEType] decodePushChannelSubscriptions:data error:error] artMap:^NSString *(ARTPushChannelSubscription *item) { return ((ARTPushChannelSubscription *)item).channel; }]; }; @@ -113,7 +113,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 diff --git a/Source/ARTPushDeviceRegistrations.m b/Source/ARTPushDeviceRegistrations.m index 179a411ab..024aea78c 100644 --- a/Source/ARTPushDeviceRegistrations.m +++ b/Source/ARTPushDeviceRegistrations.m @@ -99,7 +99,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 diff --git a/Source/ARTRest.m b/Source/ARTRest.m index 4b638dde9..a658fd325 100644 --- a/Source/ARTRest.m +++ b/Source/ARTRest.m @@ -493,8 +493,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, ^{ From eab0874c9022165d962eeac5820f81514a0461dd Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 2 Nov 2017 16:26:22 +0000 Subject: [PATCH 06/19] Fix save channel subscription --- Source/ARTPushChannelSubscriptions.m | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/ARTPushChannelSubscriptions.m b/Source/ARTPushChannelSubscriptions.m index fcdb04f33..9819028ab 100644 --- a/Source/ARTPushChannelSubscriptions.m +++ b/Source/ARTPushChannelSubscriptions.m @@ -47,14 +47,15 @@ - (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"; + 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 == 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]; From d05a0bab4ecc81aab9366c858c00a79bfb73f0e9 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 2 Nov 2017 16:27:44 +0000 Subject: [PATCH 07/19] Fix save device registration --- Source/ARTPushDeviceRegistrations.m | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Source/ARTPushDeviceRegistrations.m b/Source/ARTPushDeviceRegistrations.m index 024aea78c..778b5600e 100644 --- a/Source/ARTPushDeviceRegistrations.m +++ b/Source/ARTPushDeviceRegistrations.m @@ -46,24 +46,26 @@ - (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; - } 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"]; 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; + callback(nil); } else if (error) { [_logger error:@"%@: save device failed (%@)", NSStringFromClass(self.class), error.localizedDescription]; From 28134c2f081be1555cccb5f5fe3b1a968a77b23c Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 2 Nov 2017 18:15:17 +0000 Subject: [PATCH 08/19] Better errors information --- Source/ARTPushChannelSubscriptions.m | 8 ++++++++ Source/ARTPushDeviceRegistrations.m | 3 +++ Source/ARTRest.m | 6 +----- Source/ARTTypes.h | 4 ++++ Source/ARTTypes.m | 12 ++++++++++++ Source/ARTURLSessionServerTrust.m | 5 ++++- 6 files changed, 32 insertions(+), 6 deletions(-) diff --git a/Source/ARTPushChannelSubscriptions.m b/Source/ARTPushChannelSubscriptions.m index 9819028ab..a93ad7ea1 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; @@ -59,9 +60,12 @@ - (void)save:(ARTPushChannelSubscription *)channelSubscription callback:(void (^ } 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 @@ -179,12 +183,16 @@ - (void)_removeWhere:(NSDictionary *)params callback:(vo [_rest executeRequest:request withAuthOption:ARTAuthenticationOn completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { if (response.statusCode == 200 /*OK*/) { [_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.m b/Source/ARTPushDeviceRegistrations.m index 778b5600e..1cbc3179a 100644 --- a/Source/ARTPushDeviceRegistrations.m +++ b/Source/ARTPushDeviceRegistrations.m @@ -69,9 +69,12 @@ - (void)save:(ARTDeviceDetails *)deviceDetails callback:(void (^)(ARTErrorInfo * } 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 diff --git a/Source/ARTRest.m b/Source/ARTRest.m index a658fd325..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]; } diff --git a/Source/ARTTypes.h b/Source/ARTTypes.h index 84ca7b5df..3a8aaccf2 100644 --- a/Source/ARTTypes.h +++ b/Source/ARTTypes.h @@ -193,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 From ef281c036c6dcfc5a7275b45ceef22bb1fde776e Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 2 Nov 2017 18:35:21 +0000 Subject: [PATCH 09/19] Rename publish notification argument to data to conform specs --- Source/ARTPushAdmin.h | 2 +- Source/ARTPushAdmin.m | 4 ++-- Spec/PushAdmin.swift | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Source/ARTPushAdmin.h b/Source/ARTPushAdmin.h index a4ff4af06..bda43757d 100644 --- a/Source/ARTPushAdmin.h +++ b/Source/ARTPushAdmin.h @@ -19,7 +19,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; /// Publish a push notification. -- (void)publish:(ARTPushRecipient *)recipient notification:(ARTJsonObject *)notification callback:(nullable void (^)(ARTErrorInfo *_Nullable error))callback; +- (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 436f00f5f..4adf0b925 100644 --- a/Source/ARTPushAdmin.m +++ b/Source/ARTPushAdmin.m @@ -34,7 +34,7 @@ - (instancetype)init:(ARTRest *)rest { return self; } -- (void)publish:(ARTPushRecipient *)recipient notification:(ARTJsonObject *)notification callback:(nullable void (^)(ARTErrorInfo *_Nullable error))callback { +- (void)publish:(ARTPushRecipient *)recipient data:(ARTJsonObject *)data callback:(nullable void (^)(ARTErrorInfo *_Nullable error))callback { if (callback) { void (^userCallback)(ARTErrorInfo *error) = callback; callback = ^(ARTErrorInfo *error) { @@ -51,7 +51,7 @@ - (void)publish:(ARTPushRecipient *)recipient notification:(ARTJsonObject *)noti request.HTTPMethod = @"POST"; NSMutableDictionary *body = [NSMutableDictionary dictionary]; [body setObject:recipient forKey:@"recipient"]; - [body addEntriesFromDictionary:notification]; + [body addEntriesFromDictionary:data]; request.HTTPBody = [[_rest defaultEncoder] encode:body error:nil]; [request setValue:[[_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"]; diff --git a/Spec/PushAdmin.swift b/Spec/PushAdmin.swift index ec6cbf15b..61b2ac454 100644 --- a/Spec/PushAdmin.swift +++ b/Spec/PushAdmin.swift @@ -177,7 +177,7 @@ class PushAdmin : QuickSpec { fit("should perform an HTTP request to /push/publish") { waitUntil(timeout: testTimeout) { done in - rest.push.admin.publish(recipient, notification: payload) { error in + rest.push.admin.publish(recipient, data: payload) { error in expect(error).to(beNil()) done() } @@ -201,7 +201,7 @@ class PushAdmin : QuickSpec { it("should reject empty values/data for recipient") { waitUntil(timeout: testTimeout) { done in - rest.push.admin.publish(["client_id": ""], notification: payload) { error in + rest.push.admin.publish(["client_id": ""], data: payload) { error in expect(error).toNot(beNil()) done() } @@ -210,7 +210,7 @@ class PushAdmin : QuickSpec { it("should reject empty values/data for payload") { waitUntil(timeout: testTimeout) { done in - rest.push.admin.publish(recipient, notification: ["notification": ""]) { error in + rest.push.admin.publish(recipient, data: ["notification": ""]) { error in expect(error).toNot(beNil()) done() } @@ -219,7 +219,7 @@ class PushAdmin : QuickSpec { it("should reject an invalid recipient") { waitUntil(timeout: testTimeout) { done in - rest.push.admin.publish(["foo": "bar"], notification: payload) { error in + rest.push.admin.publish(["foo": "bar"], data: payload) { error in expect(error).toNot(beNil()) done() } @@ -228,7 +228,7 @@ class PushAdmin : QuickSpec { it("should reject an invalid notification payload") { waitUntil(timeout: testTimeout) { done in - rest.push.admin.publish(recipient, notification: ["foo": "bar"]) { error in + rest.push.admin.publish(recipient, data: ["foo": "bar"]) { error in expect(error).toNot(beNil()) done() } @@ -248,7 +248,7 @@ class PushAdmin : QuickSpec { done() } - realtime.push.admin.publish(["ablyChannel": channel.name], notification: payload) { error in + realtime.push.admin.publish(["ablyChannel": channel.name], data: payload) { error in expect(error).to(beNil()) } } From e193eca18a660d598327322e93779431dc626524 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Fri, 3 Nov 2017 10:54:08 +0000 Subject: [PATCH 10/19] Add equatable to DeviceDetails --- Source/ARTDeviceDetails.m | 49 +++++++++++++++++++++++++++++++++++ Source/ARTDevicePushDetails.m | 14 ++++++++++ 2 files changed, 63 insertions(+) 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 From 8f484a821a16fa8ce8fa6fc17ab2b829f73a958d Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Fri, 3 Nov 2017 10:54:26 +0000 Subject: [PATCH 11/19] Fix DeviceRegistrations.get --- Source/ARTPushDeviceRegistrations.h | 2 +- Source/ARTPushDeviceRegistrations.m | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Source/ARTPushDeviceRegistrations.h b/Source/ARTPushDeviceRegistrations.h index 71b1c6f40..42e824013 100644 --- a/Source/ARTPushDeviceRegistrations.h +++ b/Source/ARTPushDeviceRegistrations.h @@ -22,7 +22,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)save:(ARTDeviceDetails *)deviceDetails callback:(void (^)(ARTErrorInfo *_Nullable))callback; -- (void)get:(ARTDeviceId *)deviceId callback:(void (^)(ARTPaginatedResult *_Nullable, 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; diff --git a/Source/ARTPushDeviceRegistrations.m b/Source/ARTPushDeviceRegistrations.m index 1cbc3179a..f6f6f6370 100644 --- a/Source/ARTPushDeviceRegistrations.m +++ b/Source/ARTPushDeviceRegistrations.m @@ -81,8 +81,10 @@ - (void)save:(ARTDeviceDetails *)deviceDetails callback:(void (^)(ARTErrorInfo * }); } -- (void)get:(ARTDeviceId *)deviceId callback:(void (^)(ARTPaginatedResult *, ARTErrorInfo *))callback { - [self list:@{@"deviceId": deviceId} callback:callback]; +- (void)get:(ARTDeviceId *)deviceId callback:(void (^)(ARTDeviceDetails *, ARTErrorInfo *))callback { + [self list:@{@"deviceId": deviceId} callback:^(ARTPaginatedResult *result, ARTErrorInfo *error) { + if (callback) callback(result.items.firstObject, error); + }]; } - (void)list:(NSDictionary *)params callback:(void (^)(ARTPaginatedResult *result, ARTErrorInfo *error))callback { From f62b3266796a8e659cfa60847971252d4b63c0d4 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Fri, 3 Nov 2017 10:54:42 +0000 Subject: [PATCH 12/19] Add validations to PushAdmin.publish --- Source/ARTPushAdmin.m | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Source/ARTPushAdmin.m b/Source/ARTPushAdmin.m index 4adf0b925..52e148d17 100644 --- a/Source/ARTPushAdmin.m +++ b/Source/ARTPushAdmin.m @@ -47,6 +47,16 @@ - (void)publish:(ARTPushRecipient *)recipient data:(ARTJsonObject *)data callbac 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]; From 66d34b60a396ab815acc95c24c7e0119cf66775f Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Fri, 3 Nov 2017 10:55:30 +0000 Subject: [PATCH 13/19] Fix RHS1a --- Spec/PushAdmin.swift | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/Spec/PushAdmin.swift b/Spec/PushAdmin.swift index 61b2ac454..57b4e9a14 100644 --- a/Spec/PushAdmin.swift +++ b/Spec/PushAdmin.swift @@ -201,8 +201,11 @@ class PushAdmin : QuickSpec { it("should reject empty values/data for recipient") { waitUntil(timeout: testTimeout) { done in - rest.push.admin.publish(["client_id": ""], data: payload) { error in - expect(error).toNot(beNil()) + rest.push.admin.publish(["clientId": ""], data: payload) { error in + guard let error = error else { + fail("Error is missing"); done(); return + } + expect(error.message).to(contain("recipient is empty")) done() } } @@ -211,7 +214,10 @@ class PushAdmin : QuickSpec { it("should reject empty values/data for payload") { waitUntil(timeout: testTimeout) { done in rest.push.admin.publish(recipient, data: ["notification": ""]) { error in - expect(error).toNot(beNil()) + guard let error = error else { + fail("Error is missing"); done(); return + } + expect(error.message).to(contain("payload is empty")) done() } } @@ -220,7 +226,10 @@ class PushAdmin : QuickSpec { it("should reject an invalid recipient") { waitUntil(timeout: testTimeout) { done in rest.push.admin.publish(["foo": "bar"], data: payload) { error in - expect(error).toNot(beNil()) + guard let error = error else { + fail("Error is missing"); done(); return + } + expect(error.message).to(contain("invalid recipient")) done() } } @@ -229,7 +238,10 @@ class PushAdmin : QuickSpec { it("should reject an invalid notification payload") { waitUntil(timeout: testTimeout) { done in rest.push.admin.publish(recipient, data: ["foo": "bar"]) { error in - expect(error).toNot(beNil()) + guard let error = error else { + fail("Error is missing"); done(); return + } + expect(error.message).to(contain("invalid payload")) done() } } From 39930741700fbf678ca657e0b1dfa1abe1161ba4 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Fri, 3 Nov 2017 10:55:38 +0000 Subject: [PATCH 14/19] Fix RHS1b1 --- Spec/PushAdmin.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Spec/PushAdmin.swift b/Spec/PushAdmin.swift index 57b4e9a14..0d5d5cb8e 100644 --- a/Spec/PushAdmin.swift +++ b/Spec/PushAdmin.swift @@ -276,6 +276,9 @@ class PushAdmin : QuickSpec { 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() From b09c82246752190cc763e1393b1494adda1fc87b Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 8 Nov 2017 20:30:44 +0000 Subject: [PATCH 15/19] Fix PushAdmin implementation --- Source/ARTJsonLikeEncoder.m | 2 +- Source/ARTPushChannelSubscription.m | 4 + Source/ARTPushChannelSubscriptions.m | 8 +- Source/ARTPushDeviceRegistrations.m | 112 +++++++++++++++++++--- Source/ARTStatus.h | 1 + Source/ARTStatus.m | 4 + Spec/PushAdmin.swift | 133 ++++++++++++++++++--------- 7 files changed, 203 insertions(+), 61 deletions(-) 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/ARTPushChannelSubscription.m b/Source/ARTPushChannelSubscription.m index 9481f1b3e..441a44d45 100644 --- a/Source/ARTPushChannelSubscription.m +++ b/Source/ARTPushChannelSubscription.m @@ -26,4 +26,8 @@ - (instancetype)initWithClientId:(NSString *)clientId channel:(NSString *)channe return self; } +- (NSString *)description { + return [NSString stringWithFormat:@"%@ - \n\t deviceId: %@; clientId: %@; \n\t channel: %@;", [super description], self.deviceId, self.clientId, self.channel]; +} + @end diff --git a/Source/ARTPushChannelSubscriptions.m b/Source/ARTPushChannelSubscriptions.m index a93ad7ea1..58da42965 100644 --- a/Source/ARTPushChannelSubscriptions.m +++ b/Source/ARTPushChannelSubscriptions.m @@ -85,14 +85,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.encoders[response.MIMEType] 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 @@ -181,7 +179,7 @@ - (void)_removeWhere:(NSDictionary *)params callback:(vo [_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 == 204 /*not returning any content*/) { [_logger debug:__FILE__ line:__LINE__ message:@"%@: channel subscription removed successfully", NSStringFromClass(self.class)]; callback(nil); } diff --git a/Source/ARTPushDeviceRegistrations.m b/Source/ARTPushDeviceRegistrations.m index f6f6f6370..b166d6c0b 100644 --- a/Source/ARTPushDeviceRegistrations.m +++ b/Source/ARTPushDeviceRegistrations.m @@ -62,10 +62,17 @@ - (void)save:(ARTDeviceDetails *)deviceDetails callback:(void (^)(ARTErrorInfo * [_logger debug:__FILE__ line:__LINE__ message:@"save device with request %@", request]; [_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; - callback(nil); + 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]; @@ -81,10 +88,52 @@ - (void)save:(ARTDeviceDetails *)deviceDetails callback:(void (^)(ARTErrorInfo * }); } -- (void)get:(ARTDeviceId *)deviceId callback:(void (^)(ARTDeviceDetails *, ARTErrorInfo *))callback { - [self list:@{@"deviceId": deviceId} callback:^(ARTPaginatedResult *result, ARTErrorInfo *error) { - if (callback) callback(result.items.firstObject, error); +- (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 +}); } - (void)list:(NSDictionary *)params callback:(void (^)(ARTPaginatedResult *result, ARTErrorInfo *error))callback { @@ -114,7 +163,40 @@ - (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) { + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[[NSURL URLWithString:@"/push/deviceRegistrations"] URLByAppendingPathComponent:deviceId]]; + 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 == 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 { @@ -135,16 +217,20 @@ - (void)removeWhere:(NSDictionary *)params callback:(voi 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 == 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/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/Spec/PushAdmin.swift b/Spec/PushAdmin.swift index 0d5d5cb8e..6b652036f 100644 --- a/Spec/PushAdmin.swift +++ b/Spec/PushAdmin.swift @@ -102,6 +102,11 @@ class PushAdmin : QuickSpec { 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()) @@ -113,7 +118,7 @@ class PushAdmin : QuickSpec { defer { group.leave() } - assert(error == nil) + assert(error == nil, error?.message ?? "no message") } } @@ -123,7 +128,7 @@ class PushAdmin : QuickSpec { defer { group.leave() } - assert(error == nil) + assert(error == nil, error?.message ?? "no message") } } @@ -157,7 +162,7 @@ class PushAdmin : QuickSpec { var httpExecutor: MockHTTPExecutor! let recipient = [ - "client_id": "bob" + "clientId": "bob" ] let payload = [ @@ -175,7 +180,7 @@ class PushAdmin : QuickSpec { // RHS1a describe("publish") { - fit("should perform an HTTP request to /push/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()) @@ -183,7 +188,16 @@ class PushAdmin : QuickSpec { } } - switch extractBodyAsMsgPack(httpExecutor.requests.first) { + 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): @@ -199,69 +213,105 @@ class PushAdmin : QuickSpec { } } - it("should reject empty values/data for recipient") { + 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 - rest.push.admin.publish(["clientId": ""], data: payload) { error in - guard let error = error else { - fail("Error is missing"); done(); return - } - expect(error.message).to(contain("recipient is empty")) + channel.attach() { error in + expect(error).to(beNil()) done() } } - } - it("should reject empty values/data for payload") { waitUntil(timeout: testTimeout) { done in - rest.push.admin.publish(recipient, data: ["notification": ""]) { error in - guard let error = error else { - fail("Error is missing"); done(); return + 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(error.message).to(contain("payload is empty")) - done() + 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 reject an invalid recipient") { + 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 - rest.push.admin.publish(["foo": "bar"], data: payload) { error 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.message).to(contain("invalid recipient")) + expect(error.statusCode) == 400 + expect(error.message).to(contain("recipient must contain a deviceId, clientId, or transportType")) done() } } } - it("should reject an invalid notification payload") { + 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 - rest.push.admin.publish(recipient, data: ["foo": "bar"]) { error 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).to(contain("invalid payload")) + expect(error.message.lowercased()).to(contain("recipient is missing")) done() } } } - it("should send a notification") { + it("should fail with an empty payload") { let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) - let channel = realtime.channels.get("push-test") + let channel = realtime.channels.get("pushenabled:push_admin_publish-empty-payload") waitUntil(timeout: testTimeout) { done in - channel.subscribe { message in - guard let data = message.data as? NSDictionary else { - fail("Message data should be a dictionary"); done(); return - } - expect(data).to(equal(payload as NSDictionary)) + channel.attach() { error in + expect(error).to(beNil()) done() } + } - realtime.push.admin.publish(["ablyChannel": channel.name], data: payload) { error in - expect(error).to(beNil()) + 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() } } } @@ -286,7 +336,7 @@ class PushAdmin : QuickSpec { } } - it("should not return a device if it does not exist") { + 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 @@ -318,28 +368,28 @@ class PushAdmin : QuickSpec { } } - it("should list devices by client id") { + 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) == 2 + expect(result.items.count) == allDeviceDetails.filter({ $0.clientId == "clientA" }).count expect(error).to(beNil()) done() } } } - it("should list devices sorted") { + 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) == 0 + expect(result.items.count) == allDeviceDetails.count expect(error).to(beNil()) done() } @@ -437,17 +487,16 @@ class PushAdmin : QuickSpec { } // RHS1c2 - context("listChannels") { + 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.count) == 1 - expect(result.items.first) == "pushenabled:qux" - expect(error).to(beNil()) + expect(result.items as [String]).to(contain(allSubscriptionsChannels + [subscription.channel])) done() } } From e8b043e35d84be56c49bcd679eaee9ad2b00554a Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 9 Nov 2017 00:02:04 +0000 Subject: [PATCH 16/19] Fix ambiguous reference to member 'dataTask(with:completionHandler:)' --- Examples/Tests/Podfile | 2 +- Examples/Tests/Podfile.lock | 17 +++++++--- .../Tests/Tests.xcodeproj/project.pbxproj | 32 +++++++++++++++++-- Examples/Tests/TestsTests/TestsTests.swift | 16 +++++----- 4 files changed, 51 insertions(+), 16 deletions(-) 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) } + } From 0de801ce9edde18d15dd0d83effa804dd5b9a996 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 9 Nov 2017 00:02:46 +0000 Subject: [PATCH 17/19] fixup! Fix PushAdmin implementation --- Source/ARTPushChannelSubscription.m | 40 +++++++++++++++++++++++++++++ Spec/PushAdmin.swift | 17 ++++++++++++ 2 files changed, 57 insertions(+) diff --git a/Source/ARTPushChannelSubscription.m b/Source/ARTPushChannelSubscription.m index 441a44d45..15289bda8 100644 --- a/Source/ARTPushChannelSubscription.m +++ b/Source/ARTPushChannelSubscription.m @@ -26,8 +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/Spec/PushAdmin.swift b/Spec/PushAdmin.swift index 6b652036f..28889ef78 100644 --- a/Spec/PushAdmin.swift +++ b/Spec/PushAdmin.swift @@ -541,6 +541,17 @@ class PushAdmin : QuickSpec { 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()) @@ -548,6 +559,12 @@ class PushAdmin : QuickSpec { } } + waitUntil(timeout: testTimeout) { done in + delay(3) { + done() + } + } + waitUntil(timeout: testTimeout) { done in realtime.push.admin.channelSubscriptions.list(params) { result, error in guard let result = result else { From 1ec59d0c83dda40113d3377b972dc45fdb38f5dd Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 9 Nov 2017 00:31:30 +0000 Subject: [PATCH 18/19] CI test --- Spec/PushAdmin.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Spec/PushAdmin.swift b/Spec/PushAdmin.swift index 28889ef78..b2d0cd4d9 100644 --- a/Spec/PushAdmin.swift +++ b/Spec/PushAdmin.swift @@ -178,7 +178,7 @@ class PushAdmin : QuickSpec { } // RHS1a - describe("publish") { + fdescribe("publish") { it("should perform an HTTP request to /push/publish") { waitUntil(timeout: testTimeout) { done in @@ -318,7 +318,7 @@ class PushAdmin : QuickSpec { } - describe("Device Registrations") { + fdescribe("Device Registrations") { // RHS1b1 context("get") { @@ -452,7 +452,7 @@ class PushAdmin : QuickSpec { } - describe("Channel Subscriptions") { + fdescribe("Channel Subscriptions") { let subscription = ARTPushChannelSubscription(clientId: "newClient", channel: "pushenabled:qux") From 9fe641783faa21f4776398205ae8055ab8b65436 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 9 Nov 2017 10:27:48 +0000 Subject: [PATCH 19/19] fixup! Fix PushAdmin implementation --- Source/ARTClientOptions.h | 1 + Source/ARTClientOptions.m | 4 +- Source/ARTPushChannelSubscriptions.m | 13 ++- Source/ARTPushDeviceRegistrations.m | 19 +++- Spec/PushAdmin.swift | 131 ++++++++++++++++++++------- 5 files changed, 125 insertions(+), 43 deletions(-) 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/ARTPushChannelSubscriptions.m b/Source/ARTPushChannelSubscriptions.m index 58da42965..76717bb45 100644 --- a/Source/ARTPushChannelSubscriptions.m +++ b/Source/ARTPushChannelSubscriptions.m @@ -47,14 +47,18 @@ - (void)save:(ARTPushChannelSubscription *)channelSubscription callback:(void (^ dispatch_async(_queue, ^{ ART_TRY_OR_REPORT_CRASH_START(_rest) { - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/push/channelSubscriptions"]]; + 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 == 201 /*CREATED*/) { + if (response.statusCode == 200 /*Ok*/ || response.statusCode == 201 /*Created*/) { [_logger debug:__FILE__ line:__LINE__ message:@"channel subscription saved successfully"]; callback(nil); } @@ -174,12 +178,15 @@ - (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 == 204 /*not returning any content*/) { + 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); } diff --git a/Source/ARTPushDeviceRegistrations.m b/Source/ARTPushDeviceRegistrations.m index b166d6c0b..6a931b20f 100644 --- a/Source/ARTPushDeviceRegistrations.m +++ b/Source/ARTPushDeviceRegistrations.m @@ -46,7 +46,11 @@ - (void)save:(ARTDeviceDetails *)deviceDetails callback:(void (^)(ARTErrorInfo * dispatch_async(_queue, ^{ ART_TRY_OR_REPORT_CRASH_START(_rest) { - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[[NSURL URLWithString:@"/push/deviceRegistrations"] URLByAppendingPathComponent:deviceDetails.id]]; + 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:[components URL]]; request.HTTPMethod = @"PUT"; request.HTTPBody = [[_rest defaultEncoder] encodeDeviceDetails:deviceDetails error:nil]; [request setValue:[[_rest defaultEncoder] mimeType] forHTTPHeaderField:@"Content-Type"]; @@ -175,13 +179,17 @@ - (void)remove:(NSString *)deviceId callback:(void (^)(ARTErrorInfo *error))call dispatch_async(_queue, ^{ ART_TRY_OR_REPORT_CRASH_START(_rest) { - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[[NSURL URLWithString:@"/push/deviceRegistrations"] URLByAppendingPathComponent:deviceId]]; + 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 == 204 /*not returning any content*/) { + 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); } @@ -214,12 +222,15 @@ - (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 devices with request %@", request]; [_rest executeRequest:request withAuthOption:ARTAuthenticationOn completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { - if (response.statusCode == 204 /*not returning any content*/) { + 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); } diff --git a/Spec/PushAdmin.swift b/Spec/PushAdmin.swift index b2d0cd4d9..be66aeb85 100644 --- a/Spec/PushAdmin.swift +++ b/Spec/PushAdmin.swift @@ -414,7 +414,9 @@ class PushAdmin : QuickSpec { // RHS1b4 context("remove") { it("should unregister a device") { - let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + 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()) @@ -427,7 +429,9 @@ class PushAdmin : QuickSpec { // RHS1b3 context("save") { it("should register a device") { - let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + 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()) @@ -438,15 +442,70 @@ class PushAdmin : QuickSpec { } // RHS1b5 - context("removeWhere") { + context("removeWhere") { [allDeviceDetails] in it("should unregister a device") { - let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + 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(["clientId": "clientA"]) { error 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() + } + } } } @@ -530,7 +589,9 @@ class PushAdmin : QuickSpec { // RHS1c5 context("removeWhere") { it("should remove by cliendId") { - let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + let options = AblyTests.commonAppSetup() + options.pushFullWait = true + let realtime = ARTRealtime(options: options) let params = [ "clientId": "clientB" @@ -559,12 +620,6 @@ class PushAdmin : QuickSpec { } } - waitUntil(timeout: testTimeout) { done in - delay(3) { - done() - } - } - waitUntil(timeout: testTimeout) { done in realtime.push.admin.channelSubscriptions.list(params) { result, error in guard let result = result else { @@ -588,7 +643,9 @@ class PushAdmin : QuickSpec { } it("should remove by cliendId and channel") { - let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + let options = AblyTests.commonAppSetup() + options.pushFullWait = true + let realtime = ARTRealtime(options: options) let params = [ "clientId": "clientB", @@ -599,6 +656,17 @@ class PushAdmin : QuickSpec { 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()) @@ -616,23 +684,15 @@ class PushAdmin : QuickSpec { 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 deviceId") { - let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) + let options = AblyTests.commonAppSetup() + options.pushFullWait = true + let realtime = ARTRealtime(options: options) let params = [ - "deviceId": "subscriptionBarDevice2.deviceId", + "deviceId": "deviceDetails2ClientA", ] let expectedRemoved = [ @@ -640,6 +700,17 @@ class PushAdmin : QuickSpec { 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()) @@ -657,16 +728,6 @@ class PushAdmin : QuickSpec { 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 not remove by inexistent deviceId") {