From 77ba2986f5ace3c211269584f990ae7a873cd165 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 19 Oct 2016 13:39:57 +0100 Subject: [PATCH 01/13] Add ARTAuthDetails --- Ably.xcodeproj/project.pbxproj | 8 ++++++++ Source/ARTAuthDetails.h | 21 +++++++++++++++++++++ Source/ARTAuthDetails.m | 23 +++++++++++++++++++++++ Source/ARTProtocolMessage.h | 33 ++++++++++++++++++--------------- Source/Ably.h | 1 + 5 files changed, 71 insertions(+), 15 deletions(-) create mode 100644 Source/ARTAuthDetails.h create mode 100644 Source/ARTAuthDetails.m diff --git a/Ably.xcodeproj/project.pbxproj b/Ably.xcodeproj/project.pbxproj index c81782371..b366e36fa 100644 --- a/Ably.xcodeproj/project.pbxproj +++ b/Ably.xcodeproj/project.pbxproj @@ -98,6 +98,8 @@ D71D30041C5F7B2F002115B0 /* RealtimeClientChannels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71D30031C5F7B2F002115B0 /* RealtimeClientChannels.swift */; }; D72304701BB72CED00F1ABDA /* RealtimeClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723046F1BB72CED00F1ABDA /* RealtimeClient.swift */; }; D72768211C9C19040022F8B2 /* RestClientPresence.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72768201C9C19040022F8B2 /* RestClientPresence.swift */; }; + D73691FF1DB788C40062C150 /* ARTAuthDetails.h in Headers */ = {isa = PBXBuildFile; fileRef = D73691FD1DB788C40062C150 /* ARTAuthDetails.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D73692001DB788C40062C150 /* ARTAuthDetails.m in Sources */ = {isa = PBXBuildFile; fileRef = D73691FE1DB788C40062C150 /* ARTAuthDetails.m */; }; D746AE1D1BBB5207003ECEF8 /* ARTDataQuery.h in Headers */ = {isa = PBXBuildFile; fileRef = D746AE1A1BBB5207003ECEF8 /* ARTDataQuery.h */; settings = {ATTRIBUTES = (Public, ); }; }; D746AE1E1BBB5207003ECEF8 /* ARTDataQuery+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D746AE1B1BBB5207003ECEF8 /* ARTDataQuery+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; D746AE1F1BBB5207003ECEF8 /* ARTDataQuery.m in Sources */ = {isa = PBXBuildFile; fileRef = D746AE1C1BBB5207003ECEF8 /* ARTDataQuery.m */; }; @@ -333,6 +335,8 @@ D71D30031C5F7B2F002115B0 /* RealtimeClientChannels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealtimeClientChannels.swift; sourceTree = ""; }; D723046F1BB72CED00F1ABDA /* RealtimeClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealtimeClient.swift; sourceTree = ""; }; D72768201C9C19040022F8B2 /* RestClientPresence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestClientPresence.swift; sourceTree = ""; }; + D73691FD1DB788C40062C150 /* ARTAuthDetails.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTAuthDetails.h; sourceTree = ""; }; + D73691FE1DB788C40062C150 /* ARTAuthDetails.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTAuthDetails.m; sourceTree = ""; }; D746AE1A1BBB5207003ECEF8 /* ARTDataQuery.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTDataQuery.h; sourceTree = ""; }; D746AE1B1BBB5207003ECEF8 /* ARTDataQuery+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARTDataQuery+Private.h"; sourceTree = ""; }; D746AE1C1BBB5207003ECEF8 /* ARTDataQuery.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTDataQuery.m; sourceTree = ""; }; @@ -673,6 +677,8 @@ D7D8F81F1BC2BE15009718F2 /* ARTAuthOptions.h */, D7D5A6991CA3D9040071BD6D /* ARTAuthOptions+Private.h */, D7D8F8201BC2BE15009718F2 /* ARTAuthOptions.m */, + D73691FD1DB788C40062C150 /* ARTAuthDetails.h */, + D73691FE1DB788C40062C150 /* ARTAuthDetails.m */, D7D8F8271BC2C706009718F2 /* ARTTokenRequest.h */, D7D8F8281BC2C706009718F2 /* ARTTokenRequest.m */, D7D8F8291BC2C706009718F2 /* ARTTokenParams.h */, @@ -855,6 +861,7 @@ EBFA366E1D58B05000B09AA7 /* ARTRestPresence+Private.h in Headers */, EB2D85011CD769C800F23CDA /* ARTOSReachability.h in Headers */, 960D07971A46FFC300ED8C8C /* ARTRest+Private.h in Headers */, + D73691FF1DB788C40062C150 /* ARTAuthDetails.h in Headers */, 1C05CF201AC1D7EB00687AC9 /* ARTRealtime+Private.h in Headers */, D7F1D37A1BF4E33A001A4B5E /* ARTRestChannel+Private.h in Headers */, 85B2C2191B6FE8DE00EA5254 /* CompatibilityMacros.h in Headers */, @@ -1181,6 +1188,7 @@ 1C55427D1B148306003068DB /* ARTStatus.m in Sources */, D7B17EE41C07208B00A6958E /* ARTConnectionDetails.m in Sources */, 96BF61591A35B52C004CF2B3 /* ARTHttp.m in Sources */, + D73692001DB788C40062C150 /* ARTAuthDetails.m in Sources */, 1C578E201B3435CA00EF46EC /* ARTFallback.m in Sources */, 96A507B61A37881C0077CDF8 /* ARTNSDate+ARTUtil.m in Sources */, 850BFB4D1B79323C009D0ADD /* ARTPaginatedResult.m in Sources */, diff --git a/Source/ARTAuthDetails.h b/Source/ARTAuthDetails.h new file mode 100644 index 000000000..52ca8e49d --- /dev/null +++ b/Source/ARTAuthDetails.h @@ -0,0 +1,21 @@ +// +// ARTAuthDetails.h +// Ably +// +// Created by Ricardo Pereira on 19/10/2016. +// Copyright © 2016 Ably. All rights reserved. +// + +#import +#import "CompatibilityMacros.h" + +ART_ASSUME_NONNULL_BEGIN + +/// Used with an AUTH protocol messages to send authentication details +@interface ARTAuthDetails : NSObject + +@property (nonatomic, copy) NSString *accessToken; + +@end + +ART_ASSUME_NONNULL_END diff --git a/Source/ARTAuthDetails.m b/Source/ARTAuthDetails.m new file mode 100644 index 000000000..42c7ca9e1 --- /dev/null +++ b/Source/ARTAuthDetails.m @@ -0,0 +1,23 @@ +// +// ARTAuthDetails.m +// Ably +// +// Created by Ricardo Pereira on 19/10/2016. +// Copyright © 2016 Ably. All rights reserved. +// + +#import "ARTAuthDetails.h" + +@implementation ARTAuthDetails + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ - \n\t accessToken: %@; \n", [super description], self.accessToken]; +} + +- (id)copyWithZone:(NSZone *)zone { + ARTAuthDetails *authDetails = [[[self class] allocWithZone:zone] init]; + authDetails.accessToken = self.accessToken; + return authDetails; +} + +@end diff --git a/Source/ARTProtocolMessage.h b/Source/ARTProtocolMessage.h index 6d3eaa1f7..72cf8c08a 100644 --- a/Source/ARTProtocolMessage.h +++ b/Source/ARTProtocolMessage.h @@ -12,6 +12,7 @@ #import "ARTPresenceMessage.h" @class ARTConnectionDetails; +@class ARTAuthDetails; @class ARTErrorInfo; @class ARTMessage; @class ARTPresenceMessage; @@ -34,6 +35,7 @@ typedef NS_ENUM(NSUInteger, ARTProtocolMessageAction) { ARTProtocolMessagePresence = 14, ARTProtocolMessageMessage = 15, ARTProtocolMessageSync = 16, + ARTProtocolMessageAuth = 17, }; ART_ASSUME_NONNULL_BEGIN @@ -45,21 +47,22 @@ ART_ASSUME_NONNULL_BEGIN */ @interface ARTProtocolMessage : NSObject -@property (readwrite, assign, nonatomic) ARTProtocolMessageAction action; -@property (readwrite, assign, nonatomic) int count; -@property (art_nullable, readwrite, strong, nonatomic) ARTErrorInfo *error; -@property (art_nullable, readwrite, strong, nonatomic) NSString *id; -@property (art_nullable, readwrite, strong, nonatomic) NSString *channel; -@property (art_nullable, readwrite, strong, nonatomic) NSString *channelSerial; -@property (art_nullable, readwrite, strong, nonatomic) NSString *connectionId; -@property (art_nullable, readwrite, strong, nonatomic, getter=getConnectionKey) NSString *connectionKey; -@property (readwrite, assign, nonatomic) int64_t connectionSerial; -@property (readwrite, assign, nonatomic) int64_t msgSerial; -@property (art_nullable, readwrite, strong, nonatomic) NSDate *timestamp; -@property (art_nullable, readwrite, strong, nonatomic) __GENERIC(NSArray, ARTMessage *) *messages; -@property (art_nullable, readwrite, strong, nonatomic) __GENERIC(NSArray, ARTPresenceMessage *) *presence; -@property (readwrite, assign, nonatomic) int64_t flags; -@property (art_nullable, readwrite, nonatomic) ARTConnectionDetails *connectionDetails; +@property (assign, nonatomic) ARTProtocolMessageAction action; +@property (assign, nonatomic) int count; +@property (art_nullable, strong, nonatomic) ARTErrorInfo *error; +@property (art_nullable, strong, nonatomic) NSString *id; +@property (art_nullable, strong, nonatomic) NSString *channel; +@property (art_nullable, strong, nonatomic) NSString *channelSerial; +@property (art_nullable, strong, nonatomic) NSString *connectionId; +@property (art_nullable, strong, nonatomic, getter=getConnectionKey) NSString *connectionKey; +@property (assign, nonatomic) int64_t connectionSerial; +@property (assign, nonatomic) int64_t msgSerial; +@property (art_nullable, strong, nonatomic) NSDate *timestamp; +@property (art_nullable, strong, nonatomic) __GENERIC(NSArray, ARTMessage *) *messages; +@property (art_nullable, strong, nonatomic) __GENERIC(NSArray, ARTPresenceMessage *) *presence; +@property (assign, nonatomic) int64_t flags; +@property (art_nullable, nonatomic) ARTConnectionDetails *connectionDetails; +@property (art_nullable, nonatomic) ARTAuthDetails *auth; @end diff --git a/Source/Ably.h b/Source/Ably.h index 7d73ce429..444fd8a56 100644 --- a/Source/Ably.h +++ b/Source/Ably.h @@ -18,6 +18,7 @@ FOUNDATION_EXPORT const unsigned char ablyVersionString[]; #import "ARTTypes.h" #import "ARTAuth.h" +#import "ARTAuthDetails.h" #import "ARTConnection.h" #import "ARTConnectionDetails.h" #import "ARTHttp.h" From e2a565551acdcf4dc97e210d6899f22da7326097 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 19 Oct 2016 13:40:06 +0100 Subject: [PATCH 02/13] RTC8a --- Spec/RealtimeClient.swift | 58 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index aa5a2ace7..117b44fb1 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -352,6 +352,64 @@ class RealtimeClient: QuickSpec { } } + // RTC8 + context("Auth#authorize should upgrade the connection with current token") { + + // RTC8a + it("in the CONNECTED state and auth#authorize is called, the client must obtain a new token, send an AUTH ProtocolMessage with an auth attribute") { + let options = AblyTests.commonAppSetup() + options.autoConnect = false + options.useTokenAuth = true + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + client.setTransportClass(TestProxyTransport.self) + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + client.connect() + } + + guard let firstToken = client.auth.tokenDetails?.token else { + fail("Client has no token"); return + } + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + waitUntil(timeout: testTimeout) { done in + client.auth.authorize(nil, options: nil) { tokenDetails, error in + expect(error).to(beNil()) + guard let tokenDetails = tokenDetails else { + fail("TokenDetails is nil"); done(); return + } + + let authMessages = transport.protocolMessagesSent.filter({ $0.action == .Auth }) + expect(authMessages).to(haveCount(1)) + + guard let authMessage = authMessages.first else { + fail("Missing AUTH protocol message"); done(); return + } + + expect(authMessage.auth).toNot(beNil()) + + guard let accessToken = authMessage.auth?.accessToken else { + fail("Missing accessToken from AUTH ProtocolMessage auth attribute"); done(); return + } + + expect(accessToken).toNot(equal(firstToken)) + expect(tokenDetails.token).toNot(equal(firstToken)) + expect(tokenDetails.token).to(equal(accessToken)) + done() + } + } + } + + } + it("should never register any connection listeners for internal use with the public EventEmitter") { let options = AblyTests.commonAppSetup() options.autoConnect = false From 8aec9e00c4bc441b6a936f0c47ab83326c2e776e Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 19 Oct 2016 14:21:16 +0100 Subject: [PATCH 03/13] RTC8a1 (part 1) --- Spec/RealtimeClient.swift | 56 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index 117b44fb1..c1efd489e 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -408,6 +408,62 @@ class RealtimeClient: QuickSpec { } } + // RTC8a1 - part 1 + it("when the authentication token change is successful, then the client should receive a new CONNECTED ProtocolMessage") { + let options = AblyTests.commonAppSetup() + options.autoConnect = false + let testToken = getTestToken() + options.token = testToken + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + client.setTransportClass(TestProxyTransport.self) + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + client.connect() + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + + client.connection.once(.Connected) { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); partialDone(); return + } + expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.Connected)) + expect(stateChange.reason).toNot(beNil()) + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); partialDone(); return + } + let connectedMessages = transport.protocolMessagesReceived.filter{ $0.action == .Connected } + expect(connectedMessages).to(haveCount(2)) + + guard let connectedAfterAuth = connectedMessages.last, connectionDetailsAfterAuth = connectedAfterAuth.connectionDetails else { + fail("Missing CONNECTED protocol message after AUTH protocol message"); partialDone(); return + } + + expect(client.auth.clientId).to(equal(connectionDetailsAfterAuth.clientId)) + expect(client.connection.key).to(equal(connectionDetailsAfterAuth.connectionKey)) + partialDone() + } + + client.auth.authorize(nil, options: nil) { tokenDetails, error in + expect(error).to(beNil()) + guard let tokenDetails = tokenDetails else { + fail("TokenDetails is nil"); partialDone(); return + } + expect(tokenDetails.token).toNot(equal(testToken)) + partialDone() + } + } + + expect(client.auth.tokenDetails?.token).toNot(equal(testToken)) + } + } it("should never register any connection listeners for internal use with the public EventEmitter") { From 9e92abb093f375b1c46bb786c118ee8a2dc73526 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 19 Oct 2016 16:03:39 +0100 Subject: [PATCH 04/13] RTC8a1 (part 2) --- Spec/RealtimeClient.swift | 64 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index c1efd489e..0512b101f 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -464,6 +464,70 @@ class RealtimeClient: QuickSpec { expect(client.auth.tokenDetails?.token).toNot(equal(testToken)) } + // RTC8a1 - part 2 + it("performs an upgrade of capabilities without any loss of continuity or connectivity during the upgrade process") { + let options = AblyTests.commonAppSetup() + options.autoConnect = false + let testToken = getTestToken(capability: "{\"test\":[\"subscribe\"]}") + options.token = testToken + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + client.setTransportClass(TestProxyTransport.self) + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + client.connect() + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + + client.connection.once(.Connected) { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); partialDone(); return + } + expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.Connected)) + expect(stateChange.reason).toNot(beNil()) + partialDone() + } + + client.connection.once(.Disconnected) { _ in + fail("Lost connectivity") + } + client.connection.once(.Suspended) { _ in + fail("Lost continuity") + } + client.connection.once(.Failed) { _ in + fail("Should not receive any failure") + } + + let tokenParams = ARTTokenParams() + tokenParams.capability = "{\"*\":[\"*\"]}" + + client.auth.authorize(tokenParams, options: nil) { tokenDetails, error in + expect(error).to(beNil()) + guard let tokenDetails = tokenDetails else { + fail("TokenDetails is nil"); partialDone(); return + } + expect(tokenDetails.token).toNot(equal(testToken)) + expect(tokenDetails.capability).to(equal(tokenParams.capability)) + partialDone() + } + } + + expect(client.auth.tokenDetails?.token).toNot(equal(testToken)) + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + expect(transport.protocolMessagesReceived.filter{ $0.action == .Disconnected }).to(beEmpty()) + expect(transport.protocolMessagesReceived.filter{ $0.action == .Error }).to(beEmpty()) + } + } it("should never register any connection listeners for internal use with the public EventEmitter") { From b8642b293ea82d105d13c4315bc4399f501b6816 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 19 Oct 2016 16:11:38 +0100 Subject: [PATCH 05/13] RTC8a1 (part 3) --- Spec/RealtimeClient.swift | 58 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index 0512b101f..ed443e21e 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -528,6 +528,64 @@ class RealtimeClient: QuickSpec { expect(transport.protocolMessagesReceived.filter{ $0.action == .Error }).to(beEmpty()) } + // RTC8a1 - part 3 + it("when capabilities are downgraded, client should receive an ERROR ProtocolMessage with a channel property") { + let options = AblyTests.commonAppSetup() + options.autoConnect = false + let testToken = getTestToken() + options.token = testToken + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + client.setTransportClass(TestProxyTransport.self) + + let channel = client.channels.get("test") + waitUntil(timeout: testTimeout) { done in + client.connect() + channel.attach() { _ in + done() + } + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + + channel.once(.Failed) { error in + guard let error = error else { + fail("ErrorInfo is nil"); partialDone(); return + } + expect(error).to(equal(channel.errorReason)) + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); partialDone(); return + } + + let errorMessages = transport.protocolMessagesReceived.filter{ $0.action == .Error } + expect(errorMessages).to(haveCount(1)) + + guard let errorMessage = errorMessages.first else { + fail("Missing ERROR protocol message"); partialDone(); return + } + expect(errorMessage.channel).to(equal("test")) + partialDone() + } + + let tokenParams = ARTTokenParams() + tokenParams.capability = "{\"test\":[\"subscribe\"]}" + + client.auth.authorize(tokenParams, options: nil) { tokenDetails, error in + expect(error).to(beNil()) + guard let tokenDetails = tokenDetails else { + fail("TokenDetails is nil"); partialDone(); return + } + expect(tokenDetails.token).toNot(equal(testToken)) + expect(tokenDetails.capability).to(equal(tokenParams.capability)) + partialDone() + } + } + + expect(client.auth.tokenDetails?.token).toNot(equal(testToken)) + } + } it("should never register any connection listeners for internal use with the public EventEmitter") { From 833780996b3cb363448fad5c7789b6faa6be4d3c Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 19 Oct 2016 16:49:30 +0100 Subject: [PATCH 06/13] RTC8a2 --- Spec/RealtimeClient.swift | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index ed443e21e..9e463076d 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -586,6 +586,52 @@ class RealtimeClient: QuickSpec { expect(client.auth.tokenDetails?.token).toNot(equal(testToken)) } + // RTC8a2 + it("when the authentication token change fails, client should receive an ERROR ProtocolMessage triggering the connection to transition to the FAILED state") { + let options = AblyTests.commonAppSetup() + options.autoConnect = false + options.clientId = "ios" + options.useTokenAuth = true + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + client.setTransportClass(TestProxyTransport.self) + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + client.connect() + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + + let tokenParams = ARTTokenParams() + tokenParams.clientId = "android" + + client.connection.once(.Failed) { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); partialDone(); return + } + expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.Connected)) + expect(stateChange.reason?.code).to(equal(40102)) + expect(stateChange.reason?.description).to(contain("incompatible credentials")) + partialDone() + } + + client.auth.authorize(tokenParams, options: nil) { tokenDetails, error in + guard let error = error else { + fail("ErrorInfo is nil"); partialDone(); return + } + expect(error.code).to(equal(40102)) + expect(error.description).to(contain("incompatible credentials")) + expect(tokenDetails).to(beNil()) + partialDone() + } + } + } + } it("should never register any connection listeners for internal use with the public EventEmitter") { From 7cd41005b4c7f0244f6ccd6aacb5680be3b08dde Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 19 Oct 2016 16:55:18 +0100 Subject: [PATCH 07/13] RTC8a3 --- Spec/RealtimeClient.swift | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index 9e463076d..07da5a0ae 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -632,6 +632,40 @@ class RealtimeClient: QuickSpec { } } + // RTC8a3 + it("authorize call should be indicated as completed with the new token or error only once realtime has responded to the AUTH with either a CONNECTED or ERROR respectively") { + let options = AblyTests.commonAppSetup() + options.autoConnect = false + options.useTokenAuth = true + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + client.setTransportClass(TestProxyTransport.self) + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + client.connect() + } + + waitUntil(timeout: testTimeout) { done in + client.auth.authorize(nil, options: nil) { tokenDetails, error in + expect(error).to(beNil()) + expect(tokenDetails).toNot(beNil()) + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); done(); return + } + + expect(transport.protocolMessagesSent.filter({ $0.action == .Auth })).to(haveCount(1)) + expect(transport.protocolMessagesReceived.filter({ $0.action == .Connected })).to(haveCount(2)) + expect(transport.protocolMessagesReceived.filter({ $0.action == .Error })).to(haveCount(0)) + done() + } + } + } + } it("should never register any connection listeners for internal use with the public EventEmitter") { From 771db1b61635cbc26080fd30747b3fa3a9988317 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 19 Oct 2016 17:13:47 +0100 Subject: [PATCH 08/13] RTC8b --- Spec/RealtimeClient.swift | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index 07da5a0ae..3b5fa70c8 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -666,6 +666,44 @@ class RealtimeClient: QuickSpec { } } + // RTC8b + it("when connection is CONNECTING, all current connection attempts should be halted, and after obtaining a new token the library should immediately initiate a connection attempt using the new token") { + let options = AblyTests.clientOptions() + options.autoConnect = false + let testToken = getTestToken() + options.token = testToken + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + client.setTransportClass(TestProxyTransport.self) + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connecting) { stateChange in + expect(stateChange?.reason).to(beNil()) + + let authOptions = ARTAuthOptions() + authOptions.key = AblyTests.commonAppSetup().key + + client.auth.authorize(nil, options: authOptions) { tokenDetails, error in + expect(error).to(beNil()) + guard let tokenDetails = tokenDetails else { + fail("TokenDetails is nil"); done(); return + } + expect(tokenDetails.token).toNot(equal(testToken)) + + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Connected)) + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); done(); return + } + expect(transport.protocolMessagesSent.filter({ $0.action == .Connect })).to(haveCount(2)) + expect(transport.protocolMessagesReceived.filter({ $0.action == .Connected })).to(haveCount(1)) + done() + } + } + client.connect() + } + } + } it("should never register any connection listeners for internal use with the public EventEmitter") { From a78337dfacd04e5af94934f2964506878ac0d9ea Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 19 Oct 2016 17:19:12 +0100 Subject: [PATCH 09/13] RTC8b1 --- Spec/RealtimeClient.swift | 45 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index 3b5fa70c8..8da4542df 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -704,6 +704,51 @@ class RealtimeClient: QuickSpec { } } + // RTC8b1 + it("authorize call should complete with the new token once the connection has moved to the CONNECTED state, or with an error if the connection instead moves to the FAILED, SUSPENDED, or CLOSED states") { + let options = AblyTests.clientOptions() + options.autoConnect = false + let testToken = getTestToken() + options.token = testToken + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + client.setTransportClass(TestProxyTransport.self) + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connecting) { stateChange in + expect(stateChange?.reason).to(beNil()) + + let tokenParams = ARTTokenParams() + tokenParams.clientId = "john" + + let authOptions = ARTAuthOptions() + authOptions.authCallback = { tokenParams, completion in + completion(getTestToken(clientId: "tester"), nil) + } + + client.auth.authorize(tokenParams, options: authOptions) { tokenDetails, error in + guard let error = error else { + fail("ErrorInfo is nil"); done(); return + } + expect(error.code).to(equal(40102)) + expect(error.description).to(contain("incompatible credentials")) + expect(tokenDetails).to(beNil()) + + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Failed)) + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); done(); return + } + expect(transport.protocolMessagesSent.filter({ $0.action == .Connect })).to(haveCount(2)) + expect(transport.protocolMessagesReceived.filter({ $0.action == .Connected })).to(haveCount(0)) + expect(transport.protocolMessagesReceived.filter({ $0.action == .Error })).to(haveCount(1)) + done() + } + } + client.connect() + } + } + } it("should never register any connection listeners for internal use with the public EventEmitter") { From 301a1631f601a4376ce21103bbc7bef90c41b665 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 19 Oct 2016 17:29:01 +0100 Subject: [PATCH 10/13] RTC8c --- Spec/RealtimeClient.swift | 51 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index 8da4542df..ed5ed8f41 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -749,6 +749,57 @@ class RealtimeClient: QuickSpec { } } + // RTC8c + it("when the connection is in the DISCONNECTED, SUSPENDED, FAILED, or CLOSED state when auth#authorize is called, after obtaining a token the library should move to the CONNECTING state and initiate a connection attempt using the new token") { + let options = AblyTests.commonAppSetup() + options.autoConnect = false + let testToken = getTestToken() + options.token = testToken + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + client.setTransportClass(TestProxyTransport.self) + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + client.connect() + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + + client.connection.once(.Connected) { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); partialDone(); return + } + expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.Disconnected)) + expect(stateChange.reason).to(beNil()) + partialDone() + } + + client.auth.authorize(nil, options: nil) { tokenDetails, error in + expect(error).to(beNil()) + guard let tokenDetails = tokenDetails else { + fail("TokenDetails is nil"); partialDone(); return + } + + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Connected)) + expect(tokenDetails.token).toNot(equal(testToken)) + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); partialDone(); return + } + expect(transport.protocolMessagesSent.filter({ $0.action == .Connect })).to(haveCount(2)) + expect(transport.protocolMessagesSent.filter({ $0.action == .Auth })).to(haveCount(1)) + expect(transport.protocolMessagesReceived.filter({ $0.action == .Connected })).to(haveCount(1)) + partialDone() + } + client.simulateLostConnectionAndState() + } + } + } it("should never register any connection listeners for internal use with the public EventEmitter") { From 65eb4d6d6498f8cf67d8de554d844ed3336812f7 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 2 Nov 2016 20:09:51 +0000 Subject: [PATCH 11/13] Send AUTH protocol message on each authorize --- Source/ARTAuth+Private.h | 4 ++++ Source/ARTAuth.m | 3 ++- Source/ARTAuthDetails.h | 2 ++ Source/ARTAuthDetails.m | 7 +++++++ Source/ARTJsonLikeEncoder.m | 17 +++++++++++++++++ Source/ARTRealtime.m | 23 ++++++++++++++++++++++- 6 files changed, 54 insertions(+), 2 deletions(-) diff --git a/Source/ARTAuth+Private.h b/Source/ARTAuth+Private.h index f7458b5b8..aa7d04a41 100644 --- a/Source/ARTAuth+Private.h +++ b/Source/ARTAuth+Private.h @@ -7,6 +7,7 @@ // #import "ARTAuth.h" +#import "ARTEventEmitter.h" ART_ASSUME_NONNULL_BEGIN @@ -19,6 +20,9 @@ ART_ASSUME_NONNULL_BEGIN @property (art_nullable, nonatomic, readonly, strong) ARTTokenDetails *tokenDetails; @property (nonatomic, readonly, assign) NSTimeInterval timeOffset; +/// Internal emitter to notify about new authentications. +@property (nonatomic, readonly) __GENERIC(ARTEventEmitter, NSNull *, ARTTokenDetails *) *authorizedEmitter; + @end @interface ARTAuth (Private) diff --git a/Source/ARTAuth.m b/Source/ARTAuth.m index a15411907..013a92387 100644 --- a/Source/ARTAuth.m +++ b/Source/ARTAuth.m @@ -39,6 +39,7 @@ - (instancetype)init:(ARTRest *)rest withOptions:(ARTClientOptions *)options { _logger = rest.logger; _protocolClientId = nil; _tokenParams = options.defaultTokenParams ? : [[ARTTokenParams alloc] initWithOptions:self.options]; + _authorizedEmitter = [[ARTEventEmitter alloc] init]; [self validate:options]; [[NSNotificationCenter defaultCenter] addObserver:self @@ -53,7 +54,6 @@ - (instancetype)init:(ARTRest *)rest withOptions:(ARTClientOptions *)options { object:nil]; #endif } - return self; } @@ -358,6 +358,7 @@ - (void)authorize:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOp _tokenDetails = tokenDetails; _method = ARTAuthMethodToken; [self.logger verbose:@"RS:%p ARTAuth: token request succeeded: %@", _rest, tokenDetails]; + [_authorizedEmitter emit:[NSNull null] with:tokenDetails]; if (callback) { callback(self.tokenDetails, nil); } diff --git a/Source/ARTAuthDetails.h b/Source/ARTAuthDetails.h index 52ca8e49d..f50a255f8 100644 --- a/Source/ARTAuthDetails.h +++ b/Source/ARTAuthDetails.h @@ -16,6 +16,8 @@ ART_ASSUME_NONNULL_BEGIN @property (nonatomic, copy) NSString *accessToken; +- (instancetype)initWithToken:(NSString *)token; + @end ART_ASSUME_NONNULL_END diff --git a/Source/ARTAuthDetails.m b/Source/ARTAuthDetails.m index 42c7ca9e1..cac096c93 100644 --- a/Source/ARTAuthDetails.m +++ b/Source/ARTAuthDetails.m @@ -10,6 +10,13 @@ @implementation ARTAuthDetails +- (instancetype)initWithToken:(NSString *)token { + if (self = [super init]) { + _accessToken = token; + } + return self; +} + - (NSString *)description { return [NSString stringWithFormat:@"%@ - \n\t accessToken: %@; \n", [super description], self.accessToken]; } diff --git a/Source/ARTJsonLikeEncoder.m b/Source/ARTJsonLikeEncoder.m index 8d75191d8..b208c251f 100644 --- a/Source/ARTJsonLikeEncoder.m +++ b/Source/ARTJsonLikeEncoder.m @@ -23,6 +23,7 @@ #import "ARTStatus.h" #import "ARTTokenDetails.h" #import "ARTTokenRequest.h" +#import "ARTAuthDetails.h" #import "ARTConnectionDetails.h" #import "ARTRest+Private.h" @@ -45,6 +46,8 @@ - (ARTProtocolMessage *)protocolMessageFromDictionary:(NSDictionary *)input; - (NSDictionary *)tokenRequestToDictionary:(ARTTokenRequest *)tokenRequest; +- (NSDictionary *)authDetailsToDictionary:(ARTAuthDetails *)authDetails; + - (NSArray *)statsFromArray:(NSArray *)input; - (ARTStats *)statsFromDictionary:(NSDictionary *)input; - (ARTStatsMessageTypes *)statsMessageTypesFromDictionary:(NSDictionary *)input; @@ -291,6 +294,15 @@ - (NSDictionary *)messageToDictionary:(ARTMessage *)message { return output; } +- (NSDictionary *)authDetailsToDictionary:(ARTAuthDetails *)authDetails { + NSMutableDictionary *output = [NSMutableDictionary dictionary]; + + [output setObject:authDetails.accessToken forKey:@"accessToken"]; + + [_logger verbose:@"RS:%p ARTJsonLikeEncoder<%@>: authDetailsToDictionary %@", _rest, [_delegate formatAsString], output]; + return output; +} + - (NSArray *)messagesToArray:(NSArray *)messages { NSMutableArray *output = [NSMutableArray array]; @@ -358,6 +370,11 @@ - (NSDictionary *)protocolMessageToDictionary:(ARTProtocolMessage *)message { if (message.presence) { output[@"presence"] = [self presenceMessagesToArray:message.presence]; } + + if (message.auth) { + output[@"auth"] = [self authDetailsToDictionary:message.auth]; + } + [_logger verbose:@"RS:%p ARTJsonLikeEncoder<%@>: protocolMessageToDictionary %@", _rest, [_delegate formatAsString], output]; return output; } diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index 725cab720..a2c3462c2 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -32,6 +32,7 @@ #import "ARTStats.h" #import "ARTRealtimeTransport.h" #import "ARTFallback.h" +#import "ARTAuthDetails.h" @interface ARTConnectionStateChange () @@ -219,7 +220,7 @@ - (void)transition:(ARTRealtimeConnectionState)state withErrorInfo:(ARTErrorInfo if (errorInfo != nil) { [self.connection setErrorReason:errorInfo]; } - [self.connection emit:state with:stateChange]; + [_internalEventEmitter emit:[NSNumber numberWithInteger:state] with:stateChange]; } @@ -277,6 +278,7 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { break; } case ARTRealtimeClosing: { + [self.auth.authorizedEmitter off]; [_reachability off]; [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ [self transition:ARTRealtimeClosed]; @@ -285,6 +287,7 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { break; } case ARTRealtimeClosed: + [self.auth.authorizedEmitter off]; [_reachability off]; [self.transport close]; self.transport.delegate = nil; @@ -294,6 +297,7 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { self.rest.prioritizedHost = nil; break; case ARTRealtimeFailed: + [self.auth.authorizedEmitter off]; status = [ARTStatus state:ARTStateConnectionFailed info:stateChange.reason]; [self.transport abort:status]; self.transport.delegate = nil; @@ -301,6 +305,7 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { self.rest.prioritizedHost = nil; break; case ARTRealtimeDisconnected: { + [self.auth.authorizedEmitter off]; if (!_startedReconnection) { _startedReconnection = [NSDate date]; [_internalEventEmitter on:^(ARTConnectionStateChange *change) { @@ -326,6 +331,7 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { break; } case ARTRealtimeSuspended: { + [self.auth.authorizedEmitter off]; [self.transport close]; self.transport.delegate = nil; _transport = nil; @@ -347,6 +353,17 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { }]; } [_connectedEventEmitter emit:[NSNull null] with:nil]; + + [self.auth.authorizedEmitter off]; + __weak typeof(self) weakSelf = self; + [self.auth.authorizedEmitter on:^(ARTTokenDetails *tokenDetails) { + ARTProtocolMessage *msg = [[ARTProtocolMessage alloc] init]; + msg.action = ARTProtocolMessageAuth; + msg.auth = [[ARTAuthDetails alloc] initWithToken:tokenDetails.token]; + [weakSelf send:msg callback:^(ARTStatus *status) { + NSLog(@"%@", status); + }]; + }]; break; } case ARTRealtimeInitialized: @@ -382,6 +399,8 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { } } } + + [self.connection emit:stateChange.current with:stateChange]; } - (void)unlessStateChangesBefore:(NSTimeInterval)deadline do:(void(^)())callback { @@ -783,6 +802,8 @@ + (NSString *)protocolStr:(ARTProtocolMessageAction) action { return @"Message"; //15 case ARTProtocolMessageSync: return @"Sync"; //16 + case ARTProtocolMessageAuth: + return @"Auth"; //17 default: return [NSString stringWithFormat: @"unknown protocol state %d", (int)action]; } From eac82aaf902da20c375631f1e2d39cbf6706eb87 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 2 Nov 2016 20:11:21 +0000 Subject: [PATCH 12/13] Fix RTC8 --- Source/ARTAuth+Private.h | 14 +- Source/ARTAuth.m | 57 ++- Source/ARTRealtime+Private.h | 3 +- Source/ARTRealtime.m | 70 +++- Source/ARTRealtimeTransport.h | 12 +- Source/ARTStatus.h | 2 +- Source/ARTWebSocketTransport+Private.h | 4 +- Source/ARTWebSocketTransport.h | 2 - Source/ARTWebSocketTransport.m | 75 ++-- Spec/RealtimeClient.swift | 502 ++++++++++++++++++++++--- Spec/RealtimeClientConnection.swift | 2 +- Spec/RealtimeClientPresence.swift | 5 +- Spec/TestUtilities.swift | 2 +- Tests/ARTRealtimeAttachTest.m | 1 - 14 files changed, 625 insertions(+), 126 deletions(-) diff --git a/Source/ARTAuth+Private.h b/Source/ARTAuth+Private.h index aa7d04a41..299917b95 100644 --- a/Source/ARTAuth+Private.h +++ b/Source/ARTAuth+Private.h @@ -9,8 +9,19 @@ #import "ARTAuth.h" #import "ARTEventEmitter.h" +typedef NS_ENUM(NSUInteger, ARTAuthorizationState) { + ARTAuthorizationSucceeded, //ItemType: nil + ARTAuthorizationFailed //ItemType: NSError +}; + ART_ASSUME_NONNULL_BEGIN +/// Messages related to the ARTAuth +@protocol ARTAuthDelegate +@property (nonatomic, readonly) __GENERIC(ARTEventEmitter, NSNumber * /*ARTAuthorizationState*/, id) *authorizationEmitter; +- (void)auth:(ARTAuth *)auth didAuthorize:(ARTTokenDetails *)tokenDetails; +@end + @interface ARTAuth () @property (nonatomic, readonly, strong) ARTClientOptions *options; @@ -20,8 +31,7 @@ ART_ASSUME_NONNULL_BEGIN @property (art_nullable, nonatomic, readonly, strong) ARTTokenDetails *tokenDetails; @property (nonatomic, readonly, assign) NSTimeInterval timeOffset; -/// Internal emitter to notify about new authentications. -@property (nonatomic, readonly) __GENERIC(ARTEventEmitter, NSNull *, ARTTokenDetails *) *authorizedEmitter; +@property (art_nullable, weak) id delegate; @end diff --git a/Source/ARTAuth.m b/Source/ARTAuth.m index 013a92387..910540fff 100644 --- a/Source/ARTAuth.m +++ b/Source/ARTAuth.m @@ -23,6 +23,7 @@ #import "ARTStatus.h" #import "ARTJsonEncoder.h" #import "ARTGCD.h" +#import "ARTEventEmitter+Private.h" @implementation ARTAuth { __weak ARTRest *_rest; @@ -39,7 +40,6 @@ - (instancetype)init:(ARTRest *)rest withOptions:(ARTClientOptions *)options { _logger = rest.logger; _protocolClientId = nil; _tokenParams = options.defaultTokenParams ? : [[ARTTokenParams alloc] initWithOptions:self.options]; - _authorizedEmitter = [[ARTEventEmitter alloc] init]; [self validate:options]; [[NSNotificationCenter defaultCenter] addObserver:self @@ -346,22 +346,55 @@ - (void)authorize:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOp ARTTokenParams *currentTokenParams = [self mergeParams:tokenParams]; [self storeParams:currentTokenParams]; + // Success + void (^successBlock)(ARTTokenDetails *) = ^(ARTTokenDetails *tokenDetails) { + [self.logger verbose:@"RS:%p ARTAuth: token request succeeded: %@", _rest, tokenDetails]; + if (callback) { + callback(self.tokenDetails, nil); + } + }; + + // Failure + void (^failureBlock)(NSError *) = ^(NSError *error) { + [self.logger verbose:@"RS:%p ARTAuth: token request failed: %@", _rest, error]; + if (callback) { + callback(nil, error); + } + }; + + __weak id lastDelegate = self.delegate; + if (lastDelegate) { + // Only the last request should remain + [lastDelegate.authorizationEmitter off]; + [lastDelegate.authorizationEmitter once:[NSNumber numberWithInt:ARTAuthorizationSucceeded] callback:^(id null) { + successBlock(_tokenDetails); + [lastDelegate.authorizationEmitter off]; + }]; + [lastDelegate.authorizationEmitter once:[NSNumber numberWithInt:ARTAuthorizationFailed] callback:^(NSError *error) { + failureBlock(error); + [lastDelegate.authorizationEmitter off]; + }]; + } + // Request always a new token [self.logger verbose:@"RS:%p ARTAuth: requesting new token.", _rest]; [self requestToken:currentTokenParams withOptions:replacedOptions callback:^(ARTTokenDetails *tokenDetails, NSError *error) { if (error) { - [self.logger verbose:@"RS:%p ARTAuth: token request failed: %@", _rest, error]; - if (callback) { - callback(nil, error); - } - } else { - _tokenDetails = tokenDetails; - _method = ARTAuthMethodToken; - [self.logger verbose:@"RS:%p ARTAuth: token request succeeded: %@", _rest, tokenDetails]; - [_authorizedEmitter emit:[NSNull null] with:tokenDetails]; - if (callback) { - callback(self.tokenDetails, nil); + failureBlock(error); + if (lastDelegate) { + [lastDelegate.authorizationEmitter off]; } + return; + } + + _tokenDetails = tokenDetails; + _method = ARTAuthMethodToken; + + if (lastDelegate) { + [lastDelegate auth:self didAuthorize:tokenDetails]; + } + else { + successBlock(tokenDetails); } }]; } diff --git a/Source/ARTRealtime+Private.h b/Source/ARTRealtime+Private.h index 8c4b983d0..f7e308e10 100644 --- a/Source/ARTRealtime+Private.h +++ b/Source/ARTRealtime+Private.h @@ -14,6 +14,7 @@ #import "ARTReachability.h" #import "ARTRealtimeTransport.h" +#import "ARTAuth+Private.h" @class ARTRest; @class ARTErrorInfo; @@ -22,7 +23,7 @@ ART_ASSUME_NONNULL_BEGIN -@interface ARTRealtime () +@interface ARTRealtime () @property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSNumber *, ARTConnectionStateChange *) *internalEventEmitter; @property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSNull *, NSNull *) *connectedEventEmitter; diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index a2c3462c2..04871ef2c 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -25,7 +25,7 @@ #import "ARTPresenceMap.h" #import "ARTProtocolMessage.h" #import "ARTProtocolMessage+Private.h" -#import "ARTEventEmitter.h" +#import "ARTEventEmitter+Private.h" #import "ARTQueuedMessage.h" #import "ARTConnection+Private.h" #import "ARTConnectionDetails.h" @@ -54,6 +54,8 @@ @implementation ARTRealtime { ARTFallback *_fallbacks; } +@synthesize authorizationEmitter = _authorizationEmitter; + - (instancetype)initWithKey:(NSString *)key { return [self initWithOptions:[[ARTClientOptions alloc] initWithKey:key]]; } @@ -81,12 +83,15 @@ - (instancetype)initWithOptions:(ARTClientOptions *)options { _pendingMessageStartSerial = 0; _connection = [[ARTConnection alloc] initWithRealtime:self]; _connectionStateTtl = [ARTDefault connectionStateTtl]; + _authorizationEmitter = [[ARTEventEmitter alloc] init]; + self.auth.delegate = self; + [self.connection setState:ARTRealtimeInitialized]; [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p initialized with RS:%p", self, _rest]; self.rest.prioritizedHost = nil; - + if (options.autoConnect) { [self connect]; } @@ -94,6 +99,43 @@ - (instancetype)initWithOptions:(ARTClientOptions *)options { return self; } +- (void)auth:(ARTAuth *)auth didAuthorize:(ARTTokenDetails *)tokenDetails { + switch (self.connection.state) { + case ARTRealtimeConnected: { + // Update (send AUTH message) + [self.logger debug:__FILE__ line:__LINE__ message:@"RS:%p AUTH message using %@", _rest, tokenDetails]; + ARTProtocolMessage *msg = [[ARTProtocolMessage alloc] init]; + msg.action = ARTProtocolMessageAuth; + msg.auth = [[ARTAuthDetails alloc] initWithToken:tokenDetails.token]; + [self send:msg callback:nil]; + } + break; + case ARTRealtimeConnecting: { + switch (_transport.state) { + case ARTRealtimeTransportStateOpening: + case ARTRealtimeTransportStateOpened: { + // Halt the current connection and reconnect with the most recent token + [self.logger debug:__FILE__ line:__LINE__ message:@"RS:%p halt current connection and reconnect with %@", _rest, tokenDetails]; + [_transport abort:[ARTStatus state:ARTStateOk]]; + [_transport connectWithToken:tokenDetails.token]; + } + break; + case ARTRealtimeTransportStateClosed: + case ARTRealtimeTransportStateClosing: + // Ignore + [_authorizationEmitter off]; + break; + } + } + break; + default: + // Client state is NOT Connecting or Connected, so it should start a new connection + [self.logger debug:__FILE__ line:__LINE__ message:@"RS:%p start a connection using %@", _rest, tokenDetails]; + [self transition:ARTRealtimeConnecting]; + break; + } +} + - (id)getTransport { return _transport; } @@ -215,12 +257,12 @@ - (void)transition:(ARTRealtimeConnectionState)state withErrorInfo:(ARTErrorInfo ARTConnectionStateChange *stateChange = [[ARTConnectionStateChange alloc] initWithCurrent:state previous:self.connection.state reason:errorInfo retryIn:0]; [self.connection setState:state]; - [self transitionSideEffects:stateChange]; - if (errorInfo != nil) { [self.connection setErrorReason:errorInfo]; } + [self transitionSideEffects:stateChange]; + [_internalEventEmitter emit:[NSNumber numberWithInteger:state] with:stateChange]; } @@ -278,7 +320,6 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { break; } case ARTRealtimeClosing: { - [self.auth.authorizedEmitter off]; [_reachability off]; [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ [self transition:ARTRealtimeClosed]; @@ -287,7 +328,6 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { break; } case ARTRealtimeClosed: - [self.auth.authorizedEmitter off]; [_reachability off]; [self.transport close]; self.transport.delegate = nil; @@ -295,17 +335,17 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { _connection.id = nil; _transport = nil; self.rest.prioritizedHost = nil; + [_authorizationEmitter emit:[NSNumber numberWithInt:ARTAuthorizationFailed] with:[ARTErrorInfo createWithCode:ARTStateAuthorizationFailed message:@"Connection has been closed"]]; break; case ARTRealtimeFailed: - [self.auth.authorizedEmitter off]; status = [ARTStatus state:ARTStateConnectionFailed info:stateChange.reason]; [self.transport abort:status]; self.transport.delegate = nil; _transport = nil; self.rest.prioritizedHost = nil; + [_authorizationEmitter emit:[NSNumber numberWithInt:ARTAuthorizationFailed] with:stateChange.reason]; break; case ARTRealtimeDisconnected: { - [self.auth.authorizedEmitter off]; if (!_startedReconnection) { _startedReconnection = [NSDate date]; [_internalEventEmitter on:^(ARTConnectionStateChange *change) { @@ -331,7 +371,6 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { break; } case ARTRealtimeSuspended: { - [self.auth.authorizedEmitter off]; [self.transport close]; self.transport.delegate = nil; _transport = nil; @@ -339,6 +378,7 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { [self unlessStateChangesBefore:stateChange.retryIn do:^{ [self transition:ARTRealtimeConnecting]; }]; + [_authorizationEmitter emit:[NSNumber numberWithInt:ARTAuthorizationFailed] with:[ARTErrorInfo createWithCode:ARTStateAuthorizationFailed message:@"Connection has been suspended"]]; break; } case ARTRealtimeConnected: { @@ -353,17 +393,7 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { }]; } [_connectedEventEmitter emit:[NSNull null] with:nil]; - - [self.auth.authorizedEmitter off]; - __weak typeof(self) weakSelf = self; - [self.auth.authorizedEmitter on:^(ARTTokenDetails *tokenDetails) { - ARTProtocolMessage *msg = [[ARTProtocolMessage alloc] init]; - msg.action = ARTProtocolMessageAuth; - msg.auth = [[ARTAuthDetails alloc] initWithToken:tokenDetails.token]; - [weakSelf send:msg callback:^(ARTStatus *status) { - NSLog(@"%@", status); - }]; - }]; + [_authorizationEmitter emit:[NSNumber numberWithInt:ARTAuthorizationSucceeded] with:nil]; break; } case ARTRealtimeInitialized: diff --git a/Source/ARTRealtimeTransport.h b/Source/ARTRealtimeTransport.h index 9f8faf265..322a774c0 100644 --- a/Source/ARTRealtimeTransport.h +++ b/Source/ARTRealtimeTransport.h @@ -28,6 +28,13 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeTransportErrorType) { ARTRealtimeTransportErrorTypeOther }; +typedef NS_ENUM(NSUInteger, ARTRealtimeTransportState) { + ARTRealtimeTransportStateOpening, + ARTRealtimeTransportStateOpened, + ARTRealtimeTransportStateClosing, + ARTRealtimeTransportStateClosed, +}; + @interface ARTRealtimeTransportError : NSObject @property (nonatomic, strong) NSError *error; @@ -64,11 +71,13 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeTransportErrorType) { @property (readonly, strong, nonatomic) NSString *resumeKey; @property (readonly, strong, nonatomic) NSNumber *connectionSerial; - +@property (readonly, assign, nonatomic) ARTRealtimeTransportState state; @property (readwrite, weak, nonatomic) id delegate; + - (void)send:(ARTProtocolMessage *)msg; - (void)receive:(ARTProtocolMessage *)msg; - (void)connect; +- (void)connectWithToken:(NSString *)token; //?! - (void)connectForcingNewToken:(BOOL)forceNewToken; - (void)sendClose; - (void)sendPing; @@ -76,6 +85,7 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeTransportErrorType) { - (void)abort:(ARTStatus *)reason; - (NSString *)host; - (void)setHost:(NSString *)host; +- (ARTRealtimeTransportState)state; @end diff --git a/Source/ARTStatus.h b/Source/ARTStatus.h index 36753bf91..d023f2cbb 100644 --- a/Source/ARTStatus.h +++ b/Source/ARTStatus.h @@ -26,6 +26,7 @@ typedef NS_ENUM(NSUInteger, ARTState) { ARTStateNoClientId, ARTStateMismatchedClientId, ARTStateRequestTokenFailed, + ARTStateAuthorizationFailed, ARTStateAuthUrlIncompatibleContent, ARTStateBadConnectionState, ARTStateError = 99999 @@ -63,7 +64,6 @@ FOUNDATION_EXPORT NSString *const ARTAblyMessageNoMeansToRenewToken; + (ARTErrorInfo *)createWithCode:(NSInteger)code message:(NSString *)message; + (ARTErrorInfo *)createWithCode:(NSInteger)code status:(NSInteger)status message:(NSString *)message; -// FIXME: base NSError + (ARTErrorInfo *)createWithNSError:(NSError *)error; + (ARTErrorInfo *)wrap:(ARTErrorInfo *)error prepend:(NSString *)prepend; diff --git a/Source/ARTWebSocketTransport+Private.h b/Source/ARTWebSocketTransport+Private.h index c893d76c7..ee60beb7d 100644 --- a/Source/ARTWebSocketTransport+Private.h +++ b/Source/ARTWebSocketTransport+Private.h @@ -27,8 +27,6 @@ ART_ASSUME_NONNULL_BEGIN @property (readonly, strong, nonatomic) ARTAuth *auth; @property (readonly, strong, nonatomic) ARTClientOptions *options; -@property (readwrite, assign, nonatomic) BOOL closing; - @property (readwrite, strong, nonatomic, art_nullable) SRWebSocket *websocket; @property (readwrite, strong, nonatomic, art_nullable) NSURL *websocketURL; @@ -37,6 +35,8 @@ ART_ASSUME_NONNULL_BEGIN - (NSURL *)setupWebSocket:(__GENERIC(NSArray, NSURLQueryItem *) *)params withOptions:(ARTClientOptions *)options resumeKey:(NSString *__art_nullable)resumeKey connectionSerial:(NSNumber *__art_nullable)connectionSerial; +- (void)setState:(ARTRealtimeTransportState)state; + @end ART_ASSUME_NONNULL_END diff --git a/Source/ARTWebSocketTransport.h b/Source/ARTWebSocketTransport.h index 3e26e2cff..baa222ea4 100644 --- a/Source/ARTWebSocketTransport.h +++ b/Source/ARTWebSocketTransport.h @@ -25,8 +25,6 @@ ART_ASSUME_NONNULL_BEGIN @property (readonly, strong, nonatomic) NSNumber *connectionSerial; @property (readwrite, weak, nonatomic) id delegate; -@property (readonly, getter=getIsConnected) BOOL isConnected; - @end ART_ASSUME_NONNULL_END diff --git a/Source/ARTWebSocketTransport.m b/Source/ARTWebSocketTransport.m index a6b16d897..1cdb5db9b 100644 --- a/Source/ARTWebSocketTransport.m +++ b/Source/ARTWebSocketTransport.m @@ -36,16 +36,16 @@ ARTWsTlsError = 1015 }; -@implementation ARTWebSocketTransport +@implementation ARTWebSocketTransport { + ARTRealtimeTransportState _state; +} -// FIXME: Realtime sould be extending from RestClient - (instancetype)initWithRest:(ARTRest *)rest options:(ARTClientOptions *)options resumeKey:(NSString *)resumeKey connectionSerial:(NSNumber *)connectionSerial { self = [super init]; if (self) { _rl = CFRunLoopGetCurrent(); _websocket = nil; - _closing = NO; - + _state = ARTRealtimeTransportStateClosed; _encoder = rest.defaultEncoder; _logger = rest.logger; _auth = rest.auth; @@ -71,7 +71,9 @@ - (void)send:(ARTProtocolMessage *)msg { } - (void)sendWithData:(NSData *)data { - [self.websocket send:data]; + if (self.websocket.readyState == SR_OPEN) { + [self.websocket send:data]; + } } - (void)receive:(ARTProtocolMessage *)msg { @@ -88,6 +90,7 @@ - (void)connect { } - (void)connectForcingNewToken:(BOOL)forceNewToken { + _state = ARTRealtimeTransportStateOpening; [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p websocket connect", _delegate, self]; ARTClientOptions *options = [self.options copy]; @@ -98,7 +101,6 @@ - (void)connectForcingNewToken:(BOOL)forceNewToken { else { // Token [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p connecting with token auth; authorising", _delegate, self]; - __weak ARTWebSocketTransport *selfWeak = self; if (!forceNewToken && [self.auth tokenRemainsValid]) { // Reuse token @@ -106,20 +108,25 @@ - (void)connectForcingNewToken:(BOOL)forceNewToken { } else { // New Token - [self.auth authorize:nil options:options callback:^(ARTTokenDetails *tokenDetails, NSError *error) { - ARTWebSocketTransport *selfStrong = selfWeak; - if (!selfStrong) return; - - [selfStrong.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p authorised: %@ error: %@", _delegate, self, tokenDetails, error]; - - if (error) { - [selfStrong.logger error:@"R:%p WS:%p ARTWebSocketTransport: token auth failed with %@", _delegate, self, error.description]; - [selfStrong.delegate realtimeTransportFailed:selfStrong withError:[[ARTRealtimeTransportError alloc] initWithError:error type:ARTRealtimeTransportErrorTypeAuth url:self.websocketURL]]; - return; - } - - [selfStrong connectWithToken:tokenDetails.token]; - }]; + __weak __typeof(self) weakSelf = self; + id delegate = self.auth.delegate; + self.auth.delegate = nil; + @try { + [self.auth authorize:nil options:options callback:^(ARTTokenDetails *tokenDetails, NSError *error) { + [[weakSelf logger] debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p authorised: %@ error: %@", _delegate, self, tokenDetails, error]; + + if (error) { + [[weakSelf logger] error:@"R:%p WS:%p ARTWebSocketTransport: token auth failed with %@", _delegate, self, error.description]; + [[weakSelf delegate] realtimeTransportFailed:weakSelf withError:[[ARTRealtimeTransportError alloc] initWithError:error type:ARTRealtimeTransportErrorTypeAuth url:self.websocketURL]]; + return; + } + + [weakSelf connectWithToken:tokenDetails.token]; + }]; + } + @finally { + self.auth.delegate = delegate; + } } } } @@ -140,10 +147,6 @@ - (void)connectWithToken:(NSString *)token { [self.websocket open]; } -- (BOOL)getIsConnected { - return self.websocket.readyState == SR_OPEN; -} - - (NSURL *)setupWebSocket:(__GENERIC(NSArray, NSURLQueryItem *) *)params withOptions:(ARTClientOptions *)options resumeKey:(NSString *)resumeKey connectionSerial:(NSNumber *)connectionSerial { NSArray *queryItems = params; @@ -209,16 +212,16 @@ - (NSURL *)setupWebSocket:(__GENERIC(NSArray, NSURLQueryItem *) *)params withOpt } - (void)sendClose { - self.closing = YES; + _state = ARTRealtimeTransportStateClosing; ARTProtocolMessage *closeMessage = [[ARTProtocolMessage alloc] init]; closeMessage.action = ARTProtocolMessageClose; [self send:closeMessage]; } - (void)sendPing { - ARTProtocolMessage *closeMessage = [[ARTProtocolMessage alloc] init]; - closeMessage.action = ARTProtocolMessageHeartbeat; - [self send:closeMessage]; + ARTProtocolMessage *heartbeatMessage = [[ARTProtocolMessage alloc] init]; + heartbeatMessage.action = ARTProtocolMessageHeartbeat; + [self send:heartbeatMessage]; } - (void)close { @@ -248,6 +251,17 @@ - (NSString *)host { return self.options.realtimeHost; } +- (ARTRealtimeTransportState)state { + if (self.websocket.readyState == SR_OPEN) { + return ARTRealtimeTransportStateOpened; + } + return _state; +} + +- (void)setState:(ARTRealtimeTransportState)state { + _state = state; +} + #pragma mark - SRWebSocketDelegate - (void)webSocketDidOpen:(SRWebSocket *)websocket { @@ -275,7 +289,7 @@ - (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reas switch (code) { case ARTWsCloseNormal: - if (s.closing) { + if (_state == ARTRealtimeTransportStateClosing) { // OK [s.delegate realtimeTransportClosed:s]; } @@ -310,6 +324,8 @@ - (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reas NSAssert(true, @"WebSocket close: unknown code"); break; } + + s.state = ARTRealtimeTransportStateClosed; }); CFRunLoopWakeUp(self.rl); } @@ -323,6 +339,7 @@ - (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error { if (s) { [s.delegate realtimeTransportFailed:s withError:[self classifyError:error]]; } + s.state = ARTRealtimeTransportStateClosed; }); CFRunLoopWakeUp(self.rl); } diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index ed5ed8f41..c068d222b 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -434,7 +434,7 @@ class RealtimeClient: QuickSpec { fail("ConnectionStateChange is nil"); partialDone(); return } expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.Connected)) - expect(stateChange.reason).toNot(beNil()) + expect(stateChange.reason).to(beNil()) guard let transport = client.transport as? TestProxyTransport else { fail("TestProxyTransport is not set"); partialDone(); return @@ -446,7 +446,8 @@ class RealtimeClient: QuickSpec { fail("Missing CONNECTED protocol message after AUTH protocol message"); partialDone(); return } - expect(client.auth.clientId).to(equal(connectionDetailsAfterAuth.clientId)) + expect(client.auth.clientId).to(beNil()) + expect(connectionDetailsAfterAuth.clientId).to(beNil()) expect(client.connection.key).to(equal(connectionDetailsAfterAuth.connectionKey)) partialDone() } @@ -482,6 +483,18 @@ class RealtimeClient: QuickSpec { client.connect() } + let channel = client.channels.get("foo") + waitUntil(timeout: testTimeout) { done in + channel.once(.Failed) { error in + guard let error = error else { + fail("Error is nil"); done(); return + } + expect(error.message).to(contain("Channel denied access based on given capability")) + done() + } + channel.attach() + } + waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(2, done: done) @@ -490,7 +503,7 @@ class RealtimeClient: QuickSpec { fail("ConnectionStateChange is nil"); partialDone(); return } expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.Connected)) - expect(stateChange.reason).toNot(beNil()) + expect(stateChange.reason).to(beNil()) partialDone() } @@ -525,7 +538,20 @@ class RealtimeClient: QuickSpec { } expect(transport.protocolMessagesReceived.filter{ $0.action == .Disconnected }).to(beEmpty()) - expect(transport.protocolMessagesReceived.filter{ $0.action == .Error }).to(beEmpty()) + // Should have one error: Channel denied access + expect(transport.protocolMessagesReceived.filter{ $0.action == .Error }).to(haveCount(1)) + + // Retry Channel attach + waitUntil(timeout: testTimeout) { done in + channel.once(.Failed) { error in + fail("Should not reach Failed state"); done(); return + } + channel.once(.Attached) { error in + expect(error).to(beNil()) + done() + } + channel.attach() + } } // RTC8a1 - part 3 @@ -538,10 +564,11 @@ class RealtimeClient: QuickSpec { defer { client.dispose(); client.close() } client.setTransportClass(TestProxyTransport.self) - let channel = client.channels.get("test") + let channel = client.channels.get("foo") waitUntil(timeout: testTimeout) { done in client.connect() - channel.attach() { _ in + channel.attach() { error in + expect(error).to(beNil()) done() } } @@ -553,7 +580,8 @@ class RealtimeClient: QuickSpec { guard let error = error else { fail("ErrorInfo is nil"); partialDone(); return } - expect(error).to(equal(channel.errorReason)) + expect(error).to(beIdenticalTo(channel.errorReason)) + expect(error.code).to(equal(40160)) guard let transport = client.transport as? TestProxyTransport else { fail("TestProxyTransport is not set"); partialDone(); return @@ -565,7 +593,8 @@ class RealtimeClient: QuickSpec { guard let errorMessage = errorMessages.first else { fail("Missing ERROR protocol message"); partialDone(); return } - expect(errorMessage.channel).to(equal("test")) + expect(errorMessage.channel).to(contain("test")) + expect(errorMessage.error?.code).to(equal(error.code)) partialDone() } @@ -604,32 +633,81 @@ class RealtimeClient: QuickSpec { client.connect() } + var connectionError: NSError? + var authError: NSError? + waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(2, done: done) - let tokenParams = ARTTokenParams() - tokenParams.clientId = "android" - client.connection.once(.Failed) { stateChange in guard let stateChange = stateChange else { fail("ConnectionStateChange is nil"); partialDone(); return } expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.Connected)) - expect(stateChange.reason?.code).to(equal(40102)) - expect(stateChange.reason?.description).to(contain("incompatible credentials")) + expect(stateChange.reason).toNot(beNil()) + connectionError = stateChange.reason partialDone() } - client.auth.authorize(tokenParams, options: nil) { tokenDetails, error in + let authOptions = ARTAuthOptions() + authOptions.authCallback = { tokenParams, completion in + let invalidToken = "xxxxxxxxxxxx" + completion(invalidToken, nil) + } + + client.auth.authorize(nil, options: authOptions) { tokenDetails, error in guard let error = error else { fail("ErrorInfo is nil"); partialDone(); return } + expect(error.description).to(contain("Invalid accessToken")) + expect(tokenDetails).to(beNil()) + authError = error + partialDone() + } + } + + expect(authError).to(beIdenticalTo(connectionError)) + } + + it("authorize call should complete with an error if the request fails") { + let options = AblyTests.clientOptions() + options.autoConnect = false + let testToken = getTestToken() + options.token = testToken + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + client.setTransportClass(TestProxyTransport.self) + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + client.connect() + } + + waitUntil(timeout: testTimeout) { done in + let tokenParams = ARTTokenParams() + tokenParams.clientId = "john" + + let authOptions = ARTAuthOptions() + authOptions.authCallback = { tokenParams, completion in + completion(getTestTokenDetails(clientId: "tester"), nil) + } + + client.auth.authorize(tokenParams, options: authOptions) { tokenDetails, error in + guard let error = error else { + fail("ErrorInfo is nil"); done(); return + } expect(error.code).to(equal(40102)) expect(error.description).to(contain("incompatible credentials")) expect(tokenDetails).to(beNil()) - partialDone() + done() } } + + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Connected)) + expect(client.auth.tokenDetails!.token).to(equal(testToken)) } // RTC8a3 @@ -668,14 +746,25 @@ class RealtimeClient: QuickSpec { // RTC8b it("when connection is CONNECTING, all current connection attempts should be halted, and after obtaining a new token the library should immediately initiate a connection attempt using the new token") { - let options = AblyTests.clientOptions() + let options = AblyTests.commonAppSetup() options.autoConnect = false - let testToken = getTestToken() - options.token = testToken + options.useTokenAuth = true let client = ARTRealtime(options: options) defer { client.dispose(); client.close() } client.setTransportClass(TestProxyTransport.self) + var connections = 0 + let hook1 = TestProxyTransport.testSuite_injectIntoClassMethod(#selector(TestProxyTransport.connectWithToken)) { + connections += 1 + } + defer { hook1?.remove() } + + var connectionsOpened = 0 + let hook2 = TestProxyTransport.testSuite_injectIntoClassMethod(#selector(TestProxyTransport.webSocketDidOpen)) { + connectionsOpened += 1 + } + defer { hook2?.remove() } + waitUntil(timeout: testTimeout) { done in client.connection.once(.Connecting) { stateChange in expect(stateChange?.reason).to(beNil()) @@ -688,24 +777,27 @@ class RealtimeClient: QuickSpec { guard let tokenDetails = tokenDetails else { fail("TokenDetails is nil"); done(); return } - expect(tokenDetails.token).toNot(equal(testToken)) - + expect(tokenDetails.token).toNot(beNil()) expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Connected)) guard let transport = client.transport as? TestProxyTransport else { fail("TestProxyTransport is not set"); done(); return } - expect(transport.protocolMessagesSent.filter({ $0.action == .Connect })).to(haveCount(2)) expect(transport.protocolMessagesReceived.filter({ $0.action == .Connected })).to(haveCount(1)) done() } } client.connect() } + + expect(connections) == 2 + expect(connectionsOpened) == 1 + + expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) } - // RTC8b1 - it("authorize call should complete with the new token once the connection has moved to the CONNECTED state, or with an error if the connection instead moves to the FAILED, SUSPENDED, or CLOSED states") { + // RTC8b1 - part 1 + it("authorize call should complete with the new token once the connection has moved to the CONNECTED state") { let options = AblyTests.clientOptions() options.autoConnect = false let testToken = getTestToken() @@ -715,42 +807,157 @@ class RealtimeClient: QuickSpec { client.setTransportClass(TestProxyTransport.self) waitUntil(timeout: testTimeout) { done in - client.connection.once(.Connecting) { stateChange in + let authOptions = ARTAuthOptions() + authOptions.key = AblyTests.commonAppSetup().key + + client.auth.authorize(nil, options: authOptions) { tokenDetails, error in + expect(error).to(beNil()) + guard let tokenDetails = tokenDetails else { + fail("TokenDetails is nil"); done(); return + } + expect(tokenDetails.token).toNot(equal(testToken)) + done() + } + } + + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Connected)) + } + + // RTC8b1 - part 2 + it("authorize call should complete with an error if the connection moves to the FAILED state") { + let options = AblyTests.commonAppSetup() + options.autoConnect = false + options.useTokenAuth = true + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + client.setTransportClass(TestProxyTransport.self) + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in expect(stateChange?.reason).to(beNil()) + done() + } + client.connect() + } - let tokenParams = ARTTokenParams() - tokenParams.clientId = "john" + let hook = client.auth.testSuite_injectIntoMethodAfter(#selector(client.auth.authorize(_:options:callback:))) { + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + transport.simulateIncomingError() + } + defer { hook.remove() } - let authOptions = ARTAuthOptions() - authOptions.authCallback = { tokenParams, completion in - completion(getTestToken(clientId: "tester"), nil) + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + + client.connection.once(.Failed) { stateChange in + guard let error = stateChange?.reason else { + fail("ErrorInfo is nil"); partialDone(); return } + expect(error.message).to(contain("Fail test")) + partialDone() + } - client.auth.authorize(tokenParams, options: authOptions) { tokenDetails, error in - guard let error = error else { - fail("ErrorInfo is nil"); done(); return - } - expect(error.code).to(equal(40102)) - expect(error.description).to(contain("incompatible credentials")) - expect(tokenDetails).to(beNil()) + client.auth.authorize(nil, options: nil) { tokenDetails, error in + guard let error = error else { + fail("ErrorInfo is nil"); partialDone(); return + } + expect(error.description).to(contain("Fail test")) + expect(tokenDetails).to(beNil()) + partialDone() + } + } - expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Failed)) + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Failed)) + } - guard let transport = client.transport as? TestProxyTransport else { - fail("TestProxyTransport is not set"); done(); return - } - expect(transport.protocolMessagesSent.filter({ $0.action == .Connect })).to(haveCount(2)) - expect(transport.protocolMessagesReceived.filter({ $0.action == .Connected })).to(haveCount(0)) - expect(transport.protocolMessagesReceived.filter({ $0.action == .Error })).to(haveCount(1)) - done() + // RTC8b1 - part 3 + it("authorize call should complete with an error if the connection moves to the SUSPENDED state") { + let options = AblyTests.commonAppSetup() + options.autoConnect = false + options.useTokenAuth = true + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + client.setTransportClass(TestProxyTransport.self) + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + client.connect() + } + + let hook = client.auth.testSuite_injectIntoMethodAfter(#selector(client.auth.authorize(_:options:callback:))) { + client.onSuspended() + } + defer { hook.remove() } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + + client.connection.once(.Suspended) { _ in + partialDone() + } + + client.auth.authorize(nil, options: nil) { tokenDetails, error in + guard let error = error else { + fail("ErrorInfo is nil"); partialDone(); return } + expect(UInt(error.code)) == ARTState.AuthorizationFailed.rawValue + expect(tokenDetails).to(beNil()) + partialDone() + } + } + + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Suspended)) + } + + // RTC8b1 - part 4 + it("authorize call should complete with an error if the connection moves to the CLOSED state") { + let options = AblyTests.commonAppSetup() + options.autoConnect = false + options.useTokenAuth = true + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + client.setTransportClass(TestProxyTransport.self) + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() } client.connect() } + + let hook = client.auth.testSuite_injectIntoMethodAfter(#selector(client.auth.authorize(_:options:callback:))) { + client.close() + } + defer { hook.remove() } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + + client.connection.once(.Closed) { _ in + partialDone() + } + + client.auth.authorize(nil, options: nil) { tokenDetails, error in + guard let error = error else { + fail("ErrorInfo is nil"); partialDone(); return + } + expect(UInt(error.code)) == ARTState.AuthorizationFailed.rawValue + expect(tokenDetails).to(beNil()) + partialDone() + } + } + + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Closed)) } - // RTC8c - it("when the connection is in the DISCONNECTED, SUSPENDED, FAILED, or CLOSED state when auth#authorize is called, after obtaining a token the library should move to the CONNECTING state and initiate a connection attempt using the new token") { + // RTC8c - part 1 + it("when the connection is in the SUSPENDED state when auth#authorize is called, after obtaining a token the library should move to the CONNECTING state and initiate a connection attempt using the new token") { let options = AblyTests.commonAppSetup() options.autoConnect = false let testToken = getTestToken() @@ -767,10 +974,135 @@ class RealtimeClient: QuickSpec { client.connect() } + client.onSuspended() + expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Suspended), timeout: testTimeout) + waitUntil(timeout: testTimeout) { done in - let partialDone = AblyTests.splitDone(2, done: done) + let partialDone = AblyTests.splitDone(3, done: done) + + client.connection.once(.Connecting) { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); partialDone(); return + } + expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.Suspended)) + expect(stateChange.reason).to(beNil()) + partialDone() + } client.connection.once(.Connected) { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); partialDone(); return + } + expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.Connecting)) + expect(stateChange.reason).to(beNil()) + partialDone() + } + + client.auth.authorize(nil, options: nil) { tokenDetails, error in + expect(error).to(beNil()) + guard let tokenDetails = tokenDetails else { + fail("TokenDetails is nil"); partialDone(); return + } + + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Connected)) + expect(tokenDetails.token).toNot(equal(testToken)) + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); partialDone(); return + } + expect(transport.protocolMessagesSent.filter({ $0.action == .Auth })).to(haveCount(0)) + expect(transport.protocolMessagesReceived.filter({ $0.action == .Connected })).to(haveCount(1)) //New transport + partialDone() + } + } + } + + // RTC8c - part 2 + it("when the connection is in the CLOSED state when auth#authorize is called, after obtaining a token the library should move to the CONNECTING state and initiate a connection attempt using the new token") { + let options = AblyTests.commonAppSetup() + options.autoConnect = false + let testToken = getTestToken() + options.token = testToken + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + client.setTransportClass(TestProxyTransport.self) + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + client.connect() + } + + client.close() + expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Closed), timeout: testTimeout) + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(3, done: done) + + client.connection.once(.Connecting) { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); partialDone(); return + } + expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.Closed)) + expect(stateChange.reason).to(beNil()) + partialDone() + } + + client.connection.once(.Connected) { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); partialDone(); return + } + expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.Connecting)) + expect(stateChange.reason).to(beNil()) + partialDone() + } + + client.auth.authorize(nil, options: nil) { tokenDetails, error in + expect(error).to(beNil()) + guard let tokenDetails = tokenDetails else { + fail("TokenDetails is nil"); partialDone(); return + } + + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Connected)) + expect(tokenDetails.token).toNot(equal(testToken)) + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); partialDone(); return + } + expect(transport.protocolMessagesSent.filter({ $0.action == .Auth })).to(haveCount(0)) + expect(transport.protocolMessagesReceived.filter({ $0.action == .Connected })).to(haveCount(1)) //New transport + partialDone() + } + } + } + + // RTC8c - part 3 + it("when the connection is in the DISCONNECTED state when auth#authorize is called, after obtaining a token the library should move to the CONNECTING state and initiate a connection attempt using the new token") { + let options = AblyTests.commonAppSetup() + options.autoConnect = false + let testToken = getTestToken() + options.token = testToken + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + client.setTransportClass(TestProxyTransport.self) + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + client.connect() + } + + client.onDisconnected() + expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Disconnected), timeout: testTimeout) + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(3, done: done) + + client.connection.once(.Connecting) { stateChange in guard let stateChange = stateChange else { fail("ConnectionStateChange is nil"); partialDone(); return } @@ -779,6 +1111,15 @@ class RealtimeClient: QuickSpec { partialDone() } + client.connection.once(.Connected) { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); partialDone(); return + } + expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.Connecting)) + expect(stateChange.reason).to(beNil()) + partialDone() + } + client.auth.authorize(nil, options: nil) { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { @@ -791,12 +1132,71 @@ class RealtimeClient: QuickSpec { guard let transport = client.transport as? TestProxyTransport else { fail("TestProxyTransport is not set"); partialDone(); return } - expect(transport.protocolMessagesSent.filter({ $0.action == .Connect })).to(haveCount(2)) - expect(transport.protocolMessagesSent.filter({ $0.action == .Auth })).to(haveCount(1)) - expect(transport.protocolMessagesReceived.filter({ $0.action == .Connected })).to(haveCount(1)) + expect(transport.protocolMessagesSent.filter({ $0.action == .Auth })).to(haveCount(0)) + expect(transport.protocolMessagesReceived.filter({ $0.action == .Connected })).to(haveCount(1)) //New transport + partialDone() + } + } + } + + // RTC8c - part 4 + it("when the connection is in the FAILED state when auth#authorize is called, after obtaining a token the library should move to the CONNECTING state and initiate a connection attempt using the new token") { + let options = AblyTests.commonAppSetup() + options.autoConnect = false + let testToken = getTestToken() + options.token = testToken + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + client.setTransportClass(TestProxyTransport.self) + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + client.connect() + } + + client.onError(AblyTests.newErrorProtocolMessage()) + expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Failed), timeout: testTimeout) + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(3, done: done) + + client.connection.once(.Connecting) { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); partialDone(); return + } + expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.Failed)) + expect(stateChange.reason).to(beNil()) + partialDone() + } + + client.connection.once(.Connected) { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); partialDone(); return + } + expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.Connecting)) + expect(stateChange.reason).to(beNil()) + partialDone() + } + + client.auth.authorize(nil, options: nil) { tokenDetails, error in + expect(error).to(beNil()) + guard let tokenDetails = tokenDetails else { + fail("TokenDetails is nil"); partialDone(); return + } + + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Connected)) + expect(tokenDetails.token).toNot(equal(testToken)) + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); partialDone(); return + } + expect(transport.protocolMessagesSent.filter({ $0.action == .Auth })).to(haveCount(0)) + expect(transport.protocolMessagesReceived.filter({ $0.action == .Connected })).to(haveCount(1)) //New transport partialDone() } - client.simulateLostConnectionAndState() } } diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index c45d52455..64f765a60 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -543,7 +543,7 @@ class RealtimeClientConnection: QuickSpec { } if let webSocketTransport = client.transport as? ARTWebSocketTransport { - expect(webSocketTransport.isConnected).to(beTrue()) + expect(webSocketTransport.state).to(equal(ARTRealtimeTransportState.Opened)) } else { XCTFail("WebSocket is not the default transport") diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index 4a53f276f..9964f078e 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -244,7 +244,6 @@ class RealtimeClientPresence: QuickSpec { } } - // RTP5a it("all queued presence messages should fail immediately if the channel enters the DETACHED state") { let client = ARTRealtime(options: AblyTests.commonAppSetup()) @@ -252,11 +251,13 @@ class RealtimeClientPresence: QuickSpec { let channel = client.channels.get("test") waitUntil(timeout: testTimeout) { done in + channel.once(.Attaching) { _ in + channel.detach() + } channel.presence.enterClient("user", data: nil) { error in expect(error).toNot(beNil()) done() } - channel.detach() } } diff --git a/Spec/TestUtilities.swift b/Spec/TestUtilities.swift index 7748097be..2fb922222 100644 --- a/Spec/TestUtilities.swift +++ b/Spec/TestUtilities.swift @@ -972,7 +972,7 @@ extension ARTWebSocketTransport { func simulateIncomingNormalClose() { let CLOSE_NORMAL = 1000 - self.closing = true + self.setState(ARTRealtimeTransportState.Closing) let webSocketDelegate = self as SRWebSocketDelegate webSocketDelegate.webSocket!(nil, didCloseWithCode: CLOSE_NORMAL, reason: "", wasClean: true) } diff --git a/Tests/ARTRealtimeAttachTest.m b/Tests/ARTRealtimeAttachTest.m index 9a567c15a..f1e755a8f 100644 --- a/Tests/ARTRealtimeAttachTest.m +++ b/Tests/ARTRealtimeAttachTest.m @@ -344,7 +344,6 @@ - (void)testPresenceEnterRestricted { [realtime.auth authorize:tokenParams options:options callback:^(ARTTokenDetails *tokenDetails, NSError *error) { options.token = tokenDetails.token; - [realtime connect]; }]; [realtime.connection on:^(ARTConnectionStateChange *stateChange) { From e358b24063dc9c3646fa024b2528eec5b2b78a7b Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 22 Nov 2016 02:03:08 +0000 Subject: [PATCH 13/13] Test suite: `splitDone`, when a test fails, get the right location of the failure --- Spec/TestUtilities.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Spec/TestUtilities.swift b/Spec/TestUtilities.swift index 2fb922222..7b53024cb 100644 --- a/Spec/TestUtilities.swift +++ b/Spec/TestUtilities.swift @@ -211,14 +211,14 @@ class AblyTests { return [client] } - class func splitDone(howMany: Int, done: () -> ()) -> (() -> ()) { + class func splitDone(howMany: Int, file: StaticString = #file, line: UInt = #line, done: () -> Void) -> (() -> Void) { var left = howMany return { left -= 1 if left == 0 { done() } else if left < 0 { - fail("splitDone called more than the expected \(howMany) times") + XCTFail("splitDone called more than the expected \(howMany) times", file: file, line: line) } } }