From c44c2d5b9d8545382004b90c06e522d42df6fde3 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 12 Oct 2016 17:59:10 +0100 Subject: [PATCH 01/43] Start 0.9 version --- Ably.podspec | 2 +- Examples/Tests/Podfile.lock | 2 +- README.md | 6 +++--- Source/ARTDefault.m | 6 ++++-- Source/Info.plist | 2 +- Spec/RealtimeClient.swift | 2 +- Spec/RealtimeClientConnection.swift | 2 +- Spec/RestClient.swift | 4 ++-- 8 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Ably.podspec b/Ably.podspec index 5a1c6a8ef..3b0b6c427 100644 --- a/Ably.podspec +++ b/Ably.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Ably" - s.version = "0.8.8" + s.version = "0.9.0" s.summary = "iOS client for Ably" s.description = <<-DESC iOS client library for ably.io, the realtime messaging service, written in Objective-C and ready for Swift 2.0. diff --git a/Examples/Tests/Podfile.lock b/Examples/Tests/Podfile.lock index 3fc3dce38..6d0cd2e23 100644 --- a/Examples/Tests/Podfile.lock +++ b/Examples/Tests/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - Ably (0.8.8): + - Ably (0.9.0): - msgpack (= 0.1.8) - SocketRocket (= 0.5.1) - msgpack (0.1.8) diff --git a/README.md b/README.md index 4e6629c5d..ffa89b5d3 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ You can install Ably for iOS through CocoaPods, Carthage or manually. Add this line to your application's Podfile: # For Xcode 7.3 and newer - pod 'Ably', '~> 0.8' + pod 'Ably', '~> 0.9' And then install the dependency: @@ -26,13 +26,13 @@ And then install the dependency: Add this line to your application's Cartfile: # For Xcode 7.3 and newer - github "ably/ably-ios" ~> 0.8 + github "ably/ably-ios" ~> 0.9 And then run `carthage update` to build the framework and drag the built Ably.framework into your Xcode project. ### Manual installation -1. Get the code from GitHub [from the release page](https://github.com/ably/ably-ios/releases/tag/0.8.0), or clone it to get the latest, unstable and possibly underdocumented version: `git clone git@github.com:ably/ably-ios.git` +1. Get the code from GitHub [from the release page](https://github.com/ably/ably-ios/releases/tag/0.9.0), or clone it to get the latest, unstable and possibly underdocumented version: `git clone git@github.com:ably/ably-ios.git` 2. Drag the directory `ably-ios/ably-ios` into your project as a group. 3. Ably depends on [SocketRocket](https://github.com/facebook/SocketRocket) 0.5.1; get it [from the releases page](https://github.com/facebook/SocketRocket/releases/tag/0.5.1) and follow [its manual installation instructions](https://github.com/facebook/SocketRocket#installing-ios). 4. Ably also depends on [msgpack](https://github.com/rvi/msgpack-objective-C) 0.1.8; get it [from the releases page](https://github.com/rvi/msgpack-objective-C/releases/tag/0.1.8) and link it into your project. diff --git a/Source/ARTDefault.m b/Source/ARTDefault.m index ede152f39..5ad72338b 100644 --- a/Source/ARTDefault.m +++ b/Source/ARTDefault.m @@ -12,8 +12,10 @@ @implementation ARTDefault NSString *const ARTDefault_restHost = @"rest.ably.io"; NSString *const ARTDefault_realtimeHost = @"realtime.ably.io"; -NSString *const ARTDefault_version = @"0.8"; -NSString *const ARTDefault_libraryVersion = @"0.8.8"; +NSString *const ARTDefault_version = @"0.9"; +NSString *const ARTDefault_libraryVersion = @"0.9.0"; +NSString *const ARTDefault_ablyBundleId = @"io.ably.Ably"; +NSString *const ARTDefault_bundleVersionKey = @"CFBundleShortVersionString"; NSString *const ARTDefault_platform = @"ios-"; static int _realtimeRequestTimeout = 10.0; diff --git a/Source/Info.plist b/Source/Info.plist index b046100d6..3df65d7b1 100644 --- a/Source/Info.plist +++ b/Source/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 0.8.8 + 0.9.0 CFBundleSignature ???? CFBundleVersion diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index 626baf412..aa5a2ace7 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -36,7 +36,7 @@ class RealtimeClient: QuickSpec { channel.publish(nil, data: "message") { error in expect(error).to(beNil()) let transport = client.transport as! TestProxyTransport - expect(transport.lastUrl!.query).to(haveParam("v", withValue: "0.8")) + expect(transport.lastUrl!.query).to(haveParam("v", withValue: "0.9")) done() } } diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index 6868c1d6e..ead2efb22 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -213,7 +213,7 @@ class RealtimeClientConnection: QuickSpec { done() case .Connected: if let transport = client.transport as? TestProxyTransport, let query = transport.lastUrl?.query { - expect(query).to(haveParam("lib", withValue: "ios-0.8.8")) + expect(query).to(haveParam("lib", withValue: "ios-0.9.0")) } else { XCTFail("MockTransport isn't working") diff --git a/Spec/RestClient.swift b/Spec/RestClient.swift index 2f4d87eb1..50c16248f 100644 --- a/Spec/RestClient.swift +++ b/Spec/RestClient.swift @@ -29,7 +29,7 @@ class RestClient: QuickSpec { channel.publish(nil, data: "message") { error in expect(error).to(beNil()) let version = testHTTPExecutor.requests.first!.allHTTPHeaderFields?["X-Ably-Version"] - expect(version).to(equal("0.8")) + expect(version).to(equal("0.9")) done() } } @@ -1100,7 +1100,7 @@ class RestClient: QuickSpec { let ablyBundleLibVersion = ARTDefault.libraryVersion() expect(headerLibVersion).to(equal(ablyBundleLibVersion)) - let patternToMatch = "ios-0.8." + let patternToMatch = "ios-0.9." let match = headerLibVersion?.hasPrefix(patternToMatch) expect(match).to(beTrue()) From 46a8073428ecb56672157e264de28902775be599 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 18 Oct 2016 12:16:50 +0100 Subject: [PATCH 02/43] TO3k7 (#513) * Start 0.9 version * Add fallbackHostsUseDefault option * Fallback: add initialiser accepting client options * TO3k7 * Add ARTFallback+Private * Default fallbackHosts as Array of Strings * fixup! TO3k7 * fixup! Add fallbackHostsUseDefault option * fixup! TO3k7 --- Ably.xcodeproj/project.pbxproj | 16 +++--- Source/ARTClientOptions.h | 5 ++ Source/ARTClientOptions.m | 20 +++++++- Source/ARTDefault.h | 3 +- Source/ARTFallback+Private.h | 22 +++++++++ Source/ARTFallback.h | 10 ++-- Source/ARTFallback.m | 12 +++-- Source/ARTRealtime.m | 2 +- Source/ARTRest.m | 3 +- Source/ARTStatus.h | 14 ++++-- Source/ARTStatus.m | 2 + Source/Ably.modulemap | 1 + Spec/RealtimeClientConnection.swift | 2 +- Spec/RestClient.swift | 77 ++++++++++++++++++++++++++++- Tests/ARTFallbackTest.m | 2 + 15 files changed, 165 insertions(+), 26 deletions(-) create mode 100644 Source/ARTFallback+Private.h diff --git a/Ably.xcodeproj/project.pbxproj b/Ably.xcodeproj/project.pbxproj index 18ec5939e..c81782371 100644 --- a/Ably.xcodeproj/project.pbxproj +++ b/Ably.xcodeproj/project.pbxproj @@ -94,7 +94,7 @@ D70EAAED1BC3376200CD8B9E /* ARTRestChannel.h in Headers */ = {isa = PBXBuildFile; fileRef = D70EAAEB1BC3376200CD8B9E /* ARTRestChannel.h */; settings = {ATTRIBUTES = (Public, ); }; }; D70EAAEE1BC3376200CD8B9E /* ARTRestChannel.m in Sources */ = {isa = PBXBuildFile; fileRef = D70EAAEC1BC3376200CD8B9E /* ARTRestChannel.m */; }; D714A63E1C74D4B2002F2CA0 /* NSObject+TestSuite.swift in Sources */ = {isa = PBXBuildFile; fileRef = D714A63D1C74D4B2002F2CA0 /* NSObject+TestSuite.swift */; }; - D714A6401C75F0C5002F2CA0 /* ARTWebSocketTransport+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D714A63F1C75F0C5002F2CA0 /* ARTWebSocketTransport+Private.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D714A6401C75F0C5002F2CA0 /* ARTWebSocketTransport+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D714A63F1C75F0C5002F2CA0 /* ARTWebSocketTransport+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 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 */; }; @@ -129,6 +129,7 @@ D75A3F1B1DDE5B62002A4AAD /* ARTGCD.h in Headers */ = {isa = PBXBuildFile; fileRef = D75A3F191DDE5B62002A4AAD /* ARTGCD.h */; }; D75A3F1C1DDE5B62002A4AAD /* ARTGCD.m in Sources */ = {isa = PBXBuildFile; fileRef = D75A3F1A1DDE5B62002A4AAD /* ARTGCD.m */; }; D77394031C6F6FFE00F5478F /* ARTProtocolMessage+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D77394021C6F6FFE00F5478F /* ARTProtocolMessage+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; + D77F02A81DAF8099001B3FF9 /* ARTFallback+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D77F02A71DAF8099001B3FF9 /* ARTFallback+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; D780846E1C68B3E50083009D /* NSObject+TestSuite.m in Sources */ = {isa = PBXBuildFile; fileRef = D780846D1C68B3E50083009D /* NSObject+TestSuite.m */; }; D79FF2751D901CD50067FA6A /* ARTRealtime+TestSuite.m in Sources */ = {isa = PBXBuildFile; fileRef = D79FF2741D901CD50067FA6A /* ARTRealtime+TestSuite.m */; }; D7B17EE31C07208B00A6958E /* ARTConnectionDetails.h in Headers */ = {isa = PBXBuildFile; fileRef = D7B17EE11C07208B00A6958E /* ARTConnectionDetails.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -156,7 +157,7 @@ D7F1D3771BF4DE72001A4B5E /* ARTRealtimePresence.h in Headers */ = {isa = PBXBuildFile; fileRef = D7F1D3751BF4DE72001A4B5E /* ARTRealtimePresence.h */; settings = {ATTRIBUTES = (Public, ); }; }; D7F1D3781BF4DE72001A4B5E /* ARTRealtimePresence.m in Sources */ = {isa = PBXBuildFile; fileRef = D7F1D3761BF4DE72001A4B5E /* ARTRealtimePresence.m */; }; D7F1D37A1BF4E33A001A4B5E /* ARTRestChannel+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D7F1D3791BF4E33A001A4B5E /* ARTRestChannel+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; - EB0505FC1C5BD7C4006BA7E2 /* ARTBaseMessage+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EB0505FB1C5BD7C4006BA7E2 /* ARTBaseMessage+Private.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EB0505FC1C5BD7C4006BA7E2 /* ARTBaseMessage+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EB0505FB1C5BD7C4006BA7E2 /* ARTBaseMessage+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; EB1AE0CC1C5C1EB200D62250 /* ARTEventEmitter+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EB1AE0CB1C5C1EB200D62250 /* ARTEventEmitter+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; EB1AE0CE1C5C3A4900D62250 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1AE0CD1C5C3A4900D62250 /* Utilities.swift */; }; EB20F8D71C653F2300EF3978 /* ARTPresence+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EB20F8D61C653F1E00EF3978 /* ARTPresence+Private.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -164,7 +165,7 @@ EB2D84F71CD75CCE00F23CDA /* ARTReachability.h in Headers */ = {isa = PBXBuildFile; fileRef = EB2D84F61CD75CCE00F23CDA /* ARTReachability.h */; settings = {ATTRIBUTES = (Public, ); }; }; EB2D84FD1CD769B800F23CDA /* ARTOSReachability.m in Sources */ = {isa = PBXBuildFile; fileRef = EB2D84FC1CD769B700F23CDA /* ARTOSReachability.m */; }; EB2D85011CD769C800F23CDA /* ARTOSReachability.h in Headers */ = {isa = PBXBuildFile; fileRef = EB2D85001CD769C800F23CDA /* ARTOSReachability.h */; settings = {ATTRIBUTES = (Public, ); }; }; - EB503C881C7E4A090053AF00 /* ARTClientOptions+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EB503C871C7E4A090053AF00 /* ARTClientOptions+Private.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EB503C881C7E4A090053AF00 /* ARTClientOptions+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EB503C871C7E4A090053AF00 /* ARTClientOptions+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; EB503C8A1C7F1FE40053AF00 /* ARTLog+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EB503C891C7F1FE40053AF00 /* ARTLog+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; EB5E058D1C77027600A48B39 /* ARTCrypto+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EB5E058C1C77027600A48B39 /* ARTCrypto+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; EB7617721CB6CBFF00D0981E /* ARTRealtimePresence+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EB7617711CB6CBFE00D0981E /* ARTRealtimePresence+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -364,6 +365,7 @@ D75A3F191DDE5B62002A4AAD /* ARTGCD.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTGCD.h; sourceTree = ""; }; D75A3F1A1DDE5B62002A4AAD /* ARTGCD.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTGCD.m; sourceTree = ""; }; D77394021C6F6FFE00F5478F /* ARTProtocolMessage+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARTProtocolMessage+Private.h"; sourceTree = ""; }; + D77F02A71DAF8099001B3FF9 /* ARTFallback+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARTFallback+Private.h"; sourceTree = ""; }; D780846C1C68B3E50083009D /* NSObject+TestSuite.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSObject+TestSuite.h"; sourceTree = ""; }; D780846D1C68B3E50083009D /* NSObject+TestSuite.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSObject+TestSuite.m"; sourceTree = ""; }; D79FF2731D901CD50067FA6A /* ARTRealtime+TestSuite.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARTRealtime+TestSuite.h"; sourceTree = ""; }; @@ -668,18 +670,18 @@ D746AE331BBC29FF003ECEF8 /* Types */ = { isa = PBXGroup; children = ( - EB8AC6421C6515ED002ABA92 /* ARTTokenParams+Private.h */, - EB0505FB1C5BD7C4006BA7E2 /* ARTBaseMessage+Private.h */, D7D8F81F1BC2BE15009718F2 /* ARTAuthOptions.h */, D7D5A6991CA3D9040071BD6D /* ARTAuthOptions+Private.h */, D7D8F8201BC2BE15009718F2 /* ARTAuthOptions.m */, D7D8F8271BC2C706009718F2 /* ARTTokenRequest.h */, D7D8F8281BC2C706009718F2 /* ARTTokenRequest.m */, D7D8F8291BC2C706009718F2 /* ARTTokenParams.h */, + EB8AC6421C6515ED002ABA92 /* ARTTokenParams+Private.h */, D7D8F82A1BC2C706009718F2 /* ARTTokenParams.m */, D7D8F8231BC2C691009718F2 /* ARTTokenDetails.h */, D7D8F8241BC2C691009718F2 /* ARTTokenDetails.m */, 961343D61A42E0B7006DC822 /* ARTClientOptions.h */, + EB503C871C7E4A090053AF00 /* ARTClientOptions+Private.h */, 961343D71A42E0B7006DC822 /* ARTClientOptions.m */, D746AE201BBB60EE003ECEF8 /* ARTChannel.h */, D746AE241BBB611C003ECEF8 /* ARTChannel+Private.h */, @@ -693,6 +695,7 @@ D77394021C6F6FFE00F5478F /* ARTProtocolMessage+Private.h */, 96E408421A38939E00087F77 /* ARTProtocolMessage.m */, 96BF61621A35CDE1004CF2B3 /* ARTBaseMessage.h */, + EB0505FB1C5BD7C4006BA7E2 /* ARTBaseMessage+Private.h */, 96BF61631A35CDE1004CF2B3 /* ARTBaseMessage.m */, D746AE361BBC3201003ECEF8 /* ARTMessage.h */, D746AE371BBC3201003ECEF8 /* ARTMessage.m */, @@ -710,7 +713,6 @@ 1C55427C1B148306003068DB /* ARTStatus.m */, 96BF615C1A35C1C8004CF2B3 /* ARTTypes.h */, 96BF615D1A35C1C8004CF2B3 /* ARTTypes.m */, - EB503C871C7E4A090053AF00 /* ARTClientOptions+Private.h */, ); name = Types; sourceTree = ""; @@ -763,6 +765,7 @@ D746AE2E1BBBE7D7003ECEF8 /* ARTPaginatedResult+Private.h */, 850BFB4B1B79323C009D0ADD /* ARTPaginatedResult.m */, 1C578E1D1B3435CA00EF46EC /* ARTFallback.h */, + D77F02A71DAF8099001B3FF9 /* ARTFallback+Private.h */, 1C578E1E1B3435CA00EF46EC /* ARTFallback.m */, ); name = HTTP; @@ -837,6 +840,7 @@ 96E4083F1A3892C700087F77 /* ARTRealtimeTransport.h in Headers */, D746AE4F1BBD84E7003ECEF8 /* ARTChannelOptions.h in Headers */, D7588AF31BFF91B800BB8279 /* ARTURLSessionServerTrust.h in Headers */, + D77F02A81DAF8099001B3FF9 /* ARTFallback+Private.h in Headers */, D746AE3C1BBC5AE1003ECEF8 /* ARTRealtimeChannel.h in Headers */, 96BF61701A35FB7C004CF2B3 /* ARTAuth.h in Headers */, 96A507A11A377AA50077CDF8 /* ARTPresenceMessage.h in Headers */, diff --git a/Source/ARTClientOptions.h b/Source/ARTClientOptions.h index 4708b47f3..39276564f 100644 --- a/Source/ARTClientOptions.h +++ b/Source/ARTClientOptions.h @@ -75,6 +75,11 @@ ART_ASSUME_NONNULL_BEGIN */ @property (art_nullable, nonatomic, copy) __GENERIC(NSArray, NSString *) *fallbackHosts; +/** + Optionally allows the default fallback hosts `[a-e].ably-realtime.com` to be used when `environment` is not production or a custom realtime or REST host endpoint is being used. It is never valid to configure `fallbackHost` and set `fallbackHostsUseDefault` to `true`. + */ +@property (assign, nonatomic) BOOL fallbackHostsUseDefault; + - (BOOL)isBasicAuth; - (NSURL *)restUrl; - (NSURL *)realtimeUrl; diff --git a/Source/ARTClientOptions.m b/Source/ARTClientOptions.m index 7d41df2e7..38ba2cadd 100644 --- a/Source/ARTClientOptions.m +++ b/Source/ARTClientOptions.m @@ -10,6 +10,7 @@ #import "ARTAuthOptions+Private.h" #import "ARTDefault.h" +#import "ARTStatus.h" #import "ARTTokenParams.h" NSString *ARTDefaultEnvironment = nil; @@ -40,6 +41,8 @@ - (instancetype)initDefaults { _httpRequestTimeout = 15.0; //Seconds _httpMaxRetryDuration = 10.0; //Seconds _httpMaxRetryCount = 3; + _fallbackHosts = nil; + _fallbackHostsUseDefault = false; return self; } @@ -106,7 +109,8 @@ - (id)copyWithZone:(NSZone *)zone { options.httpMaxRetryDuration = self.httpMaxRetryDuration; options.httpOpenTimeout = self.httpOpenTimeout; options.httpRequestTimeout = self.httpRequestTimeout; - options.fallbackHosts = self.fallbackHosts; + options->_fallbackHosts = self.fallbackHosts; //ignore setter + options->_fallbackHostsUseDefault = self.fallbackHostsUseDefault; //ignore setter return options; } @@ -129,6 +133,20 @@ - (BOOL)hasCustomRealtimeHost { return _realtimeHost != nil; } +- (void)setFallbackHosts:(art_nullable __GENERIC(NSArray, NSString *) *)value { + if (_fallbackHostsUseDefault) { + [NSException raise:ARTFallbackIncompatibleOptionsException format:@"Could not setup custom fallback hosts because it is currently configured to use default fallback hosts."]; + } + _fallbackHosts = value; +} + +- (void)setFallbackHostsUseDefault:(BOOL)value { + if (_fallbackHosts) { + [NSException raise:ARTFallbackIncompatibleOptionsException format:@"Could not configure options to use default fallback hosts because a custom fallback host list is being used."]; + } + _fallbackHostsUseDefault = value; +} + + (void)setDefaultEnvironment:(NSString *)environment { ARTDefaultEnvironment = environment; } diff --git a/Source/ARTDefault.h b/Source/ARTDefault.h index e2a985d62..b65969bc6 100644 --- a/Source/ARTDefault.h +++ b/Source/ARTDefault.h @@ -7,10 +7,11 @@ // #import +#import "CompatibilityMacros.h" @interface ARTDefault : NSObject -+ (NSArray*)fallbackHosts; ++ (__GENERIC(NSArray, NSString *) *)fallbackHosts; + (NSString*)restHost; + (NSString*)realtimeHost; + (int)port; diff --git a/Source/ARTFallback+Private.h b/Source/ARTFallback+Private.h new file mode 100644 index 000000000..f264deb41 --- /dev/null +++ b/Source/ARTFallback+Private.h @@ -0,0 +1,22 @@ +// +// ARTFallback+Private.h +// Ably +// +// Created by Ricardo Pereira on 13/10/16. +// Copyright © 2016 Ably. All rights reserved. +// + +#include "ARTFallback.h" +#include "CompatibilityMacros.h" + +ART_ASSUME_NONNULL_BEGIN + +extern int (^ARTFallback_getRandomHostIndex)(int count); + +@interface ARTFallback () + +@property (readwrite, strong, nonatomic) __GENERIC(NSMutableArray, NSString *) *hosts; + +@end + +ART_ASSUME_NONNULL_END diff --git a/Source/ARTFallback.h b/Source/ARTFallback.h index f2e78fb6c..4301fe4a0 100644 --- a/Source/ARTFallback.h +++ b/Source/ARTFallback.h @@ -14,12 +14,12 @@ ART_ASSUME_NONNULL_BEGIN @class ARTHttpResponse; @class ARTClientOptions; -extern int (^ARTFallback_getRandomHostIndex)(int count); - @interface ARTFallback : NSObject -{ - -} + +/** + Init with options. + */ +- (instancetype)initWithOptions:(ARTClientOptions *)options; /** Init with fallback hosts array. diff --git a/Source/ARTFallback.m b/Source/ARTFallback.m index 2444f7ec0..957cc7944 100644 --- a/Source/ARTFallback.m +++ b/Source/ARTFallback.m @@ -6,7 +6,7 @@ // Copyright (c) 2015 Ably. All rights reserved. // -#import "ARTFallback.h" +#import "ARTFallback+Private.h" #import "ARTDefault.h" #import "ARTStatus.h" @@ -19,8 +19,6 @@ @interface ARTFallback () -@property (readwrite, strong, nonatomic) NSMutableArray * hosts; - @end @implementation ARTFallback @@ -31,7 +29,6 @@ - (instancetype)initWithFallbackHosts:(art_nullable __GENERIC(NSArray, NSString if (fallbackHosts != nil && fallbackHosts.count == 0) { return nil; } - self.hosts = [NSMutableArray array]; NSMutableArray * hostArray = [[NSMutableArray alloc] initWithArray: fallbackHosts ? fallbackHosts : [ARTDefault fallbackHosts]]; size_t count = [hostArray count]; @@ -44,6 +41,13 @@ - (instancetype)initWithFallbackHosts:(art_nullable __GENERIC(NSArray, NSString return self; } +- (instancetype)initWithOptions:(ARTClientOptions *)options { + if (options.fallbackHostsUseDefault) { + return [self initWithFallbackHosts:nil]; //default + } + return [self initWithFallbackHosts:options.fallbackHosts]; +} + - (instancetype)init { return [self initWithFallbackHosts:nil]; } diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index fff4eca62..05af554ff 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -880,7 +880,7 @@ - (void)realtimeTransportFailed:(id)transport withError:(A [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p host is down; can retry with fallback host", self]; if (!_fallbacks && [error.url.host isEqualToString:[ARTDefault realtimeHost]]) { [self.rest internetIsUp:^void(BOOL isUp) { - _fallbacks = [[ARTFallback alloc] initWithFallbackHosts:[self getClientOptions].fallbackHosts]; + _fallbacks = [[ARTFallback alloc] initWithOptions:[self getClientOptions]]; (_fallbacks != nil) ? [self reconnectWithFallback] : [self transition:ARTRealtimeFailed withErrorInfo:[ARTErrorInfo createWithNSError:error.error]]; }]; return; diff --git a/Source/ARTRest.m b/Source/ARTRest.m index 0c7bd4dbf..1823ddc82 100644 --- a/Source/ARTRest.m +++ b/Source/ARTRest.m @@ -32,7 +32,6 @@ #import "ARTTokenParams.h" #import "ARTTokenDetails.h" #import "ARTDefault.h" -#import "ARTFallback.h" #import "ARTGCD.h" @implementation ARTRest @@ -158,7 +157,7 @@ - (void)executeRequest:(NSMutableURLRequest *)request completion:(void (^)(NSHTT } if (retries < _options.httpMaxRetryCount && [self shouldRetryWithFallback:request response:response error:error]) { if (!blockFallbacks && [request.URL.host isEqualToString:(_prioritizedHost ? _prioritizedHost : [ARTDefault restHost])]) { - blockFallbacks = [[ARTFallback alloc] initWithFallbackHosts:_options.fallbackHosts]; + blockFallbacks = [[ARTFallback alloc] initWithOptions:_options]; } if (blockFallbacks) { NSString *host = [blockFallbacks popFallbackHost]; diff --git a/Source/ARTStatus.h b/Source/ARTStatus.h index 3f37407d9..376cb6310 100644 --- a/Source/ARTStatus.h +++ b/Source/ARTStatus.h @@ -32,8 +32,6 @@ typedef NS_ENUM(NSUInteger, ARTState) { }; /** - ARTCodeErrors - The list of all public error codes returned under the error domain ARTAblyErrorDomain */ typedef CF_ENUM(NSUInteger, ARTCodeError) { @@ -45,6 +43,14 @@ ART_ASSUME_NONNULL_BEGIN FOUNDATION_EXPORT NSString *const ARTAblyErrorDomain; +/** + Ably client exception names + */ +FOUNDATION_EXPORT NSString *const ARTFallbackIncompatibleOptionsException; + +/** + Ably client error class + */ @interface ARTErrorInfo : NSError @property (readonly, getter=getMessage) NSString *message; @@ -60,7 +66,9 @@ FOUNDATION_EXPORT NSString *const ARTAblyErrorDomain; @end - +/** + Ably client status class + */ @interface ARTStatus : NSObject @property (art_nullable, readonly, strong, nonatomic) ARTErrorInfo *errorInfo; diff --git a/Source/ARTStatus.m b/Source/ARTStatus.m index 4444650ff..9bc48b0dc 100644 --- a/Source/ARTStatus.m +++ b/Source/ARTStatus.m @@ -13,6 +13,8 @@ // Reverse-DNS style domain NSString *const ARTAblyErrorDomain = @"io.ably.cocoa"; +NSString *const ARTFallbackIncompatibleOptionsException = @"ARTFallbackIncompatibleOptionsException"; + NSInteger getStatusFromCode(NSInteger code) { return code / 100; } diff --git a/Source/Ably.modulemap b/Source/Ably.modulemap index d02c1269e..02ca5e62a 100644 --- a/Source/Ably.modulemap +++ b/Source/Ably.modulemap @@ -31,5 +31,6 @@ framework module Ably { header "ARTLog+Private.h" header "ARTRealtimePresence+Private.h" header "ARTRestPresence+Private.h" + header "ARTFallback+Private.h" } } diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index ead2efb22..30c71d0a6 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -2865,7 +2865,7 @@ class RealtimeClientConnection: QuickSpec { NSRegularExpression.extract(url.absoluteString, pattern: "[a-e].ably-realtime.com") } let resultFallbackHosts = urlConnections.flatMap(extractHostname) - let expectedFallbackHosts = Array(expectedHostOrder.map({ ARTDefault.fallbackHosts()[$0] as! String })) + let expectedFallbackHosts = Array(expectedHostOrder.map({ ARTDefault.fallbackHosts()[$0] })) expect(resultFallbackHosts).to(equal(expectedFallbackHosts)) } diff --git a/Spec/RestClient.swift b/Spec/RestClient.swift index 50c16248f..487115620 100644 --- a/Spec/RestClient.swift +++ b/Spec/RestClient.swift @@ -573,6 +573,79 @@ class RestClient: QuickSpec { // RSC15 context("Host Fallback") { + // TO3k7 + context("fallbackHostsUseDefault option") { + + it("allows the default fallback hosts to be used when @environment@ is not production") { + let options = ARTClientOptions(key: "xxxx:xxxx") + options.environment = "not-production" + options.fallbackHostsUseDefault = true + + let client = ARTRest(options: options) + expect(client.options.fallbackHostsUseDefault).to(beTrue()) + // Not production + expect(client.options.environment).toNot(beNil()) + expect(client.options.environment).toNot(equal("production")) + + let fallback = ARTFallback(options: client.options) + expect(fallback.hosts).to(haveCount(ARTDefault.fallbackHosts().count)) + + ARTDefault.fallbackHosts().forEach() { + expect(fallback.hosts).to(contain($0)) + } + } + + it("allows the default fallback hosts to be used when a custom Realtime or REST host endpoint is being used") { + let options = ARTClientOptions(key: "xxxx:xxxx") + options.restHost = "fake1.ably.io" + options.realtimeHost = "fake2.ably.io" + options.fallbackHostsUseDefault = true + + let client = ARTRest(options: options) + expect(client.options.fallbackHostsUseDefault).to(beTrue()) + // Custom + expect(client.options.restHost).toNot(equal(ARTDefault.restHost())) + expect(client.options.realtimeHost).toNot(equal(ARTDefault.realtimeHost())) + + let fallback = ARTFallback(options: client.options) + expect(fallback.hosts).to(haveCount(ARTDefault.fallbackHosts().count)) + + ARTDefault.fallbackHosts().forEach() { + expect(fallback.hosts).to(contain($0)) + } + } + + it("should be inactive by default") { + let options = ARTClientOptions(key: "xxxx:xxxx") + expect(options.fallbackHostsUseDefault).to(beFalse()) + } + + it("should never accept to configure @fallbackHost@ and set @fallbackHostsUseDefault@ to @true@") { + let options = ARTClientOptions(key: "xxxx:xxxx") + expect(options.fallbackHosts).to(beNil()) + expect(options.fallbackHostsUseDefault).to(beFalse()) + + expect{ options.fallbackHosts = [] }.toNot(raiseException()) + + expect{ options.fallbackHostsUseDefault = true }.to( + raiseException { exception in + expect(exception.name).to(equal(ARTFallbackIncompatibleOptionsException)) + } + ) + + options.fallbackHosts = nil + + expect{ options.fallbackHostsUseDefault = true }.toNot(raiseException()) + + expect { options.fallbackHosts = ["fake.ably.io"] }.to( + raiseException { exception in + expect(exception.name).to(equal(ARTFallbackIncompatibleOptionsException)) + } + ) + } + + } + // RSC15b it("failing HTTP requests with custom endpoint should result in an error immediately") { let options = ARTClientOptions(key: "xxxx:xxxx") @@ -792,7 +865,7 @@ class RestClient: QuickSpec { NSRegularExpression.extract(request.URL!.absoluteString, pattern: "[a-e].ably-realtime.com") } let resultFallbackHosts = testHTTPExecutor.requests.flatMap(extractHostname) - let expectedFallbackHosts = Array(expectedHostOrder.map({ ARTDefault.fallbackHosts()[$0] as! String })[0.. #import "ARTFallback.h" +#import "ARTFallback+Private.h" #import "ARTDefault.h" + @interface ARTFallbackTest : XCTestCase @end From c19e4ed92bfdc8a71ead6e50d4560530dedea01d Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 18 Oct 2016 12:23:21 +0100 Subject: [PATCH 03/43] Update RSC15a for 0.9 (#515) * Start 0.9 version * Update RSC15a: default fallback hosts * Default fallbackHosts as Array of Strings --- Spec/RestClient.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Spec/RestClient.swift b/Spec/RestClient.swift index 487115620..71c3e1aff 100644 --- a/Spec/RestClient.swift +++ b/Spec/RestClient.swift @@ -839,6 +839,14 @@ class RestClient: QuickSpec { ARTFallback_getRandomHostIndex = originalARTFallback_getRandomHostIndex } + it("default fallback hosts should match @[a-e].ably-realtime.com@") { + let defaultFallbackHosts = ARTDefault.fallbackHosts() + defaultFallbackHosts.forEach { host in + expect(host).to(match("[a-e].ably-realtime.com")) + } + expect(defaultFallbackHosts).to(haveCount(5)) + } + it("until httpMaxRetryCount has been reached") { let options = ARTClientOptions(key: "xxxx:xxxx") let client = ARTRest(options: options) From 312f5c7e1d814975d670c108eb930c41b4001ef0 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 18 Oct 2016 12:25:10 +0100 Subject: [PATCH 04/43] RSA4a (#517) * Test suite: add TestProxyHTTPExecutor.simulateIncomingServerErrorOnNextRequest * RSA4a * Fix Realtime: indicate an error and not retry the request when the server responds with a token error --- Source/ARTRealtime.m | 22 +++++++---- Spec/Auth.swift | 60 +++++++++++++++++++++++++++++ Spec/RealtimeClientConnection.swift | 4 +- Spec/TestUtilities.swift | 42 +++++++++++++++++++- 4 files changed, 118 insertions(+), 10 deletions(-) diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index 05af554ff..575c1071c 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -461,11 +461,8 @@ - (void)onDisconnected { - (void)onDisconnected:(ARTProtocolMessage *)message { [self.logger info:@"R:%p ARTRealtime disconnected", self]; - ARTErrorInfo *error; - if (message) { - error = message.error; - } - if (!_renewingToken && error && error.statusCode == 401 && error.code >= 40140 && error.code < 40150 && [self isTokenRenewable]) { + ARTErrorInfo *error = message.error; + if ([self shouldRenewToken:&error]) { [self connectWithRenewedToken]; [self transition:ARTRealtimeDisconnected withErrorInfo:error]; [self.connection setErrorReason:nil]; @@ -495,13 +492,24 @@ - (void)onError:(ARTProtocolMessage *)message { [self onChannelMessage:message]; } else { ARTErrorInfo *error = message.error; - if (!_renewingToken && error && error.statusCode == 401 && error.code >= 40140 && error.code < 40150 && [self isTokenRenewable]) { + if ([self shouldRenewToken:&error]) { [self connectWithRenewedToken]; return; } [self.connection setId:nil]; - [self transition:ARTRealtimeFailed withErrorInfo:message.error]; + [self transition:ARTRealtimeFailed withErrorInfo:error]; + } +} + +- (BOOL)shouldRenewToken:(ARTErrorInfo **)errorPtr { + if (!_renewingToken && errorPtr && *errorPtr && + (*errorPtr).statusCode == 401 && (*errorPtr).code >= 40140 && (*errorPtr).code < 40150) { + if ([self isTokenRenewable]) { + return YES; + } + *errorPtr = [ARTErrorInfo createWithCode:ARTStateRequestTokenFailed message:@"no means to renew the token is provided (either an API key, authCallback or authUrl)"]; } + return NO; } - (BOOL)isTokenRenewable { diff --git a/Spec/Auth.swift b/Spec/Auth.swift index 0f994d19a..e3c3de6e0 100644 --- a/Spec/Auth.swift +++ b/Spec/Auth.swift @@ -185,6 +185,66 @@ class Auth : QuickSpec { expect(client.auth.method).to(equal(ARTAuthMethod.Token)) } } + + // RSA4a + it("should indicate an error and not retry the request when the server responds with a token error and there is no way to renew the token") { + let options = AblyTests.clientOptions() + options.token = getTestToken() + + let rest = ARTRest(options: options) + // No means to renew the token is provided + expect(rest.options.key).to(beNil()) + expect(rest.options.authCallback).to(beNil()) + expect(rest.options.authUrl).to(beNil()) + rest.httpExecutor = testHTTPExecutor + + let channel = rest.channels.get("test") + + testHTTPExecutor.simulateIncomingServerErrorOnNextRequest(40141, description: "token revoked") + waitUntil(timeout: testTimeout) { done in + channel.publish("message", data: nil) { error in + guard let error = error else { + fail("Error is nil"); done(); return + } + expect(UInt(error.code)).to(equal(ARTState.RequestTokenFailed.rawValue)) + done() + } + } + } + + // RSA4a + it("should transition the connection to the FAILED state when the server responds with a token error and there is no way to renew the token") { + let options = AblyTests.clientOptions() + options.tokenDetails = getTestTokenDetails(ttl: 0.1) + options.autoConnect = false + + // Token will expire, expecting 40142 + waitUntil(timeout: testTimeout) { done in + delay(0.2) { done() } + } + + let realtime = ARTRealtime(options: options) + defer { realtime.dispose(); realtime.close() } + // No means to renew the token is provided + expect(realtime.options.key).to(beNil()) + expect(realtime.options.authCallback).to(beNil()) + expect(realtime.options.authUrl).to(beNil()) + realtime.setTransportClass(TestProxyTransport.self) + + let channel = realtime.channels.get("test") + + waitUntil(timeout: testTimeout) { done in + realtime.connect() + channel.publish("message", data: nil) { error in + guard let error = error else { + fail("Error is nil"); done(); return + } + expect(UInt(error.code)).to(equal(ARTState.RequestTokenFailed.rawValue)) + expect(realtime.connection.state).to(equal(ARTRealtimeConnectionState.Failed)) + done() + } + } + } } // RSA14 diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index 30c71d0a6..0a61f0fa0 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -1725,7 +1725,7 @@ class RealtimeClientConnection: QuickSpec { guard let errorInfo = errorInfo else { fail("ErrorInfo is nil"); done(); return } - expect(errorInfo.code).to(equal(40142)) //Token expired + expect(UInt(errorInfo.code)).to(equal(ARTState.RequestTokenFailed.rawValue)) done() default: break @@ -2422,7 +2422,7 @@ class RealtimeClientConnection: QuickSpec { guard let error = stateChange?.reason else { fail("Error is nil"); done(); return } - expect(error.code) == 40142 + expect(UInt(error.code)).to(equal(ARTState.RequestTokenFailed.rawValue)) expect(client.connection.errorReason).to(beIdenticalTo(error)) done() } diff --git a/Spec/TestUtilities.swift b/Spec/TestUtilities.swift index b2b85d309..7748097be 100644 --- a/Spec/TestUtilities.swift +++ b/Spec/TestUtilities.swift @@ -577,9 +577,38 @@ class MockHTTP: ARTHttp { /// Records each request and response for test purpose. class TestProxyHTTPExecutor: NSObject, ARTHTTPExecutor { + struct ErrorSimulator { + let value: Int + let description: String + let serverId = "server-test-suite" + + mutating func stubResponse(url: NSURL) -> NSHTTPURLResponse? { + return NSHTTPURLResponse(URL: url, statusCode: 401, HTTPVersion: "HTTP/1.1", headerFields: [ + "Content-Length": String(stubData?.length ?? 0), + "Content-Type": "application/json", + "X-Ably-Errorcode": String(value), + "X-Ably-Errormessage": description, + "X-Ably-Serverid": serverId, + ] + ) + } + + lazy var stubData: NSData? = { + let jsonObject = ["error": [ + "statusCode": modf(Float(self.value)/100).0, //whole number part + "code": self.value, + "message": self.description, + "serverId": self.serverId, + ] + ] + return try? NSJSONSerialization.dataWithJSONObject(jsonObject, options: NSJSONWritingOptions.init(rawValue: 0)) + }() + } + private var errorSimulator: ErrorSimulator? + var http: ARTHttp? = ARTHttp() var logger: ARTLog? - + var requests: [NSMutableURLRequest] = [] var responses: [NSHTTPURLResponse] = [] @@ -592,6 +621,13 @@ class TestProxyHTTPExecutor: NSObject, ARTHTTPExecutor { return } self.requests.append(request) + + if var simulatedError = errorSimulator, requestURL = request.URL { + defer { errorSimulator = nil } + callback?(simulatedError.stubResponse(requestURL), simulatedError.stubData, nil) + return + } + if let performEvent = beforeRequest { performEvent(request, callback) } @@ -611,6 +647,10 @@ class TestProxyHTTPExecutor: NSObject, ARTHTTPExecutor { } } + func simulateIncomingServerErrorOnNextRequest(errorValue: Int, description: String) { + errorSimulator = ErrorSimulator(value: errorValue, description: description, stubData: nil) + } + } /// Records each message for test purpose. From 8958b8a27c427c4c0fb2a00d1f43b3b5705951c3 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 18 Oct 2016 12:47:51 +0100 Subject: [PATCH 05/43] Update RSC15e (#514) --- Source/ARTRest+Private.h | 2 ++ Source/ARTRest.m | 10 +++++++++- Spec/RestClient.swift | 41 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/Source/ARTRest+Private.h b/Source/ARTRest+Private.h index be2f284ab..8f2014ff6 100644 --- a/Source/ARTRest+Private.h +++ b/Source/ARTRest+Private.h @@ -21,6 +21,8 @@ ART_ASSUME_NONNULL_BEGIN @property (readonly, strong, nonatomic) __GENERIC(id, ARTEncoder) defaultEncoder; @property (readonly, strong, nonatomic) NSString *defaultEncoding; //Content-Type @property (readonly, strong, nonatomic) NSDictionary *encoders; + +// Private prioritized host for testing only (overrides the current `restHost`) @property (readwrite, strong, nonatomic, art_nullable) NSString *prioritizedHost; @property (nonatomic, strong) id httpExecutor; diff --git a/Source/ARTRest.m b/Source/ARTRest.m index 1823ddc82..3e7583578 100644 --- a/Source/ARTRest.m +++ b/Source/ARTRest.m @@ -156,7 +156,7 @@ - (void)executeRequest:(NSMutableURLRequest *)request completion:(void (^)(NSHTT } } if (retries < _options.httpMaxRetryCount && [self shouldRetryWithFallback:request response:response error:error]) { - if (!blockFallbacks && [request.URL.host isEqualToString:(_prioritizedHost ? _prioritizedHost : [ARTDefault restHost])]) { + if (!blockFallbacks && [request.URL.host isEqualToString:[self currentHost]]) { blockFallbacks = [[ARTFallback alloc] initWithOptions:_options]; } if (blockFallbacks) { @@ -192,6 +192,14 @@ - (BOOL)shouldRetryWithFallback:(NSMutableURLRequest *)request response:(NSHTTPU return NO; } +- (NSString *)currentHost { + if (_prioritizedHost) { + // Test purpose only + return _prioritizedHost; + } + return self.options.restHost; +} + - (void)prepareAuthorisationHeader:(ARTAuthMethod)method completion:(void (^)(NSString *authorization, NSError *error))callback { [self prepareAuthorisationHeader:method force:NO completion:callback]; } diff --git a/Spec/RestClient.swift b/Spec/RestClient.swift index 71c3e1aff..c9d217a4c 100644 --- a/Spec/RestClient.swift +++ b/Spec/RestClient.swift @@ -786,7 +786,7 @@ class RestClient: QuickSpec { } // RSC15e - it("every new HTTP request is first attempted to the primary host rest.ably.io") { + it("every new HTTP request is first attempted to the default primary host rest.ably.io") { let options = ARTClientOptions(key: "xxxx:xxxx") options.httpMaxRetryCount = 1 let client = ARTRest(options: options) @@ -812,9 +812,44 @@ class RestClient: QuickSpec { if testHTTPExecutor.requests.count != 3 { return } - expect(NSRegularExpression.match(testHTTPExecutor.requests[0].URL!.absoluteString, pattern: "//rest.ably.io")).to(beTrue()) + + expect(NSRegularExpression.match(testHTTPExecutor.requests[0].URL!.absoluteString, pattern: "//\(ARTDefault.restHost())")).to(beTrue()) + expect(NSRegularExpression.match(testHTTPExecutor.requests[1].URL!.absoluteString, pattern: "//[a-e].ably-realtime.com")).to(beTrue()) + expect(NSRegularExpression.match(testHTTPExecutor.requests[2].URL!.absoluteString, pattern: "//\(ARTDefault.restHost())")).to(beTrue()) + } + + // RSC15e + it("if ClientOptions#restHost is set then every new HTTP request should first attempt ClientOptions#restHost") { + let options = ARTClientOptions(key: "xxxx:xxxx") + options.httpMaxRetryCount = 1 + options.restHost = "fake.ably.io" + let client = ARTRest(options: options) + client.httpExecutor = testHTTPExecutor + testHTTPExecutor.http = MockHTTP(network: .HostUnreachable) + let channel = client.channels.get("test") + + waitUntil(timeout: testTimeout) { done in + channel.publish(nil, data: "nil") { _ in + done() + } + } + + testHTTPExecutor.http = ARTHttp() + + waitUntil(timeout: testTimeout) { done in + channel.publish(nil, data: "nil") { _ in + done() + } + } + + expect(testHTTPExecutor.requests).to(haveCount(3)) + if testHTTPExecutor.requests.count != 3 { + return + } + + expect(NSRegularExpression.match(testHTTPExecutor.requests[0].URL!.absoluteString, pattern: "//\(client.options.restHost)")).to(beTrue()) expect(NSRegularExpression.match(testHTTPExecutor.requests[1].URL!.absoluteString, pattern: "//[a-e].ably-realtime.com")).to(beTrue()) - expect(NSRegularExpression.match(testHTTPExecutor.requests[2].URL!.absoluteString, pattern: "//rest.ably.io")).to(beTrue()) + expect(NSRegularExpression.match(testHTTPExecutor.requests[2].URL!.absoluteString, pattern: "//\(client.options.restHost)")).to(beTrue()) } // RSC15a From 137e92182b0b61929d5b3c2703ee3dff0dfca359 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 19 Oct 2016 07:56:59 +0100 Subject: [PATCH 06/43] Remove specs RSA10c and RSA10d (#522) --- Spec/Auth.swift | 66 ------------------------------------------------- 1 file changed, 66 deletions(-) diff --git a/Spec/Auth.swift b/Spec/Auth.swift index e3c3de6e0..d7bd50ab2 100644 --- a/Spec/Auth.swift +++ b/Spec/Auth.swift @@ -1718,72 +1718,6 @@ class Auth : QuickSpec { } } - // RSA10c - it("should create a new token when no token exists or current token has expired") { - let rest = ARTRest(options: AblyTests.commonAppSetup()) - - let tokenParams = ARTTokenParams() - tokenParams.ttl = 3.0 //Seconds - - // FIXME: buffer of 15s for token expiry - - // No token exists - expect(rest.auth.tokenDetails?.token).to(beNil()) - - waitUntil(timeout: testTimeout) { done in - // Create token - rest.auth.authorise(tokenParams, options: nil) { tokenDetails, error in - expect(error).to(beNil()) - expect(tokenDetails).toNot(beNil()) - expect(tokenDetails?.token).toNot(beEmpty()) - - let expiredToken = tokenDetails?.token - // New token - delay(tokenParams.ttl + 1.0) { - rest.auth.authorise(nil, options: nil) { tokenDetails, error in - expect(error).to(beNil()) - guard let tokenDetails = tokenDetails else { - XCTFail("TokenDetails is nil"); done(); return - } - expect(tokenDetails.token).toNot(equal(expiredToken)) - done() - } - } - } - } - } - - // RSA10d - it("should issue a new token even if an existing token exists when AuthOption.force is true") { - let options = AblyTests.commonAppSetup() - options.clientId = "defClientId" - let rest = ARTRest(options: options) - - let authOptions = ARTAuthOptions() - authOptions.key = options.key - authOptions.force = true - - // Current token - waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: nil) { tokenDetails, error in - expect(error).to(beNil()) - expect(tokenDetails?.token).toNot(beNil()) - - let currentToken = tokenDetails?.token - - rest.auth.authorise(nil, options: authOptions) { tokenDetails, error in - expect(error).to(beNil()) - guard let tokenDetails = tokenDetails else { - XCTFail("TokenDetails is nil"); done(); return - } - expect(tokenDetails.clientId).to(equal("defClientId")) - expect(tokenDetails.token).toNot(equal(currentToken)) - done() - } - } - } - } - // RSA10e it("should use the requestToken implementation") { let rest = ARTRest(options: AblyTests.commonAppSetup()) From ae57e2c2f4816d5d505ba3ce430a38a290be62ea Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 19 Oct 2016 08:33:17 +0100 Subject: [PATCH 07/43] RSA10l (#524) * Auth: deprecate `authorise` in favor of `authorize`. * RSA10l * Use `authorize` instead of `authorise` (close #496) --- Source/ARTAuth.h | 4 +- Source/ARTAuth.m | 4 ++ Source/ARTRest.m | 2 +- Source/ARTWebSocketTransport.m | 2 +- Spec/Auth.swift | 100 ++++++++++++++++++--------------- Spec/RestClient.swift | 2 +- Tests/ARTRealtimeAttachTest.m | 2 +- Tests/ARTRestCapabilityTest.m | 2 +- 8 files changed, 66 insertions(+), 52 deletions(-) diff --git a/Source/ARTAuth.h b/Source/ARTAuth.h index a2a6bd6f4..5b6ec8197 100644 --- a/Source/ARTAuth.h +++ b/Source/ARTAuth.h @@ -44,7 +44,9 @@ ART_ASSUME_NONNULL_BEGIN - (void)requestToken:(art_nullable ARTTokenParams *)tokenParams withOptions:(art_nullable ARTAuthOptions *)authOptions callback:(void (^)(ARTTokenDetails *__art_nullable, NSError *__art_nullable))callback; -- (void)authorise:(art_nullable ARTTokenParams *)tokenParams options:(art_nullable ARTAuthOptions *)authOptions callback:(void (^)(ARTTokenDetails *__art_nullable, NSError *__art_nullable))callback; +- (void)authorise:(art_nullable ARTTokenParams *)tokenParams options:(art_nullable ARTAuthOptions *)authOptions callback:(void (^)(ARTTokenDetails *__art_nullable, NSError *__art_nullable))callback DEPRECATED_MSG_ATTRIBUTE("method will be removed in v1.0. Use authorize: method instead."); + +- (void)authorize:(art_nullable ARTTokenParams *)tokenParams options:(art_nullable ARTAuthOptions *)authOptions callback:(void (^)(ARTTokenDetails *__art_nullable, NSError *__art_nullable))callback; - (void)createTokenRequest:(art_nullable ARTTokenParams *)tokenParams options:(art_nullable ARTAuthOptions *)options callback:(void (^)(ARTTokenRequest *__art_nullable tokenRequest, NSError *__art_nullable error))callback; diff --git a/Source/ARTAuth.m b/Source/ARTAuth.m index ab6326779..890a48459 100644 --- a/Source/ARTAuth.m +++ b/Source/ARTAuth.m @@ -317,6 +317,10 @@ - (void)executeTokenRequest:(ARTTokenRequest *)tokenRequest callback:(void (^)(A } - (void)authorise:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOptions callback:(void (^)(ARTTokenDetails *, NSError *))callback { + [self authorize:tokenParams options:authOptions callback:callback]; +} + +- (void)authorize:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOptions callback:(void (^)(ARTTokenDetails *, NSError *))callback { BOOL requestNewToken = NO; ARTAuthOptions *replacedOptions; diff --git a/Source/ARTRest.m b/Source/ARTRest.m index 3e7583578..da02801be 100644 --- a/Source/ARTRest.m +++ b/Source/ARTRest.m @@ -215,7 +215,7 @@ - (void)prepareAuthorisationHeader:(ARTAuthMethod)method force:(BOOL)force compl } else { self.options.force = force; - [self.auth authorise:nil options:self.options callback:^(ARTTokenDetails *tokenDetails, NSError *error) { + [self.auth authorize:nil options:self.options callback:^(ARTTokenDetails *tokenDetails, NSError *error) { if (error) { if (callback) callback(nil, error); return; diff --git a/Source/ARTWebSocketTransport.m b/Source/ARTWebSocketTransport.m index 2e2b61b81..3432a918d 100644 --- a/Source/ARTWebSocketTransport.m +++ b/Source/ARTWebSocketTransport.m @@ -105,7 +105,7 @@ - (void)connectForcingNewToken:(BOOL)forceNewToken { [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p connecting with token auth; authorising", _delegate, self]; __weak ARTWebSocketTransport *selfWeak = self; // Token - [self.auth authorise:nil options:options callback:^(ARTTokenDetails *tokenDetails, NSError *error) { + [self.auth authorize:nil options:options callback:^(ARTTokenDetails *tokenDetails, NSError *error) { [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p authorised: %@ error: %@", _delegate, self, tokenDetails, error]; ARTWebSocketTransport *selfStrong = selfWeak; if (!selfStrong) return; diff --git a/Spec/Auth.swift b/Spec/Auth.swift index d7bd50ab2..b66d8625a 100644 --- a/Spec/Auth.swift +++ b/Spec/Auth.swift @@ -1184,7 +1184,7 @@ class Auth : QuickSpec { let rest = ARTRest(options: options) waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(ARTTokenParams(clientId: "*"), options: nil) { _, error in + rest.auth.authorize(ARTTokenParams(clientId: "*"), options: nil) { _, error in expect(error).to(beNil()) done() } @@ -1350,7 +1350,7 @@ class Auth : QuickSpec { } waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: customOptions) { _, error in + rest.auth.authorize(nil, options: customOptions) { _, error in expect(error).to(beNil()) done() } @@ -1378,7 +1378,7 @@ class Auth : QuickSpec { expect(currentTokenRequest).toEventuallyNot(beNil(), timeout: testTimeout) waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: nil) { _, error in + rest.auth.authorize(nil, options: nil) { _, error in expect(error).to(beNil()) done() } @@ -1687,7 +1687,7 @@ class Auth : QuickSpec { expect(rest.auth.method).to(equal(ARTAuthMethod.Token)) // Reuse the valid token - rest.auth.authorise(nil, options: nil, callback: { tokenDetails, error in + rest.auth.authorize(nil, options: nil, callback: { tokenDetails, error in expect(rest.auth.method).to(equal(ARTAuthMethod.Token)) guard let tokenDetails = tokenDetails else { XCTFail("TokenDetails is nil"); done(); return @@ -1708,7 +1708,7 @@ class Auth : QuickSpec { let rest = ARTRest(options: AblyTests.commonAppSetup()) waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(ARTTokenParams(), options: ARTAuthOptions(), callback: { tokenDetails, error in + rest.auth.authorize(ARTTokenParams(), options: ARTAuthOptions(), callback: { tokenDetails, error in guard let error = error else { fail("Error is nil"); done(); return } @@ -1734,7 +1734,7 @@ class Auth : QuickSpec { expect(token).toNot(beNil()) waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: nil, callback: { tokenDetails, error in + rest.auth.authorize(nil, options: nil, callback: { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { XCTFail("TokenDetails is nil"); done(); return @@ -1754,7 +1754,7 @@ class Auth : QuickSpec { let rest = ARTRest(options: options) waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: nil) { tokenDetails, error in + rest.auth.authorize(nil, options: nil) { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { XCTFail("TokenDetails is nil"); done(); return @@ -1788,7 +1788,7 @@ class Auth : QuickSpec { authOptions.queryTime = true waitUntil(timeout: testTimeout) { done in - auth.authorise(nil, options: authOptions) { tokenDetails, error in + auth.authorize(nil, options: authOptions) { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { @@ -1796,7 +1796,7 @@ class Auth : QuickSpec { } expect(tokenDetails.token).to(equal(token)) - auth.authorise(nil, options: nil) { tokenDetails, error in + auth.authorize(nil, options: nil) { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { @@ -1829,7 +1829,7 @@ class Auth : QuickSpec { authOptions.queryTime = true waitUntil(timeout: testTimeout) { done in - auth.authorise(nil, options: authOptions) { tokenDetails, error in + auth.authorize(nil, options: authOptions) { tokenDetails, error in expect(authCallbackHasBeenInvoked).to(beTrue()) authCallbackHasBeenInvoked = false @@ -1837,7 +1837,7 @@ class Auth : QuickSpec { auth.testSuite_forceTokenToExpire() - auth.authorise(nil, options: authOptions2) { tokenDetails, error in + auth.authorize(nil, options: authOptions2) { tokenDetails, error in expect(authCallbackHasBeenInvoked).to(beFalse()) expect(auth.options.useTokenAuth).to(beFalse()) expect(auth.options.queryTime).to(beFalse()) @@ -1862,7 +1862,7 @@ class Auth : QuickSpec { waitUntil(timeout: testTimeout) { done in // First time - rest.auth.authorise(nil, options: authOptions) { tokenDetails, error in + rest.auth.authorize(nil, options: authOptions) { tokenDetails, error in expect(error).to(beNil()) expect(tokenDetails).toNot(beNil()) expect(serverTimeRequestWasMade).to(beTrue()) @@ -1870,7 +1870,7 @@ class Auth : QuickSpec { serverTimeRequestWasMade = false // Second time - rest.auth.authorise(nil, options: nil) { tokenDetails, error in + rest.auth.authorize(nil, options: nil) { tokenDetails, error in expect(error).to(beNil()) expect(tokenDetails).toNot(beNil()) expect(serverTimeRequestWasMade).to(beFalse()) @@ -1890,7 +1890,7 @@ class Auth : QuickSpec { tokenParams.capability = ExpectedTokenParams.capability waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(tokenParams, options: nil) { tokenDetails, error in + rest.auth.authorize(tokenParams, options: nil) { tokenDetails, error in expect(error).to(beNil()) expect(tokenDetails).toNot(beNil()) done() @@ -1899,7 +1899,7 @@ class Auth : QuickSpec { waitUntil(timeout: testTimeout) { done in delay(tokenParams.ttl + 1.0) { - rest.auth.authorise(nil, options: nil) { tokenDetails, error in + rest.auth.authorize(nil, options: nil) { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { XCTFail("TokenDetails is nil"); done(); return @@ -1921,7 +1921,7 @@ class Auth : QuickSpec { // ClientId null waitUntil(timeout: testTimeout) { done in - ARTRest(options: options).auth.authorise(nil, options: nil) { tokenDetails, error in + ARTRest(options: options).auth.authorize(nil, options: nil) { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { XCTFail("TokenDetails is nil"); done(); return @@ -1935,7 +1935,7 @@ class Auth : QuickSpec { // ClientId not null waitUntil(timeout: testTimeout) { done in - ARTRest(options: options).auth.authorise(nil, options: nil) { tokenDetails, error in + ARTRest(options: options).auth.authorize(nil, options: nil) { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { XCTFail("TokenDetails is nil"); done(); return @@ -1960,7 +1960,7 @@ class Auth : QuickSpec { tokenParams.capability = ExpectedTokenParams.capability waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(tokenParams, options: nil) { tokenDetails, error in + rest.auth.authorize(tokenParams, options: nil) { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { XCTFail("TokenDetails is nil"); done(); return @@ -1994,7 +1994,7 @@ class Auth : QuickSpec { } waitUntil(timeout: testTimeout) { done in - ARTRest(options: options).auth.authorise(nil, options: nil) { tokenDetails, error in + ARTRest(options: options).auth.authorize(nil, options: nil) { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { XCTFail("TokenDetails is nil"); done(); return @@ -2012,7 +2012,7 @@ class Auth : QuickSpec { options.authUrl = NSURL(string: "http://echo.ably.io")! waitUntil(timeout: testTimeout) { done in - ARTRest(options: options).auth.authorise(nil, options: nil) { tokenDetails, error in + ARTRest(options: options).auth.authorize(nil, options: nil) { tokenDetails, error in expect(error?.code).to(equal(400)) //Bad request expect(tokenDetails).to(beNil()) done() @@ -2042,7 +2042,7 @@ class Auth : QuickSpec { // Invalid TokenDetails waitUntil(timeout: testTimeout) { done in - ARTRest(options: options).auth.authorise(nil, options: nil) { tokenDetails, error in + ARTRest(options: options).auth.authorize(nil, options: nil) { tokenDetails, error in guard let error = error else { fail("Error is nil"); done(); return } @@ -2057,7 +2057,7 @@ class Auth : QuickSpec { // Valid token waitUntil(timeout: testTimeout) { done in - ARTRest(options: options).auth.authorise(nil, options: nil) { tokenDetails, error in + ARTRest(options: options).auth.authorize(nil, options: nil) { tokenDetails, error in expect(error).to(beNil()) expect(tokenDetails).toNot(beNil()) done() @@ -2076,7 +2076,7 @@ class Auth : QuickSpec { // Invalid token waitUntil(timeout: testTimeout) { done in - ARTRest(options: options).auth.authorise(nil, options: nil) { tokenDetails, error in + ARTRest(options: options).auth.authorize(nil, options: nil) { tokenDetails, error in expect(error).toNot(beNil()) expect(tokenDetails).to(beNil()) done() @@ -2088,7 +2088,7 @@ class Auth : QuickSpec { // Valid token waitUntil(timeout: testTimeout) { done in - ARTRest(options: options).auth.authorise(nil, options: nil) { tokenDetails, error in + ARTRest(options: options).auth.authorize(nil, options: nil) { tokenDetails, error in expect(error).to(beNil()) expect(tokenDetails).toNot(beNil()) done() @@ -2112,7 +2112,7 @@ class Auth : QuickSpec { tokenParams.ttl = 1.0 waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(tokenParams, options: authOptions) { tokenDetails, error in + rest.auth.authorize(tokenParams, options: authOptions) { tokenDetails, error in expect(error).to(beNil()) guard let issued = tokenDetails?.issued else { fail("TokenDetails.issued is nil"); done(); return @@ -2130,7 +2130,7 @@ class Auth : QuickSpec { authOptions.key = nil // First time waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: authOptions) { _, error in + rest.auth.authorize(nil, options: authOptions) { _, error in guard let error = error else { fail("Error is nil"); done(); return } @@ -2141,7 +2141,7 @@ class Auth : QuickSpec { // Second time waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: nil) { _, error in + rest.auth.authorize(nil, options: nil) { _, error in guard let error = error else { fail("Error is nil"); done(); return } @@ -2170,7 +2170,7 @@ class Auth : QuickSpec { authOptions.authHeaders = ["X-Ably":"Test"] waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: authOptions) { tokenDetails, error in + rest.auth.authorize(nil, options: authOptions) { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { XCTFail("TokenDetails is nil"); done(); return @@ -2188,7 +2188,7 @@ class Auth : QuickSpec { authOptions.authParams = nil authOptions.authHeaders = nil waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: authOptions) { tokenDetails, error in + rest.auth.authorize(nil, options: authOptions) { tokenDetails, error in guard let error = error else { fail("Error is nil"); done(); return } @@ -2202,7 +2202,7 @@ class Auth : QuickSpec { // Repeat waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: nil) { tokenDetails, error in + rest.auth.authorize(nil, options: nil) { tokenDetails, error in guard let error = error else { fail("Error is nil"); done(); return } @@ -2216,7 +2216,7 @@ class Auth : QuickSpec { authOptions.authUrl = nil waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: authOptions) { tokenDetails, error in + rest.auth.authorize(nil, options: authOptions) { tokenDetails, error in guard let error = error else { fail("Error is nil"); done(); return } @@ -2231,7 +2231,7 @@ class Auth : QuickSpec { // Repeat waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: nil) { tokenDetails, error in + rest.auth.authorize(nil, options: nil) { tokenDetails, error in guard let error = error else { fail("Error is nil"); done(); return } @@ -2257,7 +2257,7 @@ class Auth : QuickSpec { } waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: authOptions) { tokenDetails, error in + rest.auth.authorize(nil, options: authOptions) { tokenDetails, error in expect(error).to(beNil()) expect(tokenDetails?.token).to(equal("token")) expect(authCallbackHasBeenInvoked).to(beTrue()) @@ -2268,7 +2268,7 @@ class Auth : QuickSpec { authCallbackHasBeenInvoked = false waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: nil) { tokenDetails, error in + rest.auth.authorize(nil, options: nil) { tokenDetails, error in expect(error).to(beNil()) expect(tokenDetails?.token).to(equal("token")) expect(authCallbackHasBeenInvoked).to(beTrue()) @@ -2280,7 +2280,7 @@ class Auth : QuickSpec { authOptions.authCallback = nil waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: authOptions) { tokenDetails, error in + rest.auth.authorize(nil, options: authOptions) { tokenDetails, error in guard let error = error else { fail("Error is nil"); done(); return } @@ -2293,7 +2293,7 @@ class Auth : QuickSpec { } waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: nil) { tokenDetails, error in + rest.auth.authorize(nil, options: nil) { tokenDetails, error in guard let error = error else { fail("Error is nil"); done(); return } @@ -2318,7 +2318,7 @@ class Auth : QuickSpec { // Defaults waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: nil) { tokenDetails, error in + rest.auth.authorize(nil, options: nil) { tokenDetails, error in guard let error = error else { fail("Error is nil"); done(); return } @@ -2344,7 +2344,7 @@ class Auth : QuickSpec { defer { hook.remove() } waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(tokenParams, options: authOptions) { tokenDetails, error in + rest.auth.authorize(tokenParams, options: authOptions) { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { XCTFail("TokenDetails is nil"); done(); return @@ -2361,7 +2361,7 @@ class Auth : QuickSpec { // Subsequent authorisations waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: nil) { tokenDetails, error in + rest.auth.authorize(nil, options: nil) { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { fail("TokenDetails is nil"); done(); return @@ -2399,7 +2399,7 @@ class Auth : QuickSpec { authOptions.queryTime = true waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: authOptions, callback: { tokenDetails, error in + rest.auth.authorize(nil, options: authOptions, callback: { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { fail("TokenDetails is nil"); done(); return @@ -2415,7 +2415,7 @@ class Auth : QuickSpec { rest.auth.testSuite_forceTokenToExpire() waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: nil) { tokenDetails, error in + rest.auth.authorize(nil, options: nil) { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { fail("TokenDetails is nil"); done(); return @@ -2473,7 +2473,7 @@ class Auth : QuickSpec { defer { hook.remove() } waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: nil) { tokenDetails, error in + rest.auth.authorize(nil, options: nil) { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { fail("TokenDetails is nil"); done(); return @@ -2492,7 +2492,7 @@ class Auth : QuickSpec { rest.auth.testSuite_forceTokenToExpire() waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: nil) { tokenDetails, error in + rest.auth.authorize(nil, options: nil) { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { fail("TokenDetails is nil"); done(); return @@ -2548,7 +2548,7 @@ class Auth : QuickSpec { waitUntil(timeout: testTimeout) { done in expect(rest.auth.timeOffset).to(equal(fakeOffset)) - rest.auth.authorise(nil, options: authOptions) { tokenDetails, error in + rest.auth.authorize(nil, options: authOptions) { tokenDetails, error in expect(error).to(beNil()) expect(tokenDetails).toNot(beNil()) expect(rest.auth.timeOffset).toNot(equal(fakeOffset)) @@ -2579,6 +2579,14 @@ class Auth : QuickSpec { } } + + // RSA10l + it("has an alias method @RestClient#authorise@ and should use @RealtimeClient#authorize@") { + let rest = ARTRest(key: "xxxx:xxxx") + expect(rest.auth.respondsToSelector(#selector(ARTAuth.authorise(_:options:callback:)))) == true + expect(rest.auth.respondsToSelector(#selector(ARTAuth.authorize(_:options:callback:)))) == true + } + } describe("TokenParams") { @@ -2697,7 +2705,7 @@ class Auth : QuickSpec { reauthOptions.force = true waitUntil(timeout: testTimeout) { done in - realtime.auth.authorise(nil, options: reauthOptions) { reauthTokenDetails, error in + realtime.auth.authorize(nil, options: reauthOptions) { reauthTokenDetails, error in expect(error).to(beNil()) expect(reauthTokenDetails?.token).toNot(beNil()) done() @@ -2719,7 +2727,7 @@ class Auth : QuickSpec { it("timestamp should not be a member of any default token params") { let rest = ARTRest(options: AblyTests.commonAppSetup()) waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: nil) { _, error in + rest.auth.authorize(nil, options: nil) { _, error in expect(error).to(beNil()) guard let defaultTokenParams = rest.auth.options.defaultTokenParams else { fail("DefaultTokenParams is nil"); done(); return diff --git a/Spec/RestClient.swift b/Spec/RestClient.swift index c9d217a4c..57c9fa482 100644 --- a/Spec/RestClient.swift +++ b/Spec/RestClient.swift @@ -374,7 +374,7 @@ class RestClient: QuickSpec { options.tokenDetails = getTestTokenDetails() waitUntil(timeout: testTimeout) { done in - ARTRest(options: options).auth.authorise(nil, options: nil) { tokenDetails, error in + ARTRest(options: options).auth.authorize(nil, options: nil) { tokenDetails, error in if let e = error { XCTFail(e.description) done() diff --git a/Tests/ARTRealtimeAttachTest.m b/Tests/ARTRealtimeAttachTest.m index 27ad2dd2f..9a567c15a 100644 --- a/Tests/ARTRealtimeAttachTest.m +++ b/Tests/ARTRealtimeAttachTest.m @@ -342,7 +342,7 @@ - (void)testPresenceEnterRestricted { ARTTokenParams *tokenParams = [[ARTTokenParams alloc] initWithClientId:options.clientId]; tokenParams.capability = @"{\"canpublish:*\":[\"publish\"],\"canpublish:andpresence\":[\"presence\",\"publish\"],\"cansubscribe:*\":[\"subscribe\"]}"; - [realtime.auth authorise:tokenParams options:options callback:^(ARTTokenDetails *tokenDetails, NSError *error) { + [realtime.auth authorize:tokenParams options:options callback:^(ARTTokenDetails *tokenDetails, NSError *error) { options.token = tokenDetails.token; [realtime connect]; }]; diff --git a/Tests/ARTRestCapabilityTest.m b/Tests/ARTRestCapabilityTest.m index 88ec8cd48..466ec8777 100644 --- a/Tests/ARTRestCapabilityTest.m +++ b/Tests/ARTRestCapabilityTest.m @@ -45,7 +45,7 @@ - (void)testPublishRestricted { ARTTokenParams *tokenParams = [[ARTTokenParams alloc] initWithClientId:options.clientId]; tokenParams.capability = @"{\"canpublish:*\":[\"publish\"],\"canpublish:andpresence\":[\"presence\",\"publish\"],\"cansubscribe:*\":[\"subscribe\"]}"; - [[[ARTRest alloc] initWithOptions:options].auth authorise:tokenParams options:options callback:^(ARTTokenDetails *tokenDetails, NSError *error) { + [[[ARTRest alloc] initWithOptions:options].auth authorize:tokenParams options:options callback:^(ARTTokenDetails *tokenDetails, NSError *error) { options.token = tokenDetails.token; ARTRest *rest = [[ARTRest alloc] initWithOptions:options]; ARTRestChannel *channel = [rest.channels get:@"canpublish:test"]; From f9e39d346e3025efd57bfee28525a1198a1865ab Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 1 Nov 2016 13:02:11 +0000 Subject: [PATCH 08/43] Remove `AuthOptions.force` (#527) * Remove AuthOptions.force * Fix: make a single attempt to reissue the token and resend the request * Fix RSA10a * Fix RSC9 * Remove `prepareAuthorisationHeader` access from test suite * Fix RSA10a --- Source/ARTAuth+Private.h | 9 ++ Source/ARTAuth.m | 90 ++++++++------------ Source/ARTAuthOptions.h | 7 -- Source/ARTAuthOptions.m | 18 +--- Source/ARTRealtime.m | 19 +++-- Source/ARTRest+Private.h | 2 - Source/ARTRest.m | 84 +++++++++++-------- Source/ARTStatus.h | 5 ++ Source/ARTStatus.m | 2 + Source/ARTWebSocketTransport+Private.h | 4 + Source/ARTWebSocketTransport.h | 6 -- Source/ARTWebSocketTransport.m | 72 +++++++++------- Spec/Auth.swift | 112 +++++++++++++++---------- Spec/RestClient.swift | 18 ++-- 14 files changed, 243 insertions(+), 205 deletions(-) diff --git a/Source/ARTAuth+Private.h b/Source/ARTAuth+Private.h index aa9679ae7..f7458b5b8 100644 --- a/Source/ARTAuth+Private.h +++ b/Source/ARTAuth+Private.h @@ -38,6 +38,15 @@ ART_ASSUME_NONNULL_BEGIN // Discard the cached local clock offset - (void)discardTimeOffset; +// Configured options does have a means to renew the token automatically. +- (BOOL)canRenewTokenAutomatically:(ARTAuthOptions *)options; + +/// Does the client have a means to renew the token automatically. +- (BOOL)tokenIsRenewable; + +/// Does the client have a valid token (i.e. not expired). +- (BOOL)tokenRemainsValid; + // Private TokenDetails setter for testing only - (void)setTokenDetails:(ARTTokenDetails *)tokenDetails; diff --git a/Source/ARTAuth.m b/Source/ARTAuth.m index 890a48459..9f654cde2 100644 --- a/Source/ARTAuth.m +++ b/Source/ARTAuth.m @@ -129,7 +129,6 @@ - (void)storeOptions:(ARTAuthOptions *)customOptions { self.options.authParams = [customOptions.authParams copy]; self.options.useTokenAuth = customOptions.useTokenAuth; self.options.queryTime = false; - self.options.force = false; } - (ARTTokenParams *)mergeParams:(ARTTokenParams *)customParams { @@ -184,6 +183,26 @@ - (NSMutableURLRequest *)buildRequest:(ARTAuthOptions *)options withParams:(ARTT return request; } +- (BOOL)tokenIsRenewable { + return [self canRenewTokenAutomatically:self.options]; +} + +- (BOOL)canRenewTokenAutomatically:(ARTAuthOptions *)options { + return options.authCallback || options.authUrl || options.key; +} + +- (BOOL)tokenRemainsValid { + if (self.tokenDetails && self.tokenDetails.token) { + if (self.tokenDetails.expires == nil) { + return YES; + } + else if ([self.tokenDetails.expires timeIntervalSinceDate:[self currentDate]] > 0) { + return YES; + } + } + return NO; +} + - (void)requestToken:(ARTTokenParams *)tokenParams withOptions:(ARTAuthOptions *)authOptions callback:(void (^)(ARTTokenDetails *, NSError *))callback { @@ -192,8 +211,8 @@ - (void)requestToken:(ARTTokenParams *)tokenParams withOptions:(ARTAuthOptions * ARTTokenParams *currentTokenParams = tokenParams ? tokenParams : _tokenParams; currentTokenParams.timestamp = [self currentDate]; - if (replacedOptions.key == nil && replacedOptions.authCallback == nil && replacedOptions.authUrl == nil) { - callback(nil, [ARTErrorInfo createWithCode:ARTStateRequestTokenFailed message:@"no means to renew the token is provided (either an API key, authCallback or authUrl)"]); + if (![self canRenewTokenAutomatically:replacedOptions]) { + callback(nil, [ARTErrorInfo createWithCode:ARTStateRequestTokenFailed message:ARTAblyMessageNoMeansToRenewToken]); return; } @@ -321,64 +340,29 @@ - (void)authorise:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOp } - (void)authorize:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOptions callback:(void (^)(ARTTokenDetails *, NSError *))callback { - BOOL requestNewToken = NO; - ARTAuthOptions *replacedOptions; - if ([authOptions isOnlyForceTrue]) { - replacedOptions = [self.options copy]; - replacedOptions.force = YES; - } - else { - replacedOptions = [authOptions copy] ? : [self.options copy]; - } + ARTAuthOptions *replacedOptions = [authOptions copy] ? : [self.options copy]; [self storeOptions:replacedOptions]; ARTTokenParams *currentTokenParams = [self mergeParams:tokenParams]; [self storeParams:currentTokenParams]; - // Reuse or not reuse the current token - if (replacedOptions.force == NO && self.tokenDetails) { - if (self.tokenDetails.expires == nil) { - [self.logger verbose:@"RS:%p ARTAuth: reuse current token.", _rest]; - requestNewToken = NO; - } - else if ([self.tokenDetails.expires timeIntervalSinceDate:[self currentDate]] > 0) { - [self.logger verbose:@"RS:%p ARTAuth: current token has not expired yet. Reusing token details.", _rest]; - requestNewToken = NO; - } - else { - [self.logger verbose:@"RS:%p ARTAuth: current token has expired. Requesting new token.", _rest]; - requestNewToken = YES; - } - } - else { - if (replacedOptions.force == YES) - [self.logger verbose:@"RS:%p ARTAuth: forced requesting new token.", _rest]; - else - [self.logger verbose:@"RS:%p ARTAuth: requesting new token.", _rest]; - requestNewToken = YES; - } - - if (requestNewToken) { - [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; - [self.logger verbose:@"RS:%p ARTAuth: token request succeeded: %@", _rest, tokenDetails]; - if (callback) { - callback(self.tokenDetails, nil); - } + // 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; + [self.logger verbose:@"RS:%p ARTAuth: token request succeeded: %@", _rest, tokenDetails]; + if (callback) { + callback(self.tokenDetails, nil); } - }]; - } else { - if (callback) { - callback(self.tokenDetails, nil); } - } + }]; } - (void)createTokenRequest:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)options callback:(void (^)(ARTTokenRequest *, NSError *))callback { diff --git a/Source/ARTAuthOptions.h b/Source/ARTAuthOptions.h index 4387ed278..21a9e10ab 100644 --- a/Source/ARTAuthOptions.h +++ b/Source/ARTAuthOptions.h @@ -78,11 +78,6 @@ ART_ASSUME_NONNULL_BEGIN */ @property (readwrite, assign, nonatomic) BOOL useTokenAuth; -/** - Indicates that a new token should be requested. - */ -@property (readwrite, assign, nonatomic) BOOL force; - - (instancetype)init; - (instancetype)initWithKey:(NSString *)key; - (instancetype)initWithToken:(NSString *)token; @@ -95,8 +90,6 @@ ART_ASSUME_NONNULL_BEGIN - (BOOL)isMethodGET; - (BOOL)isMethodPOST; -- (BOOL)isOnlyForceTrue; - @end ART_ASSUME_NONNULL_END diff --git a/Source/ARTAuthOptions.m b/Source/ARTAuthOptions.m index a282a4ff8..2693d922e 100644 --- a/Source/ARTAuthOptions.m +++ b/Source/ARTAuthOptions.m @@ -70,7 +70,6 @@ - (id)copyWithZone:(NSZone *)zone { options.authParams = self.authParams; options.queryTime = self.queryTime; options.useTokenAuth = self.useTokenAuth; - options.force = self.force; return options; } @@ -120,9 +119,7 @@ - (ARTAuthOptions *)mergeWith:(ARTAuthOptions *)precedenceOptions { merged.queryTime = precedenceOptions.queryTime; if (precedenceOptions.useTokenAuth) merged.useTokenAuth = precedenceOptions.useTokenAuth; - if (precedenceOptions.force) - merged.force = precedenceOptions.force; - + return merged; } @@ -134,17 +131,4 @@ - (BOOL)isMethodGET { return [_authMethod isEqualToString:@"GET"]; } -- (BOOL)isOnlyForceTrue { - return self.key == nil && - self.token == nil && - self.tokenDetails == nil && - self.authCallback == nil && - self.authUrl == nil && - self.authHeaders == nil && - self.authParams == nil && - self.queryTime == NO && - self.useTokenAuth == NO && - self.force == YES; -} - @end diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index 575c1071c..725cab720 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -14,6 +14,7 @@ #import "ARTDefault.h" #import "ARTRest+Private.h" #import "ARTAuth+Private.h" +#import "ARTTokenDetails.h" #import "ARTMessage.h" #import "ARTClientOptions.h" #import "ARTChannelOptions.h" @@ -463,7 +464,7 @@ - (void)onDisconnected:(ARTProtocolMessage *)message { [self.logger info:@"R:%p ARTRealtime disconnected", self]; ARTErrorInfo *error = message.error; if ([self shouldRenewToken:&error]) { - [self connectWithRenewedToken]; + [self transportReconnectWithRenewedToken]; [self transition:ARTRealtimeDisconnected withErrorInfo:error]; [self.connection setErrorReason:nil]; return; @@ -493,7 +494,7 @@ - (void)onError:(ARTProtocolMessage *)message { } else { ARTErrorInfo *error = message.error; if ([self shouldRenewToken:&error]) { - [self connectWithRenewedToken]; + [self transportReconnectWithRenewedToken]; return; } [self.connection setId:nil]; @@ -504,19 +505,20 @@ - (void)onError:(ARTProtocolMessage *)message { - (BOOL)shouldRenewToken:(ARTErrorInfo **)errorPtr { if (!_renewingToken && errorPtr && *errorPtr && (*errorPtr).statusCode == 401 && (*errorPtr).code >= 40140 && (*errorPtr).code < 40150) { - if ([self isTokenRenewable]) { + if ([self.auth tokenIsRenewable]) { return YES; } - *errorPtr = [ARTErrorInfo createWithCode:ARTStateRequestTokenFailed message:@"no means to renew the token is provided (either an API key, authCallback or authUrl)"]; + *errorPtr = [ARTErrorInfo createWithCode:ARTStateRequestTokenFailed message:ARTAblyMessageNoMeansToRenewToken]; } return NO; } -- (BOOL)isTokenRenewable { - return self.options.authCallback || self.options.authUrl || self.options.key; +- (void)transportReconnectWithHost:(NSString *)host { + [self.transport setHost:host]; + [self.transport connect]; } -- (void)connectWithRenewedToken { +- (void)transportReconnectWithRenewedToken { _renewingToken = true; [_transport close]; _transport = [[_transportClass alloc] initWithRest:self.rest options:self.options resumeKey:_transport.resumeKey connectionSerial:_transport.connectionSerial]; @@ -718,8 +720,7 @@ - (BOOL)reconnectWithFallback { if (host != nil) { [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p host is down; retrying realtime connection at %@", self, host]; self.rest.prioritizedHost = host; - [self.transport setHost:host]; - [self.transport connect]; + [self transportReconnectWithHost:host]; return true; } else { _fallbacks = nil; diff --git a/Source/ARTRest+Private.h b/Source/ARTRest+Private.h index 8f2014ff6..c99f65b2f 100644 --- a/Source/ARTRest+Private.h +++ b/Source/ARTRest+Private.h @@ -44,8 +44,6 @@ ART_ASSUME_NONNULL_BEGIN - (void)executeRequest:(NSMutableURLRequest *)request withAuthOption:(ARTAuthentication)authOption completion:(void (^)(NSHTTPURLResponse *__art_nullable, NSData *__art_nullable, NSError *__art_nullable))callback; -- (void)prepareAuthorisationHeader:(ARTAuthMethod)method completion:(void (^)(NSString *__art_nonnull authorization, NSError *__art_nullable error))callback; - - (id)internetIsUp:(void (^)(BOOL isUp))cb; @end diff --git a/Source/ARTRest.m b/Source/ARTRest.m index da02801be..28b0fafd6 100644 --- a/Source/ARTRest.m +++ b/Source/ARTRest.m @@ -113,15 +113,37 @@ - (void)executeRequestWithAuthentication:(NSMutableURLRequest *)request withMeth } - (void)executeRequestWithAuthentication:(NSMutableURLRequest *)request withMethod:(ARTAuthMethod)method force:(BOOL)force completion:(void (^)(NSHTTPURLResponse *__art_nullable, NSData *__art_nullable, NSError *__art_nullable))callback { - [self prepareAuthorisationHeader:method force:force completion:^(NSString *authorization, NSError *error) { - if (error && callback) { - callback(nil, nil, error); - } else { - // RFC7235 + [self.logger debug:__FILE__ line:__LINE__ message:@"RS:%p calculating authorization %lu", self, (unsigned long)method]; + if (method == ARTAuthMethodBasic) { + // Basic + NSString *authorization = [self prepareBasicAuthorisationHeader:self.options.key]; + [request setValue:authorization forHTTPHeaderField:@"Authorization"]; + [self.logger verbose:@"RS:%p ARTRest: %@", self, authorization]; + [self executeRequest:request completion:callback]; + } + else { + if (!force && [self.auth tokenRemainsValid]) { + // Reuse token + NSString *authorization = [self prepareTokenAuthorisationHeader:self.auth.tokenDetails.token]; + [self.logger verbose:@"RS:%p ARTRest reusing token: authorization bearer in Base64 %@", self, authorization]; [request setValue:authorization forHTTPHeaderField:@"Authorization"]; [self executeRequest:request completion:callback]; } - }]; + else { + // New Token + [self.auth authorize:nil options:self.options callback:^(ARTTokenDetails *tokenDetails, NSError *error) { + if (error) { + [self.logger debug:__FILE__ line:__LINE__ message:@"RS:%p ARTRest reissuing token failed %@", self, error]; + if (callback) callback(nil, nil, error); + return; + } + NSString *authorization = [self prepareTokenAuthorisationHeader:tokenDetails.token]; + [self.logger verbose:@"RS:%p ARTRest reissuing token: authorization bearer in Base64 %@", self, authorization]; + [request setValue:authorization forHTTPHeaderField:@"Authorization"]; + [self executeRequest:request completion:callback]; + }]; + } + } } - (void)executeRequest:(NSMutableURLRequest *)request completion:(void (^)(NSHTTPURLResponse *__art_nullable, NSData *__art_nullable, NSError *__art_nullable))callback { @@ -142,9 +164,9 @@ - (void)executeRequest:(NSMutableURLRequest *)request completion:(void (^)(NSHTT [self.httpExecutor executeRequest:request completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { if (response.statusCode >= 400) { NSError *dataError = [self->_encoders[response.MIMEType] decodeError:data]; - if (dataError.code >= 40140 && dataError.code < 40150) { - // Send it again, requesting a new token (forward callback) - [self.logger debug:__FILE__ line:__LINE__ message:@"RS:%p requesting new token", self]; + if ([self shouldRenewToken:&dataError]) { + [self.logger debug:__FILE__ line:__LINE__ message:@"RS:%p retry request %@", self, request]; + // Make a single attempt to reissue the token and resend the request [self executeRequest:request withAuthOption:ARTAuthenticationNewToken completion:callback]; return; } else { @@ -179,6 +201,17 @@ - (void)executeRequest:(NSMutableURLRequest *)request completion:(void (^)(NSHTT }]; } +- (BOOL)shouldRenewToken:(NSError **)errorPtr { + if (errorPtr && *errorPtr && + (*errorPtr).code >= 40140 && (*errorPtr).code < 40150) { + if ([self.auth tokenIsRenewable]) { + return YES; + } + *errorPtr = (NSError *)[ARTErrorInfo createWithCode:ARTStateRequestTokenFailed message:ARTAblyMessageNoMeansToRenewToken]; + } + return NO; +} + - (BOOL)shouldRetryWithFallback:(NSMutableURLRequest *)request response:(NSHTTPURLResponse *)response error:(NSError *)error { if (response.statusCode >= 500 && response.statusCode <= 504) { return YES; @@ -200,32 +233,17 @@ - (NSString *)currentHost { return self.options.restHost; } -- (void)prepareAuthorisationHeader:(ARTAuthMethod)method completion:(void (^)(NSString *authorization, NSError *error))callback { - [self prepareAuthorisationHeader:method force:NO completion:callback]; +- (NSString *)prepareBasicAuthorisationHeader:(NSString *)key { + // Include key Base64 encoded in an Authorization header (RFC7235) + NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; + NSString *keyBase64 = [keyData base64EncodedStringWithOptions:0]; + return [NSString stringWithFormat:@"Basic %@", keyBase64]; } -- (void)prepareAuthorisationHeader:(ARTAuthMethod)method force:(BOOL)force completion:(void (^)(NSString *authorization, NSError *error))callback { - [self.logger debug:__FILE__ line:__LINE__ message:@"RS:%p calculating authorization %lu", self, (unsigned long)method]; - // FIXME: use encoder and should be managed on ARTAuth - if (method == ARTAuthMethodBasic) { - // Include key Base64 encoded in an Authorization header (RFC7235) - NSData *keyData = [self.options.key dataUsingEncoding:NSUTF8StringEncoding]; - NSString *keyBase64 = [keyData base64EncodedStringWithOptions:0]; - if (callback) callback([NSString stringWithFormat:@"Basic %@", keyBase64], nil); - } - else { - self.options.force = force; - [self.auth authorize:nil options:self.options callback:^(ARTTokenDetails *tokenDetails, NSError *error) { - if (error) { - if (callback) callback(nil, error); - return; - } - NSData *tokenData = [tokenDetails.token dataUsingEncoding:NSUTF8StringEncoding]; - NSString *tokenBase64 = [tokenData base64EncodedStringWithOptions:0]; - [self.logger verbose:@"RS:%p ARTRest: authorization bearer in Base64 %@", self, tokenBase64]; - if (callback) callback([NSString stringWithFormat:@"Bearer %@", tokenBase64], nil); - }]; - } +- (NSString *)prepareTokenAuthorisationHeader:(NSString *)token { + NSData *tokenData = [token dataUsingEncoding:NSUTF8StringEncoding]; + NSString *tokenBase64 = [tokenData base64EncodedStringWithOptions:0]; + return [NSString stringWithFormat:@"Bearer %@", tokenBase64]; } - (void)time:(void(^)(NSDate *time, NSError *error))callback { diff --git a/Source/ARTStatus.h b/Source/ARTStatus.h index 376cb6310..36753bf91 100644 --- a/Source/ARTStatus.h +++ b/Source/ARTStatus.h @@ -48,6 +48,11 @@ FOUNDATION_EXPORT NSString *const ARTAblyErrorDomain; */ FOUNDATION_EXPORT NSString *const ARTFallbackIncompatibleOptionsException; +/** + Ably client error messages + */ +FOUNDATION_EXPORT NSString *const ARTAblyMessageNoMeansToRenewToken; + /** Ably client error class */ diff --git a/Source/ARTStatus.m b/Source/ARTStatus.m index 9bc48b0dc..9833be8e8 100644 --- a/Source/ARTStatus.m +++ b/Source/ARTStatus.m @@ -15,6 +15,8 @@ NSString *const ARTFallbackIncompatibleOptionsException = @"ARTFallbackIncompatibleOptionsException"; +NSString *const ARTAblyMessageNoMeansToRenewToken = @"no means to renew the token is provided (either an API key, authCallback or authUrl)"; + NSInteger getStatusFromCode(NSInteger code) { return code / 100; } diff --git a/Source/ARTWebSocketTransport+Private.h b/Source/ARTWebSocketTransport+Private.h index 29753a90a..c893d76c7 100644 --- a/Source/ARTWebSocketTransport+Private.h +++ b/Source/ARTWebSocketTransport+Private.h @@ -27,12 +27,16 @@ 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; - (void)sendWithData:(NSData *)data; - (void)receiveWithData:(NSData *)data; +- (NSURL *)setupWebSocket:(__GENERIC(NSArray, NSURLQueryItem *) *)params withOptions:(ARTClientOptions *)options resumeKey:(NSString *__art_nullable)resumeKey connectionSerial:(NSNumber *__art_nullable)connectionSerial; + @end ART_ASSUME_NONNULL_END diff --git a/Source/ARTWebSocketTransport.h b/Source/ARTWebSocketTransport.h index abf0cd621..3e26e2cff 100644 --- a/Source/ARTWebSocketTransport.h +++ b/Source/ARTWebSocketTransport.h @@ -27,12 +27,6 @@ ART_ASSUME_NONNULL_BEGIN @property (readonly, getter=getIsConnected) BOOL isConnected; -@property (readwrite, assign, nonatomic) BOOL closing; - -- (NSURL *)setupWebSocket:(__GENERIC(NSArray, NSURLQueryItem *) *)params withOptions:(ARTClientOptions *)options resumeKey:(NSString *__art_nullable)resumeKey connectionSerial:(NSNumber *__art_nullable)connectionSerial; - -- (BOOL)getIsConnected; - @end ART_ASSUME_NONNULL_END diff --git a/Source/ARTWebSocketTransport.m b/Source/ARTWebSocketTransport.m index 3432a918d..a6b16d897 100644 --- a/Source/ARTWebSocketTransport.m +++ b/Source/ARTWebSocketTransport.m @@ -10,7 +10,7 @@ #import "ARTRest.h" #import "ARTRest+Private.h" -#import "ARTAuth.h" +#import "ARTAuth+Private.h" #import "ARTProtocolMessage.h" #import "ARTClientOptions.h" #import "ARTTokenParams.h" @@ -89,39 +89,55 @@ - (void)connect { - (void)connectForcingNewToken:(BOOL)forceNewToken { [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p websocket connect", _delegate, self]; - ARTClientOptions *options = self.options; - if (forceNewToken) { - options = [options copy]; - options.force = true; - } + ARTClientOptions *options = [self.options copy]; + if ([options isBasicAuth]) { // Basic - NSURLQueryItem *keyParam = [NSURLQueryItem queryItemWithName:@"key" value:options.key]; - [self setupWebSocket:@[keyParam] withOptions:options resumeKey:self.resumeKey connectionSerial:self.connectionSerial]; - // Connect - [self.websocket open]; + [self connectWithKey:options.key]; } else { + // Token [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p connecting with token auth; authorising", _delegate, self]; __weak ARTWebSocketTransport *selfWeak = self; - // Token - [self.auth authorize:nil options:options callback:^(ARTTokenDetails *tokenDetails, NSError *error) { - [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p authorised: %@ error: %@", _delegate, self, tokenDetails, error]; - ARTWebSocketTransport *selfStrong = selfWeak; - if (!selfStrong) return; - - 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; - } - - NSURLQueryItem *accessTokenParam = [NSURLQueryItem queryItemWithName:@"accessToken" value:(tokenDetails.token)]; - [selfStrong setupWebSocket:@[accessTokenParam] withOptions:selfStrong.options resumeKey:self.resumeKey connectionSerial:self.connectionSerial]; - // Connect - [selfStrong.websocket open]; - }]; - } + + if (!forceNewToken && [self.auth tokenRemainsValid]) { + // Reuse token + [self connectWithToken:self.auth.tokenDetails.token]; + } + 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]; + }]; + } + } +} + +- (void)connectWithKey:(NSString *)key { + [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p websocket connect with key", _delegate, self]; + NSURLQueryItem *keyParam = [NSURLQueryItem queryItemWithName:@"key" value:key]; + [self setupWebSocket:@[keyParam] withOptions:self.options resumeKey:self.resumeKey connectionSerial:self.connectionSerial]; + // Connect + [self.websocket open]; +} + +- (void)connectWithToken:(NSString *)token { + [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p websocket connect with token", _delegate, self]; + NSURLQueryItem *accessTokenParam = [NSURLQueryItem queryItemWithName:@"accessToken" value:token]; + [self setupWebSocket:@[accessTokenParam] withOptions:self.options resumeKey:self.resumeKey connectionSerial:self.connectionSerial]; + // Connect + [self.websocket open]; } - (BOOL)getIsConnected { diff --git a/Spec/Auth.swift b/Spec/Auth.swift index b66d8625a..8d671ea37 100644 --- a/Spec/Auth.swift +++ b/Spec/Auth.swift @@ -274,7 +274,8 @@ class Auth : QuickSpec { it("on rest") { let expectedClientId = "client_string" - let options = AblyTests.setupOptions(AblyTests.jsonRestOptions) + let options = AblyTests.commonAppSetup() + options.useTokenAuth = true options.clientId = expectedClientId let client = ARTRest(options: options) @@ -282,11 +283,13 @@ class Auth : QuickSpec { waitUntil(timeout: testTimeout) { done in // Token - client.prepareAuthorisationHeader(ARTAuthMethod.Token) { token, error in - if let e = error { - XCTFail(e.description) + client.auth.authorize(nil, options: nil) { tokenDetails, error in + expect(error).to(beNil()) + expect(client.auth.method).to(equal(ARTAuthMethod.Token)) + guard let tokenDetails = tokenDetails else { + fail("TokenDetails is nil"); done(); return } - expect(client.auth.clientId).to(equal(expectedClientId)) + expect(tokenDetails.clientId).to(equal(expectedClientId)) done() } } @@ -349,19 +352,17 @@ class Auth : QuickSpec { // RSA15b it("should permit to be unauthenticated") { - let options = AblyTests.setupOptions(AblyTests.jsonRestOptions) + let options = AblyTests.commonAppSetup() options.clientId = nil let clientBasic = ARTRest(options: options) waitUntil(timeout: testTimeout) { done in // Basic - clientBasic.prepareAuthorisationHeader(ARTAuthMethod.Basic) { token, error in - if let e = error { - XCTFail(e.description) - } + clientBasic.auth.authorize(nil, options: nil) { tokenDetails, error in + expect(error).to(beNil()) expect(clientBasic.auth.clientId).to(beNil()) - options.tokenDetails = clientBasic.auth.tokenDetails + options.tokenDetails = tokenDetails done() } } @@ -370,16 +371,12 @@ class Auth : QuickSpec { waitUntil(timeout: testTimeout) { done in // Last TokenDetails - clientToken.prepareAuthorisationHeader(ARTAuthMethod.Token) { token, error in - if let e = error { - XCTFail(e.description) - } + clientToken.auth.authorize(nil, options: nil) { tokenDetails, error in + expect(error).to(beNil()) expect(clientToken.auth.clientId).to(beNil()) done() } } - - // TODO: Realtime.connectionDetails } // RSA15c @@ -602,8 +599,9 @@ class Auth : QuickSpec { // RSA7b2 it("when tokenRequest or tokenDetails has clientId not null or wildcard string") { - let options = AblyTests.setupOptions(AblyTests.jsonRestOptions) + let options = AblyTests.commonAppSetup() options.clientId = "client_string" + options.useTokenAuth = true let client = ARTRest(options: options) client.httpExecutor = testHTTPExecutor @@ -611,10 +609,9 @@ class Auth : QuickSpec { // TokenDetails waitUntil(timeout: 10) { done in // Token - client.prepareAuthorisationHeader(ARTAuthMethod.Token) { token, error in - if let e = error { - XCTFail(e.description) - } + client.auth.authorize(nil, options: nil) { token, error in + expect(error).to(beNil()) + expect(client.auth.method).to(equal(ARTAuthMethod.Token)) expect(client.auth.clientId).to(equal(options.clientId)) done() } @@ -1270,7 +1267,6 @@ class Auth : QuickSpec { tokenParams.clientId = nil let authOptions = ARTAuthOptions() - authOptions.force = true authOptions.queryTime = true authOptions.key = options.key @@ -1674,31 +1670,60 @@ class Auth : QuickSpec { } // RSA10 - describe("authorise") { + describe("authorize") { // RSA10a - it("should create a token if needed and use it") { - let options = AblyTests.clientOptions(requestToken: true) + it("should always create a token") { + let options = AblyTests.commonAppSetup() + options.useTokenAuth = true + let rest = ARTRest(options: options) + let channel = rest.channels.get("test") + waitUntil(timeout: testTimeout) { done in - // Client with Token - let rest = ARTRest(options: options) - publishTestMessage(rest, completion: { error in + channel.publish(nil, data: "first check") { error in expect(error).to(beNil()) - expect(rest.auth.method).to(equal(ARTAuthMethod.Token)) + done() + } + } - // Reuse the valid token - rest.auth.authorize(nil, options: nil, callback: { tokenDetails, error in - expect(rest.auth.method).to(equal(ARTAuthMethod.Token)) - guard let tokenDetails = tokenDetails else { - XCTFail("TokenDetails is nil"); done(); return - } - expect(tokenDetails.token).to(equal(options.token)) + // Check that token exists + expect(rest.auth.method).to(equal(ARTAuthMethod.Token)) + guard let firstTokenDetails = rest.auth.tokenDetails else { + fail("TokenDetails is nil"); return + } + expect(firstTokenDetails.token).toNot(beNil()) - publishTestMessage(rest, completion: { error in - expect(error).to(beNil()) - done() - }) - }) + waitUntil(timeout: testTimeout) { done in + channel.publish(nil, data: "second check") { error in + expect(error).to(beNil()) + done() + } + } + + // Check that token has not changed + expect(rest.auth.method).to(equal(ARTAuthMethod.Token)) + guard let secondTokenDetails = rest.auth.tokenDetails else { + fail("TokenDetails is nil"); return + } + expect(firstTokenDetails).to(beIdenticalTo(secondTokenDetails)) + + waitUntil(timeout: testTimeout) { done in + rest.auth.authorize(nil, options: nil, callback: { tokenDetails, error in + expect(error).to(beNil()) + guard let tokenDetails = tokenDetails else { + XCTFail("TokenDetails is nil"); done(); return + } + // Check that token has changed + expect(tokenDetails.token).toNot(equal(firstTokenDetails.token)) + + channel.publish(nil, data: "third check") { error in + expect(error).to(beNil()) + guard let thirdTokenDetails = rest.auth.tokenDetails else { + fail("TokenDetails is nil"); return + } + expect(thirdTokenDetails.token).to(equal(tokenDetails.token)) + done() + } }) } } @@ -1784,7 +1809,6 @@ class Auth : QuickSpec { authOptions.authParams?.append(NSURLQueryItem(name: "type", value: "text")) authOptions.authParams?.append(NSURLQueryItem(name: "body", value: token)) authOptions.authHeaders = ["X-Ably":"Test"] - authOptions.force = true authOptions.queryTime = true waitUntil(timeout: testTimeout) { done in @@ -1803,7 +1827,6 @@ class Auth : QuickSpec { XCTFail("TokenDetails is nil"); done(); return } expect(testHTTPExecutor.requests.last?.URL?.host).to(equal("echo.ably.io")) - expect(auth.options.force).to(beFalse()) expect(auth.options.authUrl!.host).to(equal("echo.ably.io")) expect(auth.options.authHeaders!["X-Ably"]).to(equal("Test")) expect(tokenDetails.token).to(equal(token)) @@ -2702,7 +2725,6 @@ class Auth : QuickSpec { // reauthorise let reauthOptions = ARTAuthOptions(); reauthOptions.tokenDetails = secondTokenDetails - reauthOptions.force = true waitUntil(timeout: testTimeout) { done in realtime.auth.authorize(nil, options: reauthOptions) { reauthTokenDetails, error in diff --git a/Spec/RestClient.swift b/Spec/RestClient.swift index 57c9fa482..e3c888d92 100644 --- a/Spec/RestClient.swift +++ b/Spec/RestClient.swift @@ -371,22 +371,30 @@ class RestClient: QuickSpec { // RSC9 it("should use Auth to manage authentication") { let options = AblyTests.clientOptions() - options.tokenDetails = getTestTokenDetails() + guard let testTokenDetails = getTestTokenDetails() else { + fail("No test token details"); return + } + options.tokenDetails = testTokenDetails + options.authCallback = { tokenParams, completion in + completion(testTokenDetails, nil) + } + + let client = ARTRest(options: options) + expect(client.auth).to(beAnInstanceOf(ARTAuth.self)) waitUntil(timeout: testTimeout) { done in - ARTRest(options: options).auth.authorize(nil, options: nil) { tokenDetails, error in + client.auth.authorize(nil, options: nil) { tokenDetails, error in if let e = error { XCTFail(e.description) done() return } guard let tokenDetails = tokenDetails else { - XCTFail("expected tokenDetails not to be nil when error is nil") + XCTFail("expected tokenDetails to not be nil when error is nil") done() return } - // Use the same token because it is valid - expect(tokenDetails.token).to(equal(options.tokenDetails!.token)) + expect(tokenDetails.token).to(equal(testTokenDetails.token)) done() } } From 263c5a72abe676331c27a429c17da8a510f2d434 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 1 Nov 2016 14:41:56 +0000 Subject: [PATCH 09/43] Update RSC15b for 0.9 (#516) * Update RSC15b * Fix: REST fallback should only apply when the default host is used --- Source/ARTClientOptions.h | 2 ++ Source/ARTClientOptions.m | 16 ++++++--- Source/ARTFallback+Private.h | 3 ++ Source/ARTFallback.m | 24 ++++++++++++++ Source/ARTRest.m | 4 +-- Spec/RestClient.swift | 64 +++++++++++++++++++++++++++++++++--- 6 files changed, 103 insertions(+), 10 deletions(-) diff --git a/Source/ARTClientOptions.h b/Source/ARTClientOptions.h index 39276564f..bc920cfee 100644 --- a/Source/ARTClientOptions.h +++ b/Source/ARTClientOptions.h @@ -84,7 +84,9 @@ ART_ASSUME_NONNULL_BEGIN - (NSURL *)restUrl; - (NSURL *)realtimeUrl; - (BOOL)hasCustomRestHost; +- (BOOL)hasDefaultRestHost; - (BOOL)hasCustomRealtimeHost; +- (BOOL)hasDefaultRealtimeHost; @end diff --git a/Source/ARTClientOptions.m b/Source/ARTClientOptions.m index 38ba2cadd..0c536f7c1 100644 --- a/Source/ARTClientOptions.m +++ b/Source/ARTClientOptions.m @@ -92,8 +92,8 @@ - (id)copyWithZone:(NSZone *)zone { options.clientId = self.clientId; options.port = self.port; options.tlsPort = self.tlsPort; - if (self.hasCustomRestHost) options.restHost = self.restHost; - if (self.hasCustomRealtimeHost) options.realtimeHost = self.realtimeHost; + if (self->_restHost) options.restHost = self.restHost; + if (self->_realtimeHost) options.realtimeHost = self.realtimeHost; options.queueMessages = self.queueMessages; options.echoMessages = self.echoMessages; options.recover = self.recover; @@ -126,11 +126,19 @@ - (BOOL)isBasicAuth { } - (BOOL)hasCustomRestHost { - return _restHost != nil; + return (_restHost && ![_restHost isEqualToString:[ARTDefault restHost]]) || _environment; +} + +- (BOOL)hasDefaultRestHost { + return ![self hasCustomRestHost]; } - (BOOL)hasCustomRealtimeHost { - return _realtimeHost != nil; + return (_realtimeHost && ![_realtimeHost isEqualToString:[ARTDefault realtimeHost]]) || _environment; +} + +- (BOOL)hasDefaultRealtimeHost { + return ![self hasCustomRealtimeHost]; } - (void)setFallbackHosts:(art_nullable __GENERIC(NSArray, NSString *) *)value { diff --git a/Source/ARTFallback+Private.h b/Source/ARTFallback+Private.h index f264deb41..487553b58 100644 --- a/Source/ARTFallback+Private.h +++ b/Source/ARTFallback+Private.h @@ -17,6 +17,9 @@ extern int (^ARTFallback_getRandomHostIndex)(int count); @property (readwrite, strong, nonatomic) __GENERIC(NSMutableArray, NSString *) *hosts; ++ (BOOL)restShouldFallback:(NSURL *)host withOptions:(ARTClientOptions *)options; ++ (BOOL)realtimeShouldFallback:(NSURL *)host withOptions:(ARTClientOptions *)options; + @end ART_ASSUME_NONNULL_END diff --git a/Source/ARTFallback.m b/Source/ARTFallback.m index 957cc7944..9d76151fe 100644 --- a/Source/ARTFallback.m +++ b/Source/ARTFallback.m @@ -61,4 +61,28 @@ - (NSString *)popFallbackHost { return host; } ++ (BOOL)restShouldFallback:(NSURL *)url withOptions:(ARTClientOptions *)options { + // Default REST + if ([url.host isEqualToString:[ARTDefault restHost]]) { + return YES; + } + // Custom host / environment + else if (options.fallbackHostsUseDefault) { + return YES; + } + return NO; +} + ++ (BOOL)realtimeShouldFallback:(NSURL *)url withOptions:(ARTClientOptions *)options { + // Default Realtime + if ([url.host isEqualToString:[ARTDefault realtimeHost]]) { + return YES; + } + // Custom host / environment + else if (options.fallbackHostsUseDefault) { + return YES; + } + return NO; +} + @end diff --git a/Source/ARTRest.m b/Source/ARTRest.m index 28b0fafd6..49918e98b 100644 --- a/Source/ARTRest.m +++ b/Source/ARTRest.m @@ -25,7 +25,7 @@ #import "ARTClientOptions+Private.h" #import "ARTDefault.h" #import "ARTStats.h" -#import "ARTFallback.h" +#import "ARTFallback+Private.h" #import "ARTNSDictionary+ARTDictionaryUtil.h" #import "ARTNSArray+ARTFunctional.h" #import "ARTRestChannel.h" @@ -178,7 +178,7 @@ - (void)executeRequest:(NSMutableURLRequest *)request completion:(void (^)(NSHTT } } if (retries < _options.httpMaxRetryCount && [self shouldRetryWithFallback:request response:response error:error]) { - if (!blockFallbacks && [request.URL.host isEqualToString:[self currentHost]]) { + if (!blockFallbacks && [ARTFallback restShouldFallback:request.URL withOptions:_options]) { blockFallbacks = [[ARTFallback alloc] initWithOptions:_options]; } if (blockFallbacks) { diff --git a/Spec/RestClient.swift b/Spec/RestClient.swift index e3c888d92..2040e14b7 100644 --- a/Spec/RestClient.swift +++ b/Spec/RestClient.swift @@ -669,6 +669,8 @@ class RestClient: QuickSpec { done() } } + + expect(testHTTPExecutor.requests).to(haveCount(1)) } // RSC15b @@ -735,6 +737,60 @@ class RestClient: QuickSpec { expect(NSRegularExpression.match(capturedURLs[1], pattern: "//[f-j].ably-realtime.com")).to(beTrue()) } + + // RSC15b + it("applies when ClientOptions#fallbackHostsUseDefault is true") { + let options = ARTClientOptions(key: "xxxx:xxxx") + options.environment = "rsc15b" + options.fallbackHostsUseDefault = true + let client = ARTRest(options: options) + client.httpExecutor = testHTTPExecutor + testHTTPExecutor.http = MockHTTP(network: .HostUnreachable) + let channel = client.channels.get("test") + + var capturedURLs = [String]() + testHTTPExecutor.afterRequest = { request, callback in + capturedURLs.append(request.URL!.absoluteString) + if testHTTPExecutor.requests.count == 2 { + // Stop + testHTTPExecutor.http = nil + callback!(nil, nil, nil) + } + } + + waitUntil(timeout: testTimeout) { done in + channel.publish(nil, data: "nil") { _ in + done() + } + } + + expect(testHTTPExecutor.requests).to(haveCount(2)) + if testHTTPExecutor.requests.count < 2 { + return + } + + expect(NSRegularExpression.match(capturedURLs[1], pattern: "//[a-e].ably-realtime.com")).to(beTrue()) + } + + // RSC15b + it("do not apply when ClientOptions#fallbackHostsUseDefault is false") { + let options = ARTClientOptions(key: "xxxx:xxxx") + options.environment = "rsc15b" + options.fallbackHostsUseDefault = false + let client = ARTRest(options: options) + client.httpExecutor = testHTTPExecutor + testHTTPExecutor.http = MockHTTP(network: .HostUnreachable) + let channel = client.channels.get("test") + + waitUntil(timeout: testTimeout) { done in + channel.publish(nil, data: "message") { error in + expect(error!.message).to(contain("hostname could not be found")) + done() + } + } + + expect(testHTTPExecutor.requests).to(haveCount(1)) + } // RSC15b it("won't apply fallback hosts if ClientOptions#fallbackHosts array is empty") { @@ -850,14 +906,14 @@ class RestClient: QuickSpec { } } - expect(testHTTPExecutor.requests).to(haveCount(3)) - if testHTTPExecutor.requests.count != 3 { + expect(testHTTPExecutor.requests).to(haveCount(2)) + if testHTTPExecutor.requests.count != 2 { return } + expect(client.options.restHost).to(equal("fake.ably.io")) expect(NSRegularExpression.match(testHTTPExecutor.requests[0].URL!.absoluteString, pattern: "//\(client.options.restHost)")).to(beTrue()) - expect(NSRegularExpression.match(testHTTPExecutor.requests[1].URL!.absoluteString, pattern: "//[a-e].ably-realtime.com")).to(beTrue()) - expect(NSRegularExpression.match(testHTTPExecutor.requests[2].URL!.absoluteString, pattern: "//\(client.options.restHost)")).to(beTrue()) + expect(NSRegularExpression.match(testHTTPExecutor.requests[1].URL!.absoluteString, pattern: "//\(client.options.restHost)")).to(beTrue()) } // RSC15a From a8ec7cd8ec98cc92a36d2f71c919e80987894cf9 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 1 Nov 2016 16:56:17 +0000 Subject: [PATCH 10/43] Update RSA10a for 0.9 (#520) * Update RSA10a * Fix: authorize should change auth method to Token for future requests --- Source/ARTAuth.m | 2 +- Spec/Auth.swift | 50 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/Source/ARTAuth.m b/Source/ARTAuth.m index 9f654cde2..a15411907 100644 --- a/Source/ARTAuth.m +++ b/Source/ARTAuth.m @@ -340,7 +340,6 @@ - (void)authorise:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOp } - (void)authorize:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOptions callback:(void (^)(ARTTokenDetails *, NSError *))callback { - ARTAuthOptions *replacedOptions = [authOptions copy] ? : [self.options copy]; [self storeOptions:replacedOptions]; @@ -357,6 +356,7 @@ - (void)authorize:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOp } } else { _tokenDetails = tokenDetails; + _method = ARTAuthMethodToken; [self.logger verbose:@"RS:%p ARTAuth: token request succeeded: %@", _rest, tokenDetails]; if (callback) { callback(self.tokenDetails, nil); diff --git a/Spec/Auth.swift b/Spec/Auth.swift index 8d671ea37..c47f73c2c 100644 --- a/Spec/Auth.swift +++ b/Spec/Auth.swift @@ -1728,6 +1728,56 @@ class Auth : QuickSpec { } } + // RSA10a + it("should create a new token if one already exist and ensure Token Auth is used for all future requests") { + let options = AblyTests.commonAppSetup() + let testToken = getTestToken() + options.token = testToken + let rest = ARTRest(options: options) + + expect(rest.auth.tokenDetails?.token).toNot(beNil()) + waitUntil(timeout: testTimeout) { done in + rest.auth.authorize(nil, options: nil, callback: { tokenDetails, error in + guard let tokenDetails = tokenDetails else { + XCTFail("TokenDetails is nil"); done(); return + } + expect(tokenDetails.token).toNot(equal(testToken)) + expect(rest.auth.method).to(equal(ARTAuthMethod.Token)) + + publishTestMessage(rest, completion: { error in + expect(error).to(beNil()) + expect(rest.auth.method).to(equal(ARTAuthMethod.Token)) + expect(rest.auth.tokenDetails?.token).to(equal(tokenDetails.token)) + done() + }) + }) + } + } + + // RSA10a + it("should create a token immediately and ensures Token Auth is used for all future requests") { + let options = AblyTests.commonAppSetup() + let rest = ARTRest(options: options) + + expect(rest.auth.tokenDetails?.token).to(beNil()) + waitUntil(timeout: testTimeout) { done in + rest.auth.authorize(nil, options: nil, callback: { tokenDetails, error in + guard let tokenDetails = tokenDetails else { + XCTFail("TokenDetails is nil"); done(); return + } + expect(tokenDetails.token).toNot(beNil()) + expect(rest.auth.method).to(equal(ARTAuthMethod.Token)) + + publishTestMessage(rest, completion: { error in + expect(error).to(beNil()) + expect(rest.auth.method).to(equal(ARTAuthMethod.Token)) + expect(rest.auth.tokenDetails?.token).to(equal(tokenDetails.token)) + done() + }) + }) + } + } + // RSA10b it("should supports all TokenParams and AuthOptions") { let rest = ARTRest(options: AblyTests.commonAppSetup()) From 5728e01171fcc76db224bd4f130cb5b2dbfb2196 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 3 Nov 2016 16:42:05 +0000 Subject: [PATCH 11/43] Update RSA10j for 0.9 (#521) * Update RSA10j * Fix: ttl when omitted should set the default value --- Source/ARTTokenParams.m | 3 ++- Source/ARTTokenRequest.m | 3 ++- Spec/Auth.swift | 53 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/Source/ARTTokenParams.m b/Source/ARTTokenParams.m index de4e73019..4f59bb772 100644 --- a/Source/ARTTokenParams.m +++ b/Source/ARTTokenParams.m @@ -153,8 +153,9 @@ - (ARTTokenRequest *)sign:(NSString *)key withNonce:(NSString *)nonce { NSString *keyName = keyComponents[0]; NSString *keySecret = keyComponents[1]; NSString *clientId = self.clientId ? self.clientId : @""; + NSTimeInterval ttl = self.ttl ? self.ttl : [ARTDefault ttl]; - NSString *signText = [NSString stringWithFormat:@"%@\n%lld\n%@\n%@\n%lld\n%@\n", keyName, timeIntervalToMilliseconds(self.ttl), self.capability, clientId, dateToMilliseconds(self.timestamp), nonce]; + NSString *signText = [NSString stringWithFormat:@"%@\n%lld\n%@\n%@\n%lld\n%@\n", keyName, timeIntervalToMilliseconds(ttl), self.capability, clientId, dateToMilliseconds(self.timestamp), nonce]; NSString *mac = hmacForDataAndKey([signText dataUsingEncoding:NSUTF8StringEncoding], [keySecret dataUsingEncoding:NSUTF8StringEncoding]); return [[ARTTokenRequest alloc] initWithTokenParams:self keyName:keyName nonce:nonce mac:mac]; diff --git a/Source/ARTTokenRequest.m b/Source/ARTTokenRequest.m index a5929559d..effb93600 100644 --- a/Source/ARTTokenRequest.m +++ b/Source/ARTTokenRequest.m @@ -9,12 +9,13 @@ #import "ARTTokenRequest.h" #import "ARTTokenParams.h" #import "ARTAuth+Private.h" +#import "ARTDefault.h" @implementation ARTTokenRequest - (instancetype)initWithTokenParams:(ARTTokenParams *)tokenParams keyName:(NSString *)keyName nonce:(NSString *)nonce mac:(NSString *)mac { if (self = [super init]) { - self.ttl = tokenParams.ttl; + self.ttl = tokenParams.ttl ? tokenParams.ttl : [ARTDefault ttl]; self.capability = tokenParams.capability; self.clientId = tokenParams.clientId; self.timestamp = tokenParams.timestamp; diff --git a/Spec/Auth.swift b/Spec/Auth.swift index c47f73c2c..81fdd4d22 100644 --- a/Spec/Auth.swift +++ b/Spec/Auth.swift @@ -2448,6 +2448,59 @@ class Auth : QuickSpec { } } + it("example: if a client is initialised with TokenParams#ttl configured with a custom value, and a TokenParams object is passed in as an argument to #authorize with a null value for ttl, then the ttl used for every subsequent authorization will be null") { + let options = AblyTests.commonAppSetup() + options.defaultTokenParams = { + $0.ttl = 0.1; + $0.clientId = "tester"; + return $0 + }(ARTTokenParams()) + + let rest = ARTRest(options: options) + + let testTokenParams = ARTTokenParams() + testTokenParams.ttl = 0 + testTokenParams.clientId = nil + + waitUntil(timeout: testTimeout) { done in + rest.auth.authorise(testTokenParams, options: nil) { tokenDetails, error in + expect(error).to(beNil()) + guard let tokenDetails = tokenDetails else { + fail("TokenDetails is nil"); done(); return + } + guard let issued = tokenDetails.issued else { + fail("TokenDetails.issued is nil"); done(); return + } + guard let expires = tokenDetails.expires else { + fail("TokenDetails.expires is nil"); done(); return + } + expect(tokenDetails.clientId).to(beNil()) + // `ttl` when omitted, the default value is applied + expect(issued.dateByAddingTimeInterval(ARTDefault.ttl())).to(equal(expires)) + done() + } + } + + // Subsequent authorization + waitUntil(timeout: testTimeout) { done in + rest.auth.authorise(nil, options: nil) { tokenDetails, error in + expect(error).to(beNil()) + guard let tokenDetails = tokenDetails else { + fail("TokenDetails is nil"); done(); return + } + guard let issued = tokenDetails.issued else { + fail("TokenDetails.issued is nil"); done(); return + } + guard let expires = tokenDetails.expires else { + fail("TokenDetails.expires is nil"); done(); return + } + expect(tokenDetails.clientId).to(beNil()) + expect(issued.dateByAddingTimeInterval(ARTDefault.ttl())).to(equal(expires)) + done() + } + } + } + } // RSA10k From 202b908d36f85652522bdebd120952f2f6f119a9 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 30 Nov 2016 15:50:05 +0000 Subject: [PATCH 12/43] Fix RTN15h --- Spec/RealtimeClientConnection.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index 0a61f0fa0..c45d52455 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -2422,7 +2422,7 @@ class RealtimeClientConnection: QuickSpec { guard let error = stateChange?.reason else { fail("Error is nil"); done(); return } - expect(UInt(error.code)).to(equal(ARTState.RequestTokenFailed.rawValue)) + expect(error.code) == 40142 expect(client.connection.errorReason).to(beIdenticalTo(error)) done() } From dabeea6de0a13326c7a95e62af4f6ca480994fcb Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 1 Dec 2016 12:28:38 +0000 Subject: [PATCH 13/43] RTC8 (#526) * Add ARTAuthDetails * RTC8a * RTC8a1 (part 1) * RTC8a1 (part 2) * RTC8a1 (part 3) * RTC8a2 * RTC8a3 * RTC8b * RTC8b1 * RTC8c * Send AUTH protocol message on each authorize * Fix RTC8 * Test suite: `splitDone`, when a test fails, get the right location of the failure --- Ably.xcodeproj/project.pbxproj | 8 + Source/ARTAuth+Private.h | 14 + Source/ARTAuth.m | 56 +- Source/ARTAuthDetails.h | 23 + Source/ARTAuthDetails.m | 30 + Source/ARTJsonLikeEncoder.m | 17 + Source/ARTProtocolMessage.h | 33 +- Source/ARTRealtime+Private.h | 3 +- Source/ARTRealtime.m | 61 +- Source/ARTRealtimeTransport.h | 12 +- Source/ARTStatus.h | 2 +- Source/ARTWebSocketTransport+Private.h | 4 +- Source/ARTWebSocketTransport.h | 2 - Source/ARTWebSocketTransport.m | 75 ++- Source/Ably.h | 1 + Spec/RealtimeClient.swift | 850 +++++++++++++++++++++++++ Spec/RealtimeClientConnection.swift | 2 +- Spec/RealtimeClientPresence.swift | 5 +- Spec/TestUtilities.swift | 6 +- Tests/ARTRealtimeAttachTest.m | 1 - 20 files changed, 1131 insertions(+), 74 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/ARTAuth+Private.h b/Source/ARTAuth+Private.h index f7458b5b8..299917b95 100644 --- a/Source/ARTAuth+Private.h +++ b/Source/ARTAuth+Private.h @@ -7,9 +7,21 @@ // #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; @@ -19,6 +31,8 @@ ART_ASSUME_NONNULL_BEGIN @property (art_nullable, nonatomic, readonly, strong) ARTTokenDetails *tokenDetails; @property (nonatomic, readonly, assign) NSTimeInterval timeOffset; +@property (art_nullable, weak) id delegate; + @end @interface ARTAuth (Private) diff --git a/Source/ARTAuth.m b/Source/ARTAuth.m index a15411907..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; @@ -53,7 +54,6 @@ - (instancetype)init:(ARTRest *)rest withOptions:(ARTClientOptions *)options { object:nil]; #endif } - return self; } @@ -346,21 +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]; - 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/ARTAuthDetails.h b/Source/ARTAuthDetails.h new file mode 100644 index 000000000..f50a255f8 --- /dev/null +++ b/Source/ARTAuthDetails.h @@ -0,0 +1,23 @@ +// +// 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; + +- (instancetype)initWithToken:(NSString *)token; + +@end + +ART_ASSUME_NONNULL_END diff --git a/Source/ARTAuthDetails.m b/Source/ARTAuthDetails.m new file mode 100644 index 000000000..cac096c93 --- /dev/null +++ b/Source/ARTAuthDetails.m @@ -0,0 +1,30 @@ +// +// ARTAuthDetails.m +// Ably +// +// Created by Ricardo Pereira on 19/10/2016. +// Copyright © 2016 Ably. All rights reserved. +// + +#import "ARTAuthDetails.h" + +@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]; +} + +- (id)copyWithZone:(NSZone *)zone { + ARTAuthDetails *authDetails = [[[self class] allocWithZone:zone] init]; + authDetails.accessToken = self.accessToken; + return authDetails; +} + +@end 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/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/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 725cab720..04871ef2c 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -25,13 +25,14 @@ #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" #import "ARTStats.h" #import "ARTRealtimeTransport.h" #import "ARTFallback.h" +#import "ARTAuthDetails.h" @interface ARTConnectionStateChange () @@ -53,6 +54,8 @@ @implementation ARTRealtime { ARTFallback *_fallbacks; } +@synthesize authorizationEmitter = _authorizationEmitter; + - (instancetype)initWithKey:(NSString *)key { return [self initWithOptions:[[ARTClientOptions alloc] initWithKey:key]]; } @@ -80,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]; } @@ -93,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; } @@ -214,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.connection emit:state with:stateChange]; + + [self transitionSideEffects:stateChange]; + [_internalEventEmitter emit:[NSNumber numberWithInteger:state] with:stateChange]; } @@ -292,6 +335,7 @@ - (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: status = [ARTStatus state:ARTStateConnectionFailed info:stateChange.reason]; @@ -299,6 +343,7 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { self.transport.delegate = nil; _transport = nil; self.rest.prioritizedHost = nil; + [_authorizationEmitter emit:[NSNumber numberWithInt:ARTAuthorizationFailed] with:stateChange.reason]; break; case ARTRealtimeDisconnected: { if (!_startedReconnection) { @@ -333,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: { @@ -347,6 +393,7 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { }]; } [_connectedEventEmitter emit:[NSNull null] with:nil]; + [_authorizationEmitter emit:[NSNumber numberWithInt:ARTAuthorizationSucceeded] with:nil]; break; } case ARTRealtimeInitialized: @@ -382,6 +429,8 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { } } } + + [self.connection emit:stateChange.current with:stateChange]; } - (void)unlessStateChangesBefore:(NSTimeInterval)deadline do:(void(^)())callback { @@ -783,6 +832,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]; } 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/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" diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index aa5a2ace7..c068d222b 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -352,6 +352,856 @@ 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() + } + } + } + + // 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).to(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(beNil()) + expect(connectionDetailsAfterAuth.clientId).to(beNil()) + 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)) + } + + // 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() + } + + 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) + + 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).to(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()) + // 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 + 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("foo") + waitUntil(timeout: testTimeout) { done in + client.connect() + channel.attach() { error in + expect(error).to(beNil()) + 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(beIdenticalTo(channel.errorReason)) + expect(error.code).to(equal(40160)) + + 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(contain("test")) + expect(errorMessage.error?.code).to(equal(error.code)) + 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)) + } + + // 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() + } + + var connectionError: NSError? + var authError: NSError? + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + + 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).toNot(beNil()) + connectionError = stateChange.reason + partialDone() + } + + 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()) + done() + } + } + + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Connected)) + expect(client.auth.tokenDetails!.token).to(equal(testToken)) + } + + // 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() + } + } + } + + // 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.commonAppSetup() + options.autoConnect = false + 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()) + + 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(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.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 - 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() + options.token = testToken + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + client.setTransportClass(TestProxyTransport.self) + + waitUntil(timeout: testTimeout) { done 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 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() } + + 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(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)) + } + + // 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 - 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() + 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.onSuspended() + expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Suspended), 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.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 + } + expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.Disconnected)) + 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 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() + } + } + } + + } + it("should never register any connection listeners for internal use with the public EventEmitter") { let options = AblyTests.commonAppSetup() options.autoConnect = false 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..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) } } } @@ -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 89a013708dc644a8355509a9b0f27be842684c25 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Mon, 5 Dec 2016 11:37:26 +0000 Subject: [PATCH 14/43] RSA4b (#518) * RSA4b * Fix REST: should retry the request once if a token error occurs * Fix Realtime: if the token creation failed then the connection should move to the DISCONNECTED state and reports the error --- Source/ARTRealtime.m | 6 +-- Source/ARTRealtimeTransport.h | 2 +- Source/ARTRest.m | 20 +++++++- Source/ARTTypes.h | 3 +- Source/ARTWebSocketTransport.m | 11 ++++- Spec/Auth.swift | 83 ++++++++++++++++++++++++++++++++++ 6 files changed, 116 insertions(+), 9 deletions(-) diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index 04871ef2c..261f57a8c 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -292,7 +292,7 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { [_transport connect]; } - if (self.connection.state != ARTRealtimeFailed && self.connection.state != ARTRealtimeClosed) { + if (self.connection.state != ARTRealtimeFailed && self.connection.state != ARTRealtimeClosed && self.connection.state != ARTRealtimeDisconnected) { [_reachability listenForHost:[_transport host] callback:^(BOOL reachable) { if (reachable) { switch (_connection.state) { @@ -915,7 +915,7 @@ - (void)realtimeTransportClosed:(id)transport { [self transition:ARTRealtimeClosed]; } -- (void)realtimeTransportDisconnected:(id)transport { +- (void)realtimeTransportDisconnected:(id)transport withError:(ARTRealtimeTransportError *)error { if (transport != self.transport) { // Old connection return; @@ -924,7 +924,7 @@ - (void)realtimeTransportDisconnected:(id)transport { if (self.connection.state == ARTRealtimeClosing) { [self transition:ARTRealtimeClosed]; } else { - [self transition:ARTRealtimeDisconnected]; + [self transition:ARTRealtimeDisconnected withErrorInfo:[ARTErrorInfo createWithNSError:error.error]]; } } diff --git a/Source/ARTRealtimeTransport.h b/Source/ARTRealtimeTransport.h index 322a774c0..f3397cfdc 100644 --- a/Source/ARTRealtimeTransport.h +++ b/Source/ARTRealtimeTransport.h @@ -57,7 +57,7 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeTransportState) { - (void)realtimeTransportUnavailable:(id)transport; - (void)realtimeTransportClosed:(id)transport; -- (void)realtimeTransportDisconnected:(id)transport; +- (void)realtimeTransportDisconnected:(id)transport withError:(art_nullable ARTRealtimeTransportError *)error; - (void)realtimeTransportNeverConnected:(id)transport; - (void)realtimeTransportRefused:(id)transport; - (void)realtimeTransportTooBig:(id)transport; diff --git a/Source/ARTRest.m b/Source/ARTRest.m index 49918e98b..d90bc84c0 100644 --- a/Source/ARTRest.m +++ b/Source/ARTRest.m @@ -34,6 +34,12 @@ #import "ARTDefault.h" #import "ARTGCD.h" +@interface ARTRest () { + __block NSUInteger _tokenErrorRetries; +} + +@end + @implementation ARTRest - (instancetype)initWithOptions:(ARTClientOptions *)options { @@ -68,6 +74,7 @@ - (instancetype)initWithOptions:(ARTClientOptions *)options { }; _defaultEncoding = (_options.useBinaryProtocol ? [msgPackEncoder mimeType] : [jsonEncoder mimeType]); _fallbackCount = 0; + _tokenErrorRetries = 0; _auth = [[ARTAuth alloc] init:self withOptions:_options]; _channels = [[ARTRestChannels alloc] initWithRest:self]; @@ -97,9 +104,15 @@ - (void)executeRequest:(NSMutableURLRequest *)request withAuthOption:(ARTAuthent [self executeRequest:request completion:callback]; break; case ARTAuthenticationOn: + _tokenErrorRetries = 0; [self executeRequestWithAuthentication:request withMethod:self.auth.method force:NO completion:callback]; break; case ARTAuthenticationNewToken: + _tokenErrorRetries = 0; + [self executeRequestWithAuthentication:request withMethod:self.auth.method force:YES completion:callback]; + break; + case ARTAuthenticationTokenRetry: + _tokenErrorRetries = _tokenErrorRetries + 1; [self executeRequestWithAuthentication:request withMethod:self.auth.method force:YES completion:callback]; break; case ARTAuthenticationUseBasic: @@ -167,8 +180,11 @@ - (void)executeRequest:(NSMutableURLRequest *)request completion:(void (^)(NSHTT if ([self shouldRenewToken:&dataError]) { [self.logger debug:__FILE__ line:__LINE__ message:@"RS:%p retry request %@", self, request]; // Make a single attempt to reissue the token and resend the request - [self executeRequest:request withAuthOption:ARTAuthenticationNewToken completion:callback]; - return; + if (_tokenErrorRetries < 1) { + [self executeRequest:request withAuthOption:ARTAuthenticationTokenRetry completion:callback]; + return; + } + error = dataError; } else { // Return error with HTTP StatusCode if ARTErrorStatusCode does not exist if (!dataError) { diff --git a/Source/ARTTypes.h b/Source/ARTTypes.h index e416d4086..6cfb1f7b5 100644 --- a/Source/ARTTypes.h +++ b/Source/ARTTypes.h @@ -25,7 +25,8 @@ typedef NS_ENUM(NSUInteger, ARTAuthentication) { ARTAuthenticationOff, ARTAuthenticationOn, ARTAuthenticationUseBasic, - ARTAuthenticationNewToken + ARTAuthenticationNewToken, + ARTAuthenticationTokenRetry }; typedef NS_ENUM(NSUInteger, ARTAuthMethod) { diff --git a/Source/ARTWebSocketTransport.m b/Source/ARTWebSocketTransport.m index 1cdb5db9b..017b81852 100644 --- a/Source/ARTWebSocketTransport.m +++ b/Source/ARTWebSocketTransport.m @@ -117,7 +117,14 @@ - (void)connectForcingNewToken:(BOOL)forceNewToken { 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]]; + if (error.code == 40102 /*incompatible credentials*/) { + // RSA15c + [[weakSelf delegate] realtimeTransportFailed:weakSelf withError:[[ARTRealtimeTransportError alloc] initWithError:error type:ARTRealtimeTransportErrorTypeAuth url:self.websocketURL]]; + } + else { + // RSA4b + [[weakSelf delegate] realtimeTransportDisconnected:weakSelf withError:[[ARTRealtimeTransportError alloc] initWithError:error type:ARTRealtimeTransportErrorTypeAuth url:self.websocketURL]]; + } return; } @@ -301,7 +308,7 @@ - (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reas case ARTWsGoingAway: case ARTWsAbnormalClose: // Connectivity issue - [s.delegate realtimeTransportDisconnected:s]; + [s.delegate realtimeTransportDisconnected:s withError:nil]; break; case ARTWsRefuse: case ARTWsPolicyValidation: diff --git a/Spec/Auth.swift b/Spec/Auth.swift index 81fdd4d22..f9662a1d1 100644 --- a/Spec/Auth.swift +++ b/Spec/Auth.swift @@ -245,6 +245,89 @@ class Auth : QuickSpec { } } } + + // RSA4b + it("in REST, if the token creation failed or the subsequent request with the new token failed due to a token error, then the request should result in an error") { + let options = AblyTests.commonAppSetup() + options.useTokenAuth = true + + let rest = ARTRest(options: options) + rest.httpExecutor = testHTTPExecutor + + let channel = rest.channels.get("test") + + testHTTPExecutor.afterRequest = { _ in + testHTTPExecutor.simulateIncomingServerErrorOnNextRequest(40141, description: "token revoked") + } + + testHTTPExecutor.simulateIncomingServerErrorOnNextRequest(40141, description: "token revoked") + waitUntil(timeout: testTimeout) { done in + channel.publish("message", data: nil) { error in + guard let error = error else { + fail("Error is nil"); done(); return + } + expect(error.code).to(equal(40141)) + done() + } + } + + // First request and a second attempt + expect(testHTTPExecutor.requests).to(haveCount(2)) + } + + // RSA4b + it("in Realtime, if the token creation failed then the connection should move to the DISCONNECTED state and reports the error") { + let options = AblyTests.commonAppSetup() + options.authCallback = { tokenParams, completion in + completion(nil, NSError(domain: NSURLErrorDomain, code: -1003, userInfo: [NSLocalizedDescriptionKey: "A server with the specified hostname could not be found."])) + } + options.autoConnect = false + + let realtime = ARTRealtime(options: options) + defer { realtime.dispose(); realtime.close() } + + waitUntil(timeout: testTimeout) { done in + realtime.connection.once(.Failed) { _ in + fail("Should not reach Failed state"); done(); return + } + realtime.connection.once(.Disconnected) { stateChange in + guard let errorInfo = stateChange?.reason else { + fail("ErrorInfo is nil"); done(); return + } + expect(errorInfo.message).to(contain("server with the specified hostname could not be found")) + done() + } + realtime.connect() + } + } + + // RSA4b + it("in Realtime, if the connection fails due to a terminal token error, then the connection should move to the FAILED state and reports the error") { + let options = AblyTests.commonAppSetup() + options.authCallback = { tokenParams, completion in + let token = getTestToken() + let invalidToken = String(token.characters.reverse()) + completion(invalidToken, nil) + } + options.autoConnect = false + + let realtime = ARTRealtime(options: options) + defer { realtime.dispose(); realtime.close() } + + waitUntil(timeout: testTimeout) { done in + realtime.connection.once(.Failed) { stateChange in + guard let errorInfo = stateChange?.reason else { + fail("ErrorInfo is nil"); done(); return + } + expect(errorInfo.message).to(contain("No application found with id")) + done() + } + realtime.connection.once(.Disconnected) { _ in + fail("Should not reach Disconnected state"); done(); return + } + realtime.connect() + } + } } // RSA14 From 67bbcce20f1d0bae036afd1a2b725235e5af1b45 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Mon, 12 Dec 2016 09:48:24 +0000 Subject: [PATCH 15/43] RSA4c (#519) * RSA4c * Fix: 80019 and description of the underlying failure should be emitted * Fix: if connected and the client receives an ARTProtocolMessageAuth, then authorise * Fix: if authUrl or authCallback fails and is CONNECTED then the connection should remain CONNECTED - RSA4c3 * Add `artDispatchScheduled` and a way to cancel the scheduled block * Fix: authUrl/authCallback attempt times out after realtimeRequestTimeout * Test suite: reset networkConnectEvent after spec * Enhance: debug info * fixup! Fix: authUrl/authCallback attempt times out after realtimeRequestTimeout * fixup! Fix: if connected and the client receives an ARTProtocolMessageAuth, then authorise * Fix: every realtime auth attempt should check if deadline is reached * Rename createWithNSError to createFromNSError * Fix: remove Auth dependency from WebSocketTransport * Fix: should first authorize and then connect the transport --- Source/ARTAuth+Private.h | 1 + Source/ARTAuth.m | 3 + Source/ARTAuthOptions.m | 3 +- Source/ARTClientOptions.m | 4 + Source/ARTDataEncoder.m | 4 +- Source/ARTGCD.h | 3 + Source/ARTGCD.m | 18 +- Source/ARTPaginatedResult.m | 2 +- Source/ARTRealtime.m | 181 ++++++++++++-- Source/ARTRealtimeTransport.h | 6 +- Source/ARTRealtimeTransport.m | 2 - Source/ARTRest.m | 17 ++ Source/ARTRestChannel.m | 2 +- Source/ARTStatus.h | 6 +- Source/ARTStatus.m | 2 +- Source/ARTTypes.h | 3 + Source/ARTTypes.m | 8 + Source/ARTWebSocketTransport+Private.h | 1 - Source/ARTWebSocketTransport.m | 58 +---- Spec/Auth.swift | 329 ++++++++++++++++++++++++- Spec/RealtimeClient.swift | 2 +- Spec/RealtimeClientConnection.swift | 111 ++++++++- Spec/RestClientChannel.swift | 12 +- Spec/TestUtilities.swift | 64 ++++- 24 files changed, 713 insertions(+), 129 deletions(-) diff --git a/Source/ARTAuth+Private.h b/Source/ARTAuth+Private.h index 299917b95..d94470ef5 100644 --- a/Source/ARTAuth+Private.h +++ b/Source/ARTAuth+Private.h @@ -32,6 +32,7 @@ ART_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly, assign) NSTimeInterval timeOffset; @property (art_nullable, weak) id delegate; +@property (readonly, assign) BOOL authorizing; @end diff --git a/Source/ARTAuth.m b/Source/ARTAuth.m index 910540fff..f721b3c37 100644 --- a/Source/ARTAuth.m +++ b/Source/ARTAuth.m @@ -352,6 +352,7 @@ - (void)authorize:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOp if (callback) { callback(self.tokenDetails, nil); } + _authorizing = false; }; // Failure @@ -360,6 +361,7 @@ - (void)authorize:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOp if (callback) { callback(nil, error); } + _authorizing = false; }; __weak id lastDelegate = self.delegate; @@ -378,6 +380,7 @@ - (void)authorize:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOp // Request always a new token [self.logger verbose:@"RS:%p ARTAuth: requesting new token.", _rest]; + _authorizing = true; [self requestToken:currentTokenParams withOptions:replacedOptions callback:^(ARTTokenDetails *tokenDetails, NSError *error) { if (error) { failureBlock(error); diff --git a/Source/ARTAuthOptions.m b/Source/ARTAuthOptions.m index 2693d922e..e878defea 100644 --- a/Source/ARTAuthOptions.m +++ b/Source/ARTAuthOptions.m @@ -75,8 +75,7 @@ - (id)copyWithZone:(NSZone *)zone { } - (NSString *)description { - return [NSString stringWithFormat: @"%@: key=%@ token=%@ authUrl=%@ authMethod=%@ hasAuthCallback=%d", - NSStringFromClass([self class]), self.key, self.token, self.authUrl, self.authMethod, self.authCallback != nil]; + return [NSString stringWithFormat:@"%@ - \n\t key: %@; \n\t token: %@; \n\t authUrl: %@; \n\t authMethod: %@; \n\t hasAuthCallback: %d;", [super description], self.key, self.token, self.authUrl, self.authMethod, self.authCallback != nil]; } - (NSString *)token { diff --git a/Source/ARTClientOptions.m b/Source/ARTClientOptions.m index 0c536f7c1..d7e6488b7 100644 --- a/Source/ARTClientOptions.m +++ b/Source/ARTClientOptions.m @@ -46,6 +46,10 @@ - (instancetype)initDefaults { return self; } +- (NSString *)description { + return [NSString stringWithFormat:@"%@\n\t clientId: %@;", [super description], self.clientId]; +} + - (NSString*)getRestHost { if (_restHost) { return _restHost; diff --git a/Source/ARTDataEncoder.m b/Source/ARTDataEncoder.m index f22625cdc..0896f15a1 100644 --- a/Source/ARTDataEncoder.m +++ b/Source/ARTDataEncoder.m @@ -64,7 +64,7 @@ - (ARTDataEncoderOutput *)encode:(id)data { // data before encrypting. jsonEncoded = [NSJSONSerialization dataWithJSONObject:data options:0 error:&error]; if (error) { - return [[ARTDataEncoderOutput alloc] initWithData:data encoding:nil errorInfo:[ARTErrorInfo createWithNSError:error]]; + return [[ARTDataEncoderOutput alloc] initWithData:data encoding:nil errorInfo:[ARTErrorInfo createFromNSError:error]]; } encoded = data; encoding = @"json"; @@ -150,7 +150,7 @@ - (ARTDataEncoderOutput *)decode:(id)data encoding:(NSString *)encoding { NSError *error; data = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; if (error != nil) { - errorInfo = [ARTErrorInfo createWithNSError:error]; + errorInfo = [ARTErrorInfo createFromNSError:error]; } } else if (![data isKindOfClass:[NSArray class]] && ![data isKindOfClass:[NSDictionary class]]) { errorInfo = [ARTErrorInfo createWithCode:0 message:[NSString stringWithFormat:@"invalid data type for 'json' decoding: '%@'", [data class]]]; diff --git a/Source/ARTGCD.h b/Source/ARTGCD.h index 128f144b6..ddcb5498b 100644 --- a/Source/ARTGCD.h +++ b/Source/ARTGCD.h @@ -13,5 +13,8 @@ void artDispatchSpecifyMainQueue(); void artDispatchMainQueue(dispatch_block_t block); +void artDispatchGlobalQueue(dispatch_block_t block); +dispatch_block_t artDispatchScheduled(NSTimeInterval seconds, dispatch_block_t block); +void artDispatchCancel(dispatch_block_t block); #endif /* ARTGCD_h */ diff --git a/Source/ARTGCD.m b/Source/ARTGCD.m index e13803740..94798a4cc 100644 --- a/Source/ARTGCD.m +++ b/Source/ARTGCD.m @@ -22,8 +22,20 @@ void artDispatchMainQueue(dispatch_block_t block) { block(); } else { - dispatch_async(dispatch_get_main_queue(), ^{ - block(); - }); + dispatch_async(dispatch_get_main_queue(), block); } } + +void artDispatchGlobalQueue(dispatch_block_t block) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block); +} + +dispatch_block_t artDispatchScheduled(NSTimeInterval seconds, dispatch_block_t block) { + dispatch_block_t work = dispatch_block_create(0, block); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC * seconds)), dispatch_get_main_queue(), work); + return work; +} + +void artDispatchCancel(dispatch_block_t block) { + dispatch_block_cancel(block); +} diff --git a/Source/ARTPaginatedResult.m b/Source/ARTPaginatedResult.m index e03c2a0e3..462fb026f 100644 --- a/Source/ARTPaginatedResult.m +++ b/Source/ARTPaginatedResult.m @@ -104,7 +104,7 @@ + (void)executePaginated:(ARTRest *)rest withRequest:(NSMutableURLRequest *)requ [rest executeRequest:request withAuthOption:ARTAuthenticationOn completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { if (error) { - callback(nil, [ARTErrorInfo createWithNSError:error]); + callback(nil, [ARTErrorInfo createFromNSError:error]); } else { [rest.logger debug:__FILE__ line:__LINE__ message:@"Paginated response: %@", response]; [rest.logger debug:__FILE__ line:__LINE__ message:@"Paginated response data: %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]]; diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index 261f57a8c..b96c46523 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -33,6 +33,7 @@ #import "ARTRealtimeTransport.h" #import "ARTFallback.h" #import "ARTAuthDetails.h" +#import "ARTGCD.h" @interface ARTConnectionStateChange () @@ -153,7 +154,20 @@ - (NSString *)getClientId { } - (NSString *)description { - return [NSString stringWithFormat:@"Realtime: %@", self.clientId]; + NSString *info; + if (self.options.token) { + info = [NSString stringWithFormat:@"token: %@", self.options.token]; + } + else if (self.options.authUrl) { + info = [NSString stringWithFormat:@"authUrl: %@", self.options.authUrl]; + } + else if (self.options.authCallback) { + info = [NSString stringWithFormat:@"authCallback: %@", self.options.authCallback]; + } + else { + info = [NSString stringWithFormat:@"key: %@", self.options.key]; + } + return [NSString stringWithFormat:@"%@ - \n\t %@;", [super description], info]; } - (ARTAuth *)getAuth { @@ -233,7 +247,7 @@ - (void)ping:(void (^)(ARTErrorInfo *)) cb { return; } [_pingEventEmitter timed:[_pingEventEmitter once:cb] deadline:[ARTDefault realtimeRequestTimeout] onTimeout:^{ - cb([ARTErrorInfo createWithCode:0 status:ARTStateConnectionFailed message:@"timed out"]); + cb([ARTErrorInfo createWithCode:ARTCodeErrorConnectionTimedOut status:ARTStateConnectionFailed message:@"timed out"]); }]; [self.transport sendPing]; } @@ -268,11 +282,14 @@ - (void)transition:(ARTRealtimeConnectionState)state withErrorInfo:(ARTErrorInfo - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { ARTStatus *status = nil; + // Do not increase the reference count (avoid retain cycles): + // i.e. the `unlessStateChangesBefore` is setting a timer and if the `ARTRealtime` instance is released before that timer, then it could create a leak. + __weak __typeof(self) weakSelf = self; switch (stateChange.current) { case ARTRealtimeConnecting: { [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ - [self transition:ARTRealtimeDisconnected withErrorInfo:[ARTErrorInfo createWithCode:0 status:ARTStateConnectionFailed message:@"timed out"]]; + [weakSelf onConnectionTimeOut]; }]; if (!_reachability) { @@ -289,25 +306,25 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { } _transport = [[_transportClass alloc] initWithRest:self.rest options:self.options resumeKey:resumeKey connectionSerial:connectionSerial]; _transport.delegate = self; - [_transport connect]; + [self transportConnect]; } if (self.connection.state != ARTRealtimeFailed && self.connection.state != ARTRealtimeClosed && self.connection.state != ARTRealtimeDisconnected) { [_reachability listenForHost:[_transport host] callback:^(BOOL reachable) { if (reachable) { - switch (_connection.state) { + switch ([[weakSelf connection] state]) { case ARTRealtimeDisconnected: case ARTRealtimeSuspended: - [self transition:ARTRealtimeConnecting]; + [weakSelf transition:ARTRealtimeConnecting]; default: break; } } else { - switch (_connection.state) { + switch ([[weakSelf connection] state]) { case ARTRealtimeConnecting: case ARTRealtimeConnected: { ARTErrorInfo *unreachable = [ARTErrorInfo createWithCode:-1003 message:@"unreachable host"]; - [self transition:ARTRealtimeDisconnected withErrorInfo:unreachable]; + [weakSelf transition:ARTRealtimeDisconnected withErrorInfo:unreachable]; break; } default: @@ -322,7 +339,7 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { case ARTRealtimeClosing: { [_reachability off]; [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ - [self transition:ARTRealtimeClosed]; + [weakSelf transition:ARTRealtimeClosed]; }]; [self.transport sendClose]; break; @@ -365,7 +382,7 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { [stateChange setRetryIn:self.options.disconnectedRetryTimeout]; [self unlessStateChangesBefore:stateChange.retryIn do:^{ - [self transition:ARTRealtimeConnecting]; + [weakSelf transition:ARTRealtimeConnecting]; }]; break; @@ -376,7 +393,7 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { _transport = nil; [stateChange setRetryIn:self.options.suspendedRetryTimeout]; [self unlessStateChangesBefore:stateChange.retryIn do:^{ - [self transition:ARTRealtimeConnecting]; + [weakSelf transition:ARTRealtimeConnecting]; }]; [_authorizationEmitter emit:[NSNumber numberWithInt:ARTAuthorizationFailed] with:[ARTErrorInfo createWithCode:ARTStateAuthorizationFailed message:@"Connection has been suspended"]]; break; @@ -536,13 +553,26 @@ - (void)onClosed { } } +- (void)onAuth { + [self.logger info:@"R:%p server has requested an authorise", self]; + switch (self.connection.state) { + case ARTRealtimeConnecting: + case ARTRealtimeConnected: + [self transportReconnectWithRenewedToken]; + break; + default: + [self.logger error:@"Invalid Realtime state: expected Connecting or Connected"]; + break; + } +} + - (void)onError:(ARTProtocolMessage *)message { - // TODO work out which states this can be received in if (message.channel) { [self onChannelMessage:message]; } else { ARTErrorInfo *error = message.error; if ([self shouldRenewToken:&error]) { + [self.transport close]; [self transportReconnectWithRenewedToken]; return; } @@ -551,6 +581,24 @@ - (void)onError:(ARTProtocolMessage *)message { } } +- (void)onConnectionTimeOut { + ARTErrorInfo *error; + if (self.auth.authorizing && (self.options.authUrl || self.options.authCallback)) { + error = [ARTErrorInfo createWithCode:ARTCodeErrorAuthConfiguredProviderFailure status:ARTStateConnectionFailed message:@"timed out"]; + } + else { + error = [ARTErrorInfo createWithCode:ARTCodeErrorConnectionTimedOut status:ARTStateConnectionFailed message:@"timed out"]; + } + switch (self.connection.state) { + case ARTRealtimeConnected: + [self transition:ARTRealtimeConnected withErrorInfo:error]; + break; + default: + [self transition:ARTRealtimeDisconnected withErrorInfo:error]; + break; + } +} + - (BOOL)shouldRenewToken:(ARTErrorInfo **)errorPtr { if (!_renewingToken && errorPtr && *errorPtr && (*errorPtr).statusCode == 401 && (*errorPtr).code >= 40140 && (*errorPtr).code < 40150) { @@ -564,15 +612,101 @@ - (BOOL)shouldRenewToken:(ARTErrorInfo **)errorPtr { - (void)transportReconnectWithHost:(NSString *)host { [self.transport setHost:host]; - [self.transport connect]; + [self transportConnect]; } - (void)transportReconnectWithRenewedToken { _renewingToken = true; - [_transport close]; - _transport = [[_transportClass alloc] initWithRest:self.rest options:self.options resumeKey:_transport.resumeKey connectionSerial:_transport.connectionSerial]; - _transport.delegate = self; - [_transport connectForcingNewToken:true]; + [self transportConnectForcingNewToken:true]; +} + +- (void)transportConnect { + [self transportConnectForcingNewToken:false]; +} + +- (void)transportConnectForcingNewToken:(BOOL)forceNewToken { + ARTClientOptions *options = [self.options copy]; + if ([options isBasicAuth]) { + // Basic + [self.transport connectWithKey:options.key]; + } + else { + // Token + [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p connecting with token auth; authorising", self]; + + if (!forceNewToken && [self.auth tokenRemainsValid]) { + // Reuse token + [self.transport connectWithToken:self.auth.tokenDetails.token]; + } + else { + // New Token + // Transport instance couldn't exist anymore when `authorize` completes or reaches time out. + __weak __typeof(self) weakSelf = self; + + dispatch_block_t work = artDispatchScheduled([ARTDefault realtimeRequestTimeout], ^{ + [weakSelf onConnectionTimeOut]; + }); + + // Deactivate use of `ARTAuthDelegate`: `authorize` should complete without waiting for a CONNECTED state. + id delegate = self.auth.delegate; + self.auth.delegate = nil; + @try { + [self.auth authorize:nil options:options callback:^(ARTTokenDetails *tokenDetails, NSError *error) { + // Cancel scheduled work + artDispatchCancel(work); + // It's still valid? + switch ([[weakSelf connection] state]) { + case ARTRealtimeClosing: + case ARTRealtimeClosed: + return; + default: + break; + } + + [[weakSelf getLogger] debug:__FILE__ line:__LINE__ message:@"R:%p authorised: %@ error: %@", weakSelf, tokenDetails, error]; + if (error) { + [weakSelf handleTokenAuthError:error]; + return; + } + + if (forceNewToken) { + [_transport close]; + _transport = [[_transportClass alloc] initWithRest:self.rest options:self.options resumeKey:_transport.resumeKey connectionSerial:_transport.connectionSerial]; + _transport.delegate = self; + } + [[weakSelf getTransport] connectWithToken:tokenDetails.token]; + }]; + } + @finally { + self.auth.delegate = delegate; + } + } + } +} + +- (void)handleTokenAuthError:(NSError *)error { + [self.logger error:@"R:%p token auth failed with %@", self, error.description]; + if (error.code == 40102 /*incompatible credentials*/) { + // RSA15c + [self transition:ARTRealtimeFailed withErrorInfo:[ARTErrorInfo createFromNSError:error]]; + } + else if (self.options.authUrl || self.options.authCallback) { + ARTErrorInfo *errorInfo = [ARTErrorInfo createWithCode:ARTCodeErrorAuthConfiguredProviderFailure status:ARTStateConnectionFailed message:error.description]; + switch (self.connection.state) { + case ARTRealtimeConnected: + // RSA4c3 + [self.connection setErrorReason:errorInfo]; + break; + default: + // RSA4c + [self transition:ARTRealtimeDisconnected withErrorInfo:errorInfo]; + break; + } + } + else { + // RSA4b + [self transition:ARTRealtimeDisconnected withErrorInfo:[ARTErrorInfo createFromNSError:error]]; + } } - (void)onAck:(ARTProtocolMessage *)message { @@ -584,7 +718,9 @@ - (void)onNack:(ARTProtocolMessage *)message { } - (void)onChannelMessage:(ARTProtocolMessage *)message { - // TODO work out which states this can be received in / error info? + if (message.channel == nil) { + return; + } ARTRealtimeChannel *channel = [self.channels get:message.channel]; [channel onChannelMessage:message]; } @@ -886,6 +1022,9 @@ - (void)realtimeTransport:(id)transport didReceiveMessage:(ARTProtocolMessage *) case ARTProtocolMessageClosed: [self onClosed]; break; + case ARTProtocolMessageAuth: + [self onAuth]; + break; default: [self onChannelMessage:message]; break; @@ -924,7 +1063,7 @@ - (void)realtimeTransportDisconnected:(id)transport withEr if (self.connection.state == ARTRealtimeClosing) { [self transition:ARTRealtimeClosed]; } else { - [self transition:ARTRealtimeDisconnected withErrorInfo:[ARTErrorInfo createWithNSError:error.error]]; + [self transition:ARTRealtimeDisconnected withErrorInfo:[ARTErrorInfo createFromNSError:error.error]]; } } @@ -941,7 +1080,7 @@ - (void)realtimeTransportFailed:(id)transport withError:(A if (!_fallbacks && [error.url.host isEqualToString:[ARTDefault realtimeHost]]) { [self.rest internetIsUp:^void(BOOL isUp) { _fallbacks = [[ARTFallback alloc] initWithOptions:[self getClientOptions]]; - (_fallbacks != nil) ? [self reconnectWithFallback] : [self transition:ARTRealtimeFailed withErrorInfo:[ARTErrorInfo createWithNSError:error.error]]; + (_fallbacks != nil) ? [self reconnectWithFallback] : [self transition:ARTRealtimeFailed withErrorInfo:[ARTErrorInfo createFromNSError:error.error]]; }]; return; } else if (_fallbacks && [self reconnectWithFallback]) { @@ -952,7 +1091,7 @@ - (void)realtimeTransportFailed:(id)transport withError:(A if (error.type == ARTRealtimeTransportErrorTypeNoInternet) { [self transition:ARTRealtimeDisconnected]; } else { - [self transition:ARTRealtimeFailed withErrorInfo:[ARTErrorInfo createWithNSError:error.error]]; + [self transition:ARTRealtimeFailed withErrorInfo:[ARTErrorInfo createFromNSError:error.error]]; } } diff --git a/Source/ARTRealtimeTransport.h b/Source/ARTRealtimeTransport.h index f3397cfdc..0da99049c 100644 --- a/Source/ARTRealtimeTransport.h +++ b/Source/ARTRealtimeTransport.h @@ -24,7 +24,6 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeTransportErrorType) { ARTRealtimeTransportErrorTypeNoInternet, ARTRealtimeTransportErrorTypeTimeout, ARTRealtimeTransportErrorTypeBadResponse, - ARTRealtimeTransportErrorTypeAuth, ARTRealtimeTransportErrorTypeOther }; @@ -76,9 +75,8 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeTransportState) { - (void)send:(ARTProtocolMessage *)msg; - (void)receive:(ARTProtocolMessage *)msg; -- (void)connect; -- (void)connectWithToken:(NSString *)token; //?! -- (void)connectForcingNewToken:(BOOL)forceNewToken; +- (void)connectWithKey:(NSString *)key; +- (void)connectWithToken:(NSString *)token; - (void)sendClose; - (void)sendPing; - (void)close; diff --git a/Source/ARTRealtimeTransport.m b/Source/ARTRealtimeTransport.m index bf6d85fee..c793b224a 100644 --- a/Source/ARTRealtimeTransport.m +++ b/Source/ARTRealtimeTransport.m @@ -49,8 +49,6 @@ + (NSString *)typeDescription:(ARTRealtimeTransportErrorType)type { return @"Timeout"; case ARTRealtimeTransportErrorTypeBadResponse: return @"BadResponse"; - case ARTRealtimeTransportErrorTypeAuth: - return @"Auth"; case ARTRealtimeTransportErrorTypeOther: return @"Other"; } diff --git a/Source/ARTRest.m b/Source/ARTRest.m index d90bc84c0..ae375289b 100644 --- a/Source/ARTRest.m +++ b/Source/ARTRest.m @@ -96,6 +96,23 @@ - (void)dealloc { [self.logger debug:__FILE__ line:__LINE__ message:@"RS:%p dealloc", self]; } +- (NSString *)description { + NSString *info; + if (self.options.token) { + info = [NSString stringWithFormat:@"token: %@", self.options.token]; + } + else if (self.options.authUrl) { + info = [NSString stringWithFormat:@"authUrl: %@", self.options.authUrl]; + } + else if (self.options.authCallback) { + info = [NSString stringWithFormat:@"authCallback: %@", self.options.authCallback]; + } + else { + info = [NSString stringWithFormat:@"key: %@", self.options.key]; + } + return [NSString stringWithFormat:@"%@ - \n\t %@;", [super description], info]; +} + - (void)executeRequest:(NSMutableURLRequest *)request withAuthOption:(ARTAuthentication)authOption completion:(void (^)(NSHTTPURLResponse *__art_nullable, NSData *__art_nullable, NSError *__art_nullable))callback { request.URL = [NSURL URLWithString:request.URL.relativeString relativeToURL:self.baseUrl]; diff --git a/Source/ARTRestChannel.m b/Source/ARTRestChannel.m index a4d67b66a..81344d017 100644 --- a/Source/ARTRestChannel.m +++ b/Source/ARTRestChannel.m @@ -132,7 +132,7 @@ - (void)internalPostMessages:(id)data callback:(void (^)(ARTErrorInfo *__art_nul [self.logger debug:__FILE__ line:__LINE__ message:@"RS:%p post message %@", _rest, [[NSString alloc] initWithData:encodedMessage encoding:NSUTF8StringEncoding]]; [_rest executeRequest:request withAuthOption:ARTAuthenticationOn completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { if (callback) { - ARTErrorInfo *errorInfo = error ? [ARTErrorInfo createWithNSError:error] : nil; + ARTErrorInfo *errorInfo = error ? [ARTErrorInfo createFromNSError:error] : nil; callback(errorInfo); } }]; diff --git a/Source/ARTStatus.h b/Source/ARTStatus.h index d023f2cbb..eff33287a 100644 --- a/Source/ARTStatus.h +++ b/Source/ARTStatus.h @@ -37,7 +37,9 @@ typedef NS_ENUM(NSUInteger, ARTState) { */ typedef CF_ENUM(NSUInteger, ARTCodeError) { // FIXME: check hard coded errors - ARTCodeErrorAPIKeyMissing = 80001 + ARTCodeErrorAPIKeyMissing = 80001, + ARTCodeErrorConnectionTimedOut = 80014, + ARTCodeErrorAuthConfiguredProviderFailure = 80019 }; ART_ASSUME_NONNULL_BEGIN @@ -64,7 +66,7 @@ FOUNDATION_EXPORT NSString *const ARTAblyMessageNoMeansToRenewToken; + (ARTErrorInfo *)createWithCode:(NSInteger)code message:(NSString *)message; + (ARTErrorInfo *)createWithCode:(NSInteger)code status:(NSInteger)status message:(NSString *)message; -+ (ARTErrorInfo *)createWithNSError:(NSError *)error; ++ (ARTErrorInfo *)createFromNSError:(NSError *)error; + (ARTErrorInfo *)wrap:(ARTErrorInfo *)error prepend:(NSString *)prepend; - (NSString *)description; diff --git a/Source/ARTStatus.m b/Source/ARTStatus.m index 9833be8e8..cc666d136 100644 --- a/Source/ARTStatus.m +++ b/Source/ARTStatus.m @@ -31,7 +31,7 @@ + (ARTErrorInfo *)createWithCode:(NSInteger)code status:(NSInteger)status messag return [[super alloc] initWithDomain:ARTAblyErrorDomain code:code userInfo:@{@"status": [NSNumber numberWithInteger:status], NSLocalizedDescriptionKey:message}]; } -+ (ARTErrorInfo *)createWithNSError:(NSError *)error { ++ (ARTErrorInfo *)createFromNSError:(NSError *)error { if ([error isKindOfClass:[ARTErrorInfo class]]) { return (ARTErrorInfo *)error; } diff --git a/Source/ARTTypes.h b/Source/ARTTypes.h index 6cfb1f7b5..7ef087f7e 100644 --- a/Source/ARTTypes.h +++ b/Source/ARTTypes.h @@ -125,4 +125,7 @@ NSString *generateNonce(); @interface NSDictionary (ARTJsonCompatible) @end +@interface NSURL (ARTLog) +@end + ART_ASSUME_NONNULL_END diff --git a/Source/ARTTypes.m b/Source/ARTTypes.m index d2a955b9d..a36261df9 100644 --- a/Source/ARTTypes.m +++ b/Source/ARTTypes.m @@ -128,3 +128,11 @@ - (NSDictionary *)toJSON:(NSError *__art_nullable *__art_nullable)error { } @end + +@implementation NSURL (ARTLog) + +- (NSString *)description { + return [NSString stringWithFormat:@"%@", self.absoluteString]; +} + +@end diff --git a/Source/ARTWebSocketTransport+Private.h b/Source/ARTWebSocketTransport+Private.h index ee60beb7d..4f1112555 100644 --- a/Source/ARTWebSocketTransport+Private.h +++ b/Source/ARTWebSocketTransport+Private.h @@ -24,7 +24,6 @@ ART_ASSUME_NONNULL_BEGIN // From RestClient @property (readwrite, strong, nonatomic) id encoder; @property (readonly, strong, nonatomic) ARTLog *logger; -@property (readonly, strong, nonatomic) ARTAuth *auth; @property (readonly, strong, nonatomic) ARTClientOptions *options; @property (readwrite, strong, nonatomic, art_nullable) SRWebSocket *websocket; diff --git a/Source/ARTWebSocketTransport.m b/Source/ARTWebSocketTransport.m index 017b81852..ed2187bb9 100644 --- a/Source/ARTWebSocketTransport.m +++ b/Source/ARTWebSocketTransport.m @@ -10,7 +10,6 @@ #import "ARTRest.h" #import "ARTRest+Private.h" -#import "ARTAuth+Private.h" #import "ARTProtocolMessage.h" #import "ARTClientOptions.h" #import "ARTTokenParams.h" @@ -19,6 +18,7 @@ #import "ARTEncoder.h" #import "ARTDefault.h" #import "ARTRealtimeTransport.h" +#import "ARTGCD.h" enum { ARTWsNeverConnected = -1, @@ -48,7 +48,6 @@ - (instancetype)initWithRest:(ARTRest *)rest options:(ARTClientOptions *)options _state = ARTRealtimeTransportStateClosed; _encoder = rest.defaultEncoder; _logger = rest.logger; - _auth = rest.auth; _options = [options copy]; _resumeKey = resumeKey; _connectionSerial = connectionSerial; @@ -85,60 +84,8 @@ - (void)receiveWithData:(NSData *)data { [self receive:pm]; } -- (void)connect { - [self connectForcingNewToken:false]; -} - -- (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]; - - if ([options isBasicAuth]) { - // Basic - [self connectWithKey:options.key]; - } - else { - // Token - [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p connecting with token auth; authorising", _delegate, self]; - - if (!forceNewToken && [self.auth tokenRemainsValid]) { - // Reuse token - [self connectWithToken:self.auth.tokenDetails.token]; - } - else { - // New 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]; - if (error.code == 40102 /*incompatible credentials*/) { - // RSA15c - [[weakSelf delegate] realtimeTransportFailed:weakSelf withError:[[ARTRealtimeTransportError alloc] initWithError:error type:ARTRealtimeTransportErrorTypeAuth url:self.websocketURL]]; - } - else { - // RSA4b - [[weakSelf delegate] realtimeTransportDisconnected:weakSelf withError:[[ARTRealtimeTransportError alloc] initWithError:error type:ARTRealtimeTransportErrorTypeAuth url:self.websocketURL]]; - } - return; - } - - [weakSelf connectWithToken:tokenDetails.token]; - }]; - } - @finally { - self.auth.delegate = delegate; - } - } - } -} - - (void)connectWithKey:(NSString *)key { + _state = ARTRealtimeTransportStateOpening; [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p websocket connect with key", _delegate, self]; NSURLQueryItem *keyParam = [NSURLQueryItem queryItemWithName:@"key" value:key]; [self setupWebSocket:@[keyParam] withOptions:self.options resumeKey:self.resumeKey connectionSerial:self.connectionSerial]; @@ -147,6 +94,7 @@ - (void)connectWithKey:(NSString *)key { } - (void)connectWithToken:(NSString *)token { + _state = ARTRealtimeTransportStateOpening; [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p websocket connect with token", _delegate, self]; NSURLQueryItem *accessTokenParam = [NSURLQueryItem queryItemWithName:@"accessToken" value:token]; [self setupWebSocket:@[accessTokenParam] withOptions:self.options resumeKey:self.resumeKey connectionSerial:self.connectionSerial]; diff --git a/Spec/Auth.swift b/Spec/Auth.swift index f9662a1d1..7ea03fb24 100644 --- a/Spec/Auth.swift +++ b/Spec/Auth.swift @@ -348,6 +348,327 @@ class Auth : QuickSpec { expect{ ARTRest(options: options) }.to(raiseException()) } } + + // RSA4c + context("if an attempt by the realtime client library to authenticate is made using the authUrl or authCallback") { + + context("the request to authUrl fails") { + + // RSA4c1 & RSA4c2 + it("if the connection is CONNECTING, then the connection attempt should be treated as unsuccessful") { + let options = AblyTests.clientOptions() + options.autoConnect = false + options.authUrl = NSURL(string: "http://echo.ably.io")! + let realtime = ARTRealtime(options: options) + defer { realtime.dispose(); realtime.close() } + + waitUntil(timeout: testTimeout) { done in + realtime.connection.once(.Disconnected) { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); done(); return + } + expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.Connecting)) + guard let errorInfo = stateChange.reason else { + fail("ErrorInfo is nil"); done(); return + } + expect(errorInfo.code) == 80019 + done() + } + realtime.connect() + } + + guard let errorInfo = realtime.connection.errorReason else { + fail("ErrorInfo is empty"); return + } + expect(errorInfo.code) == 80019 + expect(errorInfo.message).to(contain("body param is required")) + } + + // RSA4c3 + it("if the connection is CONNECTED, then the connection should remain CONNECTED") { + let token = getTestToken() + let options = AblyTests.clientOptions() + options.authUrl = NSURL(string: "http://echo.ably.io")! + options.authParams = [NSURLQueryItem]() + options.authParams?.append(NSURLQueryItem(name: "type", value: "text")) + options.authParams?.append(NSURLQueryItem(name: "body", value: token)) + + let realtime = ARTRealtime(options: options) + defer { realtime.dispose(); realtime.close() } + + waitUntil(timeout: testTimeout) { done in + realtime.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + } + + // Token reauth will fail + realtime.options.authParams = [NSURLQueryItem]() + + // Inject AUTH + let authMessage = ARTProtocolMessage() + authMessage.action = ARTProtocolMessageAction.Auth + realtime.transport.receive(authMessage) + + expect(realtime.connection.errorReason).toEventuallyNot(beNil(), timeout: testTimeout) + guard let errorInfo = realtime.connection.errorReason else { + fail("ErrorInfo is empty"); return + } + expect(errorInfo.code) == 80019 + expect(errorInfo.message).to(contain("body param is required")) + + expect(realtime.connection.state).to(equal(ARTRealtimeConnectionState.Connected)) + } + } + + context("the request to authCallback fails") { + + // RSA4c1 & RSA4c2 + it("if the connection is CONNECTING, then the connection attempt should be treated as unsuccessful") { + let options = AblyTests.clientOptions() + options.autoConnect = false + options.authCallback = { tokenParams, completion in + completion(nil, NSError(domain: NSURLErrorDomain, code: -1003, userInfo: [NSLocalizedDescriptionKey: "A server with the specified hostname could not be found."])) + } + let realtime = ARTRealtime(options: options) + defer { realtime.dispose(); realtime.close() } + + waitUntil(timeout: testTimeout) { done in + realtime.connection.once(.Disconnected) { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); done(); return + } + expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.Connecting)) + guard let errorInfo = stateChange.reason else { + fail("ErrorInfo is nil"); done(); return + } + expect(errorInfo.code) == 80019 + done() + } + realtime.connect() + } + + guard let errorInfo = realtime.connection.errorReason else { + fail("ErrorInfo is empty"); return + } + expect(errorInfo.code) == 80019 + expect(errorInfo.message).to(contain("hostname could not be found")) + + expect(realtime.connection.state).toEventually(equal(ARTRealtimeConnectionState.Disconnected), timeout: testTimeout) + } + + // RSA4c3 + it("if the connection is CONNECTED, then the connection should remain CONNECTED") { + let options = AblyTests.clientOptions() + options.authCallback = { tokenParams, completion in + completion(getTestTokenDetails(), nil) + } + let realtime = ARTRealtime(options: options) + defer { realtime.dispose(); realtime.close() } + + waitUntil(timeout: testTimeout) { done in + realtime.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + } + + // Token should renew and fail + realtime.options.authCallback = { tokenParams, completion in + completion(nil, NSError(domain: NSURLErrorDomain, code: -1003, userInfo: [NSLocalizedDescriptionKey: "A server with the specified hostname could not be found."])) + } + + // Inject AUTH + let authMessage = ARTProtocolMessage() + authMessage.action = ARTProtocolMessageAction.Auth + realtime.transport.receive(authMessage) + + expect(realtime.connection.errorReason).toEventuallyNot(beNil(), timeout: testTimeout) + guard let errorInfo = realtime.connection.errorReason else { + fail("ErrorInfo is empty"); return + } + expect(errorInfo.code) == 80019 + expect(errorInfo.message).to(contain("hostname could not be found")) + + expect(realtime.connection.state).to(equal(ARTRealtimeConnectionState.Connected)) + } + } + + context("the provided token is in an invalid format") { + + // RSA4c1 & RSA4c2 + it("if the connection is CONNECTING, then the connection attempt should be treated as unsuccessful") { + var options = AblyTests.clientOptions() + options.autoConnect = false + options.authUrl = NSURL(string: "http://echo.ably.io")! + options.authParams = [NSURLQueryItem]() + options.authParams?.append(NSURLQueryItem(name: "type", value: "json")) + let invalidTokenFormat = "{secret_token:xxx}" + options.authParams?.append(NSURLQueryItem(name: "body", value: invalidTokenFormat)) + + let realtime = ARTRealtime(options: options) + defer { realtime.dispose(); realtime.close() } + + waitUntil(timeout: testTimeout) { done in + realtime.connection.once(.Disconnected) { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); done(); return + } + expect(stateChange.previous).to(equal(ARTRealtimeConnectionState.Connecting)) + guard let errorInfo = stateChange.reason else { + fail("ErrorInfo is nil"); done(); return + } + expect(errorInfo.code) == 80019 + done() + } + realtime.connect() + } + + guard let errorInfo = realtime.connection.errorReason else { + fail("ErrorInfo is empty"); return + } + expect(errorInfo.code) == 80019 + expect(errorInfo.message).to(contain("content response cannot be used for token request")) + + expect(realtime.connection.state).toEventually(equal(ARTRealtimeConnectionState.Disconnected), timeout: testTimeout) + } + + // RSA4c3 + it("if the connection is CONNECTED, then the connection should remain CONNECTED") { + var options = AblyTests.clientOptions() + options.authUrl = NSURL(string: "http://echo.ably.io")! + options.authParams = [NSURLQueryItem]() + options.authParams?.append(NSURLQueryItem(name: "type", value: "text")) + + let token = getTestToken() + options.authParams?.append(NSURLQueryItem(name: "body", value: token)) + + let realtime = ARTRealtime(options: options) + defer { realtime.dispose(); realtime.close() } + + waitUntil(timeout: testTimeout) { done in + realtime.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + } + + // Token should renew and fail + let invalidToken = String(token.characters.reverse()) + realtime.options.authParams = [NSURLQueryItem]() + realtime.options.authParams?.append(NSURLQueryItem(name: "type", value: "json")) + let invalidTokenFormat = "{secret_token:xxx}" + realtime.options.authParams?.append(NSURLQueryItem(name: "body", value: invalidTokenFormat)) + + realtime.connection.on() { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange should not be nil"); return + } + if stateChange.current != .Connected { + fail("Connection should remain connected") + } + } + + // Inject AUTH + let authMessage = ARTProtocolMessage() + authMessage.action = ARTProtocolMessageAction.Auth + realtime.transport.receive(authMessage) + + expect(realtime.connection.errorReason).toEventuallyNot(beNil(), timeout: testTimeout) + guard let errorInfo = realtime.connection.errorReason else { + fail("ErrorInfo is empty"); return + } + expect(errorInfo.code) == 80019 + expect(errorInfo.message).to(contain("content response cannot be used for token request")) + + expect(realtime.connection.state).to(equal(ARTRealtimeConnectionState.Connected)) + } + } + + context("the attempt times out after realtimeRequestTimeout") { + // RSA4c1 & RSA4c2 + it("if the connection is CONNECTING, then the connection attempt should be treated as unsuccessful") { + let previousRealtimeRequestTimeout = ARTDefault.realtimeRequestTimeout() + defer { ARTDefault.setRealtimeRequestTimeout(previousRealtimeRequestTimeout) } + ARTDefault.setRealtimeRequestTimeout(0.5) + + let options = AblyTests.clientOptions() + options.autoConnect = false + options.authCallback = { tokenParams, completion in + // Ignore `completion` closure to force a time out + } + + let realtime = ARTRealtime(options: options) + defer { realtime.dispose(); realtime.close() } + + waitUntil(timeout: testTimeout) { done in + realtime.connection.once(.Disconnected) { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); done(); return + } + guard let errorInfo = stateChange.reason else { + fail("ErrorInfo is nil"); done(); return + } + expect(errorInfo.code) == 80019 + done() + } + realtime.connect() + } + + guard let errorInfo = realtime.connection.errorReason else { + fail("ErrorInfo is empty"); return + } + expect(errorInfo.code) == 80019 + expect(errorInfo.message).to(contain("timed out")) + + expect(realtime.connection.state).toEventually(equal(ARTRealtimeConnectionState.Disconnected), timeout: testTimeout) + } + + // RSA4c3 + it("if the connection is CONNECTED, then the connection should remain CONNECTED") { + let options = AblyTests.clientOptions() + options.autoConnect = false + options.authCallback = { tokenParams, completion in + completion(getTestTokenDetails(), nil) + } + + let realtime = ARTRealtime(options: options) + defer { realtime.dispose(); realtime.close() } + + waitUntil(timeout: testTimeout) { done in + realtime.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + realtime.connect() + } + + let previousRealtimeRequestTimeout = ARTDefault.realtimeRequestTimeout() + defer { ARTDefault.setRealtimeRequestTimeout(previousRealtimeRequestTimeout) } + ARTDefault.setRealtimeRequestTimeout(0.5) + + // Token should renew and fail + realtime.options.authCallback = { tokenParams, completion in + // Ignore `completion` closure to force a time out + } + + // Inject AUTH + let authMessage = ARTProtocolMessage() + authMessage.action = ARTProtocolMessageAction.Auth + realtime.transport.receive(authMessage) + + expect(realtime.connection.errorReason).toEventuallyNot(beNil(), timeout: testTimeout) + guard let errorInfo = realtime.connection.errorReason else { + fail("ErrorInfo is empty"); return + } + expect(errorInfo.code) == 80019 + expect(errorInfo.message).to(contain("timed out")) + + expect(realtime.connection.state).to(equal(ARTRealtimeConnectionState.Connected)) + } + } + } } // RSA15 @@ -623,7 +944,7 @@ class Auth : QuickSpec { let options = AblyTests.commonAppSetup() options.autoConnect = false let realtime = AblyTests.newRealtime(options) - defer { realtime.close() } + defer { realtime.dispose(); realtime.close() } expect(realtime.auth.clientId).to(beNil()) waitUntil(timeout: testTimeout) { done in @@ -651,7 +972,7 @@ class Auth : QuickSpec { options.autoConnect = false options.token = getTestToken(clientId: "tester") let realtime = ARTRealtime(options: options) - defer { realtime.close() } + defer { realtime.dispose(); realtime.close() } expect(realtime.auth.clientId).to(beNil()) waitUntil(timeout: testTimeout) { done in @@ -717,7 +1038,7 @@ class Auth : QuickSpec { expect(options.clientId).to(beNil()) options.autoConnect = false let realtime = AblyTests.newRealtime(options) - defer { realtime.close() } + defer { realtime.dispose(); realtime.close() } waitUntil(timeout: testTimeout) { done in realtime.connection.once(.Connected) { stateChange in @@ -738,7 +1059,7 @@ class Auth : QuickSpec { let options = AblyTests.clientOptions() options.token = getTestToken(clientId: "*") let realtime = ARTRealtime(options: options) - defer { realtime.close() } + defer { realtime.dispose(); realtime.close() } waitUntil(timeout: testTimeout) { done in realtime.connection.on(.Connected) { _ in expect(realtime.auth.clientId).to(equal("*")) diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index c068d222b..472daf553 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -754,7 +754,7 @@ class RealtimeClient: QuickSpec { client.setTransportClass(TestProxyTransport.self) var connections = 0 - let hook1 = TestProxyTransport.testSuite_injectIntoClassMethod(#selector(TestProxyTransport.connectWithToken)) { + let hook1 = TestProxyTransport.testSuite_injectIntoClassMethod(#selector(TestProxyTransport.connectWithToken(_:))) { connections += 1 } defer { hook1?.remove() } diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index 64f765a60..0f4a5cd30 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -2614,9 +2614,13 @@ class RealtimeClientConnection: QuickSpec { defer { TestProxyTransport.network = nil } var urlConnections = [NSURL]() - TestProxyTransport.networkConnectEvent = { url in + TestProxyTransport.networkConnectEvent = { transport, url in + if client.transport !== transport { + return + } urlConnections.append(url) } + defer { TestProxyTransport.networkConnectEvent = nil } client.connect() defer { client.dispose(); client.close() } @@ -2643,12 +2647,16 @@ class RealtimeClientConnection: QuickSpec { defer { TestProxyTransport.network = nil } var urlConnections = [NSURL]() - TestProxyTransport.networkConnectEvent = { url in + TestProxyTransport.networkConnectEvent = { transport, url in + if client.transport !== transport { + return + } urlConnections.append(url) if urlConnections.count == 1 { TestProxyTransport.network = nil } } + defer { TestProxyTransport.networkConnectEvent = nil } client.connect() defer { client.dispose(); client.close() } @@ -2679,15 +2687,19 @@ class RealtimeClientConnection: QuickSpec { defer { TestProxyTransport.network = nil } var urlConnections = [NSURL]() - TestProxyTransport.networkConnectEvent = { url in + TestProxyTransport.networkConnectEvent = { transport, url in + if client.transport !== transport { + return + } urlConnections.append(url) if urlConnections.count == 1 { TestProxyTransport.network = nil } } + defer { TestProxyTransport.networkConnectEvent = nil } client.connect() - defer { client.close() } + defer { client.dispose(); client.close() } waitUntil(timeout: testTimeout) { done in channel.publish(nil, data: "message") { error in @@ -2718,12 +2730,16 @@ class RealtimeClientConnection: QuickSpec { defer { TestProxyTransport.network = nil } var urlConnections = [NSURL]() - TestProxyTransport.networkConnectEvent = { url in + TestProxyTransport.networkConnectEvent = { transport, url in + if client.transport !== transport { + return + } urlConnections.append(url) if urlConnections.count == 1 { TestProxyTransport.network = nil } } + defer { TestProxyTransport.networkConnectEvent = nil } client.connect() defer { client.dispose(); client.close() } @@ -2773,9 +2789,13 @@ class RealtimeClientConnection: QuickSpec { defer { TestProxyTransport.network = nil } var urlConnections = [NSURL]() - TestProxyTransport.networkConnectEvent = { url in + TestProxyTransport.networkConnectEvent = { transport, url in + if client.transport !== transport { + return + } urlConnections.append(url) } + defer { TestProxyTransport.networkConnectEvent = nil } client.connect() defer { client.dispose(); client.close() } @@ -2802,10 +2822,14 @@ class RealtimeClientConnection: QuickSpec { defer { TestProxyTransport.network = nil } var urlConnections = [NSURL]() - TestProxyTransport.networkConnectEvent = { url in + TestProxyTransport.networkConnectEvent = { transport, url in + if client.transport !== transport { + return + } urlConnections.append(url) TestProxyTransport.network = nil } + defer { TestProxyTransport.networkConnectEvent = nil } client.connect() defer { client.dispose(); client.close() } @@ -2845,9 +2869,13 @@ class RealtimeClientConnection: QuickSpec { defer { TestProxyTransport.network = nil } var urlConnections = [NSURL]() - TestProxyTransport.networkConnectEvent = { url in + TestProxyTransport.networkConnectEvent = { transport, url in + if client.transport !== transport { + return + } urlConnections.append(url) } + defer { TestProxyTransport.networkConnectEvent = nil } client.connect() defer { client.dispose(); client.close() } @@ -2887,12 +2915,16 @@ class RealtimeClientConnection: QuickSpec { defer { TestProxyTransport.network = nil } var urlConnections = [NSURL]() - TestProxyTransport.networkConnectEvent = { url in + TestProxyTransport.networkConnectEvent = { transport, url in + if client.transport !== transport { + return + } urlConnections.append(url) } + defer { TestProxyTransport.networkConnectEvent = nil } client.connect() - defer { client.close() } + defer { client.dispose(); client.close() } waitUntil(timeout: testTimeout) { done in channel.publish(nil, data: "message") { error in @@ -2927,12 +2959,16 @@ class RealtimeClientConnection: QuickSpec { defer { TestProxyTransport.network = nil } var urlConnections = [NSURL]() - TestProxyTransport.networkConnectEvent = { url in + TestProxyTransport.networkConnectEvent = { transport, url in + if client.transport !== transport { + return + } urlConnections.append(url) } + defer { TestProxyTransport.networkConnectEvent = nil } client.connect() - defer { client.close() } + defer { client.dispose(); client.close() } waitUntil(timeout: testTimeout) { done in channel.publish(nil, data: "message") { error in @@ -2958,13 +2994,17 @@ class RealtimeClientConnection: QuickSpec { defer { TestProxyTransport.network = nil } var urlConnections = [NSURL]() - TestProxyTransport.networkConnectEvent = { url in + TestProxyTransport.networkConnectEvent = { transport, url in + if client.transport !== transport { + return + } urlConnections.append(url) if urlConnections.count == 2 { TestProxyTransport.network = nil (client.transport as! TestProxyTransport).simulateTransportSuccess() } } + defer { TestProxyTransport.networkConnectEvent = nil } client.connect() // Because we're faking the CONNECTED state, we can't client.close() or it @@ -3583,6 +3623,51 @@ class RealtimeClientConnection: QuickSpec { } } } + + it("should abort reconnection with new token if the server has requested it to authorise and after it the connection has been closed") { + let options = AblyTests.commonAppSetup() + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + } + + client.auth.options.authCallback = { tokenParams, completion in + getTestTokenDetails(ttl: 0.1) { tokenDetails, error in + expect(error).to(beNil()) + guard let tokenDetails = tokenDetails else { + fail("TokenDetails is nil"); return + } + // Let the token expire + delay(0.1) { + completion(tokenDetails.token, nil) + } + } + } + + let authMessage = ARTProtocolMessage() + authMessage.action = .Auth + client.transport.receive(authMessage) + + client.close() + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Failed) { _ in + fail("Should not receive error 40142") + } + client.connection.once(.Connected) { _ in + fail("Should not connect") + } + client.connection.once(.Closed) { _ in + done() + } + } + } + } } } diff --git a/Spec/RestClientChannel.swift b/Spec/RestClientChannel.swift index 3b9c040de..aa58f6de9 100644 --- a/Spec/RestClientChannel.swift +++ b/Spec/RestClientChannel.swift @@ -32,7 +32,7 @@ class RestClientChannel: QuickSpec { // RSL1b context("with name and data arguments") { it("publishes the message and invokes callback with success") { - var publishError: ARTErrorInfo? = ARTErrorInfo.createWithNSError(NSError(domain: "", code: -1, userInfo: nil)) + var publishError: ARTErrorInfo? = ARTErrorInfo.createFromNSError(NSError(domain: "", code: -1, userInfo: nil)) var publishedMessage: ARTMessage? channel.publish(name, data: data) { error in @@ -51,7 +51,7 @@ class RestClientChannel: QuickSpec { // RSL1b, RSL1e context("with name only") { it("publishes the message and invokes callback with success") { - var publishError: ARTErrorInfo? = ARTErrorInfo.createWithNSError(NSError(domain: "", code: -1, userInfo: nil)) + var publishError: ARTErrorInfo? = ARTErrorInfo.createFromNSError(NSError(domain: "", code: -1, userInfo: nil)) var publishedMessage: ARTMessage? channel.publish(name, data: nil) { error in @@ -70,7 +70,7 @@ class RestClientChannel: QuickSpec { // RSL1b, RSL1e context("with data only") { it("publishes the message and invokes callback with success") { - var publishError: ARTErrorInfo? = ARTErrorInfo.createWithNSError(NSError(domain: "", code: -1, userInfo: nil)) + var publishError: ARTErrorInfo? = ARTErrorInfo.createFromNSError(NSError(domain: "", code: -1, userInfo: nil)) var publishedMessage: ARTMessage? channel.publish(nil, data: data) { error in @@ -89,7 +89,7 @@ class RestClientChannel: QuickSpec { // RSL1b, RSL1e context("with neither name nor data") { it("publishes the message and invokes callback with success") { - var publishError: ARTErrorInfo? = ARTErrorInfo.createWithNSError(NSError(domain: "", code: -1, userInfo: nil)) + var publishError: ARTErrorInfo? = ARTErrorInfo.createFromNSError(NSError(domain: "", code: -1, userInfo: nil)) var publishedMessage: ARTMessage? channel.publish(nil, data: nil) { error in @@ -107,7 +107,7 @@ class RestClientChannel: QuickSpec { context("with a Message object") { it("publishes the message and invokes callback with success") { - var publishError: ARTErrorInfo? = ARTErrorInfo.createWithNSError(NSError(domain: "", code: -1, userInfo: nil)) + var publishError: ARTErrorInfo? = ARTErrorInfo.createFromNSError(NSError(domain: "", code: -1, userInfo: nil)) var publishedMessage: ARTMessage? channel.publish([ARTMessage(name: name, data: data)]) { error in @@ -130,7 +130,7 @@ class RestClientChannel: QuickSpec { defer { client.httpExecutor = oldExecutor} client.httpExecutor = testHTTPExecutor - var publishError: ARTErrorInfo? = ARTErrorInfo.createWithNSError(NSError(domain: "", code: -1, userInfo: nil)) + var publishError: ARTErrorInfo? = ARTErrorInfo.createFromNSError(NSError(domain: "", code: -1, userInfo: nil)) var publishedMessages: [ARTMessage] = [] let messages = [ diff --git a/Spec/TestUtilities.swift b/Spec/TestUtilities.swift index 7b53024cb..665b7af2a 100644 --- a/Spec/TestUtilities.swift +++ b/Spec/TestUtilities.swift @@ -324,7 +324,7 @@ func ==(lhs: ARTAuthOptions, rhs: ARTAuthOptions) -> Bool { class PublishTestMessage { var completion: Optional<(ARTErrorInfo?)->()> - var error: ARTErrorInfo? = ARTErrorInfo.createWithNSError(NSError(domain: "", code: -1, userInfo: nil)) + var error: ARTErrorInfo? = ARTErrorInfo.createFromNSError(NSError(domain: "", code: -1, userInfo: nil)) init(client: ARTRest, failOnError: Bool = true, completion: Optional<(ARTErrorInfo?)->()> = nil) { client.channels.get("test").publish(nil, data: "message") { error in @@ -408,7 +408,7 @@ func getTestToken(key key: String? = nil, clientId: String? = nil, capability: S } /// Access TokenDetails -func getTestTokenDetails(key key: String? = nil, clientId: String? = nil, capability: String? = nil, ttl: NSTimeInterval? = nil) -> ARTTokenDetails? { +func getTestTokenDetails(key key: String? = nil, clientId: String? = nil, capability: String? = nil, ttl: NSTimeInterval? = nil, completion: (ARTTokenDetails?, NSError?) -> Void) { let options: ARTClientOptions if let key = key { options = AblyTests.clientOptions() @@ -420,9 +420,6 @@ func getTestTokenDetails(key key: String? = nil, clientId: String? = nil, capabi let client = ARTRest(options: options) - var tokenDetails: ARTTokenDetails? - var error: NSError? - var tokenParams: ARTTokenParams? = nil if let capability = capability { tokenParams = ARTTokenParams() @@ -437,7 +434,14 @@ func getTestTokenDetails(key key: String? = nil, clientId: String? = nil, capabi tokenParams!.clientId = clientId } - client.auth.requestToken(tokenParams, withOptions: nil) { _tokenDetails, _error in + client.auth.requestToken(tokenParams, withOptions: nil, callback: completion) +} + +func getTestTokenDetails(key key: String? = nil, clientId: String? = nil, capability: String? = nil, ttl: NSTimeInterval? = nil) -> ARTTokenDetails? { + var tokenDetails: ARTTokenDetails? + var error: NSError? + + getTestTokenDetails(key: key, clientId: clientId, capability: capability, ttl: ttl) { _tokenDetails, _error in tokenDetails = _tokenDetails error = _error } @@ -675,9 +679,49 @@ class TestProxyTransport: ARTWebSocketTransport { var ignoreSends = false static var network: NetworkAnswer? = nil - static var networkConnectEvent: Optional<(NSURL)->()> = nil + static var networkConnectEvent: Optional<(ARTRealtimeTransport, NSURL)->()> = nil + + override func connectWithKey(key: String) { + if let network = TestProxyTransport.network { + var hook: AspectToken? + hook = SRWebSocket.testSuite_replaceClassMethod(#selector(SRWebSocket.open)) { + if TestProxyTransport.network == nil { + return + } + func performConnectError(secondsForDelay: NSTimeInterval, error: ARTRealtimeTransportError) { + delay(secondsForDelay) { + self.delegate?.realtimeTransportFailed(self, withError: error) + hook?.remove() + } + } + let error = NSError.init(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "TestProxyTransport error"]) + switch network { + case .NoInternet, .HostUnreachable: + performConnectError(0.1, error: ARTRealtimeTransportError.init(error: error, type: .HostUnreachable, url: self.lastUrl!)) + case .RequestTimeout(let timeout): + performConnectError(0.1 + timeout, error: ARTRealtimeTransportError.init(error: error, type: .Timeout, url: self.lastUrl!)) + case .HostInternalError(let code): + performConnectError(0.1, error: ARTRealtimeTransportError.init(error: error, badResponseCode: code, url: self.lastUrl!)) + case .Host400BadRequest: + performConnectError(0.1, error: ARTRealtimeTransportError.init(error: error, badResponseCode: 400, url: self.lastUrl!)) + } + } + } + super.connectWithKey(key) + + if let performNetworkConnect = TestProxyTransport.networkConnectEvent { + func perform() { + if let lastUrl = self.lastUrl { + performNetworkConnect(self, lastUrl) + } else { + delay(0.1) { perform() } + } + } + perform() + } + } - override func connect() { + override func connectWithToken(token: String) { if let network = TestProxyTransport.network { var hook: AspectToken? hook = SRWebSocket.testSuite_replaceClassMethod(#selector(SRWebSocket.open)) { @@ -703,12 +747,12 @@ class TestProxyTransport: ARTWebSocketTransport { } } } - super.connect() + super.connectWithToken(token) if let performNetworkConnect = TestProxyTransport.networkConnectEvent { func perform() { if let lastUrl = self.lastUrl { - performNetworkConnect(lastUrl) + performNetworkConnect(self, lastUrl) } else { delay(0.1) { perform() } } From 3532a3253e50c12013ddb895c83c85769d147a62 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 14 Dec 2016 08:56:57 +0000 Subject: [PATCH 16/43] RTN22 (#537) * RTN22 * RTN22a * Fix: realtime transport can be nil * Fix: realtime should renew token by transitioning to CONNECTING --- Source/ARTRealtime+Private.h | 2 +- Source/ARTRealtime.m | 12 +-- Spec/Auth.swift | 12 +-- Spec/RealtimeClientChannel.swift | 3 +- Spec/RealtimeClientConnection.swift | 137 +++++++++++++++++++++++++++- 5 files changed, 145 insertions(+), 21 deletions(-) diff --git a/Source/ARTRealtime+Private.h b/Source/ARTRealtime+Private.h index f7e308e10..c5ee12235 100644 --- a/Source/ARTRealtime+Private.h +++ b/Source/ARTRealtime+Private.h @@ -45,7 +45,7 @@ ART_ASSUME_NONNULL_BEGIN @interface ARTRealtime () @property (readwrite, strong, nonatomic) ARTRest *rest; -@property (readonly, getter=getTransport) id transport; +@property (readonly, getter=getTransport, art_nullable) id transport; @property (readonly, strong, nonatomic, art_nonnull) id reachability; @property (readonly, getter=getLogger) ARTLog *logger; diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index b96c46523..46549bf3e 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -306,7 +306,7 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { } _transport = [[_transportClass alloc] initWithRest:self.rest options:self.options resumeKey:resumeKey connectionSerial:connectionSerial]; _transport.delegate = self; - [self transportConnect]; + [self transportConnectForcingNewToken:_renewingToken]; } if (self.connection.state != ARTRealtimeFailed && self.connection.state != ARTRealtimeClosed && self.connection.state != ARTRealtimeDisconnected) { @@ -530,9 +530,10 @@ - (void)onDisconnected:(ARTProtocolMessage *)message { [self.logger info:@"R:%p ARTRealtime disconnected", self]; ARTErrorInfo *error = message.error; if ([self shouldRenewToken:&error]) { - [self transportReconnectWithRenewedToken]; [self transition:ARTRealtimeDisconnected withErrorInfo:error]; [self.connection setErrorReason:nil]; + _renewingToken = true; + [self transition:ARTRealtimeConnecting withErrorInfo:nil]; return; } [self transition:ARTRealtimeDisconnected withErrorInfo:error]; @@ -558,6 +559,7 @@ - (void)onAuth { switch (self.connection.state) { case ARTRealtimeConnecting: case ARTRealtimeConnected: + _resuming = true; [self transportReconnectWithRenewedToken]; break; default: @@ -612,7 +614,7 @@ - (BOOL)shouldRenewToken:(ARTErrorInfo **)errorPtr { - (void)transportReconnectWithHost:(NSString *)host { [self.transport setHost:host]; - [self transportConnect]; + [self transportConnectForcingNewToken:false]; } - (void)transportReconnectWithRenewedToken { @@ -620,10 +622,6 @@ - (void)transportReconnectWithRenewedToken { [self transportConnectForcingNewToken:true]; } -- (void)transportConnect { - [self transportConnectForcingNewToken:false]; -} - - (void)transportConnectForcingNewToken:(BOOL)forceNewToken { ARTClientOptions *options = [self.options copy]; if ([options isBasicAuth]) { diff --git a/Spec/Auth.swift b/Spec/Auth.swift index 7ea03fb24..f67c06496 100644 --- a/Spec/Auth.swift +++ b/Spec/Auth.swift @@ -409,7 +409,7 @@ class Auth : QuickSpec { // Inject AUTH let authMessage = ARTProtocolMessage() authMessage.action = ARTProtocolMessageAction.Auth - realtime.transport.receive(authMessage) + realtime.transport?.receive(authMessage) expect(realtime.connection.errorReason).toEventuallyNot(beNil(), timeout: testTimeout) guard let errorInfo = realtime.connection.errorReason else { @@ -482,7 +482,7 @@ class Auth : QuickSpec { // Inject AUTH let authMessage = ARTProtocolMessage() authMessage.action = ARTProtocolMessageAction.Auth - realtime.transport.receive(authMessage) + realtime.transport?.receive(authMessage) expect(realtime.connection.errorReason).toEventuallyNot(beNil(), timeout: testTimeout) guard let errorInfo = realtime.connection.errorReason else { @@ -573,7 +573,7 @@ class Auth : QuickSpec { // Inject AUTH let authMessage = ARTProtocolMessage() authMessage.action = ARTProtocolMessageAction.Auth - realtime.transport.receive(authMessage) + realtime.transport?.receive(authMessage) expect(realtime.connection.errorReason).toEventuallyNot(beNil(), timeout: testTimeout) guard let errorInfo = realtime.connection.errorReason else { @@ -656,7 +656,7 @@ class Auth : QuickSpec { // Inject AUTH let authMessage = ARTProtocolMessage() authMessage.action = ARTProtocolMessageAction.Auth - realtime.transport.receive(authMessage) + realtime.transport?.receive(authMessage) expect(realtime.connection.errorReason).toEventuallyNot(beNil(), timeout: testTimeout) guard let errorInfo = realtime.connection.errorReason else { @@ -714,9 +714,7 @@ class Auth : QuickSpec { options.autoConnect = false let client = ARTRealtime(options: options) - defer { - client.close() - } + defer { client.dispose(); client.close() } client.setTransportClass(TestProxyTransport.self) client.connect() diff --git a/Spec/RealtimeClientChannel.swift b/Spec/RealtimeClientChannel.swift index dd75060a6..9e49e0a58 100644 --- a/Spec/RealtimeClientChannel.swift +++ b/Spec/RealtimeClientChannel.swift @@ -2430,8 +2430,7 @@ class RealtimeClientChannel: QuickSpec { protoMsg.action = .Detach protoMsg.error = ARTErrorInfo.createWithCode(123, message: "test error") protoMsg.channel = "test" - - client.realtimeTransport(client.transport, didReceiveMessage: protoMsg) + client.transport?.receive(protoMsg) expect(channel.state).to(equal(ARTRealtimeChannelState.Detached)) expect(channel.errorReason).to(equal(protoMsg.error)) diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index 0f4a5cd30..fb9fe517c 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -2359,7 +2359,6 @@ class RealtimeClientConnection: QuickSpec { } waitUntil(timeout: testTimeout) { done in - // Wait for token to expire client.connection.once(.Connected) { stateChange in expect(stateChange?.reason).to(beNil()) done() @@ -3401,6 +3400,137 @@ class RealtimeClientConnection: QuickSpec { } } + // RTN22 + it("Ably can request that a connected client re-authenticates by sending the client an AUTH ProtocolMessage") { + let options = AblyTests.commonAppSetup() + options.useTokenAuth = true + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + channel.attach { error in + expect(error).to(beNil()) + done() + } + } + + guard let initialConnectionId = client.connection.id else { + fail("ConnectionId is nil"); return + } + + guard let initialToken = client.auth.tokenDetails?.token else { + fail("Initial token is nil"); return + } + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + expect(initialToken).toNot(equal(client.auth.tokenDetails?.token)) + done() + } + + let authMessage = ARTProtocolMessage() + authMessage.action = .Auth + client.transport?.receive(authMessage) + } + + expect(client.connection.id).to(equal(initialConnectionId)) + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + let expectedMessage = ARTMessage(name: "ios", data: "message1") + + channel.subscribe() { message in + expect(message.name).to(equal(expectedMessage.name)) + expect(message.data as? String).to(equal(expectedMessage.data as? String)) + partialDone() + } + + let rest = ARTRest(options: AblyTests.clientOptions(key: options.key!)) + rest.channels.get("foo").publish([expectedMessage]) { error in + expect(error).to(beNil()) + partialDone() + } + } + + channel.off() + } + + // RTN22a + it("re-authenticate and resume the connection when the client is forcibly disconnected following a DISCONNECTED message containing an error code in the range 40140 <= code < 40150") { + let options = AblyTests.commonAppSetup() + options.token = getTestToken(key: options.key!, ttl: 5.0) + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + channel.attach { error in + expect(error).to(beNil()) + done() + } + } + + guard let initialConnectionId = client.connection.id else { + fail("ConnectionId is nil"); return + } + + guard let initialToken = client.auth.tokenDetails?.token else { + fail("Initial token is nil"); return + } + + channel.once(.Detached) { _ in + fail("Should not detach channels") + } + + var authorizeMethodCallCount = 0 + let hook = client.auth.testSuite_injectIntoMethodAfter(#selector(client.auth.authorize)) { + authorizeMethodCallCount += 1 + } + defer { hook.remove() } + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Disconnected) { stateChange in + guard let error = stateChange?.reason else { + fail("Error is nil"); done(); return + } + expect(error.code) == 40142 + done() + } + } + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + expect(initialToken).toNot(equal(client.auth.tokenDetails?.token)) + done() + } + } + + expect(client.connection.id).to(equal(initialConnectionId)) + expect(authorizeMethodCallCount) == 1 + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + let expectedMessage = ARTMessage(name: "ios", data: "message1") + + channel.subscribe() { message in + expect(message.name).to(equal(expectedMessage.name)) + expect(message.data as? String).to(equal(expectedMessage.data as? String)) + partialDone() + } + + let rest = ARTRest(options: AblyTests.clientOptions(key: options.key!)) + rest.channels.get("foo").publish([expectedMessage]) { error in + expect(error).to(beNil()) + partialDone() + } + } + + channel.off() + } + } // https://github.com/ably/ably-ios/issues/454 @@ -3417,8 +3547,7 @@ class RealtimeClientConnection: QuickSpec { let protoMsg = ARTProtocolMessage() protoMsg.action = .Disconnect protoMsg.error = ARTErrorInfo.createWithCode(123, message: "test error") - - client.realtimeTransport(client.transport, didReceiveMessage: protoMsg) + client.transport?.receive(protoMsg) expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Disconnected)) expect(client.connection.errorReason).to(equal(protoMsg.error)) @@ -3651,7 +3780,7 @@ class RealtimeClientConnection: QuickSpec { let authMessage = ARTProtocolMessage() authMessage.action = .Auth - client.transport.receive(authMessage) + client.transport?.receive(authMessage) client.close() From 4262a856070137ea914a9f137f139777c274def4 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 14 Dec 2016 12:29:08 +0000 Subject: [PATCH 17/43] Update RTL3 for 0.9 (#544) * Update RTL3a * Update RTL3b and RTL3c * RTL3d * RTL3e --- Spec/RealtimeClientChannel.swift | 206 +++++++++++++++++++++++++------ 1 file changed, 166 insertions(+), 40 deletions(-) diff --git a/Spec/RealtimeClientChannel.swift b/Spec/RealtimeClientChannel.swift index 9e49e0a58..98f59c177 100644 --- a/Spec/RealtimeClientChannel.swift +++ b/Spec/RealtimeClientChannel.swift @@ -193,19 +193,32 @@ class RealtimeClientChannel: QuickSpec { expect(channel.state).to(equal(ARTRealtimeChannelState.Attaching)) waitUntil(timeout: testTimeout) { done in - let error = AblyTests.newErrorProtocolMessage() - channel.on { errorInfo in - if channel.state == .Failed { - guard let errorInfo = errorInfo else { - fail("errorInfo is nil"); done(); return - } - expect(errorInfo).to(equal(error.error)) - expect(channel.errorReason).to(equal(errorInfo)) - done() + let pmError = AblyTests.newErrorProtocolMessage() + + channel.on { stateChange in + let partialDone = AblyTests.splitDone(2, done: done) + + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); partialDone(); return + } + guard let error = stateChange.reason else { + fail("Reason error is nil"); partialDone(); return + } + + if stateChange.current == .Failed { + expect(error).to(equal(pmError.error)) + expect(channel.errorReason).to(beIdenticalTo(error)) + partialDone() + } + else if stateChange.current == .Error { + expect(error).to(equal(pmError.error)) + partialDone() } } - client.onError(error) + + client.onError(pmError) } + expect(channel.state).to(equal(ARTRealtimeChannelState.Failed)) } @@ -218,26 +231,39 @@ class RealtimeClientChannel: QuickSpec { expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) waitUntil(timeout: testTimeout) { done in - let error = AblyTests.newErrorProtocolMessage() - channel.on { errorInfo in - if channel.state == .Failed { - guard let errorInfo = errorInfo else { - fail("errorInfo is nil"); done(); return - } - expect(errorInfo).to(equal(error.error)) - expect(channel.errorReason).to(equal(errorInfo)) - done() + let pmError = AblyTests.newErrorProtocolMessage() + + channel.on { stateChange in + let partialDone = AblyTests.splitDone(2, done: done) + + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); partialDone(); return + } + guard let error = stateChange.reason else { + fail("Reason error is nil"); partialDone(); return + } + + if stateChange.current == .Failed { + expect(error).to(equal(pmError.error)) + expect(channel.errorReason).to(beIdenticalTo(error)) + partialDone() + } + else if stateChange.current == .Error { + expect(error).to(equal(pmError.error)) + partialDone() } } - client.onError(error) + + client.onError(pmError) } + expect(channel.state).to(equal(ARTRealtimeChannelState.Failed)) } } // RTL3b - context("changes to SUSPENDED") { + context("changes to CLOSED") { it("ATTACHING channel should transition to DETACHED") { let options = AblyTests.commonAppSetup() @@ -246,16 +272,18 @@ class RealtimeClientChannel: QuickSpec { client.setTransportClass(TestProxyTransport.self) client.connect() defer { client.dispose(); client.close() } + expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) let channel = client.channels.get("test") channel.attach() let transport = client.transport as! TestProxyTransport transport.actionsIgnored += [.Attached] - expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) expect(channel.state).to(equal(ARTRealtimeChannelState.Attaching)) - client.onSuspended() - expect(channel.state).to(equal(ARTRealtimeChannelState.Detached)) + client.close() + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Closing)) + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Detached), timeout: testTimeout) + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Closed)) } it("ATTACHED channel should transition to DETACHED") { @@ -265,54 +293,152 @@ class RealtimeClientChannel: QuickSpec { let channel = client.channels.get("test") channel.attach() + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) - client.onSuspended() - expect(channel.state).to(equal(ARTRealtimeChannelState.Detached)) + client.close() + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Closing)) + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Detached), timeout: testTimeout) + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Closed)) } } - // RTL3b - context("changes to CLOSED") { + // RTL3c + context("changes to SUSPENDED") { - it("ATTACHING channel should transition to DETACHED") { + it("ATTACHING channel should transition to SUSPENDED") { let options = AblyTests.commonAppSetup() options.autoConnect = false let client = ARTRealtime(options: options) client.setTransportClass(TestProxyTransport.self) client.connect() defer { client.dispose(); client.close() } - expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) let channel = client.channels.get("test") channel.attach() let transport = client.transport as! TestProxyTransport transport.actionsIgnored += [.Attached] + expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) expect(channel.state).to(equal(ARTRealtimeChannelState.Attaching)) - client.close() - expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Closing)) - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Detached), timeout: testTimeout) - expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Closed)) + client.onSuspended() + expect(channel.state).to(equal(ARTRealtimeChannelState.Suspended)) } - it("ATTACHED channel should transition to DETACHED") { + it("ATTACHED channel should transition to SUSPENDED") { let options = AblyTests.commonAppSetup() let client = ARTRealtime(options: options) defer { client.dispose(); client.close() } let channel = client.channels.get("test") channel.attach() - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) - client.close() - expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Closing)) - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Detached), timeout: testTimeout) - expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Closed)) + client.onSuspended() + expect(channel.state).to(equal(ARTRealtimeChannelState.Suspended)) } } + // RTL3d + it("if the connection state enters the @CONNECTED@ state, then a @SUSPENDED@ channel will initiate an attach operation") { + let options = AblyTests.commonAppSetup() + options.suspendedRetryTimeout = 1.0 + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + + let channel = client.channels.get("foo") + waitUntil(timeout: testTimeout) { done in + channel.attach() { error in + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + channel.once(.Suspended) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + client.simulateSuspended() + } + + expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) + } + + // RTL3d + it("if the attach operation for the channel times out and the channel returns to the SUSPENDED state, then an ERROR event with Ably error code 91200 should be emitted") { + let options = AblyTests.commonAppSetup() + options.autoConnect = false + options.suspendedRetryTimeout = 1.0 + let client = ARTRealtime(options: options) + client.setTransportClass(TestProxyTransport.self) + client.connect() + defer { client.dispose(); client.close() } + + let channel = client.channels.get("test") + waitUntil(timeout: testTimeout) { done in + channel.attach() { error in + expect(error).to(beNil()) + done() + } + } + + defer { + TestProxyTransport.network = nil + } + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.on { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); partialDone(); return + } + switch stateChange.current { + case .Suspended: + expect(stateChange.reason).to(beNil()) + partialDone() + case .Error: + guard let error = stateChange.reason else { + fail("Error is nil"); partialDone(); return + } + expect(error.code) == 91200 + partialDone() + } + } + client.simulateSuspended() + // Force connection to timeout + TestProxyTransport.network = .RequestTimeout(timeout: options.suspendedRetryTimeout + 1) + } + } + + // RTL3e + it("if the connection state enters the DISCONNECTED state, it will have no effect on the channel states") { + let options = AblyTests.commonAppSetup() + options.token = getTestToken(ttl: 5.0) + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + + let channel = client.channels.get("test") + + channel.once(.Detached) { stateChange in + fail("Should not reach the DETACHED state") + } + + waitUntil(timeout: testTimeout) { done in + channel.attach() { error in + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Disconnected) { _ in + expect(channel.state).to(equal(ARTRealtimeChannelState.Attached)) + done() + } + } + } + } // RTL4 From e3ef83fade960a5da7f807c9b21781341e1206be Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 14 Dec 2016 19:50:45 +0000 Subject: [PATCH 18/43] RTL14 (#550) --- Spec/RealtimeClientChannel.swift | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/Spec/RealtimeClientChannel.swift b/Spec/RealtimeClientChannel.swift index 98f59c177..bff0e6872 100644 --- a/Spec/RealtimeClientChannel.swift +++ b/Spec/RealtimeClientChannel.swift @@ -2451,6 +2451,39 @@ class RealtimeClientChannel: QuickSpec { expect(channel.state).to(equal(ARTRealtimeChannelState.Attached)) } + // RTL14 + it("If an ERROR ProtocolMessage is received for this channel then the channel should immediately transition to the FAILED state, the errorReason should be set and an error should be emitted on the channel") { + let client = ARTRealtime(options: AblyTests.commonAppSetup()) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + waitUntil(timeout: testTimeout) { done in + channel.attach() { error in + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + let errorProtocolMessage = AblyTests.newErrorProtocolMessage() + errorProtocolMessage.action = .Error + errorProtocolMessage.channel = "foo" + + let partialDone = AblyTests.splitDone(2, done: done) + + channel.once(.Failed) { stateChange in + guard let error = stateChange?.reason else { + fail("Reason error is nil"); partialDone(); return + } + expect(error).to(beIdenticalTo(errorProtocolMessage.error)) + expect(channel.errorReason).to(beIdenticalTo(error)) + partialDone() + } + + client.transport.receive(errorProtocolMessage) + } + + expect(channel.state).to(equal(ARTRealtimeChannelState.Failed)) + } } context("crypto") { From 0e10475cd071852a4a74782db7adf5ee35615575 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 15 Dec 2016 15:13:44 +0000 Subject: [PATCH 19/43] UPDATE event (#559) * UPDATE event (replace ERROR event) * Fix: Connection should emit an UPDATE event * Fix RTC8a1 * Fix specs and legacy tests * RTN4h * RTN4f * RTN24 --- Source/ARTConnection+Private.h | 2 +- Source/ARTConnection.h | 2 +- Source/ARTConnection.m | 8 +- Source/ARTRealtime.m | 28 +++-- Source/ARTRealtimeChannel.m | 4 +- Source/ARTTypes.h | 33 +++++- Source/ARTTypes.m | 76 +++++++++++-- Spec/RealtimeClient.swift | 13 ++- Spec/RealtimeClientChannel.swift | 4 +- Spec/RealtimeClientConnection.swift | 162 ++++++++++++++++++++++++++++ Spec/TestUtilities.swift | 29 +++-- Tests/ARTRealtime+TestSuite.m | 2 +- 12 files changed, 316 insertions(+), 47 deletions(-) diff --git a/Source/ARTConnection+Private.h b/Source/ARTConnection+Private.h index 1634b26fb..d991b96c4 100644 --- a/Source/ARTConnection+Private.h +++ b/Source/ARTConnection+Private.h @@ -30,7 +30,7 @@ ART_ASSUME_NONNULL_BEGIN - (void)setState:(ARTRealtimeConnectionState)state; - (void)setErrorReason:(ARTErrorInfo *__art_nullable)errorReason; -- (void)emit:(ARTRealtimeConnectionState)event with:(ARTConnectionStateChange *)data; +- (void)emit:(ARTRealtimeConnectionEvent)event with:(ARTConnectionStateChange *)data; @end diff --git a/Source/ARTConnection.h b/Source/ARTConnection.h index 9b25dc083..4bec48dab 100644 --- a/Source/ARTConnection.h +++ b/Source/ARTConnection.h @@ -31,7 +31,7 @@ ART_ASSUME_NONNULL_BEGIN - (void)close; - (void)ping:(void (^)(ARTErrorInfo *__art_nullable))cb; -ART_EMBED_INTERFACE_EVENT_EMITTER(ARTRealtimeConnectionState, ARTConnectionStateChange *) +ART_EMBED_INTERFACE_EVENT_EMITTER(ARTRealtimeConnectionEvent, ARTConnectionStateChange *) @end diff --git a/Source/ARTConnection.m b/Source/ARTConnection.m index c1358dc1a..e030f7b89 100644 --- a/Source/ARTConnection.m +++ b/Source/ARTConnection.m @@ -75,7 +75,7 @@ - (NSString *)getRecoveryKey { } } -- (__GENERIC(ARTEventListener, ARTConnectionStateChange *) *)on:(ARTRealtimeConnectionState)event callback:(void (^)(ARTConnectionStateChange *))cb { +- (__GENERIC(ARTEventListener, ARTConnectionStateChange *) *)on:(ARTRealtimeConnectionEvent)event callback:(void (^)(ARTConnectionStateChange *))cb { return [_eventEmitter on:[NSNumber numberWithInt:event] callback:cb]; } @@ -83,7 +83,7 @@ - (NSString *)getRecoveryKey { return [_eventEmitter on:cb]; } -- (__GENERIC(ARTEventListener, ARTConnectionStateChange *) *)once:(ARTRealtimeConnectionState)event callback:(void (^)(ARTConnectionStateChange *))cb { +- (__GENERIC(ARTEventListener, ARTConnectionStateChange *) *)once:(ARTRealtimeConnectionEvent)event callback:(void (^)(ARTConnectionStateChange *))cb { return [_eventEmitter once:[NSNumber numberWithInt:event] callback:cb]; } @@ -94,7 +94,7 @@ - (NSString *)getRecoveryKey { - (void)off { [_eventEmitter off]; } -- (void)off:(ARTRealtimeConnectionState)event listener:listener { +- (void)off:(ARTRealtimeConnectionEvent)event listener:listener { [_eventEmitter off:[NSNumber numberWithInt:event] listener:listener]; } @@ -102,7 +102,7 @@ - (void)off:(__GENERIC(ARTEventListener, ARTConnectionStateChange *) *)listener [_eventEmitter off:listener]; } -- (void)emit:(ARTRealtimeConnectionState)event with:(ARTConnectionStateChange *)data { +- (void)emit:(ARTRealtimeConnectionEvent)event with:(ARTConnectionStateChange *)data { [_eventEmitter emit:[NSNumber numberWithInt:event] with:data]; } diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index 46549bf3e..b0839eb49 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -235,7 +235,7 @@ - (void)ping:(void (^)(ARTErrorInfo *)) cb { case ARTRealtimeClosing: case ARTRealtimeClosed: case ARTRealtimeFailed: - cb([ARTErrorInfo createWithCode:0 status:ARTStateConnectionFailed message:[NSString stringWithFormat:@"Can't ping a %@ connection", ARTRealtimeStateToStr(self.connection.state)]]); + cb([ARTErrorInfo createWithCode:0 status:ARTStateConnectionFailed message:[NSString stringWithFormat:@"Can't ping a %@ connection", ARTRealtimeConnectionStateToStr(self.connection.state)]]); return; case ARTRealtimeConnecting: case ARTRealtimeDisconnected: @@ -266,7 +266,7 @@ - (void)transition:(ARTRealtimeConnectionState)state { } - (void)transition:(ARTRealtimeConnectionState)state withErrorInfo:(ARTErrorInfo *)errorInfo { - [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p transition to %@ requested", self, ARTRealtimeStateToStr(state)]; + [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p transition to %@ requested", self, ARTRealtimeConnectionStateToStr(state)]; ARTConnectionStateChange *stateChange = [[ARTConnectionStateChange alloc] initWithCurrent:state previous:self.connection.state reason:errorInfo retryIn:0]; [self.connection setState:state]; @@ -275,12 +275,25 @@ - (void)transition:(ARTRealtimeConnectionState)state withErrorInfo:(ARTErrorInfo [self.connection setErrorReason:errorInfo]; } - [self transitionSideEffects:stateChange]; + [self transitionSideEffects:stateChange usingEvent:(ARTRealtimeConnectionEvent)stateChange.current]; [_internalEventEmitter emit:[NSNumber numberWithInteger:state] with:stateChange]; } -- (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { +- (void)updateWithErrorInfo:(art_nullable ARTErrorInfo *)errorInfo { + [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p update requested", self]; + + if (self.connection.state != ARTRealtimeConnected) { + [self.logger warn:@"R:%p update ignored because connection is not connected", self]; + return; + } + + ARTConnectionStateChange *stateChange = [[ARTConnectionStateChange alloc] initWithCurrent:self.connection.state previous:self.connection.state reason:errorInfo retryIn:0]; + + [self transitionSideEffects:stateChange usingEvent:ARTRealtimeConnectionEventUpdate]; +} + +- (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange usingEvent:(ARTRealtimeConnectionEvent)event { ARTStatus *status = nil; // Do not increase the reference count (avoid retain cycles): // i.e. the `unlessStateChangesBefore` is setting a timer and if the `ARTRealtime` instance is released before that timer, then it could create a leak. @@ -447,7 +460,7 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { } } - [self.connection emit:stateChange.current with:stateChange]; + [self.connection emit:event with:stateChange]; } - (void)unlessStateChangesBefore:(NSTimeInterval)deadline do:(void(^)())callback { @@ -468,7 +481,7 @@ - (void)unlessStateChangesBefore:(NSTimeInterval)deadline do:(void(^)())callback - (void)onHeartbeat { [self.logger verbose:@"R:%p ARTRealtime heartbeat received", self]; if(self.connection.state != ARTRealtimeConnected) { - NSString *msg = [NSString stringWithFormat:@"ARTRealtime received a ping when in state %@", ARTRealtimeStateToStr(self.connection.state)]; + NSString *msg = [NSString stringWithFormat:@"ARTRealtime received a ping when in state %@", ARTRealtimeConnectionStateToStr(self.connection.state)]; [self.logger warn:@"R:%p %@", self, msg]; } [_pingEventEmitter emit:[NSNull null] with:nil]; @@ -515,8 +528,7 @@ - (void)onConnected:(ARTProtocolMessage *)message { break; case ARTRealtimeConnected: // Renewing token. - [self transitionSideEffects:[[ARTConnectionStateChange alloc] initWithCurrent:ARTRealtimeConnected previous:ARTRealtimeConnected reason:nil]]; - [self transition:ARTRealtimeConnected withErrorInfo:message.error]; + [self updateWithErrorInfo:message.error]; default: break; } diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index 93c2ce531..41daebef0 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -397,7 +397,7 @@ - (void)setAttached:(ARTProtocolMessage *)message { if (self.state == ARTRealtimeChannelAttached) { if (message.error != nil) { _errorReason = message.error; - [self emit:ARTChannelEventError with:message.error]; + [self emit:ARTChannelEventUpdate with:message.error]; } return; } @@ -463,7 +463,7 @@ - (void)onMessage:(ARTProtocolMessage *)message { ARTErrorInfo *errorInfo = [ARTErrorInfo wrap:(ARTErrorInfo *)error.userInfo[NSLocalizedFailureReasonErrorKey] prepend:@"Failed to decode data: "]; [self.logger error:@"R:%p C:%p %@", _realtime, self, errorInfo.message]; _errorReason = errorInfo; - [self emit:ARTChannelEventError with:errorInfo]; + [self emit:ARTChannelEventUpdate with:errorInfo]; } } diff --git a/Source/ARTTypes.h b/Source/ARTTypes.h index 7ef087f7e..4ab1eeadf 100644 --- a/Source/ARTTypes.h +++ b/Source/ARTTypes.h @@ -34,6 +34,9 @@ typedef NS_ENUM(NSUInteger, ARTAuthMethod) { ARTAuthMethodToken }; + +#pragma mark - ARTRealtimeConnectionState + typedef NS_ENUM(NSUInteger, ARTRealtimeConnectionState) { ARTRealtimeInitialized, ARTRealtimeConnecting, @@ -45,8 +48,27 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeConnectionState) { ARTRealtimeFailed }; +NSString *__art_nonnull ARTRealtimeConnectionStateToStr(ARTRealtimeConnectionState state); + + +#pragma mark - ARTRealtimeConnectionEvent + +typedef NS_ENUM(NSUInteger, ARTRealtimeConnectionEvent) { + ARTRealtimeConnectionEventInitialized, + ARTRealtimeConnectionEventConnecting, + ARTRealtimeConnectionEventConnected, + ARTRealtimeConnectionEventDisconnected, + ARTRealtimeConnectionEventSuspended, + ARTRealtimeConnectionEventClosing, + ARTRealtimeConnectionEventClosed, + ARTRealtimeConnectionEventFailed, + ARTRealtimeConnectionEventUpdate +}; + +NSString *__art_nonnull ARTRealtimeConnectionEventToStr(ARTRealtimeConnectionEvent event); + -NSString *__art_nonnull ARTRealtimeStateToStr(ARTRealtimeConnectionState state); +#pragma mark - ARTRealtimeChannelState typedef NS_ENUM(NSUInteger, ARTRealtimeChannelState) { ARTRealtimeChannelInitialized, @@ -57,6 +79,11 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeChannelState) { ARTRealtimeChannelFailed }; +NSString *__art_nonnull ARTRealtimeChannelStateToStr(ARTRealtimeChannelState state); + + +#pragma mark - ARTChannelEvent + typedef NS_ENUM(NSUInteger, ARTChannelEvent) { ARTChannelEventInitialized, ARTChannelEventAttaching, @@ -64,9 +91,11 @@ typedef NS_ENUM(NSUInteger, ARTChannelEvent) { ARTChannelEventDetaching, ARTChannelEventDetached, ARTChannelEventFailed, - ARTChannelEventError + ARTChannelEventUpdate }; +NSString *__art_nonnull ARTChannelEventToStr(ARTChannelEvent event); + typedef NS_ENUM(NSInteger, ARTDataQueryError) { ARTDataQueryErrorLimit = 1, ARTDataQueryErrorTimestampRange = 2, diff --git a/Source/ARTTypes.m b/Source/ARTTypes.m index a36261df9..a00c8ca25 100644 --- a/Source/ARTTypes.m +++ b/Source/ARTTypes.m @@ -62,12 +62,17 @@ - (instancetype)initWithCurrent:(ARTRealtimeConnectionState)current previous:(AR } - (NSString *)description { - return [NSString stringWithFormat:@"%@ - \n\t current: %@; \n\t previous: %@; \n\t reason: %@; \n\t retryIn: %f; \n", [super description], ARTRealtimeStateToStr(_current), ARTRealtimeStateToStr(_previous), _reason, _retryIn]; + return [NSString stringWithFormat:@"%@ - \n\t current: %@; \n\t previous: %@; \n\t reason: %@; \n\t retryIn: %f; \n", [super description], ARTRealtimeConnectionStateToStr(_current), ARTRealtimeConnectionStateToStr(_previous), _reason, _retryIn]; } -NSString *ARTRealtimeStateToStr(ARTRealtimeConnectionState state) { - switch(state) - { +- (void)setRetryIn:(NSTimeInterval)retryIn { + _retryIn = retryIn; +} + +@end + +NSString *ARTRealtimeConnectionStateToStr(ARTRealtimeConnectionState state) { + switch(state) { case ARTRealtimeInitialized: return @"Initialized"; //0 case ARTRealtimeConnecting: @@ -84,17 +89,32 @@ - (NSString *)description { return @"Closed"; //6 case ARTRealtimeFailed: return @"Failed"; //7 - default: - return [NSString stringWithFormat: @"unknown connection state %d", (int)state]; } } -- (void)setRetryIn:(NSTimeInterval)retryIn { - _retryIn = retryIn; +NSString *ARTRealtimeConnectionEventToStr(ARTRealtimeConnectionEvent event) { + switch(event) { + case ARTRealtimeConnectionEventInitialized: + return @"Initialized"; //0 + case ARTRealtimeConnectionEventConnecting: + return @"Connecting"; //1 + case ARTRealtimeConnectionEventConnected: + return @"Connected"; //2 + case ARTRealtimeConnectionEventDisconnected: + return @"Disconnected"; //3 + case ARTRealtimeConnectionEventSuspended: + return @"Suspended"; //4 + case ARTRealtimeConnectionEventClosing: + return @"Closing"; //5 + case ARTRealtimeConnectionEventClosed: + return @"Closed"; //6 + case ARTRealtimeConnectionEventFailed: + return @"Failed"; //7 + case ARTRealtimeConnectionEventUpdate: + return @"Update"; //8 + } } -@end - @implementation NSString (ARTJsonCompatible) - (NSDictionary *)toJSON:(NSError *__art_nullable *__art_nullable)error { @@ -136,3 +156,39 @@ - (NSString *)description { } @end + +NSString *ARTRealtimeChannelStateToStr(ARTRealtimeChannelState state) { + switch(state) { + case ARTRealtimeChannelInitialized: + return @"Initialized"; //0 + case ARTRealtimeChannelAttaching: + return @"Attaching"; //1 + case ARTRealtimeChannelAttached: + return @"Attached"; //2 + case ARTRealtimeChannelDetaching: + return @"Detaching"; //3 + case ARTRealtimeChannelDetached: + return @"Detached"; //4 + case ARTRealtimeChannelFailed: + return @"Failed"; //5 + } +} + +NSString *ARTChannelEventToStr(ARTChannelEvent event) { + switch(event) { + case ARTChannelEventInitialized: + return @"Initialized"; //0 + case ARTChannelEventAttaching: + return @"Attaching"; //1 + case ARTChannelEventAttached: + return @"Attached"; //2 + case ARTChannelEventDetaching: + return @"Detaching"; //3 + case ARTChannelEventDetached: + return @"Detached"; //4 + case ARTChannelEventFailed: + return @"Failed"; //5 + case ARTChannelEventUpdate: + return @"Update"; //6 + } +} diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index 472daf553..fcf0e7bbf 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -430,6 +430,10 @@ class RealtimeClient: QuickSpec { let partialDone = AblyTests.splitDone(2, done: done) client.connection.once(.Connected) { stateChange in + fail("Should not receive a CONNECTED event because the connection is already connected"); partialDone(); return + } + + client.connection.once(.Update) { stateChange in guard let stateChange = stateChange else { fail("ConnectionStateChange is nil"); partialDone(); return } @@ -460,6 +464,8 @@ class RealtimeClient: QuickSpec { expect(tokenDetails.token).toNot(equal(testToken)) partialDone() } + + expect(client.connection.errorReason).to(beNil()) } expect(client.auth.tokenDetails?.token).toNot(equal(testToken)) @@ -498,7 +504,7 @@ class RealtimeClient: QuickSpec { waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(2, done: done) - client.connection.once(.Connected) { stateChange in + client.connection.once(.Update) { stateChange in guard let stateChange = stateChange else { fail("ConnectionStateChange is nil"); partialDone(); return } @@ -507,6 +513,9 @@ class RealtimeClient: QuickSpec { partialDone() } + client.connection.once(.Connected) { _ in + fail("Already connected") + } client.connection.once(.Disconnected) { _ in fail("Lost connectivity") } @@ -552,6 +561,8 @@ class RealtimeClient: QuickSpec { } channel.attach() } + + expect(client.auth.tokenDetails?.token).toNot(equal(testToken)) } // RTC8a1 - part 3 diff --git a/Spec/RealtimeClientChannel.swift b/Spec/RealtimeClientChannel.swift index bff0e6872..ca05891a9 100644 --- a/Spec/RealtimeClientChannel.swift +++ b/Spec/RealtimeClientChannel.swift @@ -2000,7 +2000,7 @@ class RealtimeClientChannel: QuickSpec { partlyDone() } - channel.on(.Error) { errorInfo in + channel.on(.Update) { errorInfo in guard let errorInfo = errorInfo else { return } @@ -2438,7 +2438,7 @@ class RealtimeClientChannel: QuickSpec { attachedMessageWithError.action = .Attached attachedMessageWithError.channel = "test" - channel.once(.Error) { error in + channel.once(.Update) { error in expect(error).to(beIdenticalTo(attachedMessageWithError.error)) expect(channel.errorReason).to(beIdenticalTo(error)) done() diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index fb9fe517c..6aabc89c4 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -304,6 +304,41 @@ class RealtimeClientConnection: QuickSpec { expect(events[7].rawValue).to(equal(ARTRealtimeConnectionState.Failed.rawValue), description: "Should be FAILED state") } + // RTN4h + it("should never emit a ConnectionState event for a state equal to the previous state") { + let options = AblyTests.commonAppSetup() + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + } + + client.connection.once(.Connected) { stateChange in + fail("Should not emit a Connected state") + } + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Update) { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); done(); return + } + expect(stateChange.reason).to(beNil()) + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Connected)) + expect(stateChange.current).to(equal(ARTRealtimeConnectionState.Connected)) + expect(stateChange.current).to(equal(stateChange.previous)) + done() + } + + let authMessage = ARTProtocolMessage() + authMessage.action = .Auth + client.transport.receive(authMessage) + } + } + // RTN4b it("should emit states on a new connection") { let options = AblyTests.commonAppSetup() @@ -455,6 +490,43 @@ class RealtimeClientConnection: QuickSpec { expect(errorInfo).toNot(beNil()) } + + // RTN4f + it("any state change triggered by a ProtocolMessage that contains an Error member should populate the Reason property") { + let options = AblyTests.commonAppSetup() + options.useTokenAuth = true + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + } + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + guard let originalConnectedMessage = transport.protocolMessagesReceived.filter({ $0.action == .Connected }).first else { + fail("First CONNECTED message not received"); return + } + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Update) { stateChange in + guard let error = stateChange?.reason else { + fail("Reason error is nil"); done(); return + } + expect(error.code) == 1234 + expect(error.message) == "fabricated error" + done() + } + + let connectedMessageWithError = originalConnectedMessage + connectedMessageWithError.error = ARTErrorInfo.createWithCode(1234, message: "fabricated error") + client.transport.receive(connectedMessageWithError) + } + } } class TotalReach { @@ -3533,6 +3605,96 @@ class RealtimeClientConnection: QuickSpec { } + // RTN24 + it("the client may receive a CONNECTED ProtocolMessage from Ably at any point and should emit an UPDATE event") { + let options = AblyTests.commonAppSetup() + options.authCallback = { _, completion in + completion(getTestToken(key: options.key!, ttl: 35), nil) + } + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + } + + expect(client.auth.clientId).to(beNil()) + + client.options.authCallback = { _, completion in + completion(getTestToken(key: options.key!, ttl: 5, clientId: "tester"), nil) + } + + client.connection.once(.Connected) { stateChange in + fail("Should not emit a Connected state") + } + + waitUntil(timeout: 40) { done in + client.connection.once(.Update) { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); done(); return + } + expect(stateChange.reason).to(beNil()) + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Connected)) + expect(stateChange.current).to(equal(ARTRealtimeConnectionState.Connected)) + expect(stateChange.current).to(equal(stateChange.previous)) + done() + } + } + + expect(client.auth.clientId).to(equal("tester")) + } + + // RTN24 + it("should set the Connection reason attribute based on the Error member of the CONNECTED ProtocolMessage") { + let options = AblyTests.commonAppSetup() + options.useTokenAuth = true + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + } + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + guard let originalConnectedMessage = transport.protocolMessagesReceived.filter({ $0.action == .Connected }).first else { + fail("First CONNECTED message not received"); return + } + + client.connection.once(.Connected) { stateChange in + fail("Should not emit a Connected state") + } + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Update) { stateChange in + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); done(); return + } + guard let error = stateChange.reason else { + fail("Reason error is nil"); done(); return + } + expect(error.code) == 1234 + expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Connected)) + expect(stateChange.current).to(equal(ARTRealtimeConnectionState.Connected)) + expect(stateChange.current).to(equal(stateChange.previous)) + done() + } + + let connectedMessageWithError = originalConnectedMessage + connectedMessageWithError.error = ARTErrorInfo.createWithCode(1234, message: "fabricated error") + client.transport.receive(connectedMessageWithError) + } + + expect(client.connection.errorReason).to(beNil()) + } + // https://github.com/ably/ably-ios/issues/454 it("should not move to FAILED if received DISCONNECT with an error") { let options = AblyTests.commonAppSetup() diff --git a/Spec/TestUtilities.swift b/Spec/TestUtilities.swift index 665b7af2a..67e737fcd 100644 --- a/Spec/TestUtilities.swift +++ b/Spec/TestUtilities.swift @@ -1055,7 +1055,13 @@ extension ARTAuth { extension ARTRealtimeConnectionState : CustomStringConvertible { public var description : String { - return ARTRealtimeStateToStr(self) + return ARTRealtimeConnectionStateToStr(self) + } +} + +extension ARTRealtimeConnectionEvent : CustomStringConvertible { + public var description : String { + return ARTRealtimeConnectionEventToStr(self) } } @@ -1067,20 +1073,13 @@ extension ARTProtocolMessageAction : CustomStringConvertible { extension ARTRealtimeChannelState : CustomStringConvertible { public var description : String { - switch self { - case .Initialized: - return "Initialized" - case .Attaching: - return "Attaching" - case .Attached: - return "Attached" - case .Detaching: - return "Detaching" - case .Detached: - return "Detached" - case .Failed: - return "Failed" - } + return ARTRealtimeChannelStateToStr(self) + } +} + +extension ARTChannelEvent : CustomStringConvertible { + public var description : String { + return ARTChannelEventToStr(self) } } diff --git a/Tests/ARTRealtime+TestSuite.m b/Tests/ARTRealtime+TestSuite.m index 2ab3b168d..1153731d4 100644 --- a/Tests/ARTRealtime+TestSuite.m +++ b/Tests/ARTRealtime+TestSuite.m @@ -23,7 +23,7 @@ - (void)testSuite_waitForConnectionToClose:(XCTestCase *)testCase { }]; [self.connection off]; - [self.connection once:ARTRealtimeClosed callback:^(ARTConnectionStateChange *stateChange) { + [self.connection once:ARTRealtimeConnectionEventClosed callback:^(ARTConnectionStateChange *stateChange) { [expectation fulfill]; }]; From 7f1309b19d7d13b59fd5ea9b057c3f4c3cc20045 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 20 Dec 2016 00:09:26 +0000 Subject: [PATCH 20/43] Update RTL2 for 0.9 (#543) * Add Realtime Channel Suspended state * Add ProtocolMessageActionToStr method * Add ChannelStateChange type * Update RTL2 * RTL2f: pending - functionality hasn't been deployed * Use ChannelStateChange on channel event emitter * Test suite: simulate client suspension with before suspension callback * Update tests using channel events * Fix RTL14 * Fix: Channel on suspended should transition to SUSPENDED state * Remove RTN18 * Fix: set Suspended on all channels when Connection moves to Suspended * RTL2g * Fix RTL12 * Fix RTL3d * Fix: channel should reattach when connection is Connected * Fix: should resume connection when the connection is Suspended * Fix RTN11 * Fix RTL3e * Fix RTC8a1 * Remove testSuspendingDetachesChannel * Fix RTL3d * Fix: channel is SUSPENDED then operation will result in an error --- Source/ARTProtocolMessage+Private.h | 2 + Source/ARTProtocolMessage.h | 2 + Source/ARTProtocolMessage.m | 41 +++ Source/ARTRealtime+Private.h | 3 +- Source/ARTRealtime.m | 91 ++---- Source/ARTRealtimeChannel+Private.h | 2 +- Source/ARTRealtimeChannel.h | 2 +- Source/ARTRealtimeChannel.m | 27 +- Source/ARTTypes.h | 25 ++ Source/ARTTypes.m | 41 ++- Spec/RealtimeClient.swift | 14 +- Spec/RealtimeClientChannel.swift | 400 +++++++++++++++++-------- Spec/RealtimeClientConnection.swift | 223 +++----------- Spec/TestUtilities.swift | 16 +- Tests/ARTRealtimeAttachTest.m | 83 +++-- Tests/ARTRealtimeChannelTest.m | 64 ++-- Tests/ARTRealtimeMessageTest.m | 22 +- Tests/ARTRealtimePresenceHistoryTest.m | 30 +- Tests/ARTRealtimePresenceTest.m | 83 +++-- Tests/ARTRealtimeResumeTest.m | 8 +- 20 files changed, 619 insertions(+), 560 deletions(-) diff --git a/Source/ARTProtocolMessage+Private.h b/Source/ARTProtocolMessage+Private.h index f9464f860..d094a0aec 100644 --- a/Source/ARTProtocolMessage+Private.h +++ b/Source/ARTProtocolMessage+Private.h @@ -6,6 +6,8 @@ // Copyright (c) 2014 Ably. All rights reserved. // +NSString *__art_nonnull ARTProtocolMessageActionToStr(ARTProtocolMessageAction action); + ART_ASSUME_NONNULL_BEGIN @interface ARTProtocolMessage () diff --git a/Source/ARTProtocolMessage.h b/Source/ARTProtocolMessage.h index 72cf8c08a..4d6e532c0 100644 --- a/Source/ARTProtocolMessage.h +++ b/Source/ARTProtocolMessage.h @@ -38,6 +38,8 @@ typedef NS_ENUM(NSUInteger, ARTProtocolMessageAction) { ARTProtocolMessageAuth = 17, }; +NSString *__art_nonnull ARTProtocolMessageActionToStr(ARTProtocolMessageAction action); + ART_ASSUME_NONNULL_BEGIN /** diff --git a/Source/ARTProtocolMessage.m b/Source/ARTProtocolMessage.m index a3ee70325..f73fcb433 100644 --- a/Source/ARTProtocolMessage.m +++ b/Source/ARTProtocolMessage.m @@ -116,3 +116,44 @@ - (ARTConnectionDetails *)getConnectionDetails { } @end + +NSString* ARTProtocolMessageActionToStr(ARTProtocolMessageAction action) { + switch(action) { + case ARTProtocolMessageHeartbeat: + return @"Heartbeat"; //0 + case ARTProtocolMessageAck: + return @"Ack"; //1 + case ARTProtocolMessageNack: + return @"Nack"; //2 + case ARTProtocolMessageConnect: + return @"Connect"; //3 + case ARTProtocolMessageConnected: + return @"Connected"; //4 + case ARTProtocolMessageDisconnect: + return @"Disconnect"; //5 + case ARTProtocolMessageDisconnected: + return @"Disconnected"; //6 + case ARTProtocolMessageClose: + return @"Close"; //7 + case ARTProtocolMessageClosed: + return @"Closed"; //8 + case ARTProtocolMessageError: + return @"Error"; //9 + case ARTProtocolMessageAttach: + return @"Attach"; //10 + case ARTProtocolMessageAttached: + return @"Attached"; //11 + case ARTProtocolMessageDetach: + return @"Detach"; //12 + case ARTProtocolMessageDetached: + return @"Detached"; //13 + case ARTProtocolMessagePresence: + return @"Presence"; //14 + case ARTProtocolMessageMessage: + return @"Message"; //15 + case ARTProtocolMessageSync: + return @"Sync"; //16 + case ARTProtocolMessageAuth: + return @"Auth"; //17 + } +} diff --git a/Source/ARTRealtime+Private.h b/Source/ARTRealtime+Private.h index c5ee12235..fa907b64d 100644 --- a/Source/ARTRealtime+Private.h +++ b/Source/ARTRealtime+Private.h @@ -28,8 +28,6 @@ ART_ASSUME_NONNULL_BEGIN @property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSNumber *, ARTConnectionStateChange *) *internalEventEmitter; @property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSNull *, NSNull *) *connectedEventEmitter; -+ (NSString *)protocolStr:(ARTProtocolMessageAction)action; - // State properties - (BOOL)shouldSendEvents; - (BOOL)shouldQueueEvents; @@ -48,6 +46,7 @@ ART_ASSUME_NONNULL_BEGIN @property (readonly, getter=getTransport, art_nullable) id transport; @property (readonly, strong, nonatomic, art_nonnull) id reachability; @property (readonly, getter=getLogger) ARTLog *logger; +@property (nonatomic) NSTimeInterval connectionStateTtl; /// Current protocol `msgSerial`. Starts at zero. @property (readwrite, assign, nonatomic) int64_t msgSerial; diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index b0839eb49..3f971cc98 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -48,7 +48,6 @@ @implementation ARTRealtime { BOOL _renewingToken; __GENERIC(ARTEventEmitter, NSNull *, ARTErrorInfo *) *_pingEventEmitter; NSDate *_startedReconnection; - NSTimeInterval _connectionStateTtl; Class _transportClass; Class _reachabilityClass; id _transport; @@ -312,7 +311,7 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange usingEvent if (!_transport) { NSString *resumeKey = nil; NSNumber *connectionSerial = nil; - if (stateChange.previous == ARTRealtimeFailed || stateChange.previous == ARTRealtimeDisconnected) { + if (stateChange.previous == ARTRealtimeFailed || stateChange.previous == ARTRealtimeDisconnected || stateChange.previous == ARTRealtimeSuspended) { resumeKey = self.connection.key; connectionSerial = [NSNumber numberWithLongLong:self.connection.serial]; _resuming = true; @@ -432,6 +431,12 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange usingEvent if ([self shouldSendEvents]) { [self sendQueuedMessages]; + // For every Channel + for (ARTRealtimeChannel* channel in self.channels) { + if (channel.state == ARTRealtimeChannelSuspended) { + [channel attach]; + } + } } else if (![self shouldQueueEvents]) { [self failQueuedMessages:status]; ARTStatus *channelStatus = status; @@ -440,22 +445,27 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange usingEvent } // For every Channel for (ARTRealtimeChannel* channel in self.channels) { - if (channel.state == ARTRealtimeChannelInitialized || channel.state == ARTRealtimeChannelAttaching || channel.state == ARTRealtimeChannelAttached || channel.state == ARTRealtimeChannelFailed) { - if(stateChange.current == ARTRealtimeClosing) { - //do nothing. Closed state is coming. - } - else if(stateChange.current == ARTRealtimeClosed) { - [channel detachChannel:[ARTStatus state:ARTStateOk]]; - } - else if(stateChange.current == ARTRealtimeSuspended) { - [channel detachChannel:channelStatus]; - } - else { - [channel setFailed:channelStatus]; - } - } - else { - [channel setSuspended:channelStatus]; + switch (channel.state) { + case ARTRealtimeChannelInitialized: + case ARTRealtimeChannelAttaching: + case ARTRealtimeChannelAttached: + case ARTRealtimeChannelFailed: + if (stateChange.current == ARTRealtimeClosing) { + //do nothing. Closed state is coming. + } + else if (stateChange.current == ARTRealtimeClosed) { + [channel detachChannel:[ARTStatus state:ARTStateOk]]; + } + else if (stateChange.current == ARTRealtimeSuspended) { + [channel setSuspended:channelStatus]; + } + else { + [channel setFailed:channelStatus]; + } + break; + default: + [channel setSuspended:channelStatus]; + break; } } } @@ -942,49 +952,6 @@ - (void)setReachabilityClass:(Class)reachabilityClass { _reachabilityClass = reachabilityClass; } -+ (NSString *)protocolStr:(ARTProtocolMessageAction) action { - switch(action) { - case ARTProtocolMessageHeartbeat: - return @"Heartbeat"; //0 - case ARTProtocolMessageAck: - return @"Ack"; //1 - case ARTProtocolMessageNack: - return @"Nack"; //2 - case ARTProtocolMessageConnect: - return @"Connect"; //3 - case ARTProtocolMessageConnected: - return @"Connected"; //4 - case ARTProtocolMessageDisconnect: - return @"Disconnect"; //5 - case ARTProtocolMessageDisconnected: - return @"Disconnected"; //6 - case ARTProtocolMessageClose: - return @"Close"; //7 - case ARTProtocolMessageClosed: - return @"Closed"; //8 - case ARTProtocolMessageError: - return @"Error"; //9 - case ARTProtocolMessageAttach: - return @"Attach"; //10 - case ARTProtocolMessageAttached: - return @"Attached"; //11 - case ARTProtocolMessageDetach: - return @"Detach"; //12 - case ARTProtocolMessageDetached: - return @"Detached"; //13 - case ARTProtocolMessagePresence: - return @"Presence"; //14 - case ARTProtocolMessageMessage: - return @"Message"; //15 - case ARTProtocolMessageSync: - return @"Sync"; //16 - case ARTProtocolMessageAuth: - return @"Auth"; //17 - default: - return [NSString stringWithFormat: @"unknown protocol state %d", (int)action]; - } -} - #pragma mark - ARTRealtimeTransportDelegate implementation - (void)realtimeTransport:(id)transport didReceiveMessage:(ARTProtocolMessage *)message { @@ -993,7 +960,7 @@ - (void)realtimeTransport:(id)transport didReceiveMessage:(ARTProtocolMessage *) return; } - [self.logger verbose:@"R:%p ARTRealtime didReceive Protocol Message %@ ", self, [ARTRealtime protocolStr:message.action]]; + [self.logger verbose:@"R:%p ARTRealtime didReceive Protocol Message %@ ", self, ARTProtocolMessageActionToStr(message.action)]; if (message.error) { [self.logger verbose:@"R:%p ARTRealtime Protocol Message with error %@ ", self, message.error]; diff --git a/Source/ARTRealtimeChannel+Private.h b/Source/ARTRealtimeChannel+Private.h index 36031f42f..c7199c210 100644 --- a/Source/ARTRealtimeChannel+Private.h +++ b/Source/ARTRealtimeChannel+Private.h @@ -23,7 +23,7 @@ ART_ASSUME_NONNULL_BEGIN @property (readwrite, strong, nonatomic) NSMutableArray *queuedMessages; @property (readwrite, strong, nonatomic, art_nullable) NSString *attachSerial; @property (readonly, getter=getClientId) NSString *clientId; -@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSNumber *, ARTErrorInfo *) *statesEventEmitter; +@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSNumber *, ARTChannelStateChange *) *statesEventEmitter; @property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSString *, ARTMessage *) *messagesEventEmitter; @property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSNumber *, ARTPresenceMessage *) *presenceEventEmitter; @property (readwrite, strong, nonatomic) ARTPresenceMap *presenceMap; diff --git a/Source/ARTRealtimeChannel.h b/Source/ARTRealtimeChannel.h index 2b20f4446..2d972d350 100644 --- a/Source/ARTRealtimeChannel.h +++ b/Source/ARTRealtimeChannel.h @@ -42,7 +42,7 @@ ART_ASSUME_NONNULL_BEGIN - (BOOL)history:(ARTRealtimeHistoryQuery *__art_nullable)query callback:(void(^)(__GENERIC(ARTPaginatedResult, ARTMessage *) *__art_nullable result, ARTErrorInfo *__art_nullable error))callback error:(NSError *__art_nullable *__art_nullable)errorPtr; -ART_EMBED_INTERFACE_EVENT_EMITTER(ARTChannelEvent, ARTErrorInfo *) +ART_EMBED_INTERFACE_EVENT_EMITTER(ARTChannelEvent, ARTChannelStateChange *) @end diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index 41daebef0..e81b46e7e 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -157,6 +157,7 @@ - (void)publishProtocolMessage:(ARTProtocolMessage *)pm callback:(void (^)(ARTSt [self addToQueue:pm callback:cb]; break; } + case ARTRealtimeChannelSuspended: case ARTRealtimeChannelDetaching: case ARTRealtimeChannelDetached: case ARTRealtimeChannelFailed: @@ -180,8 +181,6 @@ - (void)publishProtocolMessage:(ARTProtocolMessage *)pm callback:(void (^)(ARTSt } break; } - default: - NSAssert(NO, @"Invalid State"); } } @@ -272,19 +271,19 @@ - (void)unsubscribe:(NSString *)name listener:(ARTEventListener *) [self.messagesEventEmitter off:name listener:listener]; } -- (__GENERIC(ARTEventListener, ARTErrorInfo *) *)on:(ARTChannelEvent)event callback:(void (^)(ARTErrorInfo *))cb { +- (__GENERIC(ARTEventListener, ARTChannelStateChange *) *)on:(ARTChannelEvent)event callback:(void (^)(ARTChannelStateChange *))cb { return [self.statesEventEmitter on:[NSNumber numberWithInt:event] callback:cb]; } -- (__GENERIC(ARTEventListener, ARTErrorInfo *) *)on:(void (^)(ARTErrorInfo *))cb { +- (__GENERIC(ARTEventListener, ARTChannelStateChange *) *)on:(void (^)(ARTChannelStateChange *))cb { return [self.statesEventEmitter on:cb]; } -- (__GENERIC(ARTEventListener, ARTErrorInfo *) *)once:(ARTChannelEvent)event callback:(void (^)(ARTErrorInfo *))cb { +- (__GENERIC(ARTEventListener, ARTChannelStateChange *) *)once:(ARTChannelEvent)event callback:(void (^)(ARTChannelStateChange *))cb { return [self.statesEventEmitter once:[NSNumber numberWithInt:event] callback:cb]; } -- (__GENERIC(ARTEventListener, ARTErrorInfo *) *)once:(void (^)(ARTErrorInfo *))cb { +- (__GENERIC(ARTEventListener, ARTChannelStateChange *) *)once:(void (^)(ARTChannelStateChange *))cb { return [self.statesEventEmitter once:cb]; } @@ -295,11 +294,11 @@ - (void)off:(ARTChannelEvent)event listener:listener { [self.statesEventEmitter off:[NSNumber numberWithInt:event] listener:listener]; } -- (void)off:(__GENERIC(ARTEventListener, ARTErrorInfo *) *)listener { +- (void)off:(__GENERIC(ARTEventListener, ARTChannelStateChange *) *)listener { [self.statesEventEmitter off:listener]; } -- (void)emit:(ARTChannelEvent)event with:(ARTErrorInfo *)data { +- (void)emit:(ARTChannelEvent)event with:(ARTChannelStateChange *)data { [self.statesEventEmitter emit:[NSNumber numberWithInt:event] with:data]; } @@ -308,6 +307,7 @@ - (ARTEventListener *)timed:(ARTEventListener *)listener deadline:(NSTimeInterva } - (void)transition:(ARTRealtimeChannelState)state status:(ARTStatus *)status { + ARTChannelStateChange *stateChange = [[ARTChannelStateChange alloc] initWithCurrent:state previous:self.state reason:status.errorInfo]; self.state = state; _errorReason = status.errorInfo; @@ -321,7 +321,7 @@ - (void)transition:(ARTRealtimeChannelState)state status:(ARTStatus *)status { [_attachedEventEmitter emit:[NSNull null] with:[ARTErrorInfo createWithCode:90000 message:msg]]; } - [self emit:(ARTChannelEvent)state with:status.errorInfo]; + [self emit:(ARTChannelEvent)stateChange.current with:stateChange]; } - (void)dealloc { @@ -339,7 +339,8 @@ - (void)unlessStateChangesBefore:(NSTimeInterval)deadline do:(void(^)())callback // Already changed; do nothing. return; } - [self timed:[self once:^(ARTErrorInfo *errorInfo) { + // FIXME: should not use the global listener for internal purpose + [self timed:[self once:^(ARTChannelStateChange *stateChange) { // Any state change cancels the timeout. }] deadline:deadline onTimeout:callback]; }); @@ -397,8 +398,8 @@ - (void)setAttached:(ARTProtocolMessage *)message { if (self.state == ARTRealtimeChannelAttached) { if (message.error != nil) { _errorReason = message.error; - [self emit:ARTChannelEventUpdate with:message.error]; } + [self emit:ARTChannelEventUpdate with:[[ARTChannelStateChange alloc] initWithCurrent:self.state previous:self.state reason:message.error]]; return; } @@ -448,7 +449,7 @@ - (void)setFailed:(ARTStatus *)error { - (void)setSuspended:(ARTStatus *)error { [self failQueuedMessages:error]; - [self transition:ARTRealtimeChannelDetached status:error]; + [self transition:ARTRealtimeChannelSuspended status:error]; } - (void)onMessage:(ARTProtocolMessage *)message { @@ -463,7 +464,7 @@ - (void)onMessage:(ARTProtocolMessage *)message { ARTErrorInfo *errorInfo = [ARTErrorInfo wrap:(ARTErrorInfo *)error.userInfo[NSLocalizedFailureReasonErrorKey] prepend:@"Failed to decode data: "]; [self.logger error:@"R:%p C:%p %@", _realtime, self, errorInfo.message]; _errorReason = errorInfo; - [self emit:ARTChannelEventUpdate with:errorInfo]; + [self emit:ARTChannelEventUpdate with:[[ARTChannelStateChange alloc] initWithCurrent:self.state previous:self.state reason:errorInfo]]; } } diff --git a/Source/ARTTypes.h b/Source/ARTTypes.h index 4ab1eeadf..915e6cc6c 100644 --- a/Source/ARTTypes.h +++ b/Source/ARTTypes.h @@ -76,6 +76,7 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeChannelState) { ARTRealtimeChannelAttached, ARTRealtimeChannelDetaching, ARTRealtimeChannelDetached, + ARTRealtimeChannelSuspended, ARTRealtimeChannelFailed }; @@ -90,12 +91,14 @@ typedef NS_ENUM(NSUInteger, ARTChannelEvent) { ARTChannelEventAttached, ARTChannelEventDetaching, ARTChannelEventDetached, + ARTChannelEventSuspended, ARTChannelEventFailed, ARTChannelEventUpdate }; NSString *__art_nonnull ARTChannelEventToStr(ARTChannelEvent event); + typedef NS_ENUM(NSInteger, ARTDataQueryError) { ARTDataQueryErrorLimit = 1, ARTDataQueryErrorTimestampRange = 2, @@ -144,6 +147,28 @@ NSString *generateNonce(); @end +#pragma mark - ARTChannelStateChange + +@interface ARTChannelStateChange : NSObject + +- (instancetype)initWithCurrent:(ARTRealtimeChannelState)current + previous:(ARTRealtimeChannelState)previous + reason:(ARTErrorInfo *__art_nullable)reason; + +- (instancetype)initWithCurrent:(ARTRealtimeChannelState)current + previous:(ARTRealtimeChannelState)previous + reason:(ARTErrorInfo *__art_nullable)reason + resumed:(BOOL)resumed; + +@property (readonly, nonatomic) ARTRealtimeChannelState current; +@property (readonly, nonatomic) ARTRealtimeChannelState previous; +@property (readonly, nonatomic, art_nullable) ARTErrorInfo *reason; +@property (readonly, nonatomic) BOOL resumed; + +@end + +#pragma mark - ARTJsonCompatible + @protocol ARTJsonCompatible - (NSDictionary *__art_nullable)toJSON:(NSError *__art_nullable *__art_nullable)error; @end diff --git a/Source/ARTTypes.m b/Source/ARTTypes.m index a00c8ca25..9c5c56851 100644 --- a/Source/ARTTypes.m +++ b/Source/ARTTypes.m @@ -115,6 +115,33 @@ - (void)setRetryIn:(NSTimeInterval)retryIn { } } +#pragma mark - ARTChannelStateChange + +@implementation ARTChannelStateChange + +- (instancetype)initWithCurrent:(ARTRealtimeChannelState)current previous:(ARTRealtimeChannelState)previous reason:(ARTErrorInfo *)reason { + return [self initWithCurrent:current previous:previous reason:reason resumed:NO]; +} + +- (instancetype)initWithCurrent:(ARTRealtimeChannelState)current previous:(ARTRealtimeChannelState)previous reason:(ARTErrorInfo *)reason resumed:(BOOL)resumed { + self = [self init]; + if (self) { + _current = current; + _previous = previous; + _reason = reason; + _resumed = resumed; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ - \n\t current: %@; \n\t previous: %@; \n\t reason: %@; \n\t resumed: %d; \n", [super description], ARTRealtimeChannelStateToStr(_current), ARTRealtimeChannelStateToStr(_previous), _reason, _resumed]; +} + +@end + +#pragma mark - ARTJsonCompatible + @implementation NSString (ARTJsonCompatible) - (NSDictionary *)toJSON:(NSError *__art_nullable *__art_nullable)error { @@ -158,7 +185,7 @@ - (NSString *)description { @end NSString *ARTRealtimeChannelStateToStr(ARTRealtimeChannelState state) { - switch(state) { + switch (state) { case ARTRealtimeChannelInitialized: return @"Initialized"; //0 case ARTRealtimeChannelAttaching: @@ -169,13 +196,15 @@ - (NSString *)description { return @"Detaching"; //3 case ARTRealtimeChannelDetached: return @"Detached"; //4 + case ARTRealtimeChannelSuspended: + return @"Suspended"; //5 case ARTRealtimeChannelFailed: - return @"Failed"; //5 + return @"Failed"; //6 } } NSString *ARTChannelEventToStr(ARTChannelEvent event) { - switch(event) { + switch (event) { case ARTChannelEventInitialized: return @"Initialized"; //0 case ARTChannelEventAttaching: @@ -186,9 +215,11 @@ - (NSString *)description { return @"Detaching"; //3 case ARTChannelEventDetached: return @"Detached"; //4 + case ARTChannelEventSuspended: + return @"Suspended"; //5 case ARTChannelEventFailed: - return @"Failed"; //5 + return @"Failed"; //6 case ARTChannelEventUpdate: - return @"Update"; //6 + return @"Update"; //7 } } diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index fcf0e7bbf..c1215089b 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -491,8 +491,8 @@ class RealtimeClient: QuickSpec { let channel = client.channels.get("foo") waitUntil(timeout: testTimeout) { done in - channel.once(.Failed) { error in - guard let error = error else { + channel.once(.Failed) { stateChange in + guard let error = stateChange?.reason else { fail("Error is nil"); done(); return } expect(error.message).to(contain("Channel denied access based on given capability")) @@ -552,11 +552,11 @@ class RealtimeClient: QuickSpec { // Retry Channel attach waitUntil(timeout: testTimeout) { done in - channel.once(.Failed) { error in + channel.once(.Failed) { _ in fail("Should not reach Failed state"); done(); return } - channel.once(.Attached) { error in - expect(error).to(beNil()) + channel.once(.Attached) { stateChange in + expect(stateChange?.reason).to(beNil()) done() } channel.attach() @@ -587,8 +587,8 @@ class RealtimeClient: QuickSpec { waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(2, done: done) - channel.once(.Failed) { error in - guard let error = error else { + channel.once(.Failed) { stateChange in + guard let error = stateChange?.reason else { fail("ErrorInfo is nil"); partialDone(); return } expect(error).to(beIdenticalTo(channel.errorReason)) diff --git a/Spec/RealtimeClientChannel.swift b/Spec/RealtimeClientChannel.swift index ca05891a9..6b88b0536 100644 --- a/Spec/RealtimeClientChannel.swift +++ b/Spec/RealtimeClientChannel.swift @@ -77,7 +77,7 @@ class RealtimeClientChannel: QuickSpec { } // RTL2 - context("EventEmitter and states") { + context("EventEmitter, channel states and events") { // RTL2a it("should implement the EventEmitter and emit events for state changes") { @@ -103,16 +103,25 @@ class RealtimeClientChannel: QuickSpec { emitCounter += 1 } - var states = [ARTRealtimeChannelState]() + var states = [channel.state] waitUntil(timeout: testTimeout) { done in - channel.on { errorInfo in - states += [channel.state] - switch channel.state { + channel.on { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); done(); return + } + expect(stateChange.previous).to(equal(states.last)) + expect(channel.state).to(equal(stateChange.current)) + states += [stateChange.current] + + switch stateChange.current { case .Attached: + expect(stateChange.reason).to(beNil()) channel.detach() case .Detached: - channel.onError(AblyTests.newErrorProtocolMessage()) - case .Failed: + guard let error = stateChange.reason else { + fail("Detach state change reason is nil"); done(); return + } + expect(error.message).to(contain("channel has detached")) done() default: break @@ -120,22 +129,119 @@ class RealtimeClientChannel: QuickSpec { } channel.attach() } - channel.off() expect(channelOnMethodCalled).to(beTrue()) expect(statesEventEmitterOnMethodCalled).to(beTrue()) - expect(emitCounter).to(equal(5)) + expect(emitCounter).to(equal(4)) if states.count != 5 { fail("Expecting 5 states; got \(states)") return } - expect(states[0].rawValue).to(equal(ARTRealtimeChannelState.Attaching.rawValue), description: "Should be ATTACHING state") - expect(states[1].rawValue).to(equal(ARTRealtimeChannelState.Attached.rawValue), description: "Should be ATTACHED state") - expect(states[2].rawValue).to(equal(ARTRealtimeChannelState.Detaching.rawValue), description: "Should be DETACHING state") - expect(states[3].rawValue).to(equal(ARTRealtimeChannelState.Detached.rawValue), description: "Should be DETACHED state") - expect(states[4].rawValue).to(equal(ARTRealtimeChannelState.Failed.rawValue), description: "Should be FAILED state") + expect(states[0].rawValue).to(equal(ARTRealtimeChannelState.Initialized.rawValue), description: "Should be INITIALIZED state") + expect(states[1].rawValue).to(equal(ARTRealtimeChannelState.Attaching.rawValue), description: "Should be ATTACHING state") + expect(states[2].rawValue).to(equal(ARTRealtimeChannelState.Attached.rawValue), description: "Should be ATTACHED state") + expect(states[3].rawValue).to(equal(ARTRealtimeChannelState.Detaching.rawValue), description: "Should be DETACHING state") + expect(states[4].rawValue).to(equal(ARTRealtimeChannelState.Detached.rawValue), description: "Should be DETACHED state") + } + + // RTL2a + it("should implement the EventEmitter and emit events for FAILED state changes") { + let options = AblyTests.clientOptions() + options.token = getTestToken(capability: "{\"secret\":[\"subscribe\"]}") + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("test") + + waitUntil(timeout: testTimeout) { done in + channel.on { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); done(); return + } + expect(channel.state).to(equal(stateChange.current)) + switch stateChange.current { + case .Attaching: + expect(stateChange.reason).to(beNil()) + expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Initialized)) + case .Failed: + guard let reason = stateChange.reason else { + fail("Reason is nil"); done(); return + } + expect(reason.code) == 40160 + expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Attaching)) + done() + default: + break + } + } + channel.attach() + } + } + + // RTL2a + it("should implement the EventEmitter and emit events for SUSPENDED state changes") { + let client = ARTRealtime(options: AblyTests.commonAppSetup()) + defer { client.dispose(); client.close() } + let channel = client.channels.get("test") + + waitUntil(timeout: testTimeout) { done in + channel.attach() { error in + expect(error).to(beNil()) + done() + } + } + + client.simulateSuspended(beforeSuspension: { done in + channel.once(.Suspended) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStageChange is nil"); done(); return + } + expect(stateChange.reason).to(beNil()) + expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Attached)) + expect(channel.state).to(equal(stateChange.current)) + done() + } + }) + } + + // RTL2g + it("can emit an UPDATE event") { + let client = ARTRealtime(options: AblyTests.commonAppSetup()) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + waitUntil(timeout: testTimeout) { done in + channel.attach() { error in + expect(error).to(beNil()) + done() + } + } + + channel.on(.Attached) { _ in + fail("Should not emit Attached again") + } + defer { + channel.off() + } + + waitUntil(timeout: testTimeout) { done in + channel.on(.Update) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); done(); return + } + expect(channel.state).to(equal(ARTRealtimeChannelState.Attached)) + expect(stateChange.previous).to(equal(channel.state)) + expect(stateChange.current).to(equal(channel.state)) + expect(stateChange.resumed).to(beFalse()) + expect(stateChange.reason).to(beNil()) + done() + } + + let attachedMessage = ARTProtocolMessage() + attachedMessage.action = .Attached + attachedMessage.channel = "foo" + client.transport?.receive(attachedMessage) + } } // RTL2b @@ -157,15 +263,96 @@ class RealtimeClientChannel: QuickSpec { defer { client.dispose(); client.close() } let channel = client.channels.get("test") - let error = AblyTests.newErrorProtocolMessage() + let pmError = AblyTests.newErrorProtocolMessage() + waitUntil(timeout: testTimeout) { done in + channel.on(.Failed) { stateChange in + guard let error = stateChange?.reason else { + fail("Error is nil"); done(); return + } + expect(error).to(equal(pmError.error)) + expect(channel.errorReason).to(equal(pmError.error)) + done() + } + channel.onError(pmError) + } + } + + // RTL2d + it("a ChannelStateChange is emitted as the first argument for every channel state change") { + let client = ARTRealtime(options: AblyTests.commonAppSetup()) + defer { client.dispose(); client.close() } + let channel = client.channels.get("test") + + channel.on { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStageChange is nil"); return + } + expect(stateChange.reason).to(beNil()) + expect(stateChange.current.rawValue).to(equal(channel.state.rawValue)) + expect(stateChange.previous.rawValue).toNot(equal(channel.state.rawValue)) + } + + channel.attach() + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) + channel.off() + + waitUntil(timeout: testTimeout) { done in + channel.once(.Failed) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStageChange is nil"); done(); return + } + expect(stateChange.reason).toNot(beNil()) + expect(stateChange.current).to(equal(ARTRealtimeChannelState.Failed)) + expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Attached)) + done() + } + channel.onError(AblyTests.newErrorProtocolMessage()) + } + } + + // RTL2f + pending("ChannelStateChange will contain a resumed boolean attribute with value @true@ if the bit flag RESUMED was included") { + let options = AblyTests.commonAppSetup() + options.disconnectedRetryTimeout = 1.0 + options.tokenDetails = getTestTokenDetails(ttl: 5.0) + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("test") + + waitUntil(timeout: testTimeout) { done in + channel.on { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStageChange is nil"); done(); return + } + switch stateChange.current { + case .Attached: + expect(stateChange.resumed).to(beFalse()) + default: + expect(stateChange.resumed).to(beFalse()) + } + } + client.connection.once(.Disconnected) { stateChange in + channel.off() + guard let error = stateChange?.reason else { + fail("Error is nil"); done(); return + } + expect(error.code) == 40142 + done() + } + channel.attach() + } waitUntil(timeout: testTimeout) { done in - channel.on(.Failed) { errorInfo in - expect(errorInfo).to(equal(error.error)) - expect(channel.errorReason).to(equal(error.error)) + channel.once(.Attached) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStageChange is nil"); done(); return + } + expect(stateChange.resumed).to(beTrue()) + expect(stateChange.reason).to(beNil()) + expect(stateChange.current).to(equal(ARTRealtimeChannelState.Attached)) + expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Attached)) done() } - channel.onError(error) } } @@ -194,28 +381,14 @@ class RealtimeClientChannel: QuickSpec { waitUntil(timeout: testTimeout) { done in let pmError = AblyTests.newErrorProtocolMessage() - - channel.on { stateChange in - let partialDone = AblyTests.splitDone(2, done: done) - - guard let stateChange = stateChange else { - fail("ChannelStateChange is nil"); partialDone(); return - } - guard let error = stateChange.reason else { - fail("Reason error is nil"); partialDone(); return - } - - if stateChange.current == .Failed { - expect(error).to(equal(pmError.error)) - expect(channel.errorReason).to(beIdenticalTo(error)) - partialDone() - } - else if stateChange.current == .Error { - expect(error).to(equal(pmError.error)) - partialDone() + channel.once(.Failed) { stateChange in + guard let error = stateChange?.reason else { + fail("Reason error is nil"); done(); return } + expect(error).to(equal(pmError.error)) + expect(channel.errorReason).to(beIdenticalTo(error)) + done() } - client.onError(pmError) } @@ -232,28 +405,14 @@ class RealtimeClientChannel: QuickSpec { waitUntil(timeout: testTimeout) { done in let pmError = AblyTests.newErrorProtocolMessage() - - channel.on { stateChange in - let partialDone = AblyTests.splitDone(2, done: done) - - guard let stateChange = stateChange else { - fail("ChannelStateChange is nil"); partialDone(); return - } - guard let error = stateChange.reason else { - fail("Reason error is nil"); partialDone(); return - } - - if stateChange.current == .Failed { - expect(error).to(equal(pmError.error)) - expect(channel.errorReason).to(beIdenticalTo(error)) - partialDone() - } - else if stateChange.current == .Error { - expect(error).to(equal(pmError.error)) - partialDone() + channel.once(.Failed) { stateChange in + guard let error = stateChange?.reason else { + fail("Reason error is nil"); done(); return } + expect(error).to(equal(pmError.error)) + expect(channel.errorReason).to(equal(error)) + done() } - client.onError(pmError) } @@ -340,7 +499,7 @@ class RealtimeClientChannel: QuickSpec { } // RTL3d - it("if the connection state enters the @CONNECTED@ state, then a @SUSPENDED@ channel will initiate an attach operation") { + it("if the connection state enters the CONNECTED state, then a SUSPENDED channel will initiate an attach operation") { let options = AblyTests.commonAppSetup() options.suspendedRetryTimeout = 1.0 let client = ARTRealtime(options: options) @@ -354,26 +513,20 @@ class RealtimeClientChannel: QuickSpec { } } - waitUntil(timeout: testTimeout) { done in + client.simulateSuspended(beforeSuspension: { done in channel.once(.Suspended) { stateChange in expect(stateChange?.reason).to(beNil()) done() } - client.simulateSuspended() - } + }) expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) } // RTL3d - it("if the attach operation for the channel times out and the channel returns to the SUSPENDED state, then an ERROR event with Ably error code 91200 should be emitted") { - let options = AblyTests.commonAppSetup() - options.autoConnect = false - options.suspendedRetryTimeout = 1.0 - let client = ARTRealtime(options: options) - client.setTransportClass(TestProxyTransport.self) - client.connect() + it("if the attach operation for the channel times out and the channel returns to the SUSPENDED state") { + let client = AblyTests.newRealtime(AblyTests.commonAppSetup()) defer { client.dispose(); client.close() } let channel = client.channels.get("test") @@ -384,31 +537,15 @@ class RealtimeClientChannel: QuickSpec { } } - defer { - TestProxyTransport.network = nil - } - waitUntil(timeout: testTimeout) { done in - let partialDone = AblyTests.splitDone(2, done: done) - channel.on { stateChange in + client.simulateSuspended(beforeSuspension: { done in + channel.once(.Suspended) { stateChange in guard let stateChange = stateChange else { - fail("ChannelStateChange is nil"); partialDone(); return - } - switch stateChange.current { - case .Suspended: - expect(stateChange.reason).to(beNil()) - partialDone() - case .Error: - guard let error = stateChange.reason else { - fail("Error is nil"); partialDone(); return - } - expect(error.code) == 91200 - partialDone() + fail("ChannelStateChange is nil"); done(); return } + expect(stateChange.reason).to(beNil()) + done() } - client.simulateSuspended() - // Force connection to timeout - TestProxyTransport.network = .RequestTimeout(timeout: options.suspendedRetryTimeout + 1) - } + }) } // RTL3e @@ -423,6 +560,9 @@ class RealtimeClientChannel: QuickSpec { channel.once(.Detached) { stateChange in fail("Should not reach the DETACHED state") } + defer { + channel.off() + } waitUntil(timeout: testTimeout) { done in channel.attach() { error in @@ -694,13 +834,17 @@ class RealtimeClientChannel: QuickSpec { let channel = client.channels.get("test") channel.attach() - channel.on { errorInfo in - if channel.state == .Failed { - expect(errorInfo!.code).to(equal(40160)) + waitUntil(timeout: testTimeout) { done in + channel.once(.Failed) { stateChange in + guard let error = stateChange?.reason else { + fail("Reason error is nil"); done(); return + } + expect(error.code).to(equal(40160)) + done() } } - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Failed), timeout: testTimeout) + expect(channel.state).to(equal(ARTRealtimeChannelState.Failed)) } // RTL4f @@ -1143,8 +1287,11 @@ class RealtimeClientChannel: QuickSpec { let error = stateChange.reason if state == .Connected { let channel = client.channels.get("test") - channel.on { errorInfo in - if channel.state == .Attached { + channel.on { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStageChange is nil"); done(); return + } + if stateChange.current == .Attached { channel.publish(nil, data: "message") { errorInfo in expect(errorInfo).to(beNil()) done() @@ -1170,8 +1317,11 @@ class RealtimeClientChannel: QuickSpec { let error = stateChange.reason if state == .Connected { let channel = client.channels.get("test") - channel.on { errorInfo in - if channel.state == .Attached { + channel.on { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStageChange is nil"); done(); return + } + if stateChange.current == .Attached { channel.publish(nil, data: "message") { errorInfo in expect(errorInfo).toNot(beNil()) guard let errorInfo = errorInfo else { @@ -1206,8 +1356,11 @@ class RealtimeClientChannel: QuickSpec { TotalMessages.failed = 0 let channelToSucceed = client.channels.get("channelToSucceed") - channelToSucceed.on { errorInfo in - if channelToSucceed.state == .Attached { + channelToSucceed.on { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStageChange is nil"); return + } + if stateChange.current == .Attached { for index in 1...TotalMessages.expected { channelToSucceed.publish(nil, data: "message\(index)") { errorInfo in if errorInfo == nil { @@ -1221,8 +1374,11 @@ class RealtimeClientChannel: QuickSpec { channelToSucceed.attach() let channelToFail = client.channels.get("channelToFail") - channelToFail.on { errorInfo in - if channelToFail.state == .Attached { + channelToFail.on { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStageChange is nil"); return + } + if stateChange.current == .Attached { for index in 1...TotalMessages.expected { channelToFail.publish(nil, data: "message\(index)") { errorInfo in if errorInfo != nil { @@ -1989,6 +2145,7 @@ class RealtimeClientChannel: QuickSpec { waitUntil(timeout: testTimeout) { done in let partlyDone = AblyTests.splitDone(2, done: done) + channel.subscribe(testMessage.encoded.name) { message in expect(message.data as? NSObject).to(equal(AblyTests.base64ToData(testMessage.encrypted.data))) @@ -2000,13 +2157,12 @@ class RealtimeClientChannel: QuickSpec { partlyDone() } - channel.on(.Update) { errorInfo in - guard let errorInfo = errorInfo else { + channel.on(.Update) { stateChange in + guard let error = stateChange?.reason else { return } - expect(errorInfo.message).to(contain("Failed to decode data: unknown encoding: 'bad_encoding_type'")) - expect(errorInfo).to(beIdenticalTo(channel.errorReason)) - + expect(error.message).to(contain("Failed to decode data: unknown encoding: 'bad_encoding_type'")) + expect(error).to(beIdenticalTo(channel.errorReason)) partlyDone() } @@ -2414,6 +2570,9 @@ class RealtimeClientChannel: QuickSpec { channel.on(.Attached) { _ in fail("Should not be called") } + defer { + channel.off() + } var hook: AspectToken? waitUntil(timeout: testTimeout) { done in @@ -2425,9 +2584,8 @@ class RealtimeClientChannel: QuickSpec { done() } - let transport = client.transport as! TestProxyTransport // Inject additional ATTACHED action without an error - transport.receive(attachedMessage) + client.transport?.receive(attachedMessage) } hook!.remove() expect(channel.errorReason).to(beNil()) @@ -2438,15 +2596,17 @@ class RealtimeClientChannel: QuickSpec { attachedMessageWithError.action = .Attached attachedMessageWithError.channel = "test" - channel.once(.Update) { error in - expect(error).to(beIdenticalTo(attachedMessageWithError.error)) - expect(channel.errorReason).to(beIdenticalTo(error)) + channel.once(.Update) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); done(); return + } + expect(stateChange.reason).to(beIdenticalTo(attachedMessageWithError.error)) + expect(channel.errorReason).to(beIdenticalTo(stateChange.reason)) done() } - let transport = client.transport as! TestProxyTransport // Inject additional ATTACHED action with an error - transport.receive(attachedMessageWithError) + client.transport?.receive(attachedMessageWithError) } expect(channel.state).to(equal(ARTRealtimeChannelState.Attached)) } @@ -2468,18 +2628,16 @@ class RealtimeClientChannel: QuickSpec { errorProtocolMessage.action = .Error errorProtocolMessage.channel = "foo" - let partialDone = AblyTests.splitDone(2, done: done) - channel.once(.Failed) { stateChange in guard let error = stateChange?.reason else { - fail("Reason error is nil"); partialDone(); return + fail("Reason error is nil"); done(); return } expect(error).to(beIdenticalTo(errorProtocolMessage.error)) expect(channel.errorReason).to(beIdenticalTo(error)) - partialDone() + done() } - client.transport.receive(errorProtocolMessage) + client.transport?.receive(errorProtocolMessage) } expect(channel.state).to(equal(ARTRealtimeChannelState.Failed)) diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index 6aabc89c4..244197243 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -335,7 +335,7 @@ class RealtimeClientConnection: QuickSpec { let authMessage = ARTProtocolMessage() authMessage.action = .Auth - client.transport.receive(authMessage) + client.transport?.receive(authMessage) } } @@ -524,7 +524,7 @@ class RealtimeClientConnection: QuickSpec { let connectedMessageWithError = originalConnectedMessage connectedMessageWithError.error = ARTErrorInfo.createWithCode(1234, message: "fabricated error") - client.transport.receive(connectedMessageWithError) + client.transport?.receive(connectedMessageWithError) } } } @@ -555,8 +555,11 @@ class RealtimeClientConnection: QuickSpec { disposable.append(client) let channel = client.channels.get(channelName) - channel.on { errorInfo in - if channel.state == .Attached { + channel.on { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStageChange is nil"); return + } + if stateChange.current == .Attached { TotalReach.shared += 1 } } @@ -681,15 +684,13 @@ class RealtimeClientConnection: QuickSpec { let error = stateChange.reason if state == .Connected { let channel = client.channels.get("test") - channel.on { errorInfo in - if channel.state == .Attached { - channel.presence.enterClient("client_string", data: nil, callback: { errorInfo in - expect(errorInfo).to(beNil()) - done() - }) - } + channel.attach() { error in + expect(error).to(beNil()) + channel.presence.enterClient("client_string", data: nil, callback: { errorInfo in + expect(errorInfo).to(beNil()) + done() + }) } - channel.attach() } } } @@ -752,15 +753,13 @@ class RealtimeClientConnection: QuickSpec { let error = stateChange.reason if state == .Connected { let channel = client.channels.get("test") - channel.on { errorInfo in - if channel.state == .Attached { - channel.presence.enterClient("invalid", data: nil, callback: { errorInfo in - expect(errorInfo).toNot(beNil()) - done() - }) - } + channel.attach() { error in + expect(error).to(beNil()) + channel.presence.enterClient("invalid", data: nil, callback: { errorInfo in + expect(errorInfo).toNot(beNil()) + done() + }) } - channel.attach() } } } @@ -868,19 +867,17 @@ class RealtimeClientConnection: QuickSpec { transport.actionsIgnored += [.Ack, .Nack] waitUntil(timeout: testTimeout) { done in - channel.on { errorInfo in - if channel.state == .Attached { - channel.publish(nil, data: "message", callback: { errorInfo in - expect(errorInfo).toNot(beNil()) - done() - }) - // Wait until the message is pushed to Ably first - delay(1.0) { - transport.simulateIncomingNormalClose() - } + channel.attach() { error in + expect(error).to(beNil()) + channel.publish(nil, data: "message", callback: { errorInfo in + expect(errorInfo).toNot(beNil()) + done() + }) + // Wait until the message is pushed to Ably first + delay(1.0) { + transport.simulateIncomingNormalClose() } } - channel.attach() } } @@ -898,19 +895,17 @@ class RealtimeClientConnection: QuickSpec { transport.actionsIgnored += [.Ack, .Nack] waitUntil(timeout: testTimeout) { done in - channel.on { errorInfo in - if channel.state == .Attached { - channel.publish(nil, data: "message", callback: { errorInfo in - expect(errorInfo).toNot(beNil()) - done() - }) - // Wait until the message is pushed to Ably first - delay(1.0) { - transport.simulateIncomingError() - } + channel.attach() { error in + expect(error).to(beNil()) + channel.publish(nil, data: "message", callback: { errorInfo in + expect(errorInfo).toNot(beNil()) + done() + }) + // Wait until the message is pushed to Ably first + delay(1.0) { + transport.simulateIncomingError() } } - channel.attach() } } @@ -2095,9 +2090,9 @@ class RealtimeClientConnection: QuickSpec { } waitUntil(timeout: testTimeout) { done in - channel.once(.Attached) { error in - guard let error = error else { - fail("Error is nil"); done(); return + channel.once(.Attached) { stateChange in + guard let error = stateChange?.reason else { + fail("Reason error is nil"); done(); return } expect(error.message).to(equal("Channel injected error")) expect(channel.errorReason).to(beIdenticalTo(error)) @@ -3094,140 +3089,6 @@ class RealtimeClientConnection: QuickSpec { } - // RTN18 - context("state change side effects") { - - // RTN18a - it("when a connection enters the DISCONNECTED state, it will have no effect on the the channel states") { - let options = AblyTests.commonAppSetup() - let client = ARTRealtime(options: options) - defer { - client.dispose() - client.close() - } - - let channel = client.channels.get("test") - channel.attach() - - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) - - client.onDisconnected() - - expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Disconnected)) - expect(channel.state).to(equal(ARTRealtimeChannelState.Attached)) - - waitUntil(timeout: testTimeout + options.disconnectedRetryTimeout) { done in - channel.publish(nil, data: "queuedMessage", callback: { errorInfo in - expect(errorInfo).to(beNil()) - done() - }) - } - expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Connected)) - } - - // RTN18b - context("all channels will move to DETACHED state") { - - it("when a connection enters SUSPENDED state") { - let options = AblyTests.commonAppSetup() - options.suspendedRetryTimeout = 0.1 - let client = ARTRealtime(options: options) - defer { - client.dispose() - client.close() - } - - let channel = client.channels.get("test") - channel.attach() - - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) - - client.simulateSuspended() - - expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Suspended)) - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Detached), timeout: testTimeout) - - waitUntil(timeout: testTimeout) { done in - // Reject publishing of messages - channel.publish(nil, data: "message", callback: { errorInfo in - expect(errorInfo).toNot(beNil()) - expect(errorInfo!.code).to(equal(90001)) - done() - }) - } - - expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connecting), timeout: options.suspendedRetryTimeout + 1.0) - channel.attach() - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) - - waitUntil(timeout: testTimeout) { done in - // Accept publishing of messages - channel.publish(nil, data: "message", callback: { errorInfo in - expect(errorInfo).to(beNil()) - done() - }) - } - } - - it("when a connection enters CLOSED state") { - let options = AblyTests.commonAppSetup() - let client = ARTRealtime(options: options) - defer { - client.dispose() - client.close() - } - let channel = client.channels.get("test") - channel.attach() - - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) - - client.close() - - expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Closed), timeout: testTimeout) - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Detached), timeout: testTimeout) - - waitUntil(timeout: testTimeout) { done in - // Reject publishing of messages - channel.publish(nil, data: "message", callback: { errorInfo in - expect(errorInfo).toNot(beNil()) - expect(errorInfo!.code).to(equal(90001)) - done() - }) - } - } - - } - - // RTN18c - it("when a connection enters FAILED state, all channels will move to FAILED state") { - let options = AblyTests.commonAppSetup() - let client = ARTRealtime(options: options) - defer { - client.dispose() - client.close() - } - let channel = client.channels.get("test") - channel.attach() - - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) - - client.onError(AblyTests.newErrorProtocolMessage()) - - expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Failed)) - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Failed), timeout: testTimeout) - - waitUntil(timeout: testTimeout) { done in - // Reject publishing of messages - channel.publish(nil, data: "message", callback: { errorInfo in - expect(errorInfo).toNot(beNil()) - expect(errorInfo!.code).to(equal(90001)) - done() - }) - } - } - - } - // RTN19 it("attributes within ConnectionDetails should be used as defaults") { let options = AblyTests.commonAppSetup() @@ -3496,7 +3357,7 @@ class RealtimeClientConnection: QuickSpec { } waitUntil(timeout: testTimeout) { done in - client.connection.once(.Connected) { stateChange in + client.connection.once(.Update) { stateChange in expect(stateChange?.reason).to(beNil()) expect(initialToken).toNot(equal(client.auth.tokenDetails?.token)) done() @@ -3689,7 +3550,7 @@ class RealtimeClientConnection: QuickSpec { let connectedMessageWithError = originalConnectedMessage connectedMessageWithError.error = ARTErrorInfo.createWithCode(1234, message: "fabricated error") - client.transport.receive(connectedMessageWithError) + client.transport?.receive(connectedMessageWithError) } expect(client.connection.errorReason).to(beNil()) diff --git a/Spec/TestUtilities.swift b/Spec/TestUtilities.swift index 67e737fcd..d95fa26cc 100644 --- a/Spec/TestUtilities.swift +++ b/Spec/TestUtilities.swift @@ -356,14 +356,14 @@ class PublishTestMessage { let state = stateChange.current if state == .Connected { let channel = client.channels.get("test") - channel.on { errorInfo in - switch channel.state { + channel.on { stateChange in + switch stateChange!.current { case .Attached: channel.publish(nil, data: "message") { errorInfo in complete(errorInfo) } case .Failed: - complete(errorInfo) + complete(stateChange!.reason) default: break } @@ -992,13 +992,13 @@ extension ARTRealtime { self.onDisconnected() } - func simulateSuspended() { + func simulateSuspended(beforeSuspension beforeSuspensionCallback: (done: () -> ()) -> Void) { waitUntil(timeout: testTimeout) { done in - self.connection.on(.Closed) { _ in + self.connection.once(.Disconnected) { _ in + beforeSuspensionCallback(done: done) self.onSuspended() - done() } - self.close() + self.onDisconnected() } } @@ -1067,7 +1067,7 @@ extension ARTRealtimeConnectionEvent : CustomStringConvertible { extension ARTProtocolMessageAction : CustomStringConvertible { public var description : String { - return ARTRealtime.protocolStr(self) + return ARTProtocolMessageActionToStr(self) } } diff --git a/Tests/ARTRealtimeAttachTest.m b/Tests/ARTRealtimeAttachTest.m index f1e755a8f..c08ec288b 100644 --- a/Tests/ARTRealtimeAttachTest.m +++ b/Tests/ARTRealtimeAttachTest.m @@ -43,13 +43,13 @@ - (void)testAttachOnce { ARTRealtimeChannel *channel = [realtime.channels get:@"attach"]; __block bool hasAttached = false; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttaching) { - XCTAssertNil(errorInfo); + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttaching) { + XCTAssertNil(stateChange.reason); [channel attach]; } - if (channel.state == ARTRealtimeChannelAttached) { - XCTAssertNil(errorInfo); + if (stateChange.current == ARTRealtimeChannelAttached) { + XCTAssertNil(stateChange.reason); [channel attach]; if(!hasAttached) { @@ -60,7 +60,7 @@ - (void)testAttachOnce { XCTFail(@"duplicate call to attach shouldnt happen"); } } - if (channel.state == ARTRealtimeChannelDetached) { + if (stateChange.current == ARTRealtimeChannelDetached) { [expectation fulfill]; } }]; @@ -80,13 +80,13 @@ - (void)testAttachMultipleChannels { [channel1 attach]; ARTRealtimeChannel *channel2 = [realtime.channels get:@"test_attach_multiple2"]; [channel2 attach]; - [channel1 on:^(ARTErrorInfo *errorInfo) { - if (channel1.state == ARTRealtimeChannelAttached) { + [channel1 on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [expectation1 fulfill]; } }]; - [channel2 on:^(ARTErrorInfo *errorInfo) { - if (channel2.state == ARTRealtimeChannelAttached) { + [channel2 on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [expectation2 fulfill]; } }]; @@ -102,11 +102,11 @@ - (void)testDetach { ARTRealtimeConnectionState state = stateChange.current; if (state == ARTRealtimeConnected) { ARTRealtimeChannel *channel = [realtime.channels get:@"detach"]; - [channel on:^(ARTErrorInfo *errorInfo) { - if (channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel detach]; } - else if(channel.state == ARTRealtimeChannelDetached) { + else if (stateChange.current == ARTRealtimeChannelDetached) { [expectation fulfill]; } }]; @@ -126,14 +126,14 @@ - (void)testDetaching { ARTRealtimeConnectionState state = stateChange.current; if (state == ARTRealtimeConnected) { ARTRealtimeChannel *channel = [realtime.channels get:@"detach"]; - [channel on:^(ARTErrorInfo *errorInfo) { - if (channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel detach]; } - else if(channel.state == ARTRealtimeChannelDetaching) { + else if (stateChange.current == ARTRealtimeChannelDetaching) { detachingHit = YES; } - else if(channel.state == ARTRealtimeChannelDetached) { + else if (stateChange.current == ARTRealtimeChannelDetached) { if(detachingHit) { [expectation fulfill]; } @@ -154,18 +154,18 @@ - (void)testSkipsFromAttachingToDetaching { __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel = [realtime.channels get:@"attaching_to_detaching"]; - [channel on:^(ARTErrorInfo *errorInfo) { - if (channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { XCTFail(@"Should not have made it to attached"); } - else if( channel.state == ARTRealtimeChannelAttaching) { + else if (stateChange.current == ARTRealtimeChannelAttaching) { [channel detach]; } - else if(channel.state == ARTRealtimeChannelDetaching) { + else if (stateChange.current == ARTRealtimeChannelDetaching) { [channel off]; [expectation fulfill]; } - else if(channel.state == ARTRealtimeChannelDetached) { + else if (stateChange.current == ARTRealtimeChannelDetached) { XCTFail(@"Should not have made it to detached"); } @@ -184,15 +184,14 @@ -(void)testDetachingIgnoresDetach { if (state == ARTRealtimeConnected) { ARTRealtimeChannel *channel = [realtime.channels get:@"testDetachingIgnoresDetach"]; - [channel on:^(ARTErrorInfo *errorInfo) { - - if (channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel detach]; } - if( channel.state == ARTRealtimeChannelDetaching) { + if (stateChange.current == ARTRealtimeChannelDetaching) { [channel detach]; } - if(channel.state == ARTRealtimeChannelDetached) { + if (stateChange.current == ARTRealtimeChannelDetached) { [expectation fulfill]; } }]; @@ -212,15 +211,15 @@ - (void)testAttachFailsOnFailedConnection { if (state == ARTRealtimeConnected) { ARTRealtimeChannel *channel = [realtime.channels get:@"attach"]; __block bool hasFailed = false; - [channel on:^(ARTErrorInfo *errorInfo) { - if (channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { if(!hasFailed) { - XCTAssertNil(errorInfo); + XCTAssertNil(stateChange.reason); [realtime onError:[ARTTestUtil newErrorProtocolMessage]]; } } - else if(channel.state == ARTRealtimeChannelFailed) { - XCTAssertNotNil(errorInfo); + else if (stateChange.current == ARTRealtimeChannelFailed) { + XCTAssertNotNil(stateChange.reason); [channel attach:^(ARTErrorInfo *errorInfo) { XCTAssertNotNil(errorInfo); [expectation fulfill]; @@ -253,8 +252,8 @@ - (void)testAttachRestricted { __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel = [realtime.channels get:@"some_unpermitted_channel"]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state != ARTRealtimeChannelAttaching) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current != ARTRealtimeChannelAttaching) { XCTAssertEqual(channel.state, ARTRealtimeChannelFailed); [expectation fulfill]; [channel off]; @@ -270,8 +269,8 @@ - (void)testAttachingChannelFails { __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel1 = [realtime.channels get:@"channel"]; - [channel1 on:^(ARTErrorInfo *errorInfo) { - if (channel1.state == ARTRealtimeChannelAttaching) { + [channel1 on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttaching) { [realtime onError:[ARTTestUtil newErrorProtocolMessage]]; } else { @@ -289,11 +288,11 @@ - (void)testAttachedChannelFails { __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel1 = [realtime.channels get:@"channel"]; - [channel1 on:^(ARTErrorInfo *errorInfo) { - if (channel1.state == ARTRealtimeChannelAttached) { + [channel1 on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [realtime onError:[ARTTestUtil newErrorProtocolMessage]]; } - else if(channel1.state != ARTRealtimeChannelAttaching) { + else if (stateChange.current != ARTRealtimeChannelAttaching) { XCTAssertEqual(ARTRealtimeChannelFailed, channel1.state); [expectation fulfill]; } @@ -308,11 +307,11 @@ - (void)testChannelClosesOnClose { __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel1 = [realtime.channels get:@"channel"]; - [channel1 on:^(ARTErrorInfo *errorInfo) { - if (channel1.state == ARTRealtimeChannelAttached) { + [channel1 on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [realtime close]; } - else if(channel1.state != ARTRealtimeChannelAttaching) { + else if (stateChange.current != ARTRealtimeChannelAttaching) { XCTAssertEqual(ARTRealtimeChannelDetached, channel1.state); [expectation fulfill]; } diff --git a/Tests/ARTRealtimeChannelTest.m b/Tests/ARTRealtimeChannelTest.m index d094e687a..0b0668deb 100644 --- a/Tests/ARTRealtimeChannelTest.m +++ b/Tests/ARTRealtimeChannelTest.m @@ -45,8 +45,8 @@ - (void)testAttach { ARTRealtimeConnectionState state = stateChange.current; if (state == ARTRealtimeConnected) { ARTRealtimeChannel *channel = [realtime.channels get:@"attach"]; - [channel on:^(ARTErrorInfo *errorInfo) { - if (channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [expectation fulfill]; } }]; @@ -62,8 +62,8 @@ - (void)testAttachBeforeConnect { ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel = [realtime.channels get:@"attach_before_connect"]; [channel attach]; - [channel on:^(ARTErrorInfo *errorInfo) { - if (channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [expectation fulfill]; } }]; @@ -78,12 +78,12 @@ - (void)testAttachDetach { [channel attach]; __block BOOL attached = NO; - [channel on:^(ARTErrorInfo *errorInfo) { - if (channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { attached = YES; [channel detach]; } - if (attached && channel.state == ARTRealtimeChannelDetached) { + if (attached && stateChange.current == ARTRealtimeChannelDetached) { [expectation fulfill]; } }]; @@ -98,8 +98,8 @@ - (void)testAttachDetachAttach { [channel attach]; __block BOOL attached = false; __block int attachCount = 0; - [channel on:^(ARTErrorInfo *errorInfo) { - if (channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { attachCount++; attached = true; if (attachCount == 1) { @@ -109,7 +109,7 @@ - (void)testAttachDetachAttach { [expectation fulfill]; } } - if (attached && channel.state == ARTRealtimeChannelDetached) { + if (attached && stateChange.current == ARTRealtimeChannelDetached) { [channel attach]; } }]; @@ -149,42 +149,16 @@ - (void)testSubscribeUnsubscribe { [self waitForExpectationsWithTimeout:[ARTTestUtil timeout] handler:nil]; } -- (void)testSuspendingDetachesChannel { - ARTClientOptions *options = [ARTTestUtil newSandboxApp:self withDescription:__FUNCTION__]; - __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; - ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; - ARTRealtimeChannel *channel = [realtime.channels get:@"channel"]; - __block bool gotCb=false; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { - [realtime onSuspended]; - } - else if(channel.state == ARTRealtimeChannelDetached) { - if(!gotCb) { - [channel publish:nil data:@"will_fail" callback:^(ARTErrorInfo *errorInfo) { - XCTAssertNotNil(errorInfo); - XCTAssertEqual(90001, errorInfo.code); - gotCb = true; - [realtime close]; - [expectation fulfill]; - }]; - } - } - }]; - [channel attach]; - [self waitForExpectationsWithTimeout:[ARTTestUtil timeout] handler:nil]; -} - - (void)testFailingFailsChannel { ARTClientOptions *options = [ARTTestUtil newSandboxApp:self withDescription:__FUNCTION__]; __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel = [realtime.channels get:@"channel"]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [realtime onError:[ARTTestUtil newErrorProtocolMessage]]; } - else if(channel.state == ARTRealtimeChannelFailed) { + else if (stateChange.current == ARTRealtimeChannelFailed) { [channel publish:nil data:@"will_fail" callback:^(ARTErrorInfo *errorInfo) { XCTAssertNotNil(errorInfo); [expectation fulfill]; @@ -272,8 +246,8 @@ - (void)testAttachFails { [realtime.connection on:^(ARTConnectionStateChange *stateChange) { ARTRealtimeConnectionState state = stateChange.current; if (state == ARTRealtimeConnected) { - [channel on:^(ARTErrorInfo *errorInfo) { - if (channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [realtime onError:[ARTTestUtil newErrorProtocolMessage]]; } }]; @@ -311,15 +285,15 @@ - (void)testClientIdPreserved { __block NSUInteger attached = 0; // Channel 1 - [channel on:^(ARTErrorInfo *errorInfo) { - if (channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { attached++; } }]; // Channel 2 - [channel2 on:^(ARTErrorInfo *errorInfo) { - if (channel2.state == ARTRealtimeChannelAttached) { + [channel2 on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { attached++; } }]; diff --git a/Tests/ARTRealtimeMessageTest.m b/Tests/ARTRealtimeMessageTest.m index f063c0b52..28c41baba 100644 --- a/Tests/ARTRealtimeMessageTest.m +++ b/Tests/ARTRealtimeMessageTest.m @@ -43,8 +43,8 @@ - (void)multipleSendName:(NSString *)name count:(int)count delay:(int)delay { [channel attach]; - [channel on:^(ARTErrorInfo *errorInfo) { - if (channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel subscribe:^(ARTMessage *message) { ++numReceived; if (numReceived == count) { @@ -94,15 +94,15 @@ - (void)testSingleSendEchoText { __block NSUInteger attached = 0; // Channel 1 - [channel on:^(ARTErrorInfo *errorInfo) { - if (channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { attached++; } }]; // Channel 2 - [channel2 on:^(ARTErrorInfo *errorInfo) { - if (channel2.state == ARTRealtimeChannelAttached) { + [channel2 on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { attached++; } }]; @@ -208,9 +208,9 @@ - (void)testSubscribeAttaches { ARTRealtimeChannel *channel = [realtime.channels get:@"testSubscribeAttaches"]; [channel subscribe:^(ARTMessage *message) { }]; - [channel on:^(ARTErrorInfo *errorInfo) { - XCTAssert(!errorInfo); - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + XCTAssert(!stateChange.reason); + if (stateChange.current == ARTRealtimeChannelAttached) { [expectation fulfill]; } }]; @@ -310,8 +310,8 @@ - (void)testPublishImmediate { [channel attach]; } }]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel publish:nil data:@"testString" callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); }]; diff --git a/Tests/ARTRealtimePresenceHistoryTest.m b/Tests/ARTRealtimePresenceHistoryTest.m index 2857a34ca..c912d0897 100644 --- a/Tests/ARTRealtimePresenceHistoryTest.m +++ b/Tests/ARTRealtimePresenceHistoryTest.m @@ -61,7 +61,7 @@ - (void)runTestLimit:(int)limit forwards:(bool)forwards callback:(void (^)(ARTPa ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel = [realtime.channels get:[self channelName]]; [channel attach]; - [channel once:ARTChannelEventAttached callback:^(ARTErrorInfo *errorInfo) { + [channel once:ARTChannelEventAttached callback:^(ARTChannelStateChange *stateChange) { [channel.presence enter:[self enter1Str] callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); //second enter gets treated as an update. @@ -94,8 +94,8 @@ - (void)testPresenceHistory { ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel = [realtime.channels get:@"testSimpleText"]; [channel attach]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel.presence enter:presenceEnter callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); @@ -124,8 +124,8 @@ - (void)testForward { ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel = [realtime.channels get:@"persisted:testSimpleText"]; [channel attach]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel.presence enter:presenceEnter1 callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); [channel.presence enter:presenceEnter2 callback:^(ARTErrorInfo *errorInfo) { @@ -174,8 +174,8 @@ - (void)testSecondChannel { ARTRealtime *realtime2 = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel = [realtime1.channels get:channelName]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { ARTRealtimeChannel *channel2 = [realtime2.channels get:channelName]; [channel2.presence enter:presenceEnter1 callback:^(ARTErrorInfo *errorInfo) { @@ -232,8 +232,8 @@ - (void)testWaitTextBackward { [channel attach]; } }]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel.presence enter:presenceEnter1 callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); @@ -359,8 +359,8 @@ - (void)runTestTimeForwards:(bool) forwards limit:(int) limit callback:(void (^) int secondBatchTotal = [self secondBatchSize]; int thirdBatchTotal = [self thirdBatchSize]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel.presence enter:[self enter1Str] callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); @@ -475,8 +475,8 @@ - (void)testFromAttach { ARTRealtime *realtime2 = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel = [realtime.channels get:[self channelName]]; [channel attach]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel.presence enter:[self enter1Str] callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); [channel.presence enter:[self enter2Str] callback:^(ARTErrorInfo *errorInfo) { @@ -484,8 +484,8 @@ - (void)testFromAttach { [channel.presence update:[self updateStr] callback:^(ARTErrorInfo *errorInfo2) { XCTAssertNil(errorInfo2); ARTRealtimeChannel *channel2 = [realtime2.channels get:[self channelName]]; - [channel2 on:^(ARTErrorInfo *errorInfo) { - if(channel2.state == ARTRealtimeChannelAttached) { + [channel2 on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { ARTRealtimeHistoryQuery *query = [[ARTRealtimeHistoryQuery alloc] init]; query.direction = ARTQueryDirectionForwards; [channel2.presence history:query callback:^(ARTPaginatedResult *c2Result, ARTErrorInfo *error2) { diff --git a/Tests/ARTRealtimePresenceTest.m b/Tests/ARTRealtimePresenceTest.m index db2435951..dd762565f 100644 --- a/Tests/ARTRealtimePresenceTest.m +++ b/Tests/ARTRealtimePresenceTest.m @@ -59,14 +59,14 @@ - (void)testTwoConnections { __block NSUInteger attached = 0; // Channel 1 - [channel on:^(ARTErrorInfo *errorInfo) { - if (channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { attached++; } }]; // Channel 2 - [channel2 on:^(ARTErrorInfo *errorInfo) { - if (channel.state == ARTRealtimeChannelAttached) { + [channel2 on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { attached++; } }]; @@ -115,8 +115,8 @@ - (void)testEnterSimple { [channel attach]; __weak XCTestExpectation *expectChannel2Connected = [self expectationWithDescription:@"presence message"]; - [channel2 on:^(ARTErrorInfo *errorInfo) { - if(channel2.state == ARTRealtimeChannelAttached) { + [channel2 on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [expectChannel2Connected fulfill]; } }]; @@ -161,8 +161,8 @@ - (void)testSubscribeConnects { [channel.presence subscribe:^(ARTPresenceMessage *message) { }]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [expectation fulfill]; } }]; @@ -180,8 +180,8 @@ - (void)testUpdateConnects { [channel.presence update:@"update" callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); }]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [expectation fulfill]; } }]; @@ -209,9 +209,8 @@ - (void)testEnterBeforeConnect { [channel attach]; } }]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) - { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel.presence enter:presenceEnter callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); }]; @@ -252,8 +251,8 @@ - (void)testEnterLeaveSimple { [channel attach]; } }]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel.presence enter:presenceEnter callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); }]; @@ -293,8 +292,8 @@ - (void)testEnterEnter { [channel attach]; } }]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel.presence enter:presenceEnter callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); }]; @@ -334,8 +333,8 @@ - (void)testEnterUpdateSimple { [channel attach]; } }]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel.presence enter:presenceEnter callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); }]; @@ -374,8 +373,8 @@ - (void)testUpdateNull { [channel attach]; } }]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel.presence enter:presenceEnter callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); }]; @@ -418,8 +417,8 @@ - (void)testEnterLeaveWithoutData { [channel attach]; } }]; - [channel on:^(ARTErrorInfo *errorInfo) { - if (channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel.presence enter:presenceEnter callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); }]; @@ -452,8 +451,8 @@ - (void)testUpdateNoEnter { } }]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel.presence update:update callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); }]; @@ -515,8 +514,8 @@ - (void)testEnterOnDetached { __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel = [realtime.channels get:@"testEnterNoClientId"]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel detach]; } else if(channel.state == ARTRealtimeChannelDetached) { @@ -537,11 +536,11 @@ - (void)testEnterOnFailed { __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel = [realtime.channels get:@"testEnterNoClientId"]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel setFailed:[ARTStatus state:ARTStateError]]; } - else if(channel.state == ARTRealtimeChannelFailed) { + else if (stateChange.current == ARTRealtimeChannelFailed) { [channel.presence enter:@"thisWillFail" callback:^(ARTErrorInfo *errorInfo) { XCTAssertNotNil(errorInfo); [expectation fulfill]; @@ -605,8 +604,8 @@ - (void)testLeaveNoData { } }]; [channel attach]; - [channel on:^(ARTErrorInfo *errorInfo) { - if (channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel.presence update:enter callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); }]; @@ -677,11 +676,11 @@ - (void)testLeaveOnDetached { __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel = [realtime.channels get:@"testEnterNoClientId"]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel detach]; } - else if(channel.state == ARTRealtimeChannelDetached) { + else if (stateChange.current == ARTRealtimeChannelDetached) { XCTAssertThrows([channel.presence leave:@"thisWillFail" callback:^(ARTErrorInfo *errorInfo) {}]); [expectation fulfill]; } @@ -697,11 +696,11 @@ - (void)testLeaveOnFailed { __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel = [realtime.channels get:@"testEnterNoClientId"]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel setFailed:[ARTStatus state:ARTStateError]]; } - else if(channel.state == ARTRealtimeChannelFailed) { + else if (stateChange.current == ARTRealtimeChannelFailed) { XCTAssertThrows([channel.presence leave:@"thisWillFail" callback:^(ARTErrorInfo *errorInfo) {}]); [expectation fulfill]; } @@ -1000,15 +999,15 @@ - (void)testPresenceWithDataOnLeave { __block NSUInteger attached = 0; // Channel 1 - [channel on:^(ARTErrorInfo *errorInfo) { - if (channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { attached++; } }]; // Channel 2 - [channel2 on:^(ARTErrorInfo *errorInfo) { - if (channel2.state == ARTRealtimeChannelAttached) { + [channel2 on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { attached++; } }]; diff --git a/Tests/ARTRealtimeResumeTest.m b/Tests/ARTRealtimeResumeTest.m index aa35f4a8c..3ce393627 100644 --- a/Tests/ARTRealtimeResumeTest.m +++ b/Tests/ARTRealtimeResumeTest.m @@ -45,14 +45,14 @@ - (void)testSimpleDisconnected { ARTRealtimeChannel *channel = [realtime.channels get:channelName]; ARTRealtimeChannel *channel2 = [realtime2.channels get:channelName]; - [channel on:^(ARTErrorInfo *errorInfo) { - if(channel.state == ARTRealtimeChannelAttached) { + [channel on:^(ARTChannelStateChange *stateChange) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel2 attach]; } }]; - [channel2 on:^(ARTErrorInfo *errorInfo) { + [channel2 on:^(ARTChannelStateChange *stateChange) { //both channels are attached. lets get to work. - if(channel2.state == ARTRealtimeChannelAttached) { + if (stateChange.current == ARTRealtimeChannelAttached) { [channel2 publish:nil data:message1 callback:^(ARTErrorInfo *errorInfo) { [channel2 publish:nil data:message2 callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); From a4819afd9abe97b1f305ec42f8f8737db5d0c87f Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 21 Dec 2016 13:07:51 +0000 Subject: [PATCH 21/43] Update RTL13 for 0.9 (#549) * RTL13a * RTL13b * RTL13c * Add ClientOptions.channelRetryTimeout * RealtimeChannel reattach after timeout * Fix: if the channel receives a server initiated DETACHED message and if the channel is in the ATTACHED or SUSPENDED states, then an attempt to reattach the channel should be made immediately * Fix: move to Suspended if attach times out * Fix: if the channel receives a server initiated DETACHED message and the channel is Attaching * Remove test about #454 (replaced by RTL13) * Update RTL4f --- Source/ARTClientOptions.h | 6 + Source/ARTClientOptions.m | 2 + Source/ARTRealtimeChannel.m | 100 +++++++--- Source/ARTStatus.h | 1 + Source/ARTStatus.m | 2 + Spec/RealtimeClientChannel.swift | 333 +++++++++++++++++++++++++++---- 6 files changed, 372 insertions(+), 72 deletions(-) diff --git a/Source/ARTClientOptions.h b/Source/ARTClientOptions.h index bc920cfee..bf5a70379 100644 --- a/Source/ARTClientOptions.h +++ b/Source/ARTClientOptions.h @@ -50,6 +50,12 @@ ART_ASSUME_NONNULL_BEGIN */ @property (readwrite, assign, nonatomic) NSTimeInterval suspendedRetryTimeout; +/** + Represents the timeout (in seconds) to re-attach the channel automatically. + When a channel becomes SUSPENDED following a server initiated DETACHED, after this delay in milliseconds, if the channel is still SUSPENDED and the connection is CONNECTED, the client library will attempt to re-attach. + */ +@property (readwrite, assign, nonatomic) NSTimeInterval channelRetryTimeout; + /** Timeout for opening the connection, available in the client library if supported by the transport. */ diff --git a/Source/ARTClientOptions.m b/Source/ARTClientOptions.m index d7e6488b7..1ff1f0ace 100644 --- a/Source/ARTClientOptions.m +++ b/Source/ARTClientOptions.m @@ -37,6 +37,7 @@ - (instancetype)initDefaults { _logLevel = ARTLogLevelNone; _disconnectedRetryTimeout = 15.0; //Seconds _suspendedRetryTimeout = 30.0; //Seconds + _channelRetryTimeout = 15.0; //Seconds _httpOpenTimeout = 4.0; //Seconds _httpRequestTimeout = 15.0; //Seconds _httpMaxRetryDuration = 10.0; //Seconds @@ -109,6 +110,7 @@ - (id)copyWithZone:(NSZone *)zone { options.logHandler = self.logHandler; options.suspendedRetryTimeout = self.suspendedRetryTimeout; options.disconnectedRetryTimeout = self.disconnectedRetryTimeout; + options.channelRetryTimeout = self.channelRetryTimeout; options.httpMaxRetryCount = self.httpMaxRetryCount; options.httpMaxRetryDuration = self.httpMaxRetryDuration; options.httpOpenTimeout = self.httpOpenTimeout; diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index e81b46e7e..c7ea3f919 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -309,7 +309,10 @@ - (ARTEventListener *)timed:(ARTEventListener *)listener deadline:(NSTimeInterva - (void)transition:(ARTRealtimeChannelState)state status:(ARTStatus *)status { ARTChannelStateChange *stateChange = [[ARTChannelStateChange alloc] initWithCurrent:state previous:self.state reason:status.errorInfo]; self.state = state; - _errorReason = status.errorInfo; + + if (status.storeErrorInfo) { + _errorReason = status.errorInfo; + } if (state == ARTRealtimeChannelFailed) { [_attachedEventEmitter emit:[NSNull null] with:status.errorInfo]; @@ -410,28 +413,34 @@ - (void)setAttached:(ARTProtocolMessage *)message { [self sendQueuedMessages]; - if (message.error) { - _errorReason = message.error; - [self transition:ARTRealtimeChannelAttached status:[ARTStatus state:ARTStateError info:message.error]]; - } - else { - [self transition:ARTRealtimeChannelAttached status:[ARTStatus state:ARTStateOk]]; - } + ARTStatus *status = message.error ? [ARTStatus state:ARTStateError info:message.error] : [ARTStatus state:ARTStateOk]; + [self transition:ARTRealtimeChannelAttached status:status]; [_attachedEventEmitter emit:[NSNull null] with:nil]; } - (void)setDetached:(ARTProtocolMessage *)message { - if (self.state == ARTRealtimeChannelFailed) { - return; + switch (self.state) { + case ARTRealtimeChannelAttached: + case ARTRealtimeChannelSuspended: + [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p reattach initiated by DETACHED message", _realtime, self]; + [self reattach:nil withReason:message.error]; + return; + case ARTRealtimeChannelAttaching: { + [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p reattach initiated by DETACHED message but it is currently attaching", _realtime, self]; + ARTStatus *status = message.error ? [ARTStatus state:ARTStateError info:message.error] : [ARTStatus state:ARTStateOk]; + status.storeErrorInfo = false; + [self setSuspended:status retryIn:0]; + return; + } + case ARTRealtimeChannelFailed: + return; + default: + break; } + self.attachSerial = nil; - ARTErrorInfo *errorInfo; - if (message.error) { - errorInfo = message.error; - } else { - errorInfo = [ARTErrorInfo createWithCode:0 message:@"channel has detached"]; - } + ARTErrorInfo *errorInfo = message.error ? message.error : [ARTErrorInfo createWithCode:0 message:@"channel has detached"]; ARTStatus *reason = [ARTStatus state:ARTStateNotAttached info:errorInfo]; [self detachChannel:reason]; [_detachedEventEmitter emit:[NSNull null] with:nil]; @@ -447,9 +456,19 @@ - (void)setFailed:(ARTStatus *)error { [self transition:ARTRealtimeChannelFailed status:error]; } -- (void)setSuspended:(ARTStatus *)error { - [self failQueuedMessages:error]; - [self transition:ARTRealtimeChannelSuspended status:error]; +- (void)setSuspended:(ARTStatus *)status { + [self setSuspended:status retryIn:self.realtime.options.channelRetryTimeout]; +} + +- (void)setSuspended:(ARTStatus *)status retryIn:(NSTimeInterval)retryTimeout { + [self failQueuedMessages:status]; + [self transition:ARTRealtimeChannelSuspended status:status]; + __weak __typeof(self) weakSelf = self; + [self unlessStateChangesBefore:retryTimeout do:^{ + [weakSelf reattach:^(ARTErrorInfo *errorInfo) { + [weakSelf setSuspended:[ARTStatus state:ARTStateError info:errorInfo]]; + } withReason:nil]; + }]; } - (void)onMessage:(ARTProtocolMessage *)message { @@ -538,7 +557,7 @@ - (void)broadcastPresence:(ARTPresenceMessage *)pm { } - (void)onError:(ARTProtocolMessage *)msg { - [self transition:ARTRealtimeChannelFailed status:[ARTStatus state:ARTStateError info: msg.error]]; + [self transition:ARTRealtimeChannelFailed status:[ARTStatus state:ARTStateError info:msg.error]]; [self failQueuedMessages:[ARTStatus state:ARTStateError info: msg.error]]; } @@ -546,7 +565,7 @@ - (void)attach { [self attach:nil]; } -- (void)attach:(void (^)(ARTErrorInfo * _Nullable))callback { +- (void)attach:(void (^)(ARTErrorInfo *))callback { switch (self.state) { case ARTRealtimeChannelAttaching: [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p already attaching", _realtime, self]; @@ -556,19 +575,42 @@ - (void)attach:(void (^)(ARTErrorInfo * _Nullable))callback { [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p already attached", _realtime, self]; if (callback) callback(nil); return; + default: + break; + } + [self internalAttach:callback withReason:nil]; +} + +- (void)reattach:(void (^)(ARTErrorInfo *))callback withReason:(ARTErrorInfo *)reason { + switch (self.state) { + case ARTRealtimeChannelAttached: + case ARTRealtimeChannelSuspended: + [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p attached or suspended and will reattach", _realtime, self]; + break; + case ARTRealtimeChannelAttaching: + [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p already attaching", _realtime, self]; + if (callback) [_attachedEventEmitter once:callback]; + return; + default: + break; + } + [self internalAttach:callback withReason:reason]; +} + +- (void)internalAttach:(void (^)(ARTErrorInfo *))callback withReason:(ARTErrorInfo *)reason { + switch (self.state) { case ARTRealtimeChannelDetaching: { NSString *msg = @"can't attach when in DETACHING state"; [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p %@", _realtime, self, msg]; if (callback) callback([ARTErrorInfo createWithCode:90000 message:msg]); return; } - case ARTRealtimeChannelFailed: - _errorReason = nil; - break; default: break; } - + + _errorReason = nil; + if (![self.realtime isActive]) { [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p can't attach when not in an active state", _realtime, self]; if (callback) callback([ARTErrorInfo createWithCode:90000 message:@"Can't attach when not in an active state"]); @@ -577,7 +619,9 @@ - (void)attach:(void (^)(ARTErrorInfo * _Nullable))callback { if (callback) [_attachedEventEmitter once:callback]; // Set state: Attaching - [self transition:ARTRealtimeChannelAttaching status:[ARTStatus state:ARTStateOk]]; + ARTStatus *status = reason ? [ARTStatus state:ARTStateError info:reason] : [ARTStatus state:ARTStateOk]; + status.storeErrorInfo = false; + [self transition:ARTRealtimeChannelAttaching status:status]; [self attachAfterChecks:callback]; } @@ -595,8 +639,7 @@ - (void)attachAfterChecks:(void (^)(ARTErrorInfo * _Nullable))callback { timeouted = true; ARTErrorInfo *errorInfo = [ARTErrorInfo createWithCode:ARTStateAttachTimedOut message:@"attach timed out"]; ARTStatus *status = [ARTStatus state:ARTStateAttachTimedOut info:errorInfo]; - _errorReason = errorInfo; - [self transition:ARTRealtimeChannelFailed status:status]; + [self setSuspended:status]; [_attachedEventEmitter emit:[NSNull null] with:errorInfo]; }]; @@ -659,7 +702,6 @@ - (void)detachAfterChecks:(void (^)(ARTErrorInfo * _Nullable))callback { timeouted = true; ARTErrorInfo *errorInfo = [ARTErrorInfo createWithCode:ARTStateDetachTimedOut message:@"detach timed out"]; ARTStatus *status = [ARTStatus state:ARTStateDetachTimedOut info:errorInfo]; - _errorReason = errorInfo; [self transition:ARTRealtimeChannelFailed status:status]; [_detachedEventEmitter emit:[NSNull null] with:errorInfo]; }]; diff --git a/Source/ARTStatus.h b/Source/ARTStatus.h index eff33287a..7e7c53c38 100644 --- a/Source/ARTStatus.h +++ b/Source/ARTStatus.h @@ -79,6 +79,7 @@ FOUNDATION_EXPORT NSString *const ARTAblyMessageNoMeansToRenewToken; @interface ARTStatus : NSObject @property (art_nullable, readonly, strong, nonatomic) ARTErrorInfo *errorInfo; +@property (nonatomic, assign) BOOL storeErrorInfo; @property (nonatomic, assign) ARTState state; + (ARTStatus *)state:(ARTState) state; diff --git a/Source/ARTStatus.m b/Source/ARTStatus.m index cc666d136..c852d5c1b 100644 --- a/Source/ARTStatus.m +++ b/Source/ARTStatus.m @@ -63,6 +63,7 @@ - (instancetype)init { if (self) { _state = ARTStateOk; _errorInfo = nil; + _storeErrorInfo = false; } return self; } @@ -76,6 +77,7 @@ + (ARTStatus *)state:(ARTState)state { + (ARTStatus *)state:(ARTState)state info:(ARTErrorInfo *)info { ARTStatus * s = [ARTStatus state:state]; s.errorInfo = info; + s.storeErrorInfo = true; return s; } diff --git a/Spec/RealtimeClientChannel.swift b/Spec/RealtimeClientChannel.swift index 6b88b0536..6050385a6 100644 --- a/Spec/RealtimeClientChannel.swift +++ b/Spec/RealtimeClientChannel.swift @@ -848,34 +848,47 @@ class RealtimeClientChannel: QuickSpec { } // RTL4f - it("should transition the channel state to FAILED if ATTACHED ProtocolMessage is not received") { - let previousRealtimeRequestTimeout = ARTDefault.realtimeRequestTimeout() - defer { ARTDefault.setRealtimeRequestTimeout(previousRealtimeRequestTimeout) } - ARTDefault.setRealtimeRequestTimeout(3.0) + it("should transition the channel state to SUSPENDED if ATTACHED ProtocolMessage is not received") { let options = AblyTests.commonAppSetup() - options.autoConnect = false - let client = ARTRealtime(options: options) - client.setTransportClass(TestProxyTransport.self) - client.connect() + options.channelRetryTimeout = 1.0 + let client = AblyTests.newRealtime(options) defer { client.dispose(); client.close() } expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) - let transport = client.transport as! TestProxyTransport + + let previousRealtimeRequestTimeout = ARTDefault.realtimeRequestTimeout() + defer { ARTDefault.setRealtimeRequestTimeout(previousRealtimeRequestTimeout) } + ARTDefault.setRealtimeRequestTimeout(1.0) + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } transport.actionsIgnored += [.Attached] - var callbackCalled = false let channel = client.channels.get("test") - channel.attach { errorInfo in - expect(errorInfo).toNot(beNil()) - expect(errorInfo).to(equal(channel.errorReason)) - callbackCalled = true + waitUntil(timeout: testTimeout) { done in + channel.attach { errorInfo in + expect(errorInfo).toNot(beNil()) + expect(errorInfo).to(equal(channel.errorReason)) + done() + } } - let start = NSDate() - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Failed), timeout: testTimeout) + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Suspended), timeout: testTimeout) expect(channel.errorReason).toNot(beNil()) - expect(callbackCalled).to(beTrue()) - let end = NSDate() - expect(start.dateByAddingTimeInterval(3.0)).to(beCloseTo(end, within: 0.5)) + + transport.actionsIgnored = [] + // Automatically re-attached + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.once(.Attaching) { stateChange in + expect(stateChange?.reason).to(beNil()) + partialDone() + } + channel.once(.Attached) { stateChange in + expect(stateChange?.reason).to(beNil()) + partialDone() + } + } } it("if called with a callback should call it once attached") { @@ -2611,6 +2624,263 @@ class RealtimeClientChannel: QuickSpec { expect(channel.state).to(equal(ARTRealtimeChannelState.Attached)) } + // RTL13 + context("if the channel receives a server initiated DETACHED message when") { + + // RTL13a + it("the channel is in the ATTACHED states, an attempt to reattach the channel should be made immediately by sending a new ATTACH message and the channel should transition to the ATTACHING state with the error emitted in the ChannelStateChange event") { + let client = AblyTests.newRealtime(AblyTests.commonAppSetup()) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + channel.attach() { error in + expect(error).to(beNil()) + done() + } + } + + expect(channel.state).to(equal(ARTRealtimeChannelState.Attached)) + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + waitUntil(timeout: testTimeout) { done in + let detachedMessageWithError = AblyTests.newErrorProtocolMessage() + detachedMessageWithError.action = .Detached + detachedMessageWithError.channel = "foo" + + channel.once(.Attaching) { stateChange in + guard let error = stateChange?.reason else { + fail("Reason error is nil"); done(); return + } + expect(error).to(beIdenticalTo(detachedMessageWithError.error)) + expect(channel.errorReason).to(beNil()) + done() + } + + transport.receive(detachedMessageWithError) + } + + expect(transport.protocolMessagesSent.filter{ $0.action == .Attach }).to(haveCount(2)) + } + + // RTL13a + it("the channel is in the SUSPENDED state, an attempt to reattach the channel should be made immediately by sending a new ATTACH message and the channel should transition to the ATTACHING state with the error emitted in the ChannelStateChange event") { + let client = AblyTests.newRealtime(AblyTests.commonAppSetup()) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + } + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + let previousRealtimeRequestTimeout = ARTDefault.realtimeRequestTimeout() + defer { ARTDefault.setRealtimeRequestTimeout(previousRealtimeRequestTimeout) } + ARTDefault.setRealtimeRequestTimeout(1.0) + + // Timeout + transport.actionsIgnored += [.Attached] + + waitUntil(timeout: testTimeout) { done in + channel.once(.Suspended) { stateChange in + expect(stateChange?.reason?.message).to(contain("timed out")) + done() + } + channel.attach() + } + + transport.actionsIgnored = [] + + waitUntil(timeout: testTimeout) { done in + let detachedMessageWithError = AblyTests.newErrorProtocolMessage() + detachedMessageWithError.action = .Detached + detachedMessageWithError.channel = "foo" + + channel.once(.Attaching) { stateChange in + guard let error = stateChange?.reason else { + fail("Reason error is nil"); done(); return + } + expect(error).to(beIdenticalTo(detachedMessageWithError.error)) + expect(channel.errorReason).to(beNil()) + done() + } + + transport.receive(detachedMessageWithError) + } + + expect(transport.protocolMessagesSent.filter{ $0.action == .Attach }).to(haveCount(2)) + } + + // RTL13b + it("if the attempt to re-attach fails the channel will transition to the SUSPENDED state and the error will be emitted in the ChannelStateChange event") { + let options = AblyTests.commonAppSetup() + options.channelRetryTimeout = 1.0 + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + channel.attach { error in + expect(error).to(beNil()) + done() + } + } + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + let previousRealtimeRequestTimeout = ARTDefault.realtimeRequestTimeout() + defer { ARTDefault.setRealtimeRequestTimeout(previousRealtimeRequestTimeout) } + ARTDefault.setRealtimeRequestTimeout(1.0) + transport.actionsIgnored = [.Attached] + + let detachedMessageWithError = AblyTests.newErrorProtocolMessage() + detachedMessageWithError.action = .Detached + detachedMessageWithError.channel = "foo" + + waitUntil(timeout: testTimeout) { done in + channel.once(.Attaching) { stateChange in + guard let error = stateChange?.reason else { + fail("Reason error is nil"); done(); return + } + expect(error).to(beIdenticalTo(detachedMessageWithError.error)) + expect(channel.errorReason).to(beNil()) + done() + } + + transport.receive(detachedMessageWithError) + } + + waitUntil(timeout: testTimeout) { done in + channel.once(.Suspended) { stateChange in + guard let error = stateChange?.reason else { + fail("Reason error is nil"); done(); return + } + expect(error.message).to(contain("timed out")) + expect(channel.errorReason).to(beIdenticalTo(error)) + done() + } + } + + let start = NSDate() + waitUntil(timeout: testTimeout) { done in + channel.once(.Attaching) { _ in + let end = NSDate() + expect(start.dateByAddingTimeInterval(options.channelRetryTimeout)).to(beCloseTo(end, within: 0.5)) + done() + } + } + } + + // RTL13b + it("if the channel was already in the ATTACHING state, the channel will transition to the SUSPENDED state and the error will be emitted in the ChannelStateChange event") { + let options = AblyTests.commonAppSetup() + options.channelRetryTimeout = 1.0 + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + let detachedMessageWithError = AblyTests.newErrorProtocolMessage() + detachedMessageWithError.action = .Detached + detachedMessageWithError.channel = "foo" + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.once(.Attaching) { stateChange in + expect(stateChange?.reason).to(beNil()) + client.transport?.receive(detachedMessageWithError) + partialDone() + } + channel.once(.Suspended) { stateChange in + guard let error = stateChange?.reason else { + fail("Reason error is nil"); partialDone(); return + } + expect(error).to(beIdenticalTo(detachedMessageWithError.error)) + expect(channel.errorReason).to(beNil()) + + // Check retry + let start = NSDate() + channel.once(.Attaching) { stateChange in + let end = NSDate() + expect(start).to(beCloseTo(end, within: 0.5)) + expect(stateChange?.reason).to(beNil()) + partialDone() + } + } + channel.attach() + } + } + + // RTL13c + it("if the connection is no longer CONNECTED, then the automatic attempts to re-attach the channel must be cancelled") { + let options = AblyTests.commonAppSetup() + options.channelRetryTimeout = 1.0 + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + waitUntil(timeout: testTimeout) { done in + channel.attach { error in + expect(error).to(beNil()) + done() + } + } + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + let previousRealtimeRequestTimeout = ARTDefault.realtimeRequestTimeout() + defer { ARTDefault.setRealtimeRequestTimeout(previousRealtimeRequestTimeout) } + ARTDefault.setRealtimeRequestTimeout(1.0) + + transport.actionsIgnored = [.Attached] + let detachedMessageWithError = AblyTests.newErrorProtocolMessage() + detachedMessageWithError.action = .Detached + detachedMessageWithError.channel = "foo" + waitUntil(timeout: testTimeout) { done in + channel.once(.Attaching) { stateChange in + guard let error = stateChange?.reason else { + fail("Reason error is nil"); done(); return + } + expect(error).to(beIdenticalTo(detachedMessageWithError.error)) + expect(channel.errorReason).to(beNil()) + done() + } + transport.receive(detachedMessageWithError) + } + waitUntil(timeout: testTimeout) { done in + channel.once(.Suspended) { stateChange in + guard let error = stateChange?.reason else { + fail("Reason error is nil"); done(); return + } + expect(error.message).to(contain("timed out")) + expect(channel.errorReason).to(beIdenticalTo(error)) + done() + } + } + + channel.once(.Attaching) { _ in + fail("Should cancel the re-attach") + } + + client.simulateSuspended(beforeSuspension: { done in + channel.once(.Suspended) { _ in + done() + } + }) + } + + } + // RTL14 it("If an ERROR ProtocolMessage is received for this channel then the channel should immediately transition to the FAILED state, the errorReason should be set and an error should be emitted on the channel") { let client = ARTRealtime(options: AblyTests.commonAppSetup()) @@ -2642,6 +2912,7 @@ class RealtimeClientChannel: QuickSpec { expect(channel.state).to(equal(ARTRealtimeChannelState.Failed)) } + } context("crypto") { @@ -2730,30 +3001,6 @@ class RealtimeClientChannel: QuickSpec { } } - // https://github.com/ably/ably-ios/issues/454 - it("should not move to FAILED if received DETACH with an error") { - let options = AblyTests.commonAppSetup() - let client = ARTRealtime(options: options) - defer { - client.dispose() - client.close() - } - let channel = client.channels.get("test") - channel.attach() - - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) - - let protoMsg = ARTProtocolMessage() - protoMsg.action = .Detach - protoMsg.error = ARTErrorInfo.createWithCode(123, message: "test error") - protoMsg.channel = "test" - client.transport?.receive(protoMsg) - - expect(channel.state).to(equal(ARTRealtimeChannelState.Detached)) - expect(channel.errorReason).to(equal(protoMsg.error)) - expect(client.connection.state).to(equal(ARTRealtimeConnectionState.Connected)) - expect(client.connection.errorReason).to(beNil()) - } } } } From ed040e7f929f6d0de37c0d3ed7cc03e7894ac2a2 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 21 Dec 2016 17:02:37 +0000 Subject: [PATCH 22/43] Update RTL4 for 0.9 (#545) * RTL4i * RTL4h * Update RTL4e * Fix: attach request should be treated as though it has failed and the channel should transition to the SUSPENDED state * Fix: attach after Detaching * Fix: if it fails to detach then move back to ATTACHED * Fix RTL5f --- Source/ARTRealtimeChannel.m | 15 +- Spec/RealtimeClientChannel.swift | 232 +++++++++++++++++++++++++------ 2 files changed, 195 insertions(+), 52 deletions(-) diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index c7ea3f919..e846a75cf 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -314,7 +314,10 @@ - (void)transition:(ARTRealtimeChannelState)state status:(ARTStatus *)status { _errorReason = status.errorInfo; } - if (state == ARTRealtimeChannelFailed) { + if (state == ARTRealtimeChannelSuspended) { + [_attachedEventEmitter emit:[NSNull null] with:status.errorInfo]; + } + else if (state == ARTRealtimeChannelFailed) { [_attachedEventEmitter emit:[NSNull null] with:status.errorInfo]; [_detachedEventEmitter emit:[NSNull null] with:status.errorInfo]; } @@ -600,9 +603,10 @@ - (void)reattach:(void (^)(ARTErrorInfo *))callback withReason:(ARTErrorInfo *)r - (void)internalAttach:(void (^)(ARTErrorInfo *))callback withReason:(ARTErrorInfo *)reason { switch (self.state) { case ARTRealtimeChannelDetaching: { - NSString *msg = @"can't attach when in DETACHING state"; - [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p %@", _realtime, self, msg]; - if (callback) callback([ARTErrorInfo createWithCode:90000 message:msg]); + [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p %@", _realtime, self, @"attach after the completion of Detaching"]; + [_detachedEventEmitter once:^(ARTErrorInfo *error) { + [self attach:callback]; + }]; return; } default: @@ -640,7 +644,6 @@ - (void)attachAfterChecks:(void (^)(ARTErrorInfo * _Nullable))callback { ARTErrorInfo *errorInfo = [ARTErrorInfo createWithCode:ARTStateAttachTimedOut message:@"attach timed out"]; ARTStatus *status = [ARTStatus state:ARTStateAttachTimedOut info:errorInfo]; [self setSuspended:status]; - [_attachedEventEmitter emit:[NSNull null] with:errorInfo]; }]; if (![self.realtime shouldQueueEvents]) { @@ -702,7 +705,7 @@ - (void)detachAfterChecks:(void (^)(ARTErrorInfo * _Nullable))callback { timeouted = true; ARTErrorInfo *errorInfo = [ARTErrorInfo createWithCode:ARTStateDetachTimedOut message:@"detach timed out"]; ARTStatus *status = [ARTStatus state:ARTStateDetachTimedOut info:errorInfo]; - [self transition:ARTRealtimeChannelFailed status:status]; + [self transition:ARTRealtimeChannelAttached status:status]; [_detachedEventEmitter emit:[NSNull null] with:errorInfo]; }]; diff --git a/Spec/RealtimeClientChannel.swift b/Spec/RealtimeClientChannel.swift index 6050385a6..609ff87f6 100644 --- a/Spec/RealtimeClientChannel.swift +++ b/Spec/RealtimeClientChannel.swift @@ -613,53 +613,58 @@ class RealtimeClientChannel: QuickSpec { } } - context("results in an error if the channel state is") { - // RTL4e - it("DETACHING") { - let client = ARTRealtime(options: AblyTests.commonAppSetup()) - defer { client.dispose(); client.close() } - let channel = client.channels.get("test") - - channel.attach() - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) - channel.detach() - expect(channel.state).to(equal(ARTRealtimeChannelState.Detaching)) + // RTL4e + it("if the user does not have sufficient permissions to attach, then the channel will transition to FAILED and set the errorReason") { + let options = AblyTests.commonAppSetup() + options.token = getTestToken(key: options.key!, capability: "{\"restricted\":[\"*\"]}") + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("test") - waitUntil(timeout: testTimeout) { done in - channel.attach { error in - expect(error).toNot(beNil()) - done() + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.once(.Failed) { stateChange in + expect(stateChange?.reason?.code) == 40160 + partialDone() + } + channel.attach { error in + guard let error = error else { + fail("Error is nil"); partialDone(); return } + expect(error.code) == 40160 + partialDone() } } - // RTL4g - it("FAILED") { - let client = ARTRealtime(options: AblyTests.commonAppSetup()) - defer { client.dispose(); client.close() } - let channel = client.channels.get("test") + expect(channel.state).to(equal(ARTRealtimeChannelState.Failed)) + expect(channel.errorReason?.code) == 40160 + } - channel.attach() - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) + // RTL4g + it("if the channel is in the FAILED state, the attach request sets its errorReason to null, and proceeds with a channel attach") { + let client = ARTRealtime(options: AblyTests.commonAppSetup()) + defer { client.dispose(); client.close() } + let channel = client.channels.get("test") - let errorMsg = AblyTests.newErrorProtocolMessage() - errorMsg.channel = channel.name - client.onError(errorMsg) - expect(channel.state).to(equal(ARTRealtimeChannelState.Failed)) - expect(channel.errorReason).toNot(beNil()) + channel.attach() + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) - waitUntil(timeout: testTimeout) { done in - channel.attach { error in - expect(error).to(beNil()) - done() - } - } + let errorMsg = AblyTests.newErrorProtocolMessage() + errorMsg.channel = channel.name + client.onError(errorMsg) + expect(channel.state).to(equal(ARTRealtimeChannelState.Failed)) + expect(channel.errorReason).toNot(beNil()) - expect(channel.state).to(equal(ARTRealtimeChannelState.Attached)) - expect(channel.errorReason).to(beNil()) + waitUntil(timeout: testTimeout) { done in + channel.attach { error in + expect(error).to(beNil()) + done() + } } - } + expect(channel.state).to(equal(ARTRealtimeChannelState.Attached)) + expect(channel.errorReason).to(beNil()) + } // RTL4b context("results in an error if the connection state is") { @@ -738,7 +743,7 @@ class RealtimeClientChannel: QuickSpec { } - // RTL4h + // RTL4i context("happens when connection is CONNECTED if it's currently") { it("INITIALIZED") { let options = AblyTests.commonAppSetup() @@ -844,6 +849,7 @@ class RealtimeClientChannel: QuickSpec { } } + expect(channel.errorReason!.code).to(equal(40160)) expect(channel.state).to(equal(ARTRealtimeChannelState.Failed)) } @@ -939,6 +945,136 @@ class RealtimeClientChannel: QuickSpec { } } } + + // RTL4h + it("if the channel is in a pending state ATTACHING, do the attach operation after the completion of the pending request") { + let options = AblyTests.commonAppSetup() + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + var attachedCount = 0 + channel.on(.Attached) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); return + } + expect(stateChange.reason).to(beNil()) + attachedCount += 1 + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.once(.Attaching) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); partialDone(); return + } + expect(stateChange.reason).to(beNil()) + expect(stateChange.current).to(equal(ARTRealtimeChannelState.Attaching)) + expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Initialized)) + channel.attach() + partialDone() + } + channel.once(.Attached) { stateChange in + expect(stateChange?.reason).to(beNil()) + partialDone() + } + channel.attach() + } + + expect(attachedCount).toEventually(equal(1), timeout: testTimeout) + } + + // RTL4h + it("if the channel is in a pending state DETACHING, do the attach operation after the completion of the pending request") { + let options = AblyTests.commonAppSetup() + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + waitUntil(timeout: testTimeout) { done in + channel.attach() { error in + expect(error).to(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(4, done: done) + channel.once(.Detaching) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); partialDone(); return + } + expect(stateChange.reason).to(beNil()) + expect(stateChange.current).to(equal(ARTRealtimeChannelState.Detaching)) + expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Attached)) + channel.attach() + partialDone() + } + channel.once(.Detached) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); partialDone(); return + } + expect(stateChange.reason?.message).to(contain("channel has detached")) + expect(stateChange.current).to(equal(ARTRealtimeChannelState.Detached)) + expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Detaching)) + partialDone() + } + channel.once(.Attaching) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); partialDone(); return + } + expect(stateChange.reason).to(beNil()) + expect(stateChange.current).to(equal(ARTRealtimeChannelState.Attaching)) + expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Detached)) + partialDone() + } + channel.once(.Attached) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); partialDone(); return + } + expect(stateChange.reason).to(beNil()) + expect(stateChange.current).to(equal(ARTRealtimeChannelState.Attached)) + expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Attaching)) + partialDone() + } + channel.detach() + } + } + + it("a channel in DETACHING can actually move back to ATTACHED if it fails to detach") { + let options = AblyTests.commonAppSetup() + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + waitUntil(timeout: testTimeout) { done in + channel.attach() { error in + expect(error).to(beNil()) + done() + } + } + + let previousRealtimeRequestTimeout = ARTDefault.realtimeRequestTimeout() + defer { ARTDefault.setRealtimeRequestTimeout(previousRealtimeRequestTimeout) } + ARTDefault.setRealtimeRequestTimeout(1.0) + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + // Force timeout + transport.actionsIgnored = [.Detached] + + waitUntil(timeout: testTimeout) { done in + channel.detach() { error in + guard let error = error else { + fail("Reason error is nil"); return + } + expect(error.message).to(contain("timed out")) + expect(channel.state).to(equal(ARTRealtimeChannelState.Attached)) + done() + } + } + } + } describe("detach") { @@ -1084,10 +1220,7 @@ class RealtimeClientChannel: QuickSpec { } // RTL5f - it("should transition the channel state to FAILED if DETACHED ProtocolMessage is not received") { - let previousRealtimeRequestTimeout = ARTDefault.realtimeRequestTimeout() - defer { ARTDefault.setRealtimeRequestTimeout(previousRealtimeRequestTimeout) } - ARTDefault.setRealtimeRequestTimeout(3.0) + it("if a DETACHEDis not received within the default realtime request timeout, the detach request should be treated as though it has failed and the channel will return to its previous state") { let options = AblyTests.commonAppSetup() options.autoConnect = false let client = ARTRealtime(options: options) @@ -1099,22 +1232,29 @@ class RealtimeClientChannel: QuickSpec { let transport = client.transport as! TestProxyTransport transport.actionsIgnored += [.Detached] + let previousRealtimeRequestTimeout = ARTDefault.realtimeRequestTimeout() + defer { ARTDefault.setRealtimeRequestTimeout(previousRealtimeRequestTimeout) } + ARTDefault.setRealtimeRequestTimeout(1.0) + let channel = client.channels.get("test") channel.attach() expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) var callbackCalled = false - channel.detach { errorInfo in - expect(errorInfo).toNot(beNil()) - expect(errorInfo).to(equal(channel.errorReason)) + channel.detach { error in + guard let error = error else { + fail("Error is nil"); return + } + expect(error.message).to(contain("timed out")) + expect(error).to(equal(channel.errorReason)) callbackCalled = true } let start = NSDate() - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Failed), timeout: testTimeout) + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) expect(channel.errorReason).toNot(beNil()) expect(callbackCalled).to(beTrue()) let end = NSDate() - expect(start.dateByAddingTimeInterval(3.0)).to(beCloseTo(end, within: 0.5)) + expect(start.dateByAddingTimeInterval(1.0)).to(beCloseTo(end, within: 0.5)) } // RTL5g From a9dc94c85c519ae682dccdf4d11224c3ecedaed9 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 21 Dec 2016 21:01:32 +0000 Subject: [PATCH 23/43] Update RTL6c for 0.9 (#547) * Update RTL6c2 * Update RTL6c4 * RTL6c3 for Detached * Update RTL6c4 for channel Failed * Better code completion --- Source/ARTRealtimeChannel+Private.h | 6 +- Source/ARTRealtimeChannel.m | 12 ++-- Spec/RealtimeClientChannel.swift | 96 ++++++++++++++++++++++++++--- 3 files changed, 98 insertions(+), 16 deletions(-) diff --git a/Source/ARTRealtimeChannel+Private.h b/Source/ARTRealtimeChannel+Private.h index c7199c210..152fb9cc3 100644 --- a/Source/ARTRealtimeChannel+Private.h +++ b/Source/ARTRealtimeChannel+Private.h @@ -56,12 +56,12 @@ ART_ASSUME_NONNULL_BEGIN - (void)failQueuedMessages:(ARTStatus *)status; - (void)sendMessage:(ARTProtocolMessage *)pm callback:(void (^)(ARTStatus *))cb; -- (void)setSuspended:(ARTStatus *)error; -- (void)setFailed:(ARTStatus *)error; +- (void)setSuspended:(ARTStatus *)status; +- (void)setFailed:(ARTStatus *)status; - (void)throwOnDisconnectedOrFailed; - (void)broadcastPresence:(ARTPresenceMessage *)pm; -- (void)detachChannel:(ARTStatus *) error; +- (void)detachChannel:(ARTStatus *)status; - (void)requestContinueSync; diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index e846a75cf..c2f4a51c5 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -449,14 +449,14 @@ - (void)setDetached:(ARTProtocolMessage *)message { [_detachedEventEmitter emit:[NSNull null] with:nil]; } -- (void)detachChannel:(ARTStatus *)error { - [self failQueuedMessages:error]; - [self transition:ARTRealtimeChannelDetached status:error]; +- (void)detachChannel:(ARTStatus *)status { + [self failQueuedMessages:status]; + [self transition:ARTRealtimeChannelDetached status:status]; } -- (void)setFailed:(ARTStatus *)error { - [self failQueuedMessages:error]; - [self transition:ARTRealtimeChannelFailed status:error]; +- (void)setFailed:(ARTStatus *)status { + [self failQueuedMessages:status]; + [self transition:ARTRealtimeChannelFailed status:status]; } - (void)setSuspended:(ARTStatus *)status { diff --git a/Spec/RealtimeClientChannel.swift b/Spec/RealtimeClientChannel.swift index 609ff87f6..aa8ec6734 100644 --- a/Spec/RealtimeClientChannel.swift +++ b/Spec/RealtimeClientChannel.swift @@ -1575,6 +1575,7 @@ class RealtimeClientChannel: QuickSpec { // RTL6c2 context("the message should be queued and delivered as soon as the connection state returns to CONNECTED if the connection is") { let options = AblyTests.commonAppSetup() + options.useTokenAuth = true options.disconnectedRetryTimeout = 0.3 options.autoConnect = false var client: ARTRealtime! @@ -1636,6 +1637,39 @@ class RealtimeClientChannel: QuickSpec { expect(channel.queuedMessages).to(haveCount(1)) } } + + it("ATTACHED") { + waitUntil(timeout: testTimeout) { done in + channel.attach() { error in + expect(error).to(beNil()) + done() + } + client.connect() + } + + waitUntil(timeout: testTimeout) { done in + let tokenParams = ARTTokenParams() + tokenParams.ttl = 5.0 + client.auth.authorize(tokenParams, options: nil) { tokenDetails, error in + expect(error).to(beNil()) + expect(tokenDetails).toNot(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Disconnected) { _ in + done() + } + } + + expect(channel.state).to(equal(ARTRealtimeChannelState.Attached)) + + waitUntil(timeout: testTimeout) { done in + publish(done) + expect(channel.queuedMessages).to(haveCount(1)) + } + } } // RTL6c3 @@ -1661,8 +1695,33 @@ class RealtimeClientChannel: QuickSpec { } } + // RTL6c3 + it("implicitly attaches the channel; if the channel is in or moves to the DETACHED state before the operation succeeds, it should result in an error") { + let client = ARTRealtime(options: AblyTests.commonAppSetup()) + defer { client.dispose(); client.close() } + let channel = client.channels.get("test") + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + expect(channel.state).to(equal(ARTRealtimeChannelState.Initialized)) + channel.once(.Attaching) { stateChange in + expect(stateChange?.reason).to(beNil()) + channel.detach() + partialDone() + } + channel.publish(nil, data: "message") { error in + guard let error = error else { + fail("Error is nil"); done(); return + } + expect(error.message).to(contain("channel has detached")) + expect(channel.state).to(equal(ARTRealtimeChannelState.Detaching)) + partialDone() + } + } + } + // RTL6c4 - context("will result in an error if the connection is") { + context("will result in an error if the") { let options = AblyTests.commonAppSetup() options.disconnectedRetryTimeout = 0.1 options.suspendedRetryTimeout = 0.3 @@ -1689,7 +1748,7 @@ class RealtimeClientChannel: QuickSpec { } } - it("SUSPENDED") { + it("connection is SUSPENDED") { client.connect() expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) client.onSuspended() @@ -1699,7 +1758,7 @@ class RealtimeClientChannel: QuickSpec { } } - it("CLOSING") { + it("connection is CLOSING") { client.connect() expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) client.close() @@ -1709,7 +1768,7 @@ class RealtimeClientChannel: QuickSpec { } } - it("CLOSED") { + it("connection is CLOSED") { client.connect() expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) client.close() @@ -1719,7 +1778,7 @@ class RealtimeClientChannel: QuickSpec { } } - it("FAILED") { + it("connection is FAILED") { client.connect() expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) client.onError(AblyTests.newErrorProtocolMessage()) @@ -1729,7 +1788,7 @@ class RealtimeClientChannel: QuickSpec { } } - it("DETACHING") { + it("channel is DETACHING") { client.connect() channel.attach() expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) @@ -1740,7 +1799,7 @@ class RealtimeClientChannel: QuickSpec { } } - it("DETACHED") { + it("channel is DETACHED") { client.connect() channel.attach() expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) @@ -1750,6 +1809,29 @@ class RealtimeClientChannel: QuickSpec { publish(done) } } + + it("channel is SUSPENDED") { + client.connect() + channel.attach() + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) + channel.setSuspended(ARTStatus.state(.Ok)) + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Suspended), timeout: testTimeout) + waitUntil(timeout: testTimeout) { done in + publish(done) + } + } + + it("channel is FAILED") { + client.connect() + channel.attach() + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) + let protocolError = AblyTests.newErrorProtocolMessage() + channel.onError(protocolError) + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Failed), timeout: testTimeout) + waitUntil(timeout: testTimeout) { done in + publish(done) + } + } } } From 8637e00b1f49096c98c97a924578e6486398a2ee Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Mon, 2 Jan 2017 09:06:58 +0000 Subject: [PATCH 24/43] Add ChannelStateChange.event property (#561) --- Source/ARTRealtimeChannel.m | 10 ++++++---- Source/ARTTypes.h | 3 +++ Source/ARTTypes.m | 7 ++++--- Spec/RealtimeClientChannel.swift | 7 +++++++ 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index c2f4a51c5..6c09bbfb0 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -307,7 +307,7 @@ - (ARTEventListener *)timed:(ARTEventListener *)listener deadline:(NSTimeInterva } - (void)transition:(ARTRealtimeChannelState)state status:(ARTStatus *)status { - ARTChannelStateChange *stateChange = [[ARTChannelStateChange alloc] initWithCurrent:state previous:self.state reason:status.errorInfo]; + ARTChannelStateChange *stateChange = [[ARTChannelStateChange alloc] initWithCurrent:state previous:self.state event:(ARTChannelEvent)state reason:status.errorInfo]; self.state = state; if (status.storeErrorInfo) { @@ -327,7 +327,7 @@ - (void)transition:(ARTRealtimeChannelState)state status:(ARTStatus *)status { [_attachedEventEmitter emit:[NSNull null] with:[ARTErrorInfo createWithCode:90000 message:msg]]; } - [self emit:(ARTChannelEvent)stateChange.current with:stateChange]; + [self emit:stateChange.event with:stateChange]; } - (void)dealloc { @@ -405,7 +405,8 @@ - (void)setAttached:(ARTProtocolMessage *)message { if (message.error != nil) { _errorReason = message.error; } - [self emit:ARTChannelEventUpdate with:[[ARTChannelStateChange alloc] initWithCurrent:self.state previous:self.state reason:message.error]]; + ARTChannelStateChange *stateChange = [[ARTChannelStateChange alloc] initWithCurrent:self.state previous:self.state event:ARTChannelEventUpdate reason:message.error]; + [self emit:stateChange.event with:stateChange]; return; } @@ -486,7 +487,8 @@ - (void)onMessage:(ARTProtocolMessage *)message { ARTErrorInfo *errorInfo = [ARTErrorInfo wrap:(ARTErrorInfo *)error.userInfo[NSLocalizedFailureReasonErrorKey] prepend:@"Failed to decode data: "]; [self.logger error:@"R:%p C:%p %@", _realtime, self, errorInfo.message]; _errorReason = errorInfo; - [self emit:ARTChannelEventUpdate with:[[ARTChannelStateChange alloc] initWithCurrent:self.state previous:self.state reason:errorInfo]]; + ARTChannelStateChange *stateChange = [[ARTChannelStateChange alloc] initWithCurrent:self.state previous:self.state event:ARTChannelEventUpdate reason:errorInfo]; + [self emit:stateChange.event with:stateChange]; } } diff --git a/Source/ARTTypes.h b/Source/ARTTypes.h index 915e6cc6c..32660bb32 100644 --- a/Source/ARTTypes.h +++ b/Source/ARTTypes.h @@ -153,15 +153,18 @@ NSString *generateNonce(); - (instancetype)initWithCurrent:(ARTRealtimeChannelState)current previous:(ARTRealtimeChannelState)previous + event:(ARTChannelEvent)event reason:(ARTErrorInfo *__art_nullable)reason; - (instancetype)initWithCurrent:(ARTRealtimeChannelState)current previous:(ARTRealtimeChannelState)previous + event:(ARTChannelEvent)event reason:(ARTErrorInfo *__art_nullable)reason resumed:(BOOL)resumed; @property (readonly, nonatomic) ARTRealtimeChannelState current; @property (readonly, nonatomic) ARTRealtimeChannelState previous; +@property (readonly, nonatomic) ARTChannelEvent event; @property (readonly, nonatomic, art_nullable) ARTErrorInfo *reason; @property (readonly, nonatomic) BOOL resumed; diff --git a/Source/ARTTypes.m b/Source/ARTTypes.m index 9c5c56851..b116f4b63 100644 --- a/Source/ARTTypes.m +++ b/Source/ARTTypes.m @@ -119,15 +119,16 @@ - (void)setRetryIn:(NSTimeInterval)retryIn { @implementation ARTChannelStateChange -- (instancetype)initWithCurrent:(ARTRealtimeChannelState)current previous:(ARTRealtimeChannelState)previous reason:(ARTErrorInfo *)reason { - return [self initWithCurrent:current previous:previous reason:reason resumed:NO]; +- (instancetype)initWithCurrent:(ARTRealtimeChannelState)current previous:(ARTRealtimeChannelState)previous event:(ARTChannelEvent)event reason:(ARTErrorInfo *)reason { + return [self initWithCurrent:current previous:previous event:event reason:reason resumed:NO]; } -- (instancetype)initWithCurrent:(ARTRealtimeChannelState)current previous:(ARTRealtimeChannelState)previous reason:(ARTErrorInfo *)reason resumed:(BOOL)resumed { +- (instancetype)initWithCurrent:(ARTRealtimeChannelState)current previous:(ARTRealtimeChannelState)previous event:(ARTChannelEvent)event reason:(ARTErrorInfo *)reason resumed:(BOOL)resumed { self = [self init]; if (self) { _current = current; _previous = previous; + _event = event; _reason = reason; _resumed = resumed; } diff --git a/Spec/RealtimeClientChannel.swift b/Spec/RealtimeClientChannel.swift index aa8ec6734..e3a0e2e89 100644 --- a/Spec/RealtimeClientChannel.swift +++ b/Spec/RealtimeClientChannel.swift @@ -115,9 +115,11 @@ class RealtimeClientChannel: QuickSpec { switch stateChange.current { case .Attached: + expect(stateChange.event).to(equal(ARTChannelEvent.Attached)) expect(stateChange.reason).to(beNil()) channel.detach() case .Detached: + expect(stateChange.event).to(equal(ARTChannelEvent.Detached)) guard let error = stateChange.reason else { fail("Detach state change reason is nil"); done(); return } @@ -162,12 +164,14 @@ class RealtimeClientChannel: QuickSpec { expect(channel.state).to(equal(stateChange.current)) switch stateChange.current { case .Attaching: + expect(stateChange.event).to(equal(ARTChannelEvent.Attaching)) expect(stateChange.reason).to(beNil()) expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Initialized)) case .Failed: guard let reason = stateChange.reason else { fail("Reason is nil"); done(); return } + expect(stateChange.event).to(equal(ARTChannelEvent.Failed)) expect(reason.code) == 40160 expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Attaching)) done() @@ -199,6 +203,7 @@ class RealtimeClientChannel: QuickSpec { } expect(stateChange.reason).to(beNil()) expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Attached)) + expect(stateChange.event).to(equal(ARTChannelEvent.Suspended)) expect(channel.state).to(equal(stateChange.current)) done() } @@ -232,6 +237,7 @@ class RealtimeClientChannel: QuickSpec { expect(channel.state).to(equal(ARTRealtimeChannelState.Attached)) expect(stateChange.previous).to(equal(channel.state)) expect(stateChange.current).to(equal(channel.state)) + expect(stateChange.event).to(equal(ARTChannelEvent.Update)) expect(stateChange.resumed).to(beFalse()) expect(stateChange.reason).to(beNil()) done() @@ -2835,6 +2841,7 @@ class RealtimeClientChannel: QuickSpec { guard let stateChange = stateChange else { fail("ChannelStateChange is nil"); done(); return } + expect(stateChange.event).to(equal(ARTChannelEvent.Update)) expect(stateChange.reason).to(beIdenticalTo(attachedMessageWithError.error)) expect(channel.errorReason).to(beIdenticalTo(stateChange.reason)) done() From 3c8334fe0304a3fd647938b440879defcfd2e469 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Mon, 2 Jan 2017 09:07:18 +0000 Subject: [PATCH 25/43] Update RTN4 for 0.9 (#560) * Update RTN4e * Update RTN4f * Add ConnectionStateChange.event property --- Source/ARTRealtime.m | 12 ++++++------ Source/ARTTypes.h | 3 +++ Source/ARTTypes.m | 7 ++++--- Spec/RealtimeClientConnection.swift | 14 +++++++++++--- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index 3f971cc98..16853495d 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -267,14 +267,14 @@ - (void)transition:(ARTRealtimeConnectionState)state { - (void)transition:(ARTRealtimeConnectionState)state withErrorInfo:(ARTErrorInfo *)errorInfo { [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p transition to %@ requested", self, ARTRealtimeConnectionStateToStr(state)]; - ARTConnectionStateChange *stateChange = [[ARTConnectionStateChange alloc] initWithCurrent:state previous:self.connection.state reason:errorInfo retryIn:0]; + ARTConnectionStateChange *stateChange = [[ARTConnectionStateChange alloc] initWithCurrent:state previous:self.connection.state event:(ARTRealtimeConnectionEvent)state reason:errorInfo retryIn:0]; [self.connection setState:state]; if (errorInfo != nil) { [self.connection setErrorReason:errorInfo]; } - [self transitionSideEffects:stateChange usingEvent:(ARTRealtimeConnectionEvent)stateChange.current]; + [self transitionSideEffects:stateChange]; [_internalEventEmitter emit:[NSNumber numberWithInteger:state] with:stateChange]; } @@ -287,12 +287,12 @@ - (void)updateWithErrorInfo:(art_nullable ARTErrorInfo *)errorInfo { return; } - ARTConnectionStateChange *stateChange = [[ARTConnectionStateChange alloc] initWithCurrent:self.connection.state previous:self.connection.state reason:errorInfo retryIn:0]; + ARTConnectionStateChange *stateChange = [[ARTConnectionStateChange alloc] initWithCurrent:self.connection.state previous:self.connection.state event:ARTRealtimeConnectionEventUpdate reason:errorInfo retryIn:0]; - [self transitionSideEffects:stateChange usingEvent:ARTRealtimeConnectionEventUpdate]; + [self transitionSideEffects:stateChange]; } -- (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange usingEvent:(ARTRealtimeConnectionEvent)event { +- (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { ARTStatus *status = nil; // Do not increase the reference count (avoid retain cycles): // i.e. the `unlessStateChangesBefore` is setting a timer and if the `ARTRealtime` instance is released before that timer, then it could create a leak. @@ -470,7 +470,7 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange usingEvent } } - [self.connection emit:event with:stateChange]; + [self.connection emit:stateChange.event with:stateChange]; } - (void)unlessStateChangesBefore:(NSTimeInterval)deadline do:(void(^)())callback { diff --git a/Source/ARTTypes.h b/Source/ARTTypes.h index 32660bb32..29c6b1a48 100644 --- a/Source/ARTTypes.h +++ b/Source/ARTTypes.h @@ -133,15 +133,18 @@ NSString *generateNonce(); - (instancetype)initWithCurrent:(ARTRealtimeConnectionState)current previous:(ARTRealtimeConnectionState)previous + event:(ARTRealtimeConnectionEvent)event reason:(ARTErrorInfo *__art_nullable)reason; - (instancetype)initWithCurrent:(ARTRealtimeConnectionState)current previous:(ARTRealtimeConnectionState)previous + event:(ARTRealtimeConnectionEvent)event reason:(ARTErrorInfo *__art_nullable)reason retryIn:(NSTimeInterval)retryIn; @property (readonly, nonatomic) ARTRealtimeConnectionState current; @property (readonly, nonatomic) ARTRealtimeConnectionState previous; +@property (readonly, nonatomic) ARTRealtimeConnectionEvent event; @property (readonly, nonatomic, art_nullable) ARTErrorInfo *reason; @property (readonly, nonatomic) NSTimeInterval retryIn; diff --git a/Source/ARTTypes.m b/Source/ARTTypes.m index b116f4b63..e8c048e1c 100644 --- a/Source/ARTTypes.m +++ b/Source/ARTTypes.m @@ -46,15 +46,16 @@ NSTimeInterval millisecondsToTimeInterval(uint64_t msecs) { @implementation ARTConnectionStateChange -- (instancetype)initWithCurrent:(ARTRealtimeConnectionState)current previous:(ARTRealtimeConnectionState)previous reason:(ARTErrorInfo *)reason { - return [self initWithCurrent:current previous:previous reason:reason retryIn:(NSTimeInterval)0]; +- (instancetype)initWithCurrent:(ARTRealtimeConnectionState)current previous:(ARTRealtimeConnectionState)previous event:(ARTRealtimeConnectionEvent)event reason:(ARTErrorInfo *)reason { + return [self initWithCurrent:current previous:previous event:event reason:reason retryIn:(NSTimeInterval)0]; } -- (instancetype)initWithCurrent:(ARTRealtimeConnectionState)current previous:(ARTRealtimeConnectionState)previous reason:(ARTErrorInfo *)reason retryIn:(NSTimeInterval)retryIn { +- (instancetype)initWithCurrent:(ARTRealtimeConnectionState)current previous:(ARTRealtimeConnectionState)previous event:(ARTRealtimeConnectionEvent)event reason:(ARTErrorInfo *)reason retryIn:(NSTimeInterval)retryIn { self = [self init]; if (self) { _current = current; _previous = previous; + _event = event; _reason = reason; _retryIn = retryIn; } diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index 244197243..449d5c2ca 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -449,7 +449,7 @@ class RealtimeClientConnection: QuickSpec { defer { client.dispose(); client.close() } waitUntil(timeout: testTimeout) { done in - client.connection.once(.Connected) { stateChange in + client.connection.once(ARTRealtimeConnectionEvent.Connected) { stateChange in guard let stateChange = stateChange else { fail("ConnectionStateChange is empty"); done() return @@ -473,13 +473,17 @@ class RealtimeClientConnection: QuickSpec { var errorInfo: ARTErrorInfo? waitUntil(timeout: testTimeout) { done in connection.on { stateChange in - let stateChange = stateChange! + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); done(); return + } let state = stateChange.current let reason = stateChange.reason switch state { case .Connected: + expect(stateChange.event).to(equal(ARTRealtimeConnectionEvent.Connected)) client.onError(AblyTests.newErrorProtocolMessage()) case .Failed: + expect(stateChange.event).to(equal(ARTRealtimeConnectionEvent.Failed)) errorInfo = reason done() default: @@ -514,11 +518,15 @@ class RealtimeClientConnection: QuickSpec { waitUntil(timeout: testTimeout) { done in client.connection.once(.Update) { stateChange in - guard let error = stateChange?.reason else { + guard let stateChange = stateChange else { + fail("ConnectionStateChange is nil"); done(); return + } + guard let error = stateChange.reason else { fail("Reason error is nil"); done(); return } expect(error.code) == 1234 expect(error.message) == "fabricated error" + expect(stateChange.event).to(equal(ARTRealtimeConnectionEvent.Update)) done() } From 4668815c4e55cdea730061242fb07bd3480677b7 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 4 Jan 2017 23:19:52 +0000 Subject: [PATCH 26/43] Update RTL5 for 0.9 (#546) * RTL5j * Update RTL5a * RTL5i * Fix: if channel is SUSPENDED then the detach request transitions immediately to DETACHED state * Fix: if channel is ATTACHING then do the detach operation after the completion of the attaching * Remove testSkipsFromAttachingToDetaching * Fix: presence sync can fail * Fix RTP11b * Fix RTP9e * RTP5: pending * Fix RTL6c3 * Fix: should check channel state when a queued message is processed --- Source/ARTPresenceMap.h | 3 + Source/ARTPresenceMap.m | 30 +++++- Source/ARTRealtimeChannel.m | 61 ++++++++++--- Source/ARTRealtimePresence.m | 8 +- Spec/RealtimeClientChannel.swift | 147 ++++++++++++++++++++++++++++-- Spec/RealtimeClientPresence.swift | 37 ++++++-- Tests/ARTRealtimeAttachTest.m | 26 ------ 7 files changed, 250 insertions(+), 62 deletions(-) diff --git a/Source/ARTPresenceMap.h b/Source/ARTPresenceMap.h index d51571e47..bbf3e7c7e 100644 --- a/Source/ARTPresenceMap.h +++ b/Source/ARTPresenceMap.h @@ -10,6 +10,7 @@ #import "CompatibilityMacros.h" @class ARTPresenceMessage; +@class ARTErrorInfo; ART_ASSUME_NONNULL_BEGIN @@ -30,8 +31,10 @@ ART_ASSUME_NONNULL_BEGIN - (void)startSync; - (void)endSync; +- (void)failsSync:(ARTErrorInfo *)error; - (void)onceSyncEnds:(void (^)(__GENERIC(NSArray, ARTPresenceMessage *) *))callback; +- (void)onceSyncFails:(void (^)(ARTErrorInfo *))callback; @end diff --git a/Source/ARTPresenceMap.m b/Source/ARTPresenceMap.m index f71047e63..98144b98f 100644 --- a/Source/ARTPresenceMap.m +++ b/Source/ARTPresenceMap.m @@ -10,9 +10,15 @@ #import "ARTPresenceMessage.h" #import "ARTEventEmitter.h" +typedef NS_ENUM(NSUInteger, ARTPresenceSyncState) { + ARTPresenceSyncStarted, //ItemType: nil + ARTPresenceSyncEnded, //ItemType: NSArray* + ARTPresenceSyncFailed //ItemType: ARTErrorInfo* +}; + @interface ARTPresenceMap () { BOOL _syncStarted; - __GENERIC(ARTEventEmitter, NSNull *, __GENERIC(NSArray, ARTPresenceMessage *) *) *_syncEndedEventEmitter; + ARTEventEmitter *_syncEventEmitter; } @property (readwrite, strong, atomic) __GENERIC(NSMutableDictionary, NSString *, ARTPresenceMessage *) *recentMembers; @@ -27,7 +33,7 @@ - (id)init { _recentMembers = [NSMutableDictionary dictionary]; _syncStarted = false; _syncComplete = false; - _syncEndedEventEmitter = [[ARTEventEmitter alloc] init]; + _syncEventEmitter = [[ARTEventEmitter alloc] init]; } return self; } @@ -56,24 +62,40 @@ - (void)startSync { _recentMembers = [NSMutableDictionary dictionary]; _syncStarted = true; _syncComplete = false; + [_syncEventEmitter emit:[NSNumber numberWithInt:ARTPresenceSyncStarted] with:nil]; } - (void)endSync { [self clean]; _syncStarted = false; _syncComplete = true; - [_syncEndedEventEmitter emit:[NSNull null] with:[self.recentMembers allValues]]; + [_syncEventEmitter emit:[NSNumber numberWithInt:ARTPresenceSyncEnded] with:[self.recentMembers allValues]]; + [_syncEventEmitter off]; +} + +- (void)failsSync:(ARTErrorInfo *)error { + [self clean]; + _syncStarted = false; + _syncComplete = true; + [_syncEventEmitter emit:[NSNumber numberWithInt:ARTPresenceSyncFailed] with:error]; + [_syncEventEmitter off]; } - (void)onceSyncEnds:(void (^)(NSArray *))callback { if (self.syncInProgress) { - [_syncEndedEventEmitter once:callback]; + [_syncEventEmitter once:[NSNumber numberWithInt:ARTPresenceSyncEnded] callback:callback]; } else { callback([self.recentMembers allValues]); } } +- (void)onceSyncFails:(void (^)(ARTErrorInfo *))callback { + if (self.syncInProgress) { + [_syncEventEmitter once:[NSNumber numberWithInt:ARTPresenceSyncFailed] callback:callback]; + } +} + - (BOOL)getSyncInProgress { return _syncStarted && !_syncComplete; } diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index 6c09bbfb0..74cc169a2 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -136,25 +136,45 @@ - (void)publishPresence:(ARTPresenceMessage *)msg callback:(art_nullable void (^ } - (void)publishProtocolMessage:(ARTProtocolMessage *)pm callback:(void (^)(ARTStatus *))cb { + __weak __typeof(self) weakSelf = self; + ARTStatus *statusInvalidChannel = [ARTStatus state:ARTStateError info:[ARTErrorInfo createWithCode:90001 message:@"channel operation failed (invalid channel state)"]]; + switch (_realtime.connection.state) { case ARTRealtimeClosing: case ARTRealtimeClosed: { if (cb) { - ARTStatus *status = [ARTStatus state:ARTStateError info:[ARTErrorInfo createWithCode:90001 message:@"channel operation failed (invalid channel state)"]]; - cb(status); + cb(statusInvalidChannel); } return; } default: break; } + + void (^queuedCallback)(ARTStatus *) = ^(ARTStatus *status) { + switch ([weakSelf state]) { + case ARTRealtimeChannelDetaching: + case ARTRealtimeChannelDetached: + case ARTRealtimeChannelFailed: + if (cb) { + cb(status.state == ARTStateOk ? statusInvalidChannel : status); + } + return; + default: + break; + } + if (cb) { + cb(status); + } + }; + switch (self.state) { case ARTRealtimeChannelInitialized: [self attach]; // intentional fall-through case ARTRealtimeChannelAttaching: { - [self addToQueue:pm callback:cb]; + [self addToQueue:pm callback:queuedCallback]; break; } case ARTRealtimeChannelSuspended: @@ -163,8 +183,7 @@ - (void)publishProtocolMessage:(ARTProtocolMessage *)pm callback:(void (^)(ARTSt case ARTRealtimeChannelFailed: { if (cb) { - ARTStatus *status = [ARTStatus state:ARTStateError info:[ARTErrorInfo createWithCode:90001 message:@"channel operation failed (invalid channel state)"]]; - cb(status); + cb(statusInvalidChannel); } break; } @@ -173,10 +192,10 @@ - (void)publishProtocolMessage:(ARTProtocolMessage *)pm callback:(void (^)(ARTSt if (_realtime.connection.state == ARTRealtimeConnected) { [self sendMessage:pm callback:cb]; } else { - [self addToQueue:pm callback:cb]; + [self addToQueue:pm callback:queuedCallback]; [self.realtime.internalEventEmitter once:[NSNumber numberWithInteger:ARTRealtimeConnected] callback:^(ARTConnectionStateChange *__art_nullable change) { - [self sendQueuedMessages]; + [weakSelf sendQueuedMessages]; }]; } break; @@ -321,11 +340,6 @@ - (void)transition:(ARTRealtimeChannelState)state status:(ARTStatus *)status { [_attachedEventEmitter emit:[NSNull null] with:status.errorInfo]; [_detachedEventEmitter emit:[NSNull null] with:status.errorInfo]; } - else if (state == ARTRealtimeChannelDetaching) { - NSString *msg = @"channel is being DETACHED"; - [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p %@", _realtime, self, msg]; - [_attachedEventEmitter emit:[NSNull null] with:[ARTErrorInfo createWithCode:90000 message:msg]]; - } [self emit:stateChange.event with:stateChange]; } @@ -443,7 +457,7 @@ - (void)setDetached:(ARTProtocolMessage *)message { } self.attachSerial = nil; - + ARTErrorInfo *errorInfo = message.error ? message.error : [ARTErrorInfo createWithCode:0 message:@"channel has detached"]; ARTStatus *reason = [ARTStatus state:ARTStateNotAttached info:errorInfo]; [self detachChannel:reason]; @@ -663,8 +677,18 @@ - (void)detach:(void (^)(ARTErrorInfo * _Nullable))callback { switch (self.state) { case ARTRealtimeChannelInitialized: [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p can't detach when not attached", _realtime, self]; - if (callback) [_detachedEventEmitter once:callback]; + if (callback) callback(nil); return; + case ARTRealtimeChannelAttaching: { + [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p waiting for the completion of the attaching operation", _realtime, self]; + [_attachedEventEmitter once:^(ARTErrorInfo *errorInfo) { + if (callback && errorInfo) { + callback(errorInfo); + } + [self detach:callback]; + }]; + return; + } case ARTRealtimeChannelDetaching: [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p already detaching", _realtime, self]; if (callback) [_detachedEventEmitter once:callback]; @@ -673,6 +697,11 @@ - (void)detach:(void (^)(ARTErrorInfo * _Nullable))callback { [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p already detached", _realtime, self]; if (callback) callback(nil); return; + case ARTRealtimeChannelSuspended: + [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p transitions immediately to the detached", _realtime, self]; + [self transition:ARTRealtimeChannelDetached status:[ARTStatus state:ARTStateOk]]; + if (callback) callback(nil); + return; case ARTRealtimeChannelFailed: [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p can't detach when in a failed state", _realtime, self]; if (callback) callback([ARTErrorInfo createWithCode:90000 message:@"can't detach when in a failed state"]); @@ -720,6 +749,10 @@ - (void)detachAfterChecks:(void (^)(ARTErrorInfo * _Nullable))callback { [self.realtime.connectedEventEmitter off:reconnectedListener]; }]; } + + if (self.presenceMap.syncInProgress) { + [self.presenceMap failsSync:[ARTErrorInfo createWithCode:90000 message:@"channel is being DETACHED"]]; + } } - (void)detach { diff --git a/Source/ARTRealtimePresence.m b/Source/ARTRealtimePresence.m index 10b4def56..ed38a335e 100644 --- a/Source/ARTRealtimePresence.m +++ b/Source/ARTRealtimePresence.m @@ -62,10 +62,16 @@ - (void)get:(ARTRealtimePresenceQuery *)query callback:(void (^)(NSArray *members) { callback(members, nil); }]; + [_channel.presenceMap onceSyncFails:^(ARTErrorInfo *error) { + callback(nil, error); + }]; } else { callback(_channel.presenceMap.members.allValues, nil); } diff --git a/Spec/RealtimeClientChannel.swift b/Spec/RealtimeClientChannel.swift index e3a0e2e89..1674e76e6 100644 --- a/Spec/RealtimeClientChannel.swift +++ b/Spec/RealtimeClientChannel.swift @@ -1085,7 +1085,7 @@ class RealtimeClientChannel: QuickSpec { describe("detach") { // RTL5a - it("if state is INITIALISED, DETACHED or DETACHING nothing is done") { + it("if state is INITIALIZED or DETACHED nothing is done") { let client = ARTRealtime(options: AblyTests.commonAppSetup()) defer { client.dispose(); client.close() } @@ -1106,13 +1106,6 @@ class RealtimeClientChannel: QuickSpec { expect(channel.state).to(equal(ARTRealtimeChannelState.Detached)) } - expect(channel.state).to(equal(ARTRealtimeChannelState.Detaching)) - channel.detach { errorInfo in - expect(errorInfo).to(beNil()) - expect(channel.state).to(equal(ARTRealtimeChannelState.Detached)) - } - expect(channel.state).to(equal(ARTRealtimeChannelState.Detaching)) - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Detached), timeout: testTimeout) waitUntil(timeout: testTimeout) { done in @@ -1124,6 +1117,104 @@ class RealtimeClientChannel: QuickSpec { } } + // RTL5i + it("if the channel is in a pending state DETACHING, do the detach operation after the completion of the pending request") { + let options = AblyTests.commonAppSetup() + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + waitUntil(timeout: testTimeout) { done in + channel.attach() { error in + expect(error).to(beNil()) + done() + } + } + + var detachedCount = 0 + channel.on(.Detached) { _ in + detachedCount += 1 + } + + var detachingCount = 0 + channel.on(.Detaching) { _ in + detachingCount += 1 + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.once(.Detaching) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); done(); return + } + expect(stateChange.reason).to(beNil()) + expect(stateChange.current).to(equal(ARTRealtimeChannelState.Detaching)) + expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Attached)) + channel.detach() + partialDone() + } + channel.once(.Detached) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); done(); return + } + expect(stateChange.current).to(equal(ARTRealtimeChannelState.Detached)) + expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Detaching)) + partialDone() + } + channel.detach() + } + + waitUntil(timeout: testTimeout) { done in + delay(1.0) { + expect(detachedCount) == 1 + expect(detachingCount) == 1 + done() + } + } + + channel.off() + } + + // RTL5i + it("if the channel is in a pending state ATTACHING, do the detach operation after the completion of the pending request") { + let options = AblyTests.commonAppSetup() + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(3, done: done) + channel.once(.Attaching) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); partialDone(); return + } + expect(stateChange.reason).to(beNil()) + expect(stateChange.current).to(equal(ARTRealtimeChannelState.Attaching)) + expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Initialized)) + channel.detach() + partialDone() + } + channel.once(.Attached) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); partialDone(); return + } + expect(stateChange.reason).to(beNil()) + expect(stateChange.current).to(equal(ARTRealtimeChannelState.Attached)) + expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Attaching)) + partialDone() + } + channel.once(.Detaching) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); partialDone(); return + } + expect(stateChange.reason).to(beNil()) + expect(stateChange.current).to(equal(ARTRealtimeChannelState.Detaching)) + expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Attached)) + partialDone() + } + channel.attach() + } + } + // RTL5b it("results in an error if the connection state is FAILED") { let client = ARTRealtime(options: AblyTests.commonAppSetup()) @@ -1382,6 +1473,42 @@ class RealtimeClientChannel: QuickSpec { } } + // RTL5j + it("if the channel state is SUSPENDED, the @detach@ request transitions the channel immediately to the DETACHED state") { + let client = ARTRealtime(options: AblyTests.commonAppSetup()) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + channel.attach() { error in + expect(error).to(beNil()) + done() + } + } + + channel.setSuspended(ARTStatus.state(.Ok)) + expect(channel.state).to(equal(ARTRealtimeChannelState.Suspended)) + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.once(.Detached) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); partialDone(); return + } + expect(stateChange.reason).to(beNil()) + expect(stateChange.current).to(equal(ARTRealtimeChannelState.Detached)) + expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Suspended)) + partialDone() + } + channel.detach() { error in + expect(error).to(beNil()) + partialDone() + } + } + + expect(channel.state).to(equal(ARTRealtimeChannelState.Detached)) + } + } // RTL6 @@ -1710,7 +1837,7 @@ class RealtimeClientChannel: QuickSpec { waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(2, done: done) expect(channel.state).to(equal(ARTRealtimeChannelState.Initialized)) - channel.once(.Attaching) { stateChange in + channel.once(.Attached) { stateChange in expect(stateChange?.reason).to(beNil()) channel.detach() partialDone() @@ -1719,7 +1846,7 @@ class RealtimeClientChannel: QuickSpec { guard let error = error else { fail("Error is nil"); done(); return } - expect(error.message).to(contain("channel has detached")) + expect(error.message).to(contain("invalid channel state")) expect(channel.state).to(equal(ARTRealtimeChannelState.Detaching)) partialDone() } diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index 9964f078e..2ac0dd29e 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -226,7 +226,7 @@ class RealtimeClientPresence: QuickSpec { } // RTP5 - context("Channel state change side effects") { + pending("Channel state change side effects") { // RTP5a it("all queued presence messages should fail immediately if the channel enters the FAILED state") { @@ -956,10 +956,14 @@ class RealtimeClientPresence: QuickSpec { channel.attach() channel.detach() + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Detached), timeout: testTimeout) waitUntil(timeout: testTimeout) { done in channel.presence.update(nil) { error in - expect(error!.message).to(contain("invalid channel state")) + guard let error = error else { + fail("Error is nil"); done(); return + } + expect(error.message).to(contain("invalid channel state")) done() } } @@ -977,7 +981,10 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in channel.presence.update(nil) { error in - expect(error!.message).to(contain("invalid channel state")) + guard let error = error else { + fail("Error is nil"); done(); return + } + expect(error.message).to(contain("invalid channel state")) done() } } @@ -994,7 +1001,10 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in channel.presence.update(nil) { error in - expect(error!.message).to(contain("Channel denied access based on given capability")) + guard let error = error else { + fail("Error is nil"); done(); return + } + expect(error.message).to(contain("Channel denied access based on given capability")) done() } } @@ -1008,7 +1018,10 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in channel.presence.update(nil) { error in - expect(error!.message).to(contain("presence message without clientId")) + guard let error = error else { + fail("Error is nil"); done(); return + } + expect(error.message).to(contain("presence message without clientId")) done() } } @@ -1827,10 +1840,20 @@ class RealtimeClientPresence: QuickSpec { // RTP11b it("should result in an error if the channel moves to the DETACHED state") { - let client = ARTRealtime(options: AblyTests.commonAppSetup()) + let options = AblyTests.commonAppSetup() + + let client = ARTRealtime(options: options) defer { client.dispose(); client.close() } let channel = client.channels.get("test") + var clientMembers: ARTRealtime? + defer { clientMembers?.dispose(); clientMembers?.close() } + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("test", members: 120, options: options) { + done() + }.first + } + waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(2, done: done) channel.presence.get() { members, error in @@ -1847,7 +1870,7 @@ class RealtimeClientPresence: QuickSpec { } } - expect(channel.state).to(equal(ARTRealtimeChannelState.Detached)) + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Detached), timeout: testTimeout) } // RTP11c diff --git a/Tests/ARTRealtimeAttachTest.m b/Tests/ARTRealtimeAttachTest.m index c08ec288b..6247fcbab 100644 --- a/Tests/ARTRealtimeAttachTest.m +++ b/Tests/ARTRealtimeAttachTest.m @@ -149,32 +149,6 @@ - (void)testDetaching { [realtime testSuite_waitForConnectionToClose:self]; } -- (void)testSkipsFromAttachingToDetaching { - ARTClientOptions *options = [ARTTestUtil newSandboxApp:self withDescription:__FUNCTION__]; - __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; - ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; - ARTRealtimeChannel *channel = [realtime.channels get:@"attaching_to_detaching"]; - [channel on:^(ARTChannelStateChange *stateChange) { - if (stateChange.current == ARTRealtimeChannelAttached) { - XCTFail(@"Should not have made it to attached"); - } - else if (stateChange.current == ARTRealtimeChannelAttaching) { - [channel detach]; - } - else if (stateChange.current == ARTRealtimeChannelDetaching) { - [channel off]; - [expectation fulfill]; - } - else if (stateChange.current == ARTRealtimeChannelDetached) { - XCTFail(@"Should not have made it to detached"); - - } - }]; - [channel attach]; - [self waitForExpectationsWithTimeout:[ARTTestUtil timeout] handler:nil]; - [realtime testSuite_waitForConnectionToClose:self]; -} - -(void)testDetachingIgnoresDetach { ARTClientOptions *options = [ARTTestUtil newSandboxApp:self withDescription:__FUNCTION__]; __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; From 992bc583eb1194a693139a6ce9403ed06b16cbd3 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Mon, 9 Jan 2017 11:25:35 +0000 Subject: [PATCH 27/43] TR4i (#562) * ARTProtocolMessageFlag enum: use NS_OPTIONS to define a bitmask * TR4i * Fix: should indicate that the channel has been resumed or not (RESUMED flag) --- Source/ARTProtocolMessage+Private.h | 11 ++++++-- Source/ARTProtocolMessage.m | 12 +++++++-- Source/ARTRealtimeChannel.m | 4 +-- Spec/RealtimeClientChannel.swift | 39 +++++++++++++++++++++++++++++ Spec/RealtimeClientPresence.swift | 4 +-- 5 files changed, 62 insertions(+), 8 deletions(-) diff --git a/Source/ARTProtocolMessage+Private.h b/Source/ARTProtocolMessage+Private.h index d094a0aec..5812232e0 100644 --- a/Source/ARTProtocolMessage+Private.h +++ b/Source/ARTProtocolMessage+Private.h @@ -6,7 +6,12 @@ // Copyright (c) 2014 Ably. All rights reserved. // -NSString *__art_nonnull ARTProtocolMessageActionToStr(ARTProtocolMessageAction action); +/// ARTProtocolMessageFlag bitmask +typedef NS_OPTIONS(NSUInteger, ARTProtocolMessageFlag) { + ARTProtocolMessageFlagHasPresence = (1UL << 0), //1 + ARTProtocolMessageFlagHasBacklog = (1UL << 1), //2 + ARTProtocolMessageFlagResumed = (1UL << 2) //4 +}; ART_ASSUME_NONNULL_BEGIN @@ -15,7 +20,9 @@ ART_ASSUME_NONNULL_BEGIN @property (readwrite, assign, nonatomic) BOOL hasConnectionSerial; @property (readonly, assign, nonatomic) BOOL ackRequired; -- (BOOL)isSyncEnabled; +@property (readonly, assign, nonatomic) BOOL hasPresence; +@property (readonly, assign, nonatomic) BOOL hasBacklog; +@property (readonly, assign, nonatomic) BOOL resumed; - (BOOL)mergeFrom:(ARTProtocolMessage *)msg; diff --git a/Source/ARTProtocolMessage.m b/Source/ARTProtocolMessage.m index f73fcb433..54bec033e 100644 --- a/Source/ARTProtocolMessage.m +++ b/Source/ARTProtocolMessage.m @@ -107,8 +107,16 @@ - (BOOL)ackRequired { return self.action == ARTProtocolMessageMessage || self.action == ARTProtocolMessagePresence; } -- (BOOL)isSyncEnabled { - return self.flags & 0x1; +- (BOOL)hasPresence { + return self.flags & ARTProtocolMessageFlagHasPresence; +} + +- (BOOL)hasBacklog { + return self.flags & ARTProtocolMessageFlagHasBacklog; +} + +- (BOOL)resumed { + return self.flags & ARTProtocolMessageFlagResumed; } - (ARTConnectionDetails *)getConnectionDetails { diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index 74cc169a2..7189aeaf0 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -419,12 +419,12 @@ - (void)setAttached:(ARTProtocolMessage *)message { if (message.error != nil) { _errorReason = message.error; } - ARTChannelStateChange *stateChange = [[ARTChannelStateChange alloc] initWithCurrent:self.state previous:self.state event:ARTChannelEventUpdate reason:message.error]; + ARTChannelStateChange *stateChange = [[ARTChannelStateChange alloc] initWithCurrent:self.state previous:self.state event:ARTChannelEventUpdate reason:message.error resumed:message.resumed]; [self emit:stateChange.event with:stateChange]; return; } - if ([message isSyncEnabled]) { + if (message.hasPresence) { [self.presenceMap startSync]; [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p PresenceMap Sync started", _realtime, self]; } diff --git a/Spec/RealtimeClientChannel.swift b/Spec/RealtimeClientChannel.swift index 1674e76e6..bea897f93 100644 --- a/Spec/RealtimeClientChannel.swift +++ b/Spec/RealtimeClientChannel.swift @@ -362,6 +362,45 @@ class RealtimeClientChannel: QuickSpec { } } + // RTL2f, TR4i + it("bit flag RESUMED was included") { + let options = AblyTests.commonAppSetup() + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("test") + + waitUntil(timeout: testTimeout) { done in + channel.once(.Attached) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStageChange is nil"); done(); return + } + expect(stateChange.resumed).to(beFalse()) + expect(stateChange.reason).to(beNil()) + done() + } + channel.attach() + } + + let attachedMessage = ARTProtocolMessage() + attachedMessage.action = .Attached + attachedMessage.channel = "test" + attachedMessage.flags = 4 //Resumed + + waitUntil(timeout: testTimeout) { done in + channel.once(.Update) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStageChange is nil"); done(); return + } + expect(stateChange.resumed).to(beTrue()) + expect(stateChange.reason).to(beNil()) + expect(stateChange.current).to(equal(ARTRealtimeChannelState.Attached)) + expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Attached)) + done() + } + client.transport?.receive(attachedMessage) + } + } + } // RTL3 diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index 2ac0dd29e..694343116 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -34,7 +34,7 @@ class RealtimeClientPresence: QuickSpec { let attached = transport.protocolMessagesReceived.filter({ $0.action == .Attached })[0] expect(attached.flags & 0x1).to(equal(0)) - expect(attached.isSyncEnabled()).to(beFalse()) + expect(attached.hasPresence).to(beFalse()) expect(channel.presence.syncComplete).to(beFalse()) expect(channel.presenceMap.syncComplete).to(beFalse()) } @@ -71,7 +71,7 @@ class RealtimeClientPresence: QuickSpec { // There are members present on the channel expect(attached.flags & 0x1).to(equal(1)) - expect(attached.isSyncEnabled()).to(beTrue()) + expect(attached.hasPresence).to(beTrue()) expect(channel.presence.syncComplete).toEventually(beTrue(), timeout: testTimeout) From 5e5593761985e99512c7770ec2e32a2e4697a486 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Mon, 9 Jan 2017 18:32:17 +0000 Subject: [PATCH 28/43] Swift performance: speed up code completion (#569) * Speed up code completion - This problem is fixed on Xcode 8 but since we are still using Xcode 7 I decided to change that code. * Test suite: addMembersSequentiallyToChannel should return the realtime client * Test suite: set AsyncDefaults.Timeout with default value --- Spec/RealtimeClientPresence.swift | 64 +++++++++++++++---------------- Spec/RestClientPresence.swift | 49 ++++++++++++----------- Spec/TestUtilities.swift | 9 +++-- 3 files changed, 64 insertions(+), 58 deletions(-) diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index 694343116..16d91ebb0 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -45,14 +45,15 @@ class RealtimeClientPresence: QuickSpec { var disposable = [ARTRealtime]() defer { for clientItem in disposable { + clientItem.dispose() clientItem.close() } } waitUntil(timeout: testTimeout) { done in - disposable += AblyTests.addMembersSequentiallyToChannel("test", members: 250, options: options) { + disposable += [AblyTests.addMembersSequentiallyToChannel("test", members: 250, options: options) { done() - } + }] } options.autoConnect = false @@ -85,12 +86,12 @@ class RealtimeClientPresence: QuickSpec { let options = AblyTests.commonAppSetup() options.disconnectedRetryTimeout = 1.0 var clientSecondary: ARTRealtime! - defer { clientSecondary.close() } + defer { clientSecondary.dispose(); clientSecondary.close() } waitUntil(timeout: testTimeout) { done in clientSecondary = AblyTests.addMembersSequentiallyToChannel("test", members: 150, options: options) { done() - }.first + } } let client = AblyTests.newRealtime(options) @@ -126,12 +127,12 @@ class RealtimeClientPresence: QuickSpec { it("should receive all 250 members") { let options = AblyTests.commonAppSetup() var clientSource: ARTRealtime! - defer { clientSource.close() } + defer { clientSource.dispose(); clientSource.close() } waitUntil(timeout: testTimeout) { done in clientSource = AblyTests.addMembersSequentiallyToChannel("test", members: 250, options: options) { done() - }.first + } } let clientTarget = ARTRealtime(options: options) @@ -777,11 +778,9 @@ class RealtimeClientPresence: QuickSpec { expect(sent.action).to(equal(ARTPresenceAction.Update)) expect(sent.clientId).to(beNil()) - let received = transport.protocolMessagesReceived - .filter({ $0.action == .Presence }) - .map({ $0.presence! }) - .reduce([], combine: +) - .filter({ $0.action == .Update })[0] + let receivedPresenceProtocolMessages = transport.protocolMessagesReceived.filter({ $0.action == .Presence }) + let receivedPresenceMessages = receivedPresenceProtocolMessages.flatMap({ $0.presence! }) + let received = receivedPresenceMessages.filter({ $0.action == .Update })[0] expect(received.action).to(equal(ARTPresenceAction.Update)) expect(received.clientId).to(equal("john")) } @@ -856,7 +855,7 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in clientSecondary = AblyTests.addMembersSequentiallyToChannel("test", members: 100, options: options) { done() - }.first + } } let client = AblyTests.newRealtime(options) @@ -1114,11 +1113,9 @@ class RealtimeClientPresence: QuickSpec { expect(sent.action).to(equal(ARTPresenceAction.Leave)) expect(sent.clientId).to(beNil()) - let received = transport.protocolMessagesReceived - .filter({ $0.action == .Presence }) - .map({ $0.presence! }) - .reduce([], combine: +) - .filter({ $0.action == .Leave })[0] + let receivedPresenceProtocolMessages = transport.protocolMessagesReceived.filter({ $0.action == .Presence }) + let receivedPresenceMessages = receivedPresenceProtocolMessages.flatMap({ $0.presence! }) + let received = receivedPresenceMessages.filter({ $0.action == .Leave })[0] expect(received.action).to(equal(ARTPresenceAction.Leave)) expect(received.clientId).to(equal("john")) } @@ -1699,15 +1696,16 @@ class RealtimeClientPresence: QuickSpec { var disposable = [ARTRealtime]() defer { for clientItem in disposable { + clientItem.dispose() clientItem.close() } } let expectedData = "online" waitUntil(timeout: testTimeout) { done in - disposable += AblyTests.addMembersSequentiallyToChannel("test", members: 150, data:expectedData, options: options) { + disposable += [AblyTests.addMembersSequentiallyToChannel("test", members: 150, data:expectedData, options: options) { done() - } + }] } let client = ARTRealtime(options: options) @@ -1851,7 +1849,7 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in clientMembers = AblyTests.addMembersSequentiallyToChannel("test", members: 120, options: options) { done() - }.first + } } waitUntil(timeout: testTimeout) { done in @@ -1880,12 +1878,12 @@ class RealtimeClientPresence: QuickSpec { it("waitForSync is true, should wait until SYNC is complete before returning a list of members") { let options = AblyTests.commonAppSetup() var clientSecondary: ARTRealtime! - defer { clientSecondary.close() } + defer { clientSecondary.dispose(); clientSecondary.close() } waitUntil(timeout: testTimeout) { done in clientSecondary = AblyTests.addMembersSequentiallyToChannel("test", members: 150, options: options) { done() - }.first + } } let client = AblyTests.newRealtime(options) @@ -1918,12 +1916,12 @@ class RealtimeClientPresence: QuickSpec { it("waitForSync is false, should return immediately the known set of presence members") { let options = AblyTests.commonAppSetup() var clientSecondary: ARTRealtime! - defer { clientSecondary.close() } + defer { clientSecondary.dispose(); clientSecondary.close() } waitUntil(timeout: testTimeout) { done in clientSecondary = AblyTests.addMembersSequentiallyToChannel("test", members: 150, options: options) { done() - }.first + } } let client = AblyTests.newRealtime(options) @@ -2009,14 +2007,14 @@ class RealtimeClientPresence: QuickSpec { let options = AblyTests.commonAppSetup() var clientSecondary: ARTRealtime! - defer { clientSecondary.close() } + defer { clientSecondary.dispose(); clientSecondary.close() } let expectedData = ["x", "y"] let expectedPattern = "^user(\\d+)$" waitUntil(timeout: testTimeout) { done in clientSecondary = AblyTests.addMembersSequentiallyToChannel("test", members: 150, data: expectedData, options: options) { done() - }.first + } } let client = ARTRealtime(options: options) @@ -2138,14 +2136,15 @@ class RealtimeClientPresence: QuickSpec { var disposable = [ARTRealtime]() defer { for clientItem in disposable { + clientItem.dispose() clientItem.close() } } waitUntil(timeout: testTimeout) { done in - disposable += AblyTests.addMembersSequentiallyToChannel("test", members: 25, options: options) { + disposable += [AblyTests.addMembersSequentiallyToChannel("test", members: 25, options: options) { done() - } + }] } let client = ARTRealtime(options: options) @@ -2158,9 +2157,9 @@ class RealtimeClientPresence: QuickSpec { } waitUntil(timeout: testTimeout) { done in - disposable += AblyTests.addMembersSequentiallyToChannel("test", startFrom: 26, members: 35, options: options) { + disposable += [AblyTests.addMembersSequentiallyToChannel("test", startFrom: 26, members: 35, options: options) { done() - } + }] } let query = ARTRealtimeHistoryQuery() @@ -2188,14 +2187,15 @@ class RealtimeClientPresence: QuickSpec { var disposable = [ARTRealtime]() defer { for clientItem in disposable { + clientItem.dispose() clientItem.close() } } waitUntil(timeout: testTimeout) { done in - disposable += AblyTests.addMembersSequentiallyToChannel("test", members: 250, options: options) { + disposable += [AblyTests.addMembersSequentiallyToChannel("test", members: 250, options: options) { done() - } + }] } let client = AblyTests.newRealtime(options) diff --git a/Spec/RestClientPresence.swift b/Spec/RestClientPresence.swift index ab7e824b7..b83622135 100644 --- a/Spec/RestClientPresence.swift +++ b/Spec/RestClientPresence.swift @@ -26,6 +26,7 @@ class RestClientPresence: QuickSpec { var disposable = [ARTRealtime]() defer { for clientItem in disposable { + clientItem.dispose() clientItem.close() } } @@ -34,9 +35,9 @@ class RestClientPresence: QuickSpec { let expectedPattern = "^user(\\d+)$" waitUntil(timeout: testTimeout) { done in // Load 150 members (2 pages) - disposable += AblyTests.addMembersSequentiallyToChannel("test", members: 150, data:expectedData, options: options) { + disposable += [AblyTests.addMembersSequentiallyToChannel("test", members: 150, data:expectedData, options: options) { done() - } + }] } waitUntil(timeout: testTimeout) { done in @@ -131,22 +132,23 @@ class RestClientPresence: QuickSpec { var disposable = [ARTRealtime]() defer { for clientItem in disposable { + clientItem.dispose() clientItem.close() } } waitUntil(timeout: testTimeout) { done in // One connection - disposable += AblyTests.addMembersSequentiallyToChannel("test", members: 6, options: options) { + disposable += [AblyTests.addMembersSequentiallyToChannel("test", members: 6, options: options) { done() - } + }] } waitUntil(timeout: testTimeout) { done in // Another connection - disposable += AblyTests.addMembersSequentiallyToChannel("test", members: 3, startFrom: 7, options: options) { + disposable += [AblyTests.addMembersSequentiallyToChannel("test", members: 3, startFrom: 7, options: options) { done() - } + }] } let query = ARTRealtimePresenceQuery() @@ -180,14 +182,14 @@ class RestClientPresence: QuickSpec { let channel = client.channels.get("test") var realtime: ARTRealtime! - defer { realtime.close() } + defer { realtime.dispose(); realtime.close() } let expectedData = "online" let expectedPattern = "^user(\\d+)$" waitUntil(timeout: testTimeout) { done in realtime = AblyTests.addMembersSequentiallyToChannel("test", members: 150, data: expectedData, options: options) { done() - }.first + } } waitUntil(timeout: testTimeout) { done in @@ -244,14 +246,15 @@ class RestClientPresence: QuickSpec { var disposable = [ARTRealtime]() defer { for clientItem in disposable { + clientItem.dispose() clientItem.close() } } waitUntil(timeout: testTimeout) { done in - disposable += AblyTests.addMembersSequentiallyToChannel("test", members: 10, data:nil, options: options) { + disposable += [AblyTests.addMembersSequentiallyToChannel("test", members: 10, data:nil, options: options) { done() - } + }] } let query = ARTDataQuery() @@ -299,12 +302,12 @@ class RestClientPresence: QuickSpec { let channel = client.channels.get("test") var realtime: ARTRealtime! - defer { realtime.close() } + defer { realtime.dispose(); realtime.close() } waitUntil(timeout: testTimeout) { done in realtime = AblyTests.addMembersSequentiallyToChannel("test", members: 1, options: options) { done() - }.first + } } let query = ARTDataQuery() @@ -338,22 +341,23 @@ class RestClientPresence: QuickSpec { var disposable = [ARTRealtime]() defer { for clientItem in disposable { + clientItem.dispose() clientItem.close() } } waitUntil(timeout: testTimeout) { done in // One connection - disposable += AblyTests.addMembersSequentiallyToChannel("test", members: 6, options: options) { + disposable += [AblyTests.addMembersSequentiallyToChannel("test", members: 6, options: options) { done() - } + }] } waitUntil(timeout: testTimeout) { done in // Another connection - disposable += AblyTests.addMembersSequentiallyToChannel("test", members: 3, startFrom: 7, options: options) { + disposable += [AblyTests.addMembersSequentiallyToChannel("test", members: 3, startFrom: 7, options: options) { done() - } + }] } let query = ARTRealtimePresenceQuery() @@ -392,6 +396,7 @@ class RestClientPresence: QuickSpec { var disposable = [ARTRealtime]() defer { for clientItem in disposable { + clientItem.dispose() clientItem.close() } } @@ -399,9 +404,9 @@ class RestClientPresence: QuickSpec { let query = ARTDataQuery() waitUntil(timeout: testTimeout) { done in - disposable += AblyTests.addMembersSequentiallyToChannel("test", members: 25, options: options) { + disposable += [AblyTests.addMembersSequentiallyToChannel("test", members: 25, options: options) { done() - } + }] } waitUntil(timeout: testTimeout) { done in @@ -417,9 +422,9 @@ class RestClientPresence: QuickSpec { } waitUntil(timeout: testTimeout) { done in - disposable += AblyTests.addMembersSequentiallyToChannel("test", members: 3, options: options) { + disposable += [AblyTests.addMembersSequentiallyToChannel("test", members: 3, options: options) { done() - } + }] } waitUntil(timeout: testTimeout) { done in @@ -431,9 +436,9 @@ class RestClientPresence: QuickSpec { } waitUntil(timeout: testTimeout) { done in - disposable += AblyTests.addMembersSequentiallyToChannel("test", members: 10, options: options) { + disposable += [AblyTests.addMembersSequentiallyToChannel("test", members: 10, options: options) { done() - } + }] } waitUntil(timeout: testTimeout) { done in diff --git a/Spec/TestUtilities.swift b/Spec/TestUtilities.swift index d95fa26cc..47fdb99b7 100644 --- a/Spec/TestUtilities.swift +++ b/Spec/TestUtilities.swift @@ -28,8 +28,8 @@ enum CryptoTest: String { class Configuration : QuickConfiguration { override class func configure(configuration: Quick.Configuration!) { - configuration.beforeEach { - + configuration.beforeSuite { + AsyncDefaults.Timeout = testTimeout } } } @@ -189,7 +189,7 @@ class AblyTests { return NSProcessInfo.processInfo().globallyUniqueString } - class func addMembersSequentiallyToChannel(channelName: String, members: Int = 1, startFrom: Int = 1, data: AnyObject? = nil, options: ARTClientOptions, done: ()->()) -> [ARTRealtime] { + class func addMembersSequentiallyToChannel(channelName: String, members: Int = 1, startFrom: Int = 1, data: AnyObject? = nil, options: ARTClientOptions, done: ()->()) -> ARTRealtime { let client = ARTRealtime(options: options) let channel = client.channels.get(channelName) @@ -208,7 +208,8 @@ class AblyTests { } } } - return [client] + + return client } class func splitDone(howMany: Int, file: StaticString = #file, line: UInt = #line, done: () -> Void) -> (() -> Void) { From 7a9ea7e48265968c2d32647c45907dc7321f5d03 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 15 Dec 2016 08:23:28 +0000 Subject: [PATCH 29/43] Remove pending tests (#542) * Fix: RTP6c pending test * RSL1g4: remove pending * RTP2: remove pending * Fix RTC8 --- Spec/Auth.swift | 119 +++++++++++++----------------- Spec/RealtimeClientPresence.swift | 29 +++++--- Spec/RestClientChannel.swift | 2 +- 3 files changed, 71 insertions(+), 79 deletions(-) diff --git a/Spec/Auth.swift b/Spec/Auth.swift index f67c06496..601dfc801 100644 --- a/Spec/Auth.swift +++ b/Spec/Auth.swift @@ -2865,7 +2865,7 @@ class Auth : QuickSpec { testTokenParams.clientId = nil waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(testTokenParams, options: nil) { tokenDetails, error in + rest.auth.authorize(testTokenParams, options: nil) { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { fail("TokenDetails is nil"); done(); return @@ -2885,7 +2885,7 @@ class Auth : QuickSpec { // Subsequent authorization waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: nil) { tokenDetails, error in + rest.auth.authorize(nil, options: nil) { tokenDetails, error in expect(error).to(beNil()) guard let tokenDetails = tokenDetails else { fail("TokenDetails is nil"); done(); return @@ -3150,103 +3150,90 @@ class Auth : QuickSpec { } } } - - describe("Reauth") { - // RTC8 - pending("should use authorise({force: true}) to reauth with a token with a different set of capabilities") { - // init ARTRest - let restOptions = AblyTests.setupOptions(AblyTests.jsonRestOptions) - let rest = ARTRest(options: restOptions) - - // get first token - let tokenParams = ARTTokenParams() - tokenParams.capability = "{\"wrongchannel\": [\"*\"]}" - tokenParams.clientId = "testClientId" - - var firstToken = "" - - waitUntil(timeout: testTimeout) { done in - rest.auth.requestToken(tokenParams, withOptions: nil) { tokenDetails, error in - expect(error).to(beNil()) - expect(tokenDetails).toNot(beNil()) - firstToken = tokenDetails!.token - done() - } - } - expect(firstToken).toNot(beNil()) - expect(firstToken.characters.count > 0).to(beTrue()) - // init ARTRealtime - let realtimeOptions = AblyTests.commonAppSetup() - realtimeOptions.token = firstToken - realtimeOptions.clientId = "testClientId" + describe("Reauth") { - let realtime = ARTRealtime(options:realtimeOptions) + // RTC8 + it("should use authorise({force: true}) to reauth with a token with a different set of capabilities") { + let options = AblyTests.commonAppSetup() + let initialToken = getTestToken(capability: "{\"restricted\":[\"*\"]}", clientId: "tester") + options.token = initialToken + let realtime = ARTRealtime(options: options) defer { realtime.dispose(); realtime.close() } + let channel = realtime.channels.get("foo") - // wait for connected state waitUntil(timeout: testTimeout) { done in - realtime.connection.once(.Connected) { stateChange in - expect(stateChange!.reason).to(beNil()) - expect(stateChange?.current).to(equal(realtime.connection.state)) + channel.attach { error in + guard let error = error else { + fail("Error is nil"); done(); return + } + expect(error.code) == 40160 done() } - realtime.connect() } - // create a `rightchannel` channel and check can't attach to it - let channel = realtime.channels.get("rightchannel") + let tokenParams = ARTTokenParams() + tokenParams.capability = "{\"\(channel.name)\":[\"*\"]}" + tokenParams.clientId = "tester" waitUntil(timeout: testTimeout) { done in - channel.attach() { error in - expect(error).toNot(beNil()) - expect(error!.code).to(equal(40160)) + realtime.auth.authorize(tokenParams, options: nil) { tokenDetails, error in + expect(error).to(beNil()) + expect(tokenDetails).toNot(beNil()) done() } } - // get second token - let secondTokenParams = ARTTokenParams() - secondTokenParams.capability = "{\"wrongchannel\": [\"*\"], \"rightchannel\": [\"*\"]}" - secondTokenParams.clientId = "testClientId" - - var secondToken = "" - var secondTokenDetails: ARTTokenDetails? + expect(realtime.auth.tokenDetails?.token).toNot(equal(initialToken)) + expect(realtime.auth.tokenDetails?.capability).to(equal(tokenParams.capability)) waitUntil(timeout: testTimeout) { done in - rest.auth.requestToken(secondTokenParams, withOptions: nil) { tokenDetails, error in + channel.attach { error in expect(error).to(beNil()) - expect(tokenDetails).toNot(beNil()) - - secondToken = tokenDetails!.token - secondTokenDetails = tokenDetails done() } } - expect(secondToken).toNot(beNil()) - expect(secondToken.characters.count > 0).to(beTrue()) - expect(secondToken).toNot(equal(firstToken)) + } - // reauthorise - let reauthOptions = ARTAuthOptions(); - reauthOptions.tokenDetails = secondTokenDetails + // RTC8 + it("for a token change that fails due to an incompatible token, which should result in the connection entering the FAILED state") { + let options = AblyTests.commonAppSetup() + options.clientId = "tester" + options.useTokenAuth = true + let realtime = ARTRealtime(options: options) + defer { realtime.dispose(); realtime.close() } waitUntil(timeout: testTimeout) { done in - realtime.auth.authorize(nil, options: reauthOptions) { reauthTokenDetails, error in - expect(error).to(beNil()) - expect(reauthTokenDetails?.token).toNot(beNil()) + realtime.connection.on(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) done() } } - // re-attach to the channel + guard let initialToken = realtime.auth.tokenDetails?.token else { + fail("TokenDetails is nil"); return + } + + let tokenParams = ARTTokenParams() + tokenParams.capability = "{\"restricted\":[\"*\"]}" + tokenParams.clientId = "secret" + waitUntil(timeout: testTimeout) { done in - channel.attach() { error in - expect(error).to(beNil()) + realtime.auth.authorize(tokenParams, options: nil) { tokenDetails, error in + guard let error = error else { + fail("Error is nil"); done(); return + } + expect(error.code) == 40102 + expect(tokenDetails).to(beNil()) done() } } + + expect(realtime.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) + expect(realtime.auth.tokenDetails?.token).to(equal(initialToken)) + expect(realtime.auth.tokenDetails?.capability).toNot(equal(tokenParams.capability)) } + } describe("TokenParams") { diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index 16d91ebb0..b479edd69 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -847,7 +847,7 @@ class RealtimeClientPresence: QuickSpec { } // RTP2 - pending("should be used a PresenceMap to maintain a list of members") { + it("should be used a PresenceMap to maintain a list of members") { let options = AblyTests.commonAppSetup() var clientSecondary: ARTRealtime! defer { clientSecondary.close() } @@ -1290,37 +1290,42 @@ class RealtimeClientPresence: QuickSpec { } // RTP6c - pending("should result in an error if the channel is in the FAILED state") { + it("should result in an error if the channel is in the FAILED state") { let client = ARTRealtime(options: AblyTests.commonAppSetup()) defer { client.dispose(); client.close() } let channel = client.channels.get("test") - channel.onError(AblyTests.newErrorProtocolMessage()) + let protocolError = AblyTests.newErrorProtocolMessage() + channel.onError(protocolError) waitUntil(timeout: testTimeout) { done in - channel.presence.subscribe { member in - // TODO: missing error + channel.presence.subscribeWithAttachCallback({ error in + expect(channel.state).to(equal(ARTRealtimeChannelState.Failed)) + expect(error).toNot(beNil()) done() - } + }, callback: { member in + fail("Should not be called") + }) } - expect(channel.presenceEventEmitter.anyListeners).to(haveCount(0)) } // RTP6c - pending("should result in an error if the channel moves to the FAILED state") { + it("should result in an error if the channel moves to the FAILED state") { let client = ARTRealtime(options: AblyTests.commonAppSetup()) defer { client.dispose(); client.close() } let channel = client.channels.get("test") waitUntil(timeout: testTimeout) { done in let error = AblyTests.newErrorProtocolMessage() - channel.presence.subscribe { _ in - // TODO: missing error + channel.presence.subscribeWithAttachCallback({ error in + expect(channel.state).to(equal(ARTRealtimeChannelState.Failed)) + expect(error).toNot(beNil()) done() - } + }, callback: { member in + fail("Should not be called") + }) channel.onError(error) } - expect(channel.presenceEventEmitter.anyListeners).to(haveCount(0)) } } diff --git a/Spec/RestClientChannel.swift b/Spec/RestClientChannel.swift index aa58f6de9..7ff47e950 100644 --- a/Spec/RestClientChannel.swift +++ b/Spec/RestClientChannel.swift @@ -264,7 +264,7 @@ class RestClientChannel: QuickSpec { } // RSL1g4 - pending("when publishing a Message with an explicit clientId that is incompatible with the identified client’s clientId") { + it("when publishing a Message with an explicit clientId that is incompatible with the identified client’s clientId") { let options = AblyTests.commonAppSetup() options.clientId = "john" let client = ARTRest(options: options) From 905cac5a2f665f69794839ee91ffd05c1d7746ca Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 24 Jan 2017 12:49:54 +0000 Subject: [PATCH 30/43] Update RTP3 (#566) --- Spec/RealtimeClientPresence.swift | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index b479edd69..631bab397 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -101,7 +101,9 @@ class RealtimeClientPresence: QuickSpec { var lastSyncSerial: String? waitUntil(timeout: testTimeout) { done in channel.attach() { _ in - let transport = client.transport as! TestProxyTransport + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } transport.afterProcessingReceivedMessage = { protocolMessage in if protocolMessage.action == .Sync { lastSyncSerial = protocolMessage.channelSerial @@ -112,15 +114,33 @@ class RealtimeClientPresence: QuickSpec { } } + expect(channel.presenceMap.members).toNot(haveCount(150)) expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connecting), timeout: options.disconnectedRetryTimeout + 1.0) expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) - //Client library requests a SYNC resume by sending a SYNC ProtocolMessage with the last received sync serial number - let transport = client.transport as! TestProxyTransport - expect(transport.protocolMessagesSent.filter{ $0.action == .Sync }).toEventually(haveCount(1), timeout: testTimeout) - expect(transport.protocolMessagesSent.filter{ $0.action == .Sync }.first!.channelSerial).to(equal(lastSyncSerial)) + // Client library requests a SYNC resume by sending a SYNC ProtocolMessage with the last received sync serial number + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + let syncSentProtocolMessages = transport.protocolMessagesSent.filter({ $0.action == .Sync }) + guard let syncSentMessage = syncSentProtocolMessages.last where syncSentProtocolMessages.count == 1 else { + fail("Should send one SYNC protocol message"); return + } + expect(syncSentMessage.channelSerial).to(equal(lastSyncSerial)) expect(transport.protocolMessagesReceived.filter{ $0.action == .Sync }).toEventually(haveCount(2), timeout: testTimeout) + + waitUntil(timeout: testTimeout) { done in + channel.presence.get { members, error in + expect(error).to(beNil()) + guard let members = members else { + fail("No present members"); done(); return + } + expect(members).to(haveCount(150)) + done() + } + } } // RTP4 From 1cf7687ce362344ccd5ff6e9ee4abbc4906d9391 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 24 Jan 2017 16:40:47 +0000 Subject: [PATCH 31/43] Update RTP8d (#572) --- Spec/RealtimeClientPresence.swift | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index 631bab397..d646a96e6 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -1185,6 +1185,36 @@ class RealtimeClientPresence: QuickSpec { expect(channel.state).to(equal(ARTRealtimeChannelState.Failed)) } + // RTP8d + it("should result in an error if the channel is in the DETACHED state") { + let options = AblyTests.commonAppSetup() + options.clientId = "john" + + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("test") + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.attach { error in + expect(error).to(beNil()) + partialDone() + } + channel.detach { error in + expect(error).to(beNil()) + partialDone() + } + } + expect(channel.state).to(equal(ARTRealtimeChannelState.Detached)) + + waitUntil(timeout: testTimeout) { done in + channel.presence.enter("online") { error in + expect(error!.message).to(contain("invalid channel state")) + done() + } + } + } + } // RTP10 From 3c36fab5aee3d6f420de91d0d282986626306a28 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Sun, 5 Feb 2017 15:52:18 +0000 Subject: [PATCH 32/43] RTP17 (#571) * PresenceMap: list of internal members * RTP17 * Add ARTPresenceActionToStr method * RTP17: pending --- Source/ARTPresenceMap.h | 4 + Source/ARTPresenceMessage.h | 2 + Source/ARTPresenceMessage.m | 15 +++ Spec/RealtimeClientPresence.swift | 158 ++++++++++++++++++++++++++++++ Spec/TestUtilities.swift | 6 ++ 5 files changed, 185 insertions(+) diff --git a/Source/ARTPresenceMap.h b/Source/ARTPresenceMap.h index bbf3e7c7e..34bb5c91a 100644 --- a/Source/ARTPresenceMap.h +++ b/Source/ARTPresenceMap.h @@ -21,6 +21,10 @@ ART_ASSUME_NONNULL_BEGIN /// The key is the clientId and the value is the latest relevant ARTPresenceMessage for that clientId. @property (readonly, atomic, getter=getMembers) __GENERIC(NSDictionary, NSString *, ARTPresenceMessage *) *members; +/// List of internal members. +/// The key is the clientId and the value is the latest relevant ARTPresenceMessage for that clientId. +@property (readonly, atomic) NSDictionary *localMembers; + @property (readwrite, nonatomic, assign) int64_t syncMsgSerial; @property (readwrite, nonatomic, nullable) NSString *syncChannelSerial; @property (readonly, nonatomic, assign) BOOL syncComplete; diff --git a/Source/ARTPresenceMessage.h b/Source/ARTPresenceMessage.h index 69c3629d4..4178538b9 100644 --- a/Source/ARTPresenceMessage.h +++ b/Source/ARTPresenceMessage.h @@ -17,6 +17,8 @@ typedef NS_ENUM(NSUInteger, ARTPresenceAction) { ARTPresenceUpdate }; +NSString *__art_nonnull ARTPresenceActionToStr(ARTPresenceAction action); + /// List of members present on a channel @interface ARTPresenceMessage : ARTBaseMessage diff --git a/Source/ARTPresenceMessage.m b/Source/ARTPresenceMessage.m index 774ba3ede..fb72fb2ba 100644 --- a/Source/ARTPresenceMessage.m +++ b/Source/ARTPresenceMessage.m @@ -39,3 +39,18 @@ - (NSString *)memberKey { } @end + +NSString *ARTPresenceActionToStr(ARTPresenceAction action) { + switch (action) { + case ARTPresenceAbsent: + return @"Absent"; //0 + case ARTPresencePresent: + return @"Present"; //1 + case ARTPresenceEnter: + return @"Enter"; //2 + case ARTPresenceLeave: + return @"Leave"; //3 + case ARTPresenceUpdate: + return @"Update"; //4 + } +} diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index d646a96e6..6d631f466 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -1456,6 +1456,164 @@ class RealtimeClientPresence: QuickSpec { } + // RTP17 + pending("private and internal PresenceMap containing only members that match the current connectionId") { + + it("any ENTER, PRESENT, UPDATE or LEAVE event that matches the current connectionId should be applied to this object") { + let options = AblyTests.commonAppSetup() + + options.clientId = "a" + let clientA = ARTRealtime(options: options) + defer { clientA.dispose(); clientA.close() } + let channelA = clientA.channels.get("foo") + + options.clientId = "b" + let clientB = ARTRealtime(options: options) + defer { clientB.dispose(); clientB.close() } + let channelB = clientB.channels.get("foo") + + // ENTER + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channelA.presence.subscribe { presence in + guard let currentConnectionId = clientA.connection.id else { + fail("ClientA should be connected"); partialDone(); return + } + expect(presence.action).to(equal(ARTPresenceAction.Enter)) + expect(presence.connectionId).to(equal(currentConnectionId)) + expect(channelA.presenceMap.members).to(haveCount(1)) + expect(channelA.presenceMap.localMembers).to(haveCount(1)) + channelA.presence.unsubscribe() + partialDone() + } + channelB.presence.subscribe { presence in + guard let currentConnectionId = clientB.connection.id else { + fail("ClientB should be connected"); partialDone(); return + } + expect(presence.action).to(equal(ARTPresenceAction.Enter)) + expect(presence.connectionId).toNot(equal(currentConnectionId)) + expect(channelB.presenceMap.members).to(haveCount(1)) + expect(channelB.presenceMap.localMembers).to(haveCount(0)) + channelB.presence.unsubscribe() + partialDone() + } + channelA.presence.enter(nil) + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channelA.presence.subscribe { presence in + guard let currentConnectionId = clientA.connection.id else { + fail("ClientA should be connected"); partialDone(); return + } + expect(presence.action).to(equal(ARTPresenceAction.Enter)) + expect(presence.connectionId).toNot(equal(currentConnectionId)) + expect(channelA.presenceMap.members).to(haveCount(2)) + expect(channelA.presenceMap.localMembers).to(haveCount(1)) + channelA.presence.unsubscribe() + partialDone() + } + channelB.presence.subscribe { presence in + guard let currentConnectionId = clientB.connection.id else { + fail("ClientB should be connected"); partialDone(); return + } + expect(presence.action).to(equal(ARTPresenceAction.Enter)) + expect(presence.connectionId).to(equal(currentConnectionId)) + expect(channelB.presenceMap.members).to(haveCount(2)) + expect(channelB.presenceMap.localMembers).to(haveCount(1)) + channelB.presence.unsubscribe() + partialDone() + } + channelB.presence.enter(nil) + } + + // UPDATE + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channelA.presence.subscribe { presence in + guard let currentConnectionId = clientA.connection.id else { + fail("ClientA should be connected"); partialDone(); return + } + expect(presence.action).to(equal(ARTPresenceAction.Update)) + expect(presence.data as? String).to(equal("hello")) + expect(presence.connectionId).toNot(equal(currentConnectionId)) + expect(channelA.presenceMap.members).to(haveCount(2)) + expect(channelA.presenceMap.localMembers).to(haveCount(1)) + channelA.presence.unsubscribe() + partialDone() + } + channelB.presence.subscribe { presence in + guard let currentConnectionId = clientB.connection.id else { + fail("ClientB should be connected"); partialDone(); return + } + expect(presence.action).to(equal(ARTPresenceAction.Update)) + expect(presence.data as? String).to(equal("hello")) + expect(presence.connectionId).to(equal(currentConnectionId)) + expect(channelB.presenceMap.members).to(haveCount(2)) + expect(channelB.presenceMap.localMembers).to(haveCount(1)) + channelB.presence.unsubscribe() + partialDone() + } + channelB.presence.update("hello") + } + + // LEAVE + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channelA.presence.subscribe { presence in + guard let currentConnectionId = clientA.connection.id else { + fail("ClientA should be connected"); partialDone(); return + } + expect(presence.action).to(equal(ARTPresenceAction.Leave)) + expect(presence.data as? String).to(equal("bye")) + expect(presence.connectionId).toNot(equal(currentConnectionId)) + expect(channelA.presenceMap.members).to(haveCount(1)) + expect(channelA.presenceMap.localMembers).to(haveCount(1)) + channelA.presence.unsubscribe() + partialDone() + } + channelB.presence.subscribe { presence in + guard let currentConnectionId = clientB.connection.id else { + fail("ClientB should be connected"); partialDone(); return + } + expect(presence.action).to(equal(ARTPresenceAction.Leave)) + expect(presence.data as? String).to(equal("bye")) + expect(presence.connectionId).to(equal(currentConnectionId)) + expect(channelB.presenceMap.members).to(haveCount(1)) + expect(channelB.presenceMap.localMembers).to(haveCount(0)) + channelB.presence.unsubscribe() + partialDone() + } + channelB.presence.leave("bye") + } + } + + // RTP17a + it("all members belonging to the current connection are published as a PresenceMessage on the Channel by the server irrespective of whether the client has permission to subscribe or the Channel is configured to publish presence events") { + let options = AblyTests.commonAppSetup() + options.tokenDetails = getTestTokenDetails(capability: "{\"foo\":[\"presence\",\"publish\"]}") + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + waitUntil(timeout: testTimeout) { done in + channel.presence.enterClient("tester", data: nil) { error in + expect(error).to(beNil()) + done() + } + } + waitUntil(timeout: testTimeout) { done in + channel.presence.get { members, error in + expect(error).to(beNil()) + expect(members).to(haveCount(1)) + expect(channel.presenceMap.members).to(haveCount(1)) + expect(channel.presenceMap.localMembers).to(haveCount(1)) + done() + } + } + } + + } + // RTP15d it("callback can be provided that will be called upon success") { let options = AblyTests.commonAppSetup() diff --git a/Spec/TestUtilities.swift b/Spec/TestUtilities.swift index 47fdb99b7..e33dfe7d7 100644 --- a/Spec/TestUtilities.swift +++ b/Spec/TestUtilities.swift @@ -1084,6 +1084,12 @@ extension ARTChannelEvent : CustomStringConvertible { } } +extension ARTPresenceAction : CustomStringConvertible { + public var description : String { + return ARTPresenceActionToStr(self) + } +} + // MARK: - Custom Nimble Matchers /// A Nimble matcher that succeeds when two dates are quite the same. From 55e6f96b0163cde4270b8dad53bbcbaa97357638 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Sun, 5 Feb 2017 22:51:04 +0000 Subject: [PATCH 33/43] Update RTP2 for 0.9 (#563) * RTP2: remove pending * RTP2a * RTP2b * Test suite: set Nimble.AsyncDefaults.Timeout - increase the default timeout value from all async expectations * RTP2c * RTP2d * RTP2e * RTP2f * RTP2g * Test suite: ARTPresenceMessage convenience initializer * Test suite: NSDate custom operators - convenience for use of `dateByAddingTimeInterval` * Fix RTP2b1 * Fix RTP2b2 * PresenceMap: compare for newness * Fix RTP2 * Remove warnings --- Source/ARTPresenceMap.h | 6 +- Source/ARTPresenceMap.m | 68 +++- Source/ARTPresenceMessage.h | 2 + Source/ARTPresenceMessage.m | 29 ++ Source/ARTRealtimeChannel.m | 25 +- Spec/RealtimeClientChannel.swift | 8 +- Spec/RealtimeClientPresence.swift | 565 +++++++++++++++++++++++++++++- Spec/TestUtilities.swift | 21 ++ Tests/ARTRealtimePresenceTest.m | 4 +- Tests/ARTRealtimeRecoverTest.m | 6 +- 10 files changed, 703 insertions(+), 31 deletions(-) diff --git a/Source/ARTPresenceMap.h b/Source/ARTPresenceMap.h index 34bb5c91a..e6ff51ec1 100644 --- a/Source/ARTPresenceMap.h +++ b/Source/ARTPresenceMap.h @@ -11,6 +11,7 @@ @class ARTPresenceMessage; @class ARTErrorInfo; +@class ARTLog; ART_ASSUME_NONNULL_BEGIN @@ -30,7 +31,10 @@ ART_ASSUME_NONNULL_BEGIN @property (readonly, nonatomic, assign) BOOL syncComplete; @property (readonly, nonatomic, getter=getSyncInProgress) BOOL syncInProgress; -- (void)put:(ARTPresenceMessage *)message; +- (instancetype)init UNAVAILABLE_ATTRIBUTE; +- (instancetype)initWithLogger:(ARTLog *)logger; + +- (BOOL)add:(ARTPresenceMessage *)message; - (void)clean; - (void)startSync; diff --git a/Source/ARTPresenceMap.m b/Source/ARTPresenceMap.m index 98144b98f..ea5628c5f 100644 --- a/Source/ARTPresenceMap.m +++ b/Source/ARTPresenceMap.m @@ -9,6 +9,7 @@ #import "ARTPresenceMap.h" #import "ARTPresenceMessage.h" #import "ARTEventEmitter.h" +#import "ARTLog.h" typedef NS_ENUM(NSUInteger, ARTPresenceSyncState) { ARTPresenceSyncStarted, //ItemType: nil @@ -25,11 +26,14 @@ @interface ARTPresenceMap () { @end -@implementation ARTPresenceMap +@implementation ARTPresenceMap { + __weak ARTLog *_logger; +} -- (id)init { +- (instancetype)initWithLogger:(ARTLog *)logger { self = [super init]; if(self) { + _logger = logger; _recentMembers = [NSMutableDictionary dictionary]; _syncStarted = false; _syncComplete = false; @@ -42,11 +46,65 @@ - (id)init { return self.recentMembers; } -- (void)put:(ARTPresenceMessage *)message { +- (BOOL)add:(ARTPresenceMessage *)message { ARTPresenceMessage *latest = [self.recentMembers objectForKey:message.clientId]; - if (!latest || !message.timestamp || [latest.timestamp timeIntervalSince1970] <= [message.timestamp timeIntervalSince1970]) { - [self.recentMembers setObject:message forKey:message.clientId]; + if ([self isNewestPresence:message comparingWith:latest]) { + ARTPresenceMessage *messageCopy = [message copy]; + switch (message.action) { + case ARTPresenceEnter: + case ARTPresenceUpdate: + messageCopy.action = ARTPresencePresent; + break; + case ARTPresenceLeave: + if (self.syncInProgress) { + messageCopy.action = ARTPresenceAbsent; + } + break; + default: + break; + } + [self.recentMembers setObject:messageCopy forKey:message.clientId]; + return YES; + } + return NO; +} + +- (BOOL)isNewestPresence:(nonnull ARTPresenceMessage *)received comparingWith:(ARTPresenceMessage *)latest __attribute__((warn_unused_result)) { + if (latest == nil) { + return YES; + } + + NSArray *receivedMessageIdParts = [received.id componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@":"]]; + if (receivedMessageIdParts.count != 3) { + [_logger error:@"Received presence message id is invalid %@", received.id]; + return !received.timestamp || + [latest.timestamp timeIntervalSince1970] <= [received.timestamp timeIntervalSince1970]; } + NSString *receivedConnectionId = [receivedMessageIdParts objectAtIndex:0]; + NSInteger receivedMsgSerial = [[receivedMessageIdParts objectAtIndex:1] integerValue]; + NSInteger receivedIndex = [[receivedMessageIdParts objectAtIndex:2] integerValue]; + + if ([receivedConnectionId isEqualToString:received.connectionId]) { + NSArray *latestRegisteredIdParts = [latest.id componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@":"]]; + if (latestRegisteredIdParts.count != 3) { + [_logger error:@"Latest registered presence message id is invalid %@", latest.id]; + return !received.timestamp || + [latest.timestamp timeIntervalSince1970] <= [received.timestamp timeIntervalSince1970]; + } + NSInteger latestRegisteredMsgSerial = [[latestRegisteredIdParts objectAtIndex:1] integerValue]; + NSInteger latestRegisteredIndex = [[latestRegisteredIdParts objectAtIndex:2] integerValue]; + + if (receivedMsgSerial > latestRegisteredMsgSerial) { + return YES; + } + else if (receivedMsgSerial == latestRegisteredMsgSerial && receivedIndex > latestRegisteredIndex) { + return YES; + } + return NO; + } + + return !received.timestamp || + [latest.timestamp timeIntervalSince1970] <= [received.timestamp timeIntervalSince1970]; } - (void)clean { diff --git a/Source/ARTPresenceMessage.h b/Source/ARTPresenceMessage.h index 4178538b9..352e723a6 100644 --- a/Source/ARTPresenceMessage.h +++ b/Source/ARTPresenceMessage.h @@ -26,4 +26,6 @@ NSString *__art_nonnull ARTPresenceActionToStr(ARTPresenceAction action); - (NSString *)memberKey; +- (BOOL)isEqualToPresenceMessage:(ARTPresenceMessage *)presence; + @end diff --git a/Source/ARTPresenceMessage.m b/Source/ARTPresenceMessage.m index fb72fb2ba..bf2ca1e7c 100644 --- a/Source/ARTPresenceMessage.m +++ b/Source/ARTPresenceMessage.m @@ -38,6 +38,35 @@ - (NSString *)memberKey { return [NSString stringWithFormat:@"%@:%@", self.connectionId, self.clientId]; } +- (BOOL)isEqualToPresenceMessage:(ARTPresenceMessage *)presence { + if (!presence) { + return NO; + } + + BOOL haveEqualConnectionId = (!self.connectionId && !presence.connectionId) || [self.connectionId isEqualToString:presence.connectionId]; + BOOL haveEqualCliendId = (!self.clientId && !presence.clientId) || [self.clientId isEqualToString:presence.clientId]; + + return haveEqualConnectionId && haveEqualCliendId; +} + +#pragma mark - NSObject + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[ARTPresenceMessage class]]) { + return NO; + } + + return [self isEqualToPresenceMessage:(ARTPresenceMessage *)object]; +} + +- (NSUInteger)hash { + return [self.connectionId hash] ^ [self.clientId hash]; +} + @end NSString *ARTPresenceActionToStr(ARTPresenceAction action) { diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index 7189aeaf0..4d4a42d51 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -47,7 +47,7 @@ - (instancetype)initWithRealtime:(ARTRealtime *)realtime andName:(NSString *)nam _state = ARTRealtimeChannelInitialized; _queuedMessages = [NSMutableArray array]; _attachSerial = nil; - _presenceMap = [[ARTPresenceMap alloc] init]; + _presenceMap = [[ARTPresenceMap alloc] initWithLogger:self.logger]; _lastPresenceAction = ARTPresenceAbsent; _statesEventEmitter = [[ARTEventEmitter alloc] init]; @@ -64,6 +64,10 @@ + (instancetype)channelWithRealtime:(ARTRealtime *)realtime andName:(NSString *) return [[ARTRealtimeChannel alloc] initWithRealtime:realtime andName:name withOptions:options]; } +- (ARTLog *)getLogger { + return _realtime.logger; +} + - (ARTRealtimePresence *)getPresence { if (!_realtimePresence) { _realtimePresence = [[ARTRealtimePresence alloc] initWithChannel:self]; @@ -85,8 +89,8 @@ - (void)internalPostMessages:(id)data callback:(void (^)(ARTErrorInfo *__art_nul } - (void)requestContinueSync { - [self.logger info:@"R:%p C:%p ARTRealtime requesting to continue sync operation after reconnect", _realtime, self]; - + [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p ARTRealtime requesting to continue sync operation after reconnect", _realtime, self]; + ARTProtocolMessage * msg = [[ARTProtocolMessage alloc] init]; msg.action = ARTProtocolMessageSync; msg.msgSerial = self.presenceMap.syncMsgSerial; @@ -541,12 +545,12 @@ - (void)onPresence:(ARTProtocolMessage *)message { presence.id = [NSString stringWithFormat:@"%@:%d", message.id, i]; } - [self.presenceMap onceSyncEnds:^(__GENERIC(NSArray, ARTPresenceMessage *) *msgs) { - [self.presenceMap put:presence]; - [self.presenceMap clean]; - + if ([self.presenceMap add:presence]) { [self broadcastPresence:presence]; - }]; + } + if (!self.presenceMap.syncInProgress) { + [self.presenceMap clean]; + } ++i; } @@ -561,8 +565,9 @@ - (void)onSync:(ARTProtocolMessage *)message { for (int i=0; i<[message.presence count]; i++) { ARTPresenceMessage *presence = [message.presence objectAtIndex:i]; - [self.presenceMap put:presence]; - [self broadcastPresence:presence]; + if ([self.presenceMap add:presence]) { + [self broadcastPresence:presence]; + } } if ([self isLastChannelSerial:message.channelSerial]) { diff --git a/Spec/RealtimeClientChannel.swift b/Spec/RealtimeClientChannel.swift index bea897f93..f5bf6f794 100644 --- a/Spec/RealtimeClientChannel.swift +++ b/Spec/RealtimeClientChannel.swift @@ -19,7 +19,7 @@ class RealtimeClientChannel: QuickSpec { it("should process all incoming messages and presence messages as soon as a Channel becomes attached") { let options = AblyTests.commonAppSetup() let client1 = AblyTests.newRealtime(options) - defer { client1.close() } + defer { client1.dispose(); client1.close() } let channel1 = client1.channels.get("room") waitUntil(timeout: testTimeout) { done in @@ -31,7 +31,7 @@ class RealtimeClientChannel: QuickSpec { options.clientId = "Client 2" let client2 = AblyTests.newRealtime(options) - defer { client2.close() } + defer { client2.dispose(); client2.close() } let channel2 = client2.channels.get(channel1.name) channel2.subscribe("Client 1") { message in @@ -68,12 +68,12 @@ class RealtimeClientChannel: QuickSpec { expect(channel1.presenceMap.members).toEventually(haveCount(2), timeout: testTimeout) expect(channel1.presenceMap.members).to(allKeysPass({ $0.hasPrefix("Client") })) - expect(channel1.presenceMap.members).to(allValuesPass({ $0.action == .Enter })) + expect(channel1.presenceMap.members).to(allValuesPass({ $0.action == .Present })) expect(channel2.presenceMap.members).toEventually(haveCount(2), timeout: testTimeout) expect(channel2.presenceMap.members).to(allKeysPass({ $0.hasPrefix("Client") })) expect(channel2.presenceMap.members["Client 1"]!.action).to(equal(ARTPresenceAction.Present)) - expect(channel2.presenceMap.members["Client 2"]!.action).to(equal(ARTPresenceAction.Enter)) + expect(channel2.presenceMap.members["Client 2"]!.action).to(equal(ARTPresenceAction.Present)) } // RTL2 diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index 6d631f466..e4bc1b4d0 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -12,6 +12,12 @@ import Nimble import Foundation class RealtimeClientPresence: QuickSpec { + + override func setUp() { + super.setUp() + AsyncDefaults.Timeout = testTimeout + } + override func spec() { describe("Presence") { @@ -870,7 +876,7 @@ class RealtimeClientPresence: QuickSpec { it("should be used a PresenceMap to maintain a list of members") { let options = AblyTests.commonAppSetup() var clientSecondary: ARTRealtime! - defer { clientSecondary.close() } + defer { clientSecondary.dispose(); clientSecondary.close() } waitUntil(timeout: testTimeout) { done in clientSecondary = AblyTests.addMembersSequentiallyToChannel("test", members: 100, options: options) { @@ -889,7 +895,7 @@ class RealtimeClientPresence: QuickSpec { } var user50PresentTimestamp: NSDate? - channel.presenceMap.testSuite_getArgumentFrom(#selector(ARTPresenceMap.put(_:)), atIndex: 0) { arg0 in + channel.presenceMap.testSuite_getArgumentFrom(#selector(ARTPresenceMap.add(_:)), atIndex: 0) { arg0 in let member = arg0 as! ARTPresenceMessage if member.clientId == "user50" && member.action == .Present { user50PresentTimestamp = member.timestamp @@ -897,8 +903,11 @@ class RealtimeClientPresence: QuickSpec { } waitUntil(timeout: testTimeout) { done in - channel.attach() { _ in - let transport = client.transport as! TestProxyTransport + channel.attach() { error in + expect(error).to(beNil()) + guard let transport = client.transport as? TestProxyTransport else { + fail("Transport is nil"); done(); return + } transport.beforeProcessingReceivedMessage = { protocolMessage in // A leave event for a member can arrive before that member is later registered as present as part of the initial SYNC operation. if protocolMessage.action == .Sync { @@ -908,15 +917,20 @@ class RealtimeClientPresence: QuickSpec { client.onChannelMessage(msg) done() } + transport.beforeProcessingReceivedMessage = nil } } } + channel.presence.unsubscribe() waitUntil(timeout: testTimeout) { done in channel.presence.get { members, error in expect(error).to(beNil()) - expect(members).to(haveCount(99)) - expect(members!.filter{ $0.clientId == "user50" }).to(haveCount(0)) + guard let members = members else { + fail("Members is nil"); done(); return + } + expect(members.count) == 99 + expect(members.filter{ $0.clientId == "user50" }).to(haveCount(0)) done() } } @@ -924,6 +938,545 @@ class RealtimeClientPresence: QuickSpec { expect(user50LeaveTimestamp).to(beGreaterThan(user50PresentTimestamp)) } + // RTP2 + context("PresenceMap") { + + // RTP2a + it("all incoming presence messages must be compared for newness with the matching member already in the PresenceMap") { + let options = AblyTests.commonAppSetup() + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + channel.presence.enterClient("tester", data: nil) { error in + expect(error).to(beNil()) + done() + } + } + + guard let intialPresenceMessage = channel.presenceMap.members["tester"] else { + fail("Missing Presence message"); return + } + + expect(intialPresenceMessage.memberKey()).to(equal("\(client.connection.id!):tester")) + + var compareForNewnessMethodCalls = 0 + let hook = channel.presenceMap.testSuite_injectIntoMethodAfter(NSSelectorFromString("isNewestPresence:comparingWith:")) { + compareForNewnessMethodCalls += 1 + } + defer { hook.remove() } + + waitUntil(timeout: testTimeout) { done in + channel.presence.enterClient("tester", data: nil) { error in + expect(error).to(beNil()) + done() + } + } + + guard let updatedPresenceMessage = channel.presenceMap.members["tester"] else { + fail("Missing Presence message"); return + } + + expect(intialPresenceMessage.memberKey()).to(equal(updatedPresenceMessage.memberKey())) + expect(intialPresenceMessage.timestamp).toNot(equal(updatedPresenceMessage.timestamp)) + + expect(compareForNewnessMethodCalls) == 1 + } + + // RTP2b + context("compare for newness") { + + context("presence message has a connectionId which is not an initial substring of its id") { + // RTP2b1 + it("compares them by timestamp numerically") { + let options = AblyTests.commonAppSetup() + let now = NSDate() + + var clientMembers: ARTRealtime? + defer { clientMembers?.dispose(); clientMembers?.close() } + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 100, options: options) { + done() + } + } + + let clientSubscribed = AblyTests.newRealtime(options) + defer { clientSubscribed.dispose(); clientSubscribed.close() } + let channelSubscribed = clientSubscribed.channels.get("foo") + + let presenceData: [ARTPresenceMessage] = [ + ARTPresenceMessage(clientId: "a", action: .Enter, connectionId: "one", id: "one:0:0", timestamp: now), + ARTPresenceMessage(clientId: "a", action: .Leave, connectionId: "one", id: "fabricated:0:1", timestamp: now + 1), + ARTPresenceMessage(clientId: "b", action: .Enter, connectionId: "one", id: "one:0:2", timestamp: now), + ARTPresenceMessage(clientId: "b", action: .Leave, connectionId: "one", id: "fabricated:0:3", timestamp: now - 1), + ARTPresenceMessage(clientId: "c", action: .Enter, connectionId: "one", id: "fabricated:0:4", timestamp: now), + ARTPresenceMessage(clientId: "c", action: .Leave, connectionId: "one", id: "fabricated:0:5", timestamp: now - 1), + ] + + guard let transport = clientSubscribed.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + waitUntil(timeout: testTimeout) { done in + transport.afterProcessingReceivedMessage = { protocolMessage in + // Receive the first Sync message from Ably service + if protocolMessage.action == .Sync { + + // Inject a fabricated Presence message + let presenceMessage = ARTProtocolMessage() + presenceMessage.action = .Presence + presenceMessage.channel = protocolMessage.channel + presenceMessage.connectionSerial = protocolMessage.connectionSerial + 1 + presenceMessage.timestamp = NSDate() + presenceMessage.presence = presenceData + + transport.receive(presenceMessage) + + // Simulate an end to the sync + let endSyncMessage = ARTProtocolMessage() + endSyncMessage.action = .Sync + endSyncMessage.channel = protocolMessage.channel + endSyncMessage.channelSerial = "validserialprefix:" //with no part after the `:` this indicates the end to the SYNC + endSyncMessage.connectionSerial = protocolMessage.connectionSerial + 2 + endSyncMessage.timestamp = NSDate() + + transport.afterProcessingReceivedMessage = nil + transport.receive(endSyncMessage) + + // Stop the next sync message from Ably service because we already injected the end of the sync + transport.actionsIgnored = [.Sync] + + done() + } + } + channelSubscribed.attach() + } + + waitUntil(timeout: testTimeout) { done in + channelSubscribed.presence.get { members, error in + expect(error).to(beNil()) + guard let members = members else { + fail("Members is nil"); done(); return + } + expect(members).to(haveCount(102)) //100 initial members + "b" + "c", client "a" is discarded + expect(members.filter{ $0.clientId == "a" }).to(beEmpty()) + expect(members.filter{ $0.clientId == "b" }).to(haveCount(1)) + expect(members.filter{ $0.clientId == "b" }.first?.timestamp).to(equal(now)) + expect(members.filter{ $0.clientId == "c" }).to(haveCount(1)) + expect(members.filter{ $0.clientId == "c" }.first?.timestamp).to(equal(now)) + done() + } + } + } + } + + // RTP2b2 + it("split the id of both presence messages") { + let options = AblyTests.commonAppSetup() + let now = NSDate() + + var clientMembers: ARTRealtime? + defer { clientMembers?.dispose(); clientMembers?.close() } + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 100, options: options) { + done() + } + } + + let clientSubscribed = AblyTests.newRealtime(options) + defer { clientSubscribed.dispose(); clientSubscribed.close() } + let channelSubscribed = clientSubscribed.channels.get("foo") + + let presenceData: [ARTPresenceMessage] = [ + ARTPresenceMessage(clientId: "a", action: .Enter, connectionId: "one", id: "one:0:0", timestamp: now), + ARTPresenceMessage(clientId: "a", action: .Leave, connectionId: "one", id: "one:1:0", timestamp: now - 1), + ARTPresenceMessage(clientId: "b", action: .Enter, connectionId: "one", id: "one:2:2", timestamp: now), + ARTPresenceMessage(clientId: "b", action: .Leave, connectionId: "one", id: "one:2:1", timestamp: now + 1), + ARTPresenceMessage(clientId: "c", action: .Enter, connectionId: "one", id: "one:4:4", timestamp: now), + ARTPresenceMessage(clientId: "c", action: .Leave, connectionId: "one", id: "one:3:5", timestamp: now + 1), + ] + + guard let transport = clientSubscribed.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + waitUntil(timeout: testTimeout) { done in + transport.afterProcessingReceivedMessage = { protocolMessage in + // Receive the first Sync message from Ably service + if protocolMessage.action == .Sync { + + // Inject a fabricated Presence message + let presenceMessage = ARTProtocolMessage() + presenceMessage.action = .Presence + presenceMessage.channel = protocolMessage.channel + presenceMessage.connectionSerial = protocolMessage.connectionSerial + 1 + presenceMessage.timestamp = NSDate() + presenceMessage.presence = presenceData + + transport.receive(presenceMessage) + + // Simulate an end to the sync + let endSyncMessage = ARTProtocolMessage() + endSyncMessage.action = .Sync + endSyncMessage.channel = protocolMessage.channel + endSyncMessage.channelSerial = "validserialprefix:" //with no part after the `:` this indicates the end to the SYNC + endSyncMessage.connectionSerial = protocolMessage.connectionSerial + 2 + endSyncMessage.timestamp = NSDate() + + transport.afterProcessingReceivedMessage = nil + transport.receive(endSyncMessage) + + // Stop the next sync message from Ably service because we already injected the end of the sync + transport.actionsIgnored = [.Sync] + + done() + } + } + channelSubscribed.attach() + } + + waitUntil(timeout: testTimeout) { done in + channelSubscribed.presence.get { members, error in + expect(error).to(beNil()) + guard let members = members else { + fail("Members is nil"); done(); return + } + expect(members).to(haveCount(102)) //100 initial members + "b" + "c", client "a" is discarded + expect(members.filter{ $0.clientId == "a" }).to(beEmpty()) + expect(members.filter{ $0.clientId == "b" }).to(haveCount(1)) + expect(members.filter{ $0.clientId == "b" }.first?.timestamp).to(equal(now)) + expect(members.filter{ $0.clientId == "c" }).to(haveCount(1)) + expect(members.filter{ $0.clientId == "c" }.first?.timestamp).to(equal(now)) + done() + } + } + } + + } + + // RTP2c + context("all presence messages from a SYNC must also be compared for newness in the same way as they would from a PRESENCE") { + + it("discard members where messages have arrived before the SYNC") { + let options = AblyTests.commonAppSetup() + let timeBeforeSync = NSDate() + + var clientMembers: ARTRealtime? + defer { clientMembers?.dispose(); clientMembers?.close() } + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 120, options: options) { + done() + } + } + guard let membersConnectionId = clientMembers?.connection.id else { + fail("Members client isn't connected"); return + } + + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + channel.presence.subscribe(.Leave) { leave in + expect(leave.clientId).to(equal("user110")) + fail("Should not fire Leave event for member `user110` because it's out of date") + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(3, done: done) + transport.beforeProcessingReceivedMessage = { protocolMessage in + if protocolMessage.action == .Sync { + let injectLeave = ARTPresenceMessage() + injectLeave.action = .Leave + injectLeave.connectionId = membersConnectionId + injectLeave.clientId = "user110" + injectLeave.timestamp = timeBeforeSync + protocolMessage.presence?.append(injectLeave) + transport.beforeProcessingReceivedMessage = nil + partialDone() + } + } + channel.presenceMap.testSuite_injectIntoMethodAfter(#selector(ARTPresenceMap.endSync)) { + expect(channel.presenceMap.syncInProgress).to(beFalse()) + expect(channel.presenceMap.members).to(haveCount(120)) + expect(channel.presenceMap.members.filter{ _, presence in presence.clientId == "user110" && presence.action == .Present }).to(haveCount(1)) + partialDone() + } + channel.attach() { error in + expect(error).to(beNil()) + partialDone() + } + } + } + + it("accept members where message have arrived after the SYNC") { + let options = AblyTests.commonAppSetup() + + var clientMembers: ARTRealtime? + defer { clientMembers?.dispose(); clientMembers?.close() } + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 120, options: options) { + done() + } + } + guard let membersConnectionId = clientMembers?.connection.id else { + fail("Members client isn't connected"); return + } + + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(4, done: done) + channel.presence.subscribe(.Leave) { leave in + expect(leave.clientId).to(equal("user110")) + partialDone() + } + transport.beforeProcessingReceivedMessage = { protocolMessage in + if protocolMessage.action == .Sync { + let injectLeave = ARTPresenceMessage() + injectLeave.action = .Leave + injectLeave.connectionId = membersConnectionId + injectLeave.clientId = "user110" + injectLeave.timestamp = NSDate() + 1 + protocolMessage.presence?.append(injectLeave) + transport.beforeProcessingReceivedMessage = nil + partialDone() + } + } + channel.presenceMap.testSuite_injectIntoMethodAfter(#selector(ARTPresenceMap.endSync)) { + expect(channel.presenceMap.syncInProgress).to(beFalse()) + expect(channel.presenceMap.members).to(haveCount(119)) + expect(channel.presenceMap.members.filter{ _, presence in presence.clientId == "user110" }).to(beEmpty()) + partialDone() + } + channel.attach() { error in + expect(error).to(beNil()) + partialDone() + } + } + } + + } + + // RTP2d + it("if action of ENTER arrives, it should be added to the presence map with the action set to PRESENT") { + let options = AblyTests.commonAppSetup() + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.presence.subscribe(.Enter) { _ in + partialDone() + } + channel.presence.enterClient("tester", data: nil) { error in + expect(error).to(beNil()) + partialDone() + } + } + + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Present }).to(haveCount(1)) + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Enter }).to(beEmpty()) + } + + // RTP2d + it("if action of UPDATE arrives, it should be added to the presence map with the action set to PRESENT") { + let options = AblyTests.commonAppSetup() + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(3, done: done) + channel.presence.subscribe(.Update) { _ in + partialDone() + } + channel.presence.enterClient("tester", data: nil) { error in + expect(error).to(beNil()) + partialDone() + } + channel.presence.updateClient("tester", data: nil) { error in + expect(error).to(beNil()) + partialDone() + } + } + + expect(channel.presenceMap.members).to(haveCount(1)) + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Present }).to(haveCount(1)) + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Update }).to(beEmpty()) + } + + // RTP2d + it("if action of PRESENT arrives, it should be added to the presence map with the action set to PRESENT") { + let options = AblyTests.commonAppSetup() + + var clientMembers: ARTRealtime! + defer { clientMembers.dispose(); clientMembers.close() } + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 1, options: options) { + done() + } + } + + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.presenceMap.testSuite_injectIntoMethodAfter(#selector(ARTPresenceMap.endSync)) { + expect(channel.presenceMap.syncInProgress).to(beFalse()) + partialDone() + } + channel.attach() { error in + expect(error).to(beNil()) + partialDone() + } + } + + expect(channel.presenceMap.members).to(haveCount(1)) + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Present }).to(haveCount(1)) + } + + // RTP2e + it("if a SYNC is not in progress, then when a presence message with an action of LEAVE arrives, that memberKey should be deleted from the presence map, if present") { + let options = AblyTests.commonAppSetup() + + var clientMembers: ARTRealtime? + defer { clientMembers?.dispose(); clientMembers?.close() } + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 20, options: options) { + done() + } + } + + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + channel.attach() + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + waitUntil(timeout: testTimeout) { done in + transport.afterProcessingReceivedMessage = { protocolMessage in + if protocolMessage.action == .Sync { + done() + } + } + } + + expect(channel.presenceMap.syncInProgress).toEventually(beFalse(), timeout: testTimeout) + + guard let user11MemberKey = channel.presenceMap.members["user11"]?.memberKey() else { + fail("user11 memberKey is not present"); return + } + + waitUntil(timeout: testTimeout) { done in + channel.presence.subscribe(.Leave) { presence in + expect(presence.clientId).to(equal("user11")) + done() + } + clientMembers?.channels.get("foo").presence.leaveClient("user11", data: nil) + } + + expect(channel.presenceMap.members.filter{ _, presence in presence.memberKey() == user11MemberKey }).to(beEmpty()) + } + + // RTP2f + it("if a SYNC is in progress, then when a presence message with an action of LEAVE arrives, it should be stored in the presence map with the action set to ABSENT") { + let options = AblyTests.commonAppSetup() + + var clientMembers: ARTRealtime? + defer { clientMembers?.dispose(); clientMembers?.close() } + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 20, options: options) { + done() + } + } + + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + channel.attach() + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(3, done: done) + channel.presenceMap.testSuite_injectIntoMethodAfter(#selector(ARTPresenceMap.startSync)) { + expect(channel.presenceMap.syncInProgress).to(beTrue()) + + channel.presence.subscribe(.Leave) { leave in + expect(leave.clientId).to(equal("user11")) + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Leave }).to(beEmpty()) + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Absent }).to(haveCount(1)) + partialDone() + } + + // Inject a fabricated Presence message + let leaveMessage = ARTProtocolMessage() + leaveMessage.action = .Presence + leaveMessage.channel = channel.name + leaveMessage.connectionSerial = client.connection.serial + 1 + leaveMessage.timestamp = NSDate() + leaveMessage.presence = [ + ARTPresenceMessage(clientId: "user11", action: .Leave, connectionId: "another", id: "another:123:0", timestamp: NSDate()) + ] + transport.receive(leaveMessage) + } + channel.presenceMap.testSuite_injectIntoMethodBefore(#selector(ARTPresenceMap.endSync)) { + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Absent }).to(haveCount(1)) + partialDone() + } + channel.presenceMap.testSuite_injectIntoMethodAfter(#selector(ARTPresenceMap.endSync)) { + expect(channel.presenceMap.syncInProgress).to(beFalse()) + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Leave }).to(beEmpty()) + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Absent }).to(beEmpty()) + partialDone() + } + } + + expect(channel.presenceMap.members).to(haveCount(19)) + } + + // RTP2g + it("any incoming presence message that passes the newness check should be emitted on the Presence object, with an event name set to its original action") { + let options = AblyTests.commonAppSetup() + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.presence.enterClient("tester", data: nil) { error in + expect(error).to(beNil()) + partialDone() + } + channel.presence.subscribe(.Enter) { _ in + partialDone() + } + } + + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Present }).to(haveCount(1)) + expect(channel.presenceMap.members.filter{ _, presence in presence.action == .Enter }).to(beEmpty()) + } + + } + // RTP8 context("enter") { // RTP8h diff --git a/Spec/TestUtilities.swift b/Spec/TestUtilities.swift index e33dfe7d7..71ead971d 100644 --- a/Spec/TestUtilities.swift +++ b/Spec/TestUtilities.swift @@ -947,6 +947,14 @@ public func >=(lhs: NSDate, rhs: NSDate) -> Bool { return (lhs > rhs || lhs == rhs) } +public func -(lhs: NSDate, rhs: NSTimeInterval) -> NSDate { + return lhs.dateByAddingTimeInterval(-rhs) +} + +public func +(lhs: NSDate, rhs: NSTimeInterval) -> NSDate { + return lhs.dateByAddingTimeInterval(rhs) +} + extension NSRegularExpression { class func match(value: String?, pattern: String) -> Bool { @@ -1054,6 +1062,19 @@ extension ARTAuth { } +extension ARTPresenceMessage { + + convenience init(clientId: String, action: ARTPresenceAction, connectionId: String, id: String, timestamp: NSDate = NSDate()) { + self.init() + self.action = action + self.clientId = clientId + self.connectionId = connectionId + self.id = id + self.timestamp = timestamp + } + +} + extension ARTRealtimeConnectionState : CustomStringConvertible { public var description : String { return ARTRealtimeConnectionStateToStr(self) diff --git a/Tests/ARTRealtimePresenceTest.m b/Tests/ARTRealtimePresenceTest.m index dd762565f..06a1480a3 100644 --- a/Tests/ARTRealtimePresenceTest.m +++ b/Tests/ARTRealtimePresenceTest.m @@ -483,7 +483,7 @@ - (void)testEnterAndGet { XCTAssert(!error); XCTAssertEqual(2, members.count); XCTAssertEqual(members[0].action, ARTPresencePresent); - XCTAssertEqual(members[1].action, ARTPresenceEnter); + XCTAssertEqual(members[1].action, ARTPresencePresent); XCTAssertEqualObjects([members[0] data], enterData); XCTAssertEqualObjects([members[1] data], enterData); [expectation fulfill]; @@ -966,7 +966,7 @@ - (void)testPresenceWithData { [channel.presence get:^(NSArray *members, ARTErrorInfo *error) { XCTAssert(!error); XCTAssertEqual(1, members.count); - XCTAssertEqual(members[0].action, ARTPresenceEnter); + XCTAssertEqual(members[0].action, ARTPresencePresent); XCTAssertEqualObjects(members[0].clientId, [self getClientId]); XCTAssertEqualObjects([members[0] data], @"someDataPayload"); [expectation fulfill]; diff --git a/Tests/ARTRealtimeRecoverTest.m b/Tests/ARTRealtimeRecoverTest.m index abcaf909c..24ab92b56 100644 --- a/Tests/ARTRealtimeRecoverTest.m +++ b/Tests/ARTRealtimeRecoverTest.m @@ -36,7 +36,7 @@ - (void)testRecoverDisconnected { __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; __block NSString *firstConnectionId = nil; - [realtime.connection once:ARTRealtimeConnected callback:^(ARTConnectionStateChange *stateChange) { + [realtime.connection once:ARTRealtimeConnectionEventConnected callback:^(ARTConnectionStateChange *stateChange) { firstConnectionId = realtime.connection.id; ARTRealtimeChannel *channel = [realtime.channels get:channelName]; // Sending a message @@ -48,7 +48,7 @@ - (void)testRecoverDisconnected { [self waitForExpectationsWithTimeout:[ARTTestUtil timeout] handler:nil]; __weak XCTestExpectation *expectation2 = [self expectationWithDescription:[NSString stringWithFormat:@"%s-2", __FUNCTION__]]; - [realtime.connection once:ARTRealtimeDisconnected callback:^(ARTConnectionStateChange *stateChange) { + [realtime.connection once:ARTRealtimeConnectionEventDisconnected callback:^(ARTConnectionStateChange *stateChange) { options.recover = nil; ARTRealtime *realtimeNonRecovered = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *c2 = [realtimeNonRecovered.channels get:channelName]; @@ -71,7 +71,7 @@ - (void)testRecoverDisconnected { XCTAssertEqualObjects(c2Message, [message data]); [expectation3 fulfill]; }]; - [realtimeRecovered.connection once:ARTRealtimeConnected callback:^(ARTConnectionStateChange *stateChange) { + [realtimeRecovered.connection once:ARTRealtimeConnectionEventConnected callback:^(ARTConnectionStateChange *stateChange) { XCTAssertEqualObjects(realtimeRecovered.connection.id, firstConnectionId); }]; [self waitForExpectationsWithTimeout:[ARTTestUtil timeout] handler:nil]; From 55ae920f91b9ab5fc9427fc61b9a5bb8ee281a2b Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Sun, 5 Feb 2017 22:51:26 +0000 Subject: [PATCH 34/43] RTP18 (#567) * Test suite: ARTPresenceMessage convenience initializer * RTP18 * RTP18a * RTP18b * RTP18c --- Spec/RealtimeClientPresence.swift | 133 ++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index e4bc1b4d0..7627fd810 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -149,6 +149,139 @@ class RealtimeClientPresence: QuickSpec { } } + // RTP18 + context("realtime system reserves the right to initiate a sync of the presence members at any point once a channel is attached") { + + // RTP18a, RTP18b + it("should do a new sync whenever a SYNC ProtocolMessage is received with a channel attribute and a new sync sequence identifier in the channelSerial attribute") { + let options = AblyTests.commonAppSetup() + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + waitUntil(timeout: testTimeout) { done in + channel.attach() { error in + expect(error).to(beNil()) + done() + } + } + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + expect(channel.presenceMap.syncInProgress).to(beFalse()) + expect(channel.presenceMap.members).to(beEmpty()) + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.presence.subscribe(.Present) { error in + expect(channel.presence.syncComplete).to(beFalse()) + partialDone() + } + + guard let lastConnectionSerial = transport.protocolMessagesReceived.last?.connectionSerial else { + fail("No protocol message has been received yet"); done(); return + } + + // Inject a SYNC Presence message (first page) + let sync1Message = ARTProtocolMessage() + sync1Message.action = .Sync + sync1Message.channel = channel.name + sync1Message.channelSerial = "sequenceid:cursor" + sync1Message.connectionSerial = lastConnectionSerial + 1 + sync1Message.timestamp = NSDate() + sync1Message.presence = [ + ARTPresenceMessage(clientId: "a", action: .Present, connectionId: "another", id: "another:0:0"), + ARTPresenceMessage(clientId: "b", action: .Present, connectionId: "another", id: "another:0:1"), + ] + transport.receive(sync1Message) + + // Inject a SYNC Presence message (last page) + let sync2Message = ARTProtocolMessage() + sync2Message.action = .Sync + sync2Message.channel = channel.name + sync2Message.channelSerial = "sequenceid:" //indicates SYNC is complete + sync2Message.connectionSerial = lastConnectionSerial + 2 + sync2Message.timestamp = NSDate() + sync2Message.presence = [ + ARTPresenceMessage(clientId: "a", action: .Leave, connectionId: "another", id: "another:1:0"), + ] + transport.receive(sync2Message) + } + + expect(channel.presence.syncComplete).to(beTrue()) + waitUntil(timeout: testTimeout) { done in + channel.presence.get { members, error in + expect(error).to(beNil()) + guard let members = members where members.count == 1 else { + fail("Should at least have 1 member"); done(); return + } + expect(members[0].clientId).to(equal("b")) + done() + } + } + } + + // RTP18c, RTP18b + it("when a SYNC is sent with no channelSerial attribute then the sync data is entirely contained within that ProtocolMessage") { + let options = AblyTests.commonAppSetup() + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + waitUntil(timeout: testTimeout) { done in + channel.attach() { error in + expect(error).to(beNil()) + done() + } + } + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + expect(channel.presenceMap.syncInProgress).to(beFalse()) + expect(channel.presenceMap.members).to(beEmpty()) + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.presence.subscribe(.Present) { error in + expect(channel.presence.syncComplete).to(beFalse()) + partialDone() + } + + guard let lastConnectionSerial = transport.protocolMessagesReceived.last?.connectionSerial else { + fail("No protocol message has been received yet"); done(); return + } + + // Inject a SYNC Presence message (entirely contained) + let syncMessage = ARTProtocolMessage() + syncMessage.action = .Sync + syncMessage.channel = channel.name + syncMessage.connectionSerial = lastConnectionSerial + 1 + syncMessage.timestamp = NSDate() + syncMessage.presence = [ + ARTPresenceMessage(clientId: "a", action: .Present, connectionId: "another", id: "another:0:0"), + ARTPresenceMessage(clientId: "b", action: .Present, connectionId: "another", id: "another:0:1"), + ARTPresenceMessage(clientId: "a", action: .Leave, connectionId: "another", id: "another:1:0"), + ] + transport.receive(syncMessage) + } + + expect(channel.presence.syncComplete).to(beTrue()) + waitUntil(timeout: testTimeout) { done in + channel.presence.get { members, error in + expect(error).to(beNil()) + guard let members = members where members.count == 1 else { + fail("Should at least have 1 member"); done(); return + } + expect(members[0].clientId).to(equal("b")) + done() + } + } + } + + } + // RTP4 it("should receive all 250 members") { let options = AblyTests.commonAppSetup() From eaac953025aa770a26e009cfe9e29e87eef4ca51 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Mon, 6 Feb 2017 11:16:30 +0000 Subject: [PATCH 35/43] Update RSA9h for 0.9 (#574) * Auth: optional arguments * Update RSA9h --- Source/ARTAuth.h | 8 +++-- Source/ARTAuth.m | 13 ++++++++ Spec/Auth.swift | 49 +++++++++++++++++++++++++++++ Spec/RealtimeClientConnection.swift | 2 +- 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/Source/ARTAuth.h b/Source/ARTAuth.h index 5b6ec8197..96223689b 100644 --- a/Source/ARTAuth.h +++ b/Source/ARTAuth.h @@ -43,13 +43,17 @@ ART_ASSUME_NONNULL_BEGIN */ - (void)requestToken:(art_nullable ARTTokenParams *)tokenParams withOptions:(art_nullable ARTAuthOptions *)authOptions callback:(void (^)(ARTTokenDetails *__art_nullable, NSError *__art_nullable))callback; +- (void)requestToken:(void (^)(ARTTokenDetails *__art_nullable, NSError *__art_nullable))callback; -- (void)authorise:(art_nullable ARTTokenParams *)tokenParams options:(art_nullable ARTAuthOptions *)authOptions callback:(void (^)(ARTTokenDetails *__art_nullable, NSError *__art_nullable))callback DEPRECATED_MSG_ATTRIBUTE("method will be removed in v1.0. Use authorize: method instead."); +- (void)authorise:(art_nullable ARTTokenParams *)tokenParams options:(art_nullable ARTAuthOptions *)authOptions callback:(void (^)(ARTTokenDetails *__art_nullable, NSError *__art_nullable))callback DEPRECATED_MSG_ATTRIBUTE("method will be removed in v1.0. Use 'authorize:' method instead."); -- (void)authorize:(art_nullable ARTTokenParams *)tokenParams options:(art_nullable ARTAuthOptions *)authOptions callback:(void (^)(ARTTokenDetails *__art_nullable, NSError *__art_nullable))callback; +- (void)authorize:(art_nullable ARTTokenParams *)tokenParams options:(art_nullable ARTAuthOptions *)authOptions + callback:(void (^)(ARTTokenDetails *__art_nullable, NSError *__art_nullable))callback; +- (void)authorize:(void (^)(ARTTokenDetails *__art_nullable, NSError *__art_nullable))callback; - (void)createTokenRequest:(art_nullable ARTTokenParams *)tokenParams options:(art_nullable ARTAuthOptions *)options callback:(void (^)(ARTTokenRequest *__art_nullable tokenRequest, NSError *__art_nullable error))callback; +- (void)createTokenRequest:(void (^)(ARTTokenRequest *__art_nullable tokenRequest, NSError *__art_nullable error))callback; @end diff --git a/Source/ARTAuth.m b/Source/ARTAuth.m index f721b3c37..c9c39e9fd 100644 --- a/Source/ARTAuth.m +++ b/Source/ARTAuth.m @@ -203,6 +203,11 @@ - (BOOL)tokenRemainsValid { return NO; } +- (void)requestToken:(void (^)(ARTTokenDetails *, NSError *))callback { + // If the object arguments are omitted, the client library configured defaults are used + [self requestToken:_tokenParams withOptions:_options callback:callback]; +} + - (void)requestToken:(ARTTokenParams *)tokenParams withOptions:(ARTAuthOptions *)authOptions callback:(void (^)(ARTTokenDetails *, NSError *))callback { @@ -339,6 +344,10 @@ - (void)authorise:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOp [self authorize:tokenParams options:authOptions callback:callback]; } +- (void)authorize:(void (^)(ARTTokenDetails *, NSError *))callback { + [self authorize:_tokenParams options:_options callback:callback]; +} + - (void)authorize:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOptions callback:(void (^)(ARTTokenDetails *, NSError *))callback { ARTAuthOptions *replacedOptions = [authOptions copy] ? : [self.options copy]; [self storeOptions:replacedOptions]; @@ -402,6 +411,10 @@ - (void)authorize:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOp }]; } +- (void)createTokenRequest:(void (^)(ARTTokenRequest *, NSError *))callback { + [self createTokenRequest:_tokenParams options:_options callback:callback]; +} + - (void)createTokenRequest:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)options callback:(void (^)(ARTTokenRequest *, NSError *))callback { ARTAuthOptions *replacedOptions = options ? : self.options; ARTTokenParams *currentTokenParams = tokenParams ? : _tokenParams; diff --git a/Spec/Auth.swift b/Spec/Auth.swift index 601dfc801..f6aa03b34 100644 --- a/Spec/Auth.swift +++ b/Spec/Auth.swift @@ -1801,6 +1801,55 @@ class Auth : QuickSpec { } } + // RSA9h + it("should use configured defaults if the object arguments are omitted") { + let options = AblyTests.commonAppSetup() + let rest = ARTRest(options: options) + + let tokenParams = ARTTokenParams() + tokenParams.clientId = "tester" + tokenParams.ttl = 2000 + tokenParams.capability = "{\"foo:*\":[\"publish\"]}" + + let authOptions = ARTAuthOptions() + authOptions.queryTime = true + authOptions.key = options.key + + var serverTimeRequestCount = 0 + let hook = rest.testSuite_injectIntoMethodAfter(#selector(rest.time(_:))) { + serverTimeRequestCount += 1 + } + defer { hook.remove() } + + waitUntil(timeout: testTimeout) { done in + rest.auth.createTokenRequest(tokenParams, options: authOptions) { tokenRequest, error in + expect(error).to(beNil()) + guard let tokenRequest = tokenRequest else { + XCTFail("TokenRequest is nil"); done(); return + } + expect(tokenRequest.clientId) == tokenParams.clientId + expect(tokenRequest.ttl) == tokenParams.ttl + expect(tokenRequest.capability) == tokenParams.capability + done() + } + } + + waitUntil(timeout: testTimeout) { done in + rest.auth.createTokenRequest { tokenRequest, error in + expect(error).to(beNil()) + guard let tokenRequest = tokenRequest else { + XCTFail("TokenRequest is nil"); done(); return + } + expect(tokenRequest.clientId).to(beNil()) + expect(tokenRequest.ttl) == ARTDefault.ttl() + expect(tokenRequest.capability) == "{\"*\":[\"*\"]}" + done() + } + } + + expect(serverTimeRequestCount) == 1 + } + // RSA9a it("should create and sign a TokenRequest") { let rest = ARTRest(options: AblyTests.commonAppSetup()) diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index 449d5c2ca..fab1100c5 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -3426,7 +3426,7 @@ class RealtimeClientConnection: QuickSpec { } var authorizeMethodCallCount = 0 - let hook = client.auth.testSuite_injectIntoMethodAfter(#selector(client.auth.authorize)) { + let hook = client.auth.testSuite_injectIntoMethodAfter(#selector(client.auth.authorize(_:options:callback:))) { authorizeMethodCallCount += 1 } defer { hook.remove() } From 210f60e537e7976cf57ac554dff83cb71302747f Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Mon, 6 Feb 2017 12:00:02 +0000 Subject: [PATCH 36/43] Update RSA8e for 0.9 (#573) * Update RSA8e * Auth: optional arguments * Comments --- Source/ARTAuth.m | 2 +- Spec/Auth.swift | 60 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/Source/ARTAuth.m b/Source/ARTAuth.m index c9c39e9fd..316fdeaf8 100644 --- a/Source/ARTAuth.m +++ b/Source/ARTAuth.m @@ -211,7 +211,7 @@ - (void)requestToken:(void (^)(ARTTokenDetails *, NSError *))callback { - (void)requestToken:(ARTTokenParams *)tokenParams withOptions:(ARTAuthOptions *)authOptions callback:(void (^)(ARTTokenDetails *, NSError *))callback { - // The values replace all corresponding. + // If options, params passed in, they're used instead of stored, don't merge them ARTAuthOptions *replacedOptions = authOptions ? authOptions : self.options; ARTTokenParams *currentTokenParams = tokenParams ? tokenParams : _tokenParams; currentTokenParams.timestamp = [self currentDate]; diff --git a/Spec/Auth.swift b/Spec/Auth.swift index f6aa03b34..60483cba9 100644 --- a/Spec/Auth.swift +++ b/Spec/Auth.swift @@ -1122,6 +1122,66 @@ class Auth : QuickSpec { } } } + + // RSA8e + it("should use configured defaults if the object arguments are omitted") { + let options = AblyTests.commonAppSetup() + options.clientId = "tester" + let rest = ARTRest(options: options) + + waitUntil(timeout: testTimeout) { done in + rest.auth.requestToken(nil, withOptions: nil) { tokenDetails, error in + expect(error).to(beNil()) + expect(tokenDetails).toNot(beNil()) + expect(tokenDetails!.capability).to(equal("{\"*\":[\"*\"]}")) + expect(tokenDetails!.clientId).to(equal("tester")) + done() + } + } + + let tokenParams = ARTTokenParams() + tokenParams.ttl = 2000 + tokenParams.capability = "{\"cansubscribe:*\":[\"subscribe\"]}" + tokenParams.clientId = nil + + let authOptions = ARTAuthOptions() + authOptions.key = options.key + + // Provide TokenParams and Options + waitUntil(timeout: testTimeout) { done in + rest.auth.requestToken(tokenParams, withOptions: authOptions) { tokenDetails, error in + expect(error).to(beNil()) + expect(tokenDetails).toNot(beNil()) + expect(tokenDetails!.capability).to(equal("{\"cansubscribe:*\":[\"subscribe\"]}")) + expect(tokenDetails!.clientId).to(beNil()) + expect(tokenDetails!.expires!.timeIntervalSince1970 - tokenDetails!.issued!.timeIntervalSince1970).to(equal(tokenParams.ttl)) + done() + } + } + + // Provide TokenParams as null + waitUntil(timeout: testTimeout) { done in + rest.auth.requestToken(nil, withOptions: authOptions) { tokenDetails, error in + expect(error).to(beNil()) + expect(tokenDetails).toNot(beNil()) + expect(tokenDetails!.capability).to(equal("{\"*\":[\"*\"]}")) + expect(tokenDetails!.clientId).to(equal("tester")) + expect(tokenDetails!.expires!.timeIntervalSince1970 - tokenDetails!.issued!.timeIntervalSince1970).to(equal(ARTDefault.ttl())) + done() + } + } + + // Omit arguments + waitUntil(timeout: testTimeout) { done in + rest.auth.requestToken { tokenDetails, error in + expect(error).to(beNil()) + expect(tokenDetails).toNot(beNil()) + expect(tokenDetails!.capability).to(equal("{\"*\":[\"*\"]}")) + expect(tokenDetails!.clientId).to(equal("tester")) + done() + } + } + } } // RSA8c From d9e828c9b1d5c011384bcad191bdfec41464c97b Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Mon, 6 Feb 2017 12:33:21 +0000 Subject: [PATCH 37/43] Update RSA10g for 0.9 (#575) * Auth: optional arguments * Update RSA10g * Auth: subsequent authorizations with stored values --- Source/ARTAuth.m | 2 +- Spec/Auth.swift | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/Source/ARTAuth.m b/Source/ARTAuth.m index 316fdeaf8..04edfcb03 100644 --- a/Source/ARTAuth.m +++ b/Source/ARTAuth.m @@ -345,7 +345,7 @@ - (void)authorise:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOp } - (void)authorize:(void (^)(ARTTokenDetails *, NSError *))callback { - [self authorize:_tokenParams options:_options callback:callback]; + [self authorize:_options.defaultTokenParams options:_options callback:callback]; } - (void)authorize:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOptions callback:(void (^)(ARTTokenDetails *, NSError *))callback { diff --git a/Spec/Auth.swift b/Spec/Auth.swift index 60483cba9..9fdb54722 100644 --- a/Spec/Auth.swift +++ b/Spec/Auth.swift @@ -2497,6 +2497,44 @@ class Auth : QuickSpec { } } + it("should use configured defaults if the object arguments are omitted") { + let options = AblyTests.commonAppSetup() + let rest = ARTRest(options: options) + + let tokenParams = ARTTokenParams() + tokenParams.clientId = ExpectedTokenParams.clientId + tokenParams.ttl = ExpectedTokenParams.ttl + tokenParams.capability = ExpectedTokenParams.capability + + let authOptions = ARTAuthOptions() + var authCallbackCalled = 0 + authOptions.authCallback = { tokenParams, completion in + expect(tokenParams.clientId) == ExpectedTokenParams.clientId + expect(tokenParams.ttl) == ExpectedTokenParams.ttl + expect(tokenParams.capability) == ExpectedTokenParams.capability + authCallbackCalled += 1 + completion(getTestTokenDetails(key: options.key), nil) + } + + waitUntil(timeout: testTimeout) { done in + rest.auth.authorize(tokenParams, options: authOptions) { tokenDetails, error in + expect(error).to(beNil()) + expect(tokenDetails).toNot(beNil()) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + rest.auth.authorize { tokenDetails, error in + expect(error).to(beNil()) + expect(tokenDetails).toNot(beNil()) + done() + } + } + + expect(authCallbackCalled) == 2 + } + } // RSA10h From 18e43ea585ed5424b205d40c6bf028db36a61b47 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Mon, 6 Feb 2017 16:40:24 +0000 Subject: [PATCH 38/43] Update RTP5 for 0.9 (#570) * Update RTP5 * Update RTP5a * Update RTP5b * RTP5c * Test suite: replaceAcksWithNacks - better code completion * RTP5f * PresenceMap: existing members before Sync * Fix: presence get members should not wait for sync if sync is not in progress * PresenceMap: reenter local member * Test suite: replacing acks with nacks even with Presence action * Fix: should queue messages before the attach operation * Test suite: ARTPresenceAction description * Better debug info * Fix: should continue incrementing msgSerial serially if the connection resumes successfully * Remove warnings * Test suite: timings --- Ably.xcodeproj/project.pbxproj | 4 + Source/ARTBaseMessage.h | 2 +- Source/ARTJsonLikeEncoder.m | 4 +- Source/ARTLog.h | 1 + Source/ARTLog.m | 8 + Source/ARTPresenceMap.h | 21 +- Source/ARTPresenceMap.m | 129 +++++-- Source/ARTPresenceMessage+Private.h | 15 + Source/ARTPresenceMessage.h | 10 +- Source/ARTPresenceMessage.m | 5 +- Source/ARTRealtime.m | 13 +- Source/ARTRealtimeChannel+Private.h | 4 +- Source/ARTRealtimeChannel.m | 81 ++-- Source/ARTRealtimePresence.m | 3 +- Source/ARTRest.m | 6 +- Source/ARTWebSocketTransport.m | 6 +- Source/Ably.modulemap | 1 + Spec/RealtimeClientConnection.swift | 176 +++++++++ Spec/RealtimeClientPresence.swift | 553 ++++++++++++++++++++++++++-- Spec/RestClient.swift | 10 +- Spec/TestUtilities.swift | 6 +- 21 files changed, 945 insertions(+), 113 deletions(-) create mode 100644 Source/ARTPresenceMessage+Private.h diff --git a/Ably.xcodeproj/project.pbxproj b/Ably.xcodeproj/project.pbxproj index b366e36fa..6a280294c 100644 --- a/Ably.xcodeproj/project.pbxproj +++ b/Ably.xcodeproj/project.pbxproj @@ -159,6 +159,7 @@ D7F1D3771BF4DE72001A4B5E /* ARTRealtimePresence.h in Headers */ = {isa = PBXBuildFile; fileRef = D7F1D3751BF4DE72001A4B5E /* ARTRealtimePresence.h */; settings = {ATTRIBUTES = (Public, ); }; }; D7F1D3781BF4DE72001A4B5E /* ARTRealtimePresence.m in Sources */ = {isa = PBXBuildFile; fileRef = D7F1D3761BF4DE72001A4B5E /* ARTRealtimePresence.m */; }; D7F1D37A1BF4E33A001A4B5E /* ARTRestChannel+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D7F1D3791BF4E33A001A4B5E /* ARTRestChannel+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; + D7F2B8B21E42410D00B65151 /* ARTPresenceMessage+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D7F2B8B11E42410D00B65151 /* ARTPresenceMessage+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; EB0505FC1C5BD7C4006BA7E2 /* ARTBaseMessage+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EB0505FB1C5BD7C4006BA7E2 /* ARTBaseMessage+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; EB1AE0CC1C5C1EB200D62250 /* ARTEventEmitter+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EB1AE0CB1C5C1EB200D62250 /* ARTEventEmitter+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; EB1AE0CE1C5C3A4900D62250 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1AE0CD1C5C3A4900D62250 /* Utilities.swift */; }; @@ -399,6 +400,7 @@ D7F1D3751BF4DE72001A4B5E /* ARTRealtimePresence.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARTRealtimePresence.h; sourceTree = ""; }; D7F1D3761BF4DE72001A4B5E /* ARTRealtimePresence.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTRealtimePresence.m; sourceTree = ""; }; D7F1D3791BF4E33A001A4B5E /* ARTRestChannel+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARTRestChannel+Private.h"; sourceTree = ""; }; + D7F2B8B11E42410D00B65151 /* ARTPresenceMessage+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARTPresenceMessage+Private.h"; sourceTree = ""; }; E3ECA6832E9694DC9EFC5DDD /* Pods-AblySpec.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AblySpec.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AblySpec/Pods-AblySpec.debug.xcconfig"; sourceTree = ""; }; EB0505FB1C5BD7C4006BA7E2 /* ARTBaseMessage+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARTBaseMessage+Private.h"; sourceTree = ""; }; EB1AE0CB1C5C1EB200D62250 /* ARTEventEmitter+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ARTEventEmitter+Private.h"; sourceTree = ""; }; @@ -708,6 +710,7 @@ D746AE261BBB61C9003ECEF8 /* ARTPresence.h */, D746AE271BBB61C9003ECEF8 /* ARTPresence.m */, 96A5079F1A377AA50077CDF8 /* ARTPresenceMessage.h */, + D7F2B8B11E42410D00B65151 /* ARTPresenceMessage+Private.h */, 96A507A01A377AA50077CDF8 /* ARTPresenceMessage.m */, 1C2B0FFB1B136A6D00E3633C /* ARTPresenceMap.h */, 1C2B0FFC1B136A6D00E3633C /* ARTPresenceMap.m */, @@ -858,6 +861,7 @@ D746AE251BBB611C003ECEF8 /* ARTChannel+Private.h in Headers */, D7F1D3731BF4DE07001A4B5E /* ARTRestPresence.h in Headers */, D7B17EE31C07208B00A6958E /* ARTConnectionDetails.h in Headers */, + D7F2B8B21E42410D00B65151 /* ARTPresenceMessage+Private.h in Headers */, EBFA366E1D58B05000B09AA7 /* ARTRestPresence+Private.h in Headers */, EB2D85011CD769C800F23CDA /* ARTOSReachability.h in Headers */, 960D07971A46FFC300ED8C8C /* ARTRest+Private.h in Headers */, diff --git a/Source/ARTBaseMessage.h b/Source/ARTBaseMessage.h index 0d809e797..cdc87c5c5 100644 --- a/Source/ARTBaseMessage.h +++ b/Source/ARTBaseMessage.h @@ -14,7 +14,7 @@ ART_ASSUME_NONNULL_BEGIN @interface ARTBaseMessage : NSObject /// A unique id for this message -@property (strong, nonatomic) NSString *id; +@property (nullable, strong, nonatomic) NSString *id; /// The timestamp for this message @property (strong, nonatomic, art_nullable) NSDate *timestamp; diff --git a/Source/ARTJsonLikeEncoder.m b/Source/ARTJsonLikeEncoder.m index b208c251f..e7bd720b0 100644 --- a/Source/ARTJsonLikeEncoder.m +++ b/Source/ARTJsonLikeEncoder.m @@ -696,7 +696,7 @@ - (void)writeData:(id)data encoding:(NSString *)encoding toDictionary:(NSMutable - (id)decode:(NSData *)data { id decoded = [_delegate decode:data]; - [_logger verbose:@"RS:%p ARTJsonLikeEncoder<%@> decoding '%@'; got: %@", _rest, [_delegate formatAsString], data, decoded]; + [_logger debug:@"RS:%p ARTJsonLikeEncoder<%@> decoding '%@'; got: %@", _rest, [_delegate formatAsString], data, decoded]; return decoded; } @@ -718,7 +718,7 @@ - (NSArray *)decodeArray:(NSData *)data { - (NSData *)encode:(id)obj { NSData *encoded = [_delegate encode:obj]; - [_logger verbose:@"RS:%p ARTJsonLikeEncoder<%@> encoding '%@'; got: %@", _rest, [_delegate formatAsString], obj, encoded]; + [_logger debug:@"RS:%p ARTJsonLikeEncoder<%@> encoding '%@'; got: %@", _rest, [_delegate formatAsString], obj, encoded]; return encoded; } diff --git a/Source/ARTLog.h b/Source/ARTLog.h index 2cfd3b953..cf204e443 100644 --- a/Source/ARTLog.h +++ b/Source/ARTLog.h @@ -37,6 +37,7 @@ typedef NS_ENUM(NSUInteger, ARTLogLevel) { @interface ARTLog (Shorthand) - (void)verbose:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2); +- (void)verbose:(const char *)fileName line:(NSUInteger)line message:(NSString *)message, ... NS_FORMAT_FUNCTION(3,4); - (void)debug:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2); - (void)debug:(const char *)fileName line:(NSUInteger)line message:(NSString *)message, ... NS_FORMAT_FUNCTION(3,4); - (void)info:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2); diff --git a/Source/ARTLog.m b/Source/ARTLog.m index 394e4a87c..c98ab0717 100644 --- a/Source/ARTLog.m +++ b/Source/ARTLog.m @@ -113,6 +113,14 @@ - (void)verbose:(NSString *)format, ... { va_end(args); } + +- (void)verbose:(const char *)fileName line:(NSUInteger)line message:(NSString *)message, ... { + va_list args; + va_start(args, message); + [self log:[[NSString alloc] initWithFormat:[NSString stringWithFormat:@"(%@:%lu) %@", [[NSString stringWithUTF8String:fileName] lastPathComponent], (unsigned long)line, message] arguments:args] level:ARTLogLevelVerbose]; + va_end(args); +} + - (void)debug:(NSString *)format, ... { va_list args; va_start(args, format); diff --git a/Source/ARTPresenceMap.h b/Source/ARTPresenceMap.h index e6ff51ec1..3775047cb 100644 --- a/Source/ARTPresenceMap.h +++ b/Source/ARTPresenceMap.h @@ -9,33 +9,44 @@ #import #import "CompatibilityMacros.h" +@class ARTPresenceMap; @class ARTPresenceMessage; @class ARTErrorInfo; @class ARTLog; ART_ASSUME_NONNULL_BEGIN +/// ARTPresenceMapDelegate +@protocol ARTPresenceMapDelegate +@property (nonatomic, readonly) NSString *connectionId; +- (void)map:(ARTPresenceMap *)map didRemovedMemberNoLongerPresent:(ARTPresenceMessage *)presence; +- (void)map:(ARTPresenceMap *)map shouldReenterLocalMember:(ARTPresenceMessage *)presence; +@end + /// Used to maintain a list of members present on a channel @interface ARTPresenceMap : NSObject /// List of members. /// The key is the clientId and the value is the latest relevant ARTPresenceMessage for that clientId. -@property (readonly, atomic, getter=getMembers) __GENERIC(NSDictionary, NSString *, ARTPresenceMessage *) *members; +@property (readonly, atomic) NSDictionary *members; /// List of internal members. /// The key is the clientId and the value is the latest relevant ARTPresenceMessage for that clientId. -@property (readonly, atomic) NSDictionary *localMembers; +@property (readonly, atomic) NSMutableSet *localMembers; + +@property (nullable, weak) id delegate; @property (readwrite, nonatomic, assign) int64_t syncMsgSerial; @property (readwrite, nonatomic, nullable) NSString *syncChannelSerial; -@property (readonly, nonatomic, assign) BOOL syncComplete; -@property (readonly, nonatomic, getter=getSyncInProgress) BOOL syncInProgress; +@property (readonly, nonatomic, assign) NSUInteger syncSessionId; +@property (readonly, nonatomic, getter=syncComplete) BOOL syncComplete; +@property (readonly, nonatomic, getter=syncInProgress) BOOL syncInProgress; - (instancetype)init UNAVAILABLE_ATTRIBUTE; - (instancetype)initWithLogger:(ARTLog *)logger; - (BOOL)add:(ARTPresenceMessage *)message; -- (void)clean; +- (void)reset; - (void)startSync; - (void)endSync; diff --git a/Source/ARTPresenceMap.m b/Source/ARTPresenceMap.m index ea5628c5f..97e0472ea 100644 --- a/Source/ARTPresenceMap.m +++ b/Source/ARTPresenceMap.m @@ -8,22 +8,24 @@ #import "ARTPresenceMap.h" #import "ARTPresenceMessage.h" +#import "ARTPresenceMessage+Private.h" #import "ARTEventEmitter.h" #import "ARTLog.h" typedef NS_ENUM(NSUInteger, ARTPresenceSyncState) { + ARTPresenceSyncInitialized, ARTPresenceSyncStarted, //ItemType: nil ARTPresenceSyncEnded, //ItemType: NSArray* ARTPresenceSyncFailed //ItemType: ARTErrorInfo* }; @interface ARTPresenceMap () { - BOOL _syncStarted; + ARTPresenceSyncState _syncState; ARTEventEmitter *_syncEventEmitter; + NSMutableDictionary *_members; + NSMutableSet *_localMembers; } -@property (readwrite, strong, atomic) __GENERIC(NSMutableDictionary, NSString *, ARTPresenceMessage *) *recentMembers; - @end @implementation ARTPresenceMap { @@ -34,41 +36,76 @@ - (instancetype)initWithLogger:(ARTLog *)logger { self = [super init]; if(self) { _logger = logger; - _recentMembers = [NSMutableDictionary dictionary]; - _syncStarted = false; - _syncComplete = false; + [self reset]; + _syncSessionId = 0; + _syncState = ARTPresenceSyncInitialized; _syncEventEmitter = [[ARTEventEmitter alloc] init]; } return self; } -- (__GENERIC(NSDictionary, NSString *, ARTPresenceMessage *) *)getMembers { - return self.recentMembers; +- (NSDictionary *)members { + return _members; +} + +- (NSMutableSet *)localMembers { + return _localMembers; } - (BOOL)add:(ARTPresenceMessage *)message { - ARTPresenceMessage *latest = [self.recentMembers objectForKey:message.clientId]; + ARTPresenceMessage *latest = [_members objectForKey:message.clientId]; if ([self isNewestPresence:message comparingWith:latest]) { ARTPresenceMessage *messageCopy = [message copy]; switch (message.action) { case ARTPresenceEnter: case ARTPresenceUpdate: messageCopy.action = ARTPresencePresent; + // intentional fallthrough + case ARTPresencePresent: + [self internalAdd:messageCopy]; break; case ARTPresenceLeave: - if (self.syncInProgress) { - messageCopy.action = ARTPresenceAbsent; - } + [self internalRemove:messageCopy]; break; default: break; } - [self.recentMembers setObject:messageCopy forKey:message.clientId]; return YES; } + latest.syncSessionId = _syncSessionId; return NO; } +- (void)internalAdd:(ARTPresenceMessage *)message { + [self internalAdd:message withSessionId:_syncSessionId]; +} + +- (void)internalAdd:(ARTPresenceMessage *)message withSessionId:(NSUInteger)sessionId { + message.syncSessionId = sessionId; + [_members setObject:message forKey:message.clientId]; + // Local member + if ([message.connectionId isEqualToString:[self.delegate connectionId]]) { + [_localMembers addObject:message]; + [_logger debug:__FILE__ line:__LINE__ message:@"local member %@ added", message.memberKey]; + } +} + +- (void)internalRemove:(ARTPresenceMessage *)message { + [self internalRemove:message force:false]; +} + +- (void)internalRemove:(ARTPresenceMessage *)message force:(BOOL)force { + if (!force && self.syncInProgress) { + message.action = ARTPresenceAbsent; + // Should be removed after Sync ends + [self internalAdd:message withSessionId:message.syncSessionId]; + } + else { + [_members removeObjectForKey:message.clientId]; + [_localMembers removeObject:message]; + } +} + - (BOOL)isNewestPresence:(nonnull ARTPresenceMessage *)received comparingWith:(ARTPresenceMessage *)latest __attribute__((warn_unused_result)) { if (latest == nil) { return YES; @@ -100,6 +137,8 @@ - (BOOL)isNewestPresence:(nonnull ARTPresenceMessage *)received comparingWith:(A else if (receivedMsgSerial == latestRegisteredMsgSerial && receivedIndex > latestRegisteredIndex) { return YES; } + + [_logger debug:__FILE__ line:__LINE__ message:@"Presence member \"%@\" with action %@ has been ignored", received.memberKey, ARTPresenceActionToStr(received.action)]; return NO; } @@ -107,34 +146,58 @@ - (BOOL)isNewestPresence:(nonnull ARTPresenceMessage *)received comparingWith:(A [latest.timestamp timeIntervalSince1970] <= [received.timestamp timeIntervalSince1970]; } -- (void)clean { - for (NSString *key in [self.recentMembers allKeys]) { - ARTPresenceMessage *message = [self.recentMembers objectForKey:key]; - if (message.action == ARTPresenceAbsent || message.action == ARTPresenceLeave) { - [self.recentMembers removeObjectForKey:key]; +- (void)cleanUpAbsentMembers { + NSSet *filteredMembers = [_members keysOfEntriesPassingTest:^BOOL(NSString *key, ARTPresenceMessage *message, BOOL *stop) { + return message.action == ARTPresenceAbsent; + }]; + for (NSString *key in filteredMembers) { + [self internalRemove:[_members objectForKey:key] force:true]; + } +} + +- (void)leaveMembersNotPresentInSync { + for (ARTPresenceMessage *member in [_members allValues]) { + if (member.syncSessionId != _syncSessionId) { + // Handle members that have not been added or updated in the PresenceMap during the sync process + ARTPresenceMessage *leave = [member copy]; + [self internalRemove:member]; + [self.delegate map:self didRemovedMemberNoLongerPresent:leave]; } } } +- (void)reenterLocalMembersMissingFromSync { + NSSet *filteredLocalMembers = [_localMembers filteredSetUsingPredicate:[NSPredicate predicateWithFormat:@"syncSessionId != %lu", (unsigned long)_syncSessionId]]; + for (ARTPresenceMessage *localMember in filteredLocalMembers) { + ARTPresenceMessage *reenter = [localMember copy]; + [self internalRemove:localMember]; + [self.delegate map:self shouldReenterLocalMember:reenter]; + } +} + +- (void)reset { + _members = [NSMutableDictionary dictionary]; + _localMembers = [NSMutableSet set]; +} + - (void)startSync { - _recentMembers = [NSMutableDictionary dictionary]; - _syncStarted = true; - _syncComplete = false; + _syncSessionId++; + _syncState = ARTPresenceSyncStarted; [_syncEventEmitter emit:[NSNumber numberWithInt:ARTPresenceSyncStarted] with:nil]; } - (void)endSync { - [self clean]; - _syncStarted = false; - _syncComplete = true; - [_syncEventEmitter emit:[NSNumber numberWithInt:ARTPresenceSyncEnded] with:[self.recentMembers allValues]]; + [self cleanUpAbsentMembers]; + [self leaveMembersNotPresentInSync]; + _syncState = ARTPresenceSyncEnded; + [self reenterLocalMembersMissingFromSync]; + [_syncEventEmitter emit:[NSNumber numberWithInt:ARTPresenceSyncEnded] with:[_members allValues]]; [_syncEventEmitter off]; } - (void)failsSync:(ARTErrorInfo *)error { - [self clean]; - _syncStarted = false; - _syncComplete = true; + [self reset]; + _syncState = ARTPresenceSyncFailed; [_syncEventEmitter emit:[NSNumber numberWithInt:ARTPresenceSyncFailed] with:error]; [_syncEventEmitter off]; } @@ -144,7 +207,7 @@ - (void)onceSyncEnds:(void (^)(NSArray *))callback { [_syncEventEmitter once:[NSNumber numberWithInt:ARTPresenceSyncEnded] callback:callback]; } else { - callback([self.recentMembers allValues]); + callback([_members allValues]); } } @@ -154,8 +217,12 @@ - (void)onceSyncFails:(void (^)(ARTErrorInfo *))callback { } } -- (BOOL)getSyncInProgress { - return _syncStarted && !_syncComplete; +- (BOOL)syncComplete { + return !(_syncState == ARTPresenceSyncInitialized || _syncState == ARTPresenceSyncStarted); +} + +- (BOOL)syncInProgress { + return _syncState == ARTPresenceSyncStarted; } #pragma mark private diff --git a/Source/ARTPresenceMessage+Private.h b/Source/ARTPresenceMessage+Private.h new file mode 100644 index 000000000..edd3941e1 --- /dev/null +++ b/Source/ARTPresenceMessage+Private.h @@ -0,0 +1,15 @@ +// +// ARTPresenceMessage+Private.h +// Ably +// +// Created by Ricardo Pereira on 1/2/17. +// Copyright © 2017 Ably. All rights reserved. +// + +#import "ARTPresenceMessage.h" + +@interface ARTPresenceMessage () + +@property (readwrite, assign, nonatomic) NSUInteger syncSessionId; + +@end diff --git a/Source/ARTPresenceMessage.h b/Source/ARTPresenceMessage.h index 352e723a6..c0a044ee6 100644 --- a/Source/ARTPresenceMessage.h +++ b/Source/ARTPresenceMessage.h @@ -17,15 +17,19 @@ typedef NS_ENUM(NSUInteger, ARTPresenceAction) { ARTPresenceUpdate }; -NSString *__art_nonnull ARTPresenceActionToStr(ARTPresenceAction action); +NSString *_Nonnull ARTPresenceActionToStr(ARTPresenceAction action); + +ART_ASSUME_NONNULL_BEGIN /// List of members present on a channel @interface ARTPresenceMessage : ARTBaseMessage @property (readwrite, assign, nonatomic) ARTPresenceAction action; -- (NSString *)memberKey; +- (nonnull NSString *)memberKey; -- (BOOL)isEqualToPresenceMessage:(ARTPresenceMessage *)presence; +- (BOOL)isEqualToPresenceMessage:(nonnull ARTPresenceMessage *)presence; @end + +ART_ASSUME_NONNULL_END diff --git a/Source/ARTPresenceMessage.m b/Source/ARTPresenceMessage.m index bf2ca1e7c..a3d51041c 100644 --- a/Source/ARTPresenceMessage.m +++ b/Source/ARTPresenceMessage.m @@ -6,7 +6,7 @@ // Copyright (c) 2014 Ably. All rights reserved. // -#import "ARTPresenceMessage.h" +#import "ARTPresenceMessage+Private.h" @implementation ARTPresenceMessage @@ -15,6 +15,7 @@ - (instancetype)init { if (self) { // Default _action = ARTPresenceEnter; + _syncSessionId = 0; } return self; } @@ -22,6 +23,7 @@ - (instancetype)init { - (id)copyWithZone:(NSZone *)zone { ARTPresenceMessage *message = [super copyWithZone:zone]; message->_action = self.action; + message->_syncSessionId = self.syncSessionId; return message; } @@ -30,6 +32,7 @@ - (NSString *)description { [description deleteCharactersInRange:NSMakeRange(description.length - (description.length>2 ? 2:0), 2)]; [description appendFormat:@",\n"]; [description appendFormat:@" action: %lu\n", (unsigned long)self.action]; + [description appendFormat:@" syncSessionId: %lu\n", (unsigned long)self.syncSessionId]; [description appendFormat:@"}"]; return description; } diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index 16853495d..96c19aae5 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -88,7 +88,7 @@ - (instancetype)initWithOptions:(ARTClientOptions *)options { [self.connection setState:ARTRealtimeInitialized]; - [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p initialized with RS:%p", self, _rest]; + [self.logger verbose:__FILE__ line:__LINE__ message:@"R:%p initialized with RS:%p", self, _rest]; self.rest.prioritizedHost = nil; @@ -174,7 +174,7 @@ - (ARTAuth *)getAuth { } - (void)dealloc { - [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p dealloc", self]; + [self.logger verbose:__FILE__ line:__LINE__ message:@"R:%p dealloc", self]; if (_connection) { [_connection off]; @@ -265,7 +265,7 @@ - (void)transition:(ARTRealtimeConnectionState)state { } - (void)transition:(ARTRealtimeConnectionState)state withErrorInfo:(ARTErrorInfo *)errorInfo { - [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p transition to %@ requested", self, ARTRealtimeConnectionStateToStr(state)]; + [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p realtime state transitions to %tu - %@", self, state, ARTRealtimeConnectionStateToStr(state)]; ARTConnectionStateChange *stateChange = [[ARTConnectionStateChange alloc] initWithCurrent:state previous:self.connection.state event:(ARTRealtimeConnectionEvent)state reason:errorInfo retryIn:0]; [self.connection setState:state]; @@ -508,12 +508,14 @@ - (void)onConnected:(ARTProtocolMessage *)message { for (ARTRealtimeChannel *channel in self.channels) { [channel detachChannel:[ARTStatus state:ARTStateConnectionDisconnected info:message.error]]; } + _resuming = false; } else if (message.error) { [self.logger warn:@"R:%p ARTRealtime: connection has resumed with non-fatal error %@", self, message.error.message]; // The error will be emitted on `transition` } - _resuming = false; + + [self.logger debug:@"RT:%p connection \"%@\" has reconnected and resumed successfully", self, self.connection.id]; for (ARTRealtimeChannel *channel in self.channels) { if (channel.presenceMap.syncInProgress) { @@ -528,6 +530,7 @@ - (void)onConnected:(ARTProtocolMessage *)message { [self.connection setKey:message.connectionKey]; if (!_resuming) { [self.connection setSerial:message.connectionSerial]; + [self.logger debug:@"RT:%p msgSerial of connection \"%@\" has been reset", self, self.connection.id]; self.msgSerial = 0; self.pendingMessageStartSerial = 0; } @@ -542,6 +545,8 @@ - (void)onConnected:(ARTProtocolMessage *)message { default: break; } + + _resuming = false; } - (void)onDisconnected { diff --git a/Source/ARTRealtimeChannel+Private.h b/Source/ARTRealtimeChannel+Private.h index 152fb9cc3..e20f4ad4b 100644 --- a/Source/ARTRealtimeChannel+Private.h +++ b/Source/ARTRealtimeChannel+Private.h @@ -9,14 +9,14 @@ #import "ARTRestChannel.h" #import "ARTRealtimeChannel.h" +#import "ARTPresenceMap.h" #import "ARTEventEmitter.h" -@class ARTPresenceMap; @class ARTProtocolMessage; ART_ASSUME_NONNULL_BEGIN -@interface ARTRealtimeChannel () +@interface ARTRealtimeChannel () @property (readonly, weak, nonatomic) ARTRealtime *realtime; @property (readonly, strong, nonatomic) ARTRestChannel *restChannel; diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index 4d4a42d51..a5de1019b 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -48,6 +48,7 @@ - (instancetype)initWithRealtime:(ARTRealtime *)realtime andName:(NSString *)nam _queuedMessages = [NSMutableArray array]; _attachSerial = nil; _presenceMap = [[ARTPresenceMap alloc] initWithLogger:self.logger]; + _presenceMap.delegate = self; _lastPresenceAction = ARTPresenceAbsent; _statesEventEmitter = [[ARTEventEmitter alloc] init]; @@ -174,13 +175,12 @@ - (void)publishProtocolMessage:(ARTProtocolMessage *)pm callback:(void (^)(ARTSt switch (self.state) { case ARTRealtimeChannelInitialized: + [self addToQueue:pm callback:queuedCallback]; [self attach]; - // intentional fall-through + break; case ARTRealtimeChannelAttaching: - { [self addToQueue:pm callback:queuedCallback]; break; - } case ARTRealtimeChannelSuspended: case ARTRealtimeChannelDetaching: case ARTRealtimeChannelDetached: @@ -330,6 +330,7 @@ - (ARTEventListener *)timed:(ARTEventListener *)listener deadline:(NSTimeInterva } - (void)transition:(ARTRealtimeChannelState)state status:(ARTStatus *)status { + [self.logger debug:__FILE__ line:__LINE__ message:@"channel state transitions to %tu - %@", state, ARTRealtimeChannelStateToStr(state)]; ARTChannelStateChange *stateChange = [[ARTChannelStateChange alloc] initWithCurrent:state previous:self.state event:(ARTChannelEvent)state reason:status.errorInfo]; self.state = state; @@ -337,12 +338,20 @@ - (void)transition:(ARTRealtimeChannelState)state status:(ARTStatus *)status { _errorReason = status.errorInfo; } - if (state == ARTRealtimeChannelSuspended) { - [_attachedEventEmitter emit:[NSNull null] with:status.errorInfo]; - } - else if (state == ARTRealtimeChannelFailed) { - [_attachedEventEmitter emit:[NSNull null] with:status.errorInfo]; - [_detachedEventEmitter emit:[NSNull null] with:status.errorInfo]; + switch (state) { + case ARTRealtimeChannelSuspended: + [_attachedEventEmitter emit:[NSNull null] with:status.errorInfo]; + break; + case ARTRealtimeChannelDetached: + [self.presenceMap failsSync:status.errorInfo]; + break; + case ARTRealtimeChannelFailed: + [_attachedEventEmitter emit:[NSNull null] with:status.errorInfo]; + [_detachedEventEmitter emit:[NSNull null] with:status.errorInfo]; + [self.presenceMap failsSync:status.errorInfo]; + break; + default: + break; } [self emit:stateChange.event with:stateChange]; @@ -383,6 +392,7 @@ - (bool)isLastChannelSerial:(NSString *)channelSerial { } - (void)onChannelMessage:(ARTProtocolMessage *)message { + [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p received channel message %tu - %@", _realtime, self, message.action, ARTProtocolMessageActionToStr(message.action)]; switch (message.action) { case ARTProtocolMessageAttached: [self setAttached:message]; @@ -419,6 +429,16 @@ - (void)setAttached:(ARTProtocolMessage *)message { } self.attachSerial = message.channelSerial; + if (message.hasPresence) { + [self.presenceMap startSync]; + [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p PresenceMap Sync started", _realtime, self]; + } + else if ([self.presenceMap.members count] > 0 || [self.presenceMap.localMembers count] > 0) { + // When an ATTACHED message is received without a HAS_PRESENCE flag and PresenceMap has existing members + [self.presenceMap startSync]; + [self.presenceMap endSync]; + } + if (self.state == ARTRealtimeChannelAttached) { if (message.error != nil) { _errorReason = message.error; @@ -428,11 +448,6 @@ - (void)setAttached:(ARTProtocolMessage *)message { return; } - if (message.hasPresence) { - [self.presenceMap startSync]; - [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p PresenceMap Sync started", _realtime, self]; - } - [self sendQueuedMessages]; ARTStatus *status = message.error ? [ARTStatus state:ARTStateError info:message.error] : [ARTStatus state:ARTStateOk]; @@ -524,6 +539,7 @@ - (void)onMessage:(ARTProtocolMessage *)message { } - (void)onPresence:(ARTProtocolMessage *)message { + [self.logger debug:__FILE__ line:__LINE__ message:@"handle PRESENCE message"]; int i = 0; ARTDataEncoder *dataEncoder = self.dataEncoder; for (ARTPresenceMessage *p in message.presence) { @@ -548,20 +564,19 @@ - (void)onPresence:(ARTProtocolMessage *)message { if ([self.presenceMap add:presence]) { [self broadcastPresence:presence]; } - if (!self.presenceMap.syncInProgress) { - [self.presenceMap clean]; - } ++i; } } - (void)onSync:(ARTProtocolMessage *)message { + [self.logger debug:__FILE__ line:__LINE__ message:@"handle SYNC message"]; self.presenceMap.syncMsgSerial = message.msgSerial; self.presenceMap.syncChannelSerial = message.channelSerial; - if (message.action == ARTProtocolMessageSync) - [self.logger info:@"R:%p C:%p ARTRealtime Sync message received", _realtime, self]; + if (!self.presenceMap.syncInProgress) { + [self.presenceMap startSync]; + } for (int i=0; i<[message.presence count]; i++) { ARTPresenceMessage *presence = [message.presence objectAtIndex:i]; @@ -592,11 +607,11 @@ - (void)attach { - (void)attach:(void (^)(ARTErrorInfo *))callback { switch (self.state) { case ARTRealtimeChannelAttaching: - [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p already attaching", _realtime, self]; + [self.realtime.logger verbose:__FILE__ line:__LINE__ message:@"R:%p C:%p already attaching", _realtime, self]; if (callback) [_attachedEventEmitter once:callback]; return; case ARTRealtimeChannelAttached: - [self.realtime.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p already attached", _realtime, self]; + [self.realtime.logger verbose:__FILE__ line:__LINE__ message:@"R:%p C:%p already attached", _realtime, self]; if (callback) callback(nil); return; default: @@ -801,4 +816,28 @@ - (BOOL)history:(ARTRealtimeHistoryQuery *)query callback:(void (^)(__GENERIC(AR } } +#pragma mark - ARTPresenceMapDelegate + +- (NSString *)connectionId { + return _realtime.connection.id; +} + +- (void)map:(ARTPresenceMap *)map didRemovedMemberNoLongerPresent:(ARTPresenceMessage *)presence { + presence.action = ARTPresenceLeave; + presence.id = nil; + presence.timestamp = [NSDate date]; + [self broadcastPresence:presence]; + [self.logger debug:__FILE__ line:__LINE__ message:@"Member \"%@\" no longer present", presence.memberKey]; +} + +- (void)map:(ARTPresenceMap *)map shouldReenterLocalMember:(ARTPresenceMessage *)presence { + [self.presence enterClient:presence.clientId data:presence.data callback:^(ARTErrorInfo *error) { + NSString *message = [NSString stringWithFormat:@"Re-entering member \"%@\" as failed with code %ld (%@)", presence.clientId, (long)error.code, error.message]; + ARTErrorInfo *reenterError = [ARTErrorInfo createWithCode:91004 message:message]; + ARTChannelStateChange *stateChange = [[ARTChannelStateChange alloc] initWithCurrent:self.state previous:self.state event:ARTChannelEventUpdate reason:reenterError resumed:true]; + [self emit:stateChange.event with:stateChange]; + }]; + [self.logger debug:__FILE__ line:__LINE__ message:@"Re-entering local member \"%@\"", presence.memberKey]; +} + @end diff --git a/Source/ARTRealtimePresence.m b/Source/ARTRealtimePresence.m index ed38a335e..91c47c230 100644 --- a/Source/ARTRealtimePresence.m +++ b/Source/ARTRealtimePresence.m @@ -64,8 +64,7 @@ - (void)get:(ARTRealtimePresenceQuery *)query callback:(void (^)(NSArray *members) { callback(members, nil); }]; diff --git a/Source/ARTRest.m b/Source/ARTRest.m index ae375289b..6ad47e3ef 100644 --- a/Source/ARTRest.m +++ b/Source/ARTRest.m @@ -62,7 +62,7 @@ - (instancetype)initWithOptions:(ARTClientOptions *)options { } _http = [[ARTHttp alloc] init]; - [_logger debug:__FILE__ line:__LINE__ message:@"RS:%p %p alloc HTTP", self, _http]; + [_logger verbose:__FILE__ line:__LINE__ message:@"RS:%p %p alloc HTTP", self, _http]; _httpExecutor = _http; _httpExecutor.logger = _logger; @@ -79,7 +79,7 @@ - (instancetype)initWithOptions:(ARTClientOptions *)options { _auth = [[ARTAuth alloc] init:self withOptions:_options]; _channels = [[ARTRestChannels alloc] initWithRest:self]; - [self.logger debug:__FILE__ line:__LINE__ message:@"RS:%p initialized", self]; + [self.logger verbose:__FILE__ line:__LINE__ message:@"RS:%p initialized", self]; } return self; } @@ -93,7 +93,7 @@ - (instancetype)initWithToken:(NSString *)token { } - (void)dealloc { - [self.logger debug:__FILE__ line:__LINE__ message:@"RS:%p dealloc", self]; + [self.logger verbose:__FILE__ line:__LINE__ message:@"RS:%p dealloc", self]; } - (NSString *)description { diff --git a/Source/ARTWebSocketTransport.m b/Source/ARTWebSocketTransport.m index ed2187bb9..dcf4a044e 100644 --- a/Source/ARTWebSocketTransport.m +++ b/Source/ARTWebSocketTransport.m @@ -52,19 +52,19 @@ - (instancetype)initWithRest:(ARTRest *)rest options:(ARTClientOptions *)options _resumeKey = resumeKey; _connectionSerial = connectionSerial; - [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p alloc", _delegate, self]; + [self.logger verbose:__FILE__ line:__LINE__ message:@"R:%p WS:%p alloc", _delegate, self]; } return self; } - (void)dealloc { - [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p dealloc", _delegate, self]; + [self.logger verbose:__FILE__ line:__LINE__ message:@"R:%p WS:%p dealloc", _delegate, self]; self.websocket.delegate = nil; self.websocket = nil; } - (void)send:(ARTProtocolMessage *)msg { - [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p sending action %lu with %@", _delegate, self, (unsigned long)msg.action, msg.messages]; + [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p WS:%p websocket sending action %tu - %@", _delegate, self, msg.action, ARTProtocolMessageActionToStr(msg.action)]; NSData *data = [self.encoder encodeProtocolMessage:msg]; [self sendWithData:data]; } diff --git a/Source/Ably.modulemap b/Source/Ably.modulemap index 02ca5e62a..eefc538b3 100644 --- a/Source/Ably.modulemap +++ b/Source/Ably.modulemap @@ -22,6 +22,7 @@ framework module Ably { header "ARTRestChannel+Private.h" header "ARTPaginatedResult+Private.h" header "ARTPresence+Private.h" + header "ARTPresenceMessage+Private.h" header "ARTProtocolMessage+Private.h" header "ARTTokenParams+Private.h" header "ARTURLSessionServerTrust.h" diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index fab1100c5..3093c18cb 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -856,6 +856,182 @@ class RealtimeClientConnection: QuickSpec { expect(nacks[0].msgSerial).to(equal(6)) expect(nacks[0].count).to(equal(1)) } + + it("should continue incrementing msgSerial serially if the connection resumes successfully") { + let options = AblyTests.commonAppSetup() + options.clientId = "tester" + options.tokenDetails = getTestTokenDetails(key: options.key!, ttl: 5.0, clientId: options.clientId) + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + + let channel = client.channels.get("foo") + waitUntil(timeout: testTimeout) { done in + channel.publish(nil, data: "message") { error in + expect(error).to(beNil()) + done() + } + } + + guard let initialConnectionId = client.connection.id else { + fail("Connection ID is empty"); return + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(4, done: done) + (1...3).forEach { index in + channel.publish(nil, data: "message\(index)") { error in + if error == nil { + partialDone() + } + } + } + channel.presence.enterClient("invalid", data: nil) { error in + expect(error).toNot(beNil()) + partialDone() + } + + } + + expect(client.msgSerial) == 5 + + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Disconnected) { stateChange in + expect(stateChange?.reason).toNot(beNil()) + // Token expired + done() + } + } + + // Reconnected and resumed + expect(client.connection.id).to(equal(initialConnectionId)) + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(4, done: done) + (1...3).forEach { index in + channel.publish(nil, data: "message\(index)") { error in + if error == nil { + partialDone() + } + } + } + channel.presence.enterClient("invalid", data: nil) { error in + expect(error).toNot(beNil()) + partialDone() + } + } + + guard let reconnectedTransport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + let acks = reconnectedTransport.protocolMessagesReceived.filter({ $0.action == .Ack }) + let nacks = reconnectedTransport.protocolMessagesReceived.filter({ $0.action == .Nack }) + + if acks.count != 1 { + fail("Received invalid number of ACK responses: \(acks.count)") + return + } + // Messages covered in a single ACK response + expect(acks[0].msgSerial) == 5 // [0] 1st publish + [1,2,3] publish + [4] enter with invalid client + [5] queued messages + expect(acks[0].count) == 1 + + if nacks.count != 1 { + fail("Received invalid number of NACK responses: \(nacks.count)") + return + } + expect(nacks[0].msgSerial) == 6 + expect(nacks[0].count) == 1 + + expect(client.msgSerial) == 7 + } + + it("should reset msgSerial serially if the connection does not resume") { + let options = AblyTests.commonAppSetup() + options.disconnectedRetryTimeout = 1.0 + options.clientId = "tester" + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + + let channel = client.channels.get("foo") + waitUntil(timeout: testTimeout) { done in + channel.publish(nil, data: "message") { error in + expect(error).to(beNil()) + done() + } + } + + guard let initialConnectionId = client.connection.id else { + fail("Connection ID is empty"); return + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(4, done: done) + (1...3).forEach { index in + channel.publish(nil, data: "message\(index)") { error in + if error == nil { + partialDone() + } + } + } + channel.presence.enterClient("invalid", data: nil) { error in + expect(error).toNot(beNil()) + partialDone() + } + + } + + expect(client.msgSerial) == 5 + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + client.connection.once(.Disconnected) { _ in + partialDone() + } + client.connection.once(.Connected) { _ in + channel.attach() + partialDone() + } + client.simulateLostConnectionAndState() + } + + // Reconnected but not resumed + expect(client.connection.id).toNot(equal(initialConnectionId)) + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(4, done: done) + (1...3).forEach { index in + channel.publish(nil, data: "message\(index)") { error in + if error == nil { + partialDone() + } + } + } + channel.presence.enterClient("invalid", data: nil) { error in + expect(error).toNot(beNil()) + partialDone() + } + } + + guard let reconnectedTransport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + let acks = reconnectedTransport.protocolMessagesReceived.filter({ $0.action == .Ack }) + let nacks = reconnectedTransport.protocolMessagesReceived.filter({ $0.action == .Nack }) + + if acks.count != 1 { + fail("Received invalid number of ACK responses: \(acks.count)") + return + } + // Messages covered in a single ACK response + expect(acks[0].msgSerial) == 0 + expect(acks[0].count) == 1 + + if nacks.count != 1 { + fail("Received invalid number of NACK responses: \(nacks.count)") + return + } + expect(nacks[0].msgSerial) == 1 + expect(nacks[0].count) == 1 + + expect(client.msgSerial) == 2 + } } // RTN7c diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index 7627fd810..d87ca1d7b 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -386,49 +386,113 @@ class RealtimeClientPresence: QuickSpec { } // RTP5 - pending("Channel state change side effects") { + context("Channel state change side effects") { // RTP5a - it("all queued presence messages should fail immediately if the channel enters the FAILED state") { - let client = ARTRealtime(options: AblyTests.commonAppSetup()) - defer { client.dispose(); client.close() } - let channel = client.channels.get("test") + context("if the channel enters the FAILED state") { - waitUntil(timeout: testTimeout) { done in - let protocolError = AblyTests.newErrorProtocolMessage() - channel.presence.enterClient("user", data: nil) { error in - expect(error).to(beIdenticalTo(protocolError.error)) - done() + it("all queued presence messages should fail immediately") { + let client = ARTRealtime(options: AblyTests.commonAppSetup()) + defer { client.dispose(); client.close() } + let channel = client.channels.get("test") + + waitUntil(timeout: testTimeout) { done in + let protocolError = AblyTests.newErrorProtocolMessage() + channel.presence.enterClient("user", data: nil) { error in + expect(error).to(beIdenticalTo(protocolError.error)) + expect(channel.queuedMessages).to(haveCount(0)) + done() + } + expect(channel.queuedMessages).to(haveCount(1)) + channel.onError(protocolError) } - channel.onError(protocolError) } - } - // RTP5a - it("all queued presence messages should fail immediately if the channel enters the DETACHED state") { - let client = ARTRealtime(options: AblyTests.commonAppSetup()) - defer { client.dispose(); client.close() } - let channel = client.channels.get("test") + it("should clear the PresenceMap including local members and does not emit any presence events") { + let client = ARTRealtime(options: AblyTests.commonAppSetup()) + defer { client.dispose(); client.close() } + let channel = client.channels.get("test") + waitUntil(timeout: testTimeout) { done in + channel.presence.enterClient("user", data: nil) { error in + expect(error).to(beNil()) + done() + } + } - waitUntil(timeout: testTimeout) { done in - channel.once(.Attaching) { _ in - channel.detach() + expect(channel.presenceMap.members).to(haveCount(1)) + expect(channel.presenceMap.localMembers).to(haveCount(1)) + + channel.subscribe() { _ in + fail("Shouldn't receive any presence event") } - channel.presence.enterClient("user", data: nil) { error in - expect(error).toNot(beNil()) - done() + defer { channel.off() } + + waitUntil(timeout: testTimeout) { done in + channel.once(.Failed) { _ in + expect(channel.presenceMap.members).to(beEmpty()) + expect(channel.presenceMap.localMembers).to(beEmpty()) + done() + } + channel.onError(AblyTests.newErrorProtocolMessage()) } } + } - } + // RTP5a + context("if the channel enters the DETACHED state") { + it("all queued presence messages should fail immediately") { + let client = ARTRealtime(options: AblyTests.commonAppSetup()) + defer { client.dispose(); client.close() } + let channel = client.channels.get("test") - // RTP5 - context("Channel state change side effects") { + waitUntil(timeout: testTimeout) { done in + channel.once(.Attaching) { _ in + channel.detach() + } + channel.presence.enterClient("user", data: nil) { error in + expect(error).toNot(beNil()) + expect(channel.queuedMessages).to(haveCount(0)) + done() + } + expect(channel.queuedMessages).to(haveCount(1)) + } + } + + it("should clear the PresenceMap including local members and does not emit any presence events") { + let client = ARTRealtime(options: AblyTests.commonAppSetup()) + defer { client.dispose(); client.close() } + let channel = client.channels.get("test") + waitUntil(timeout: testTimeout) { done in + channel.presence.enterClient("user", data: nil) { error in + expect(error).to(beNil()) + done() + } + } + + expect(channel.presenceMap.members).to(haveCount(1)) + expect(channel.presenceMap.localMembers).to(haveCount(1)) + + channel.subscribe() { _ in + fail("Shouldn't receive any presence event") + } + defer { channel.off() } + + waitUntil(timeout: testTimeout) { done in + channel.once(.Detached) { _ in + expect(channel.presenceMap.members).to(beEmpty()) + expect(channel.presenceMap.localMembers).to(beEmpty()) + done() + } + channel.detach() + } + } + + } // RTP5b - it("all queued presence messages will be sent immediately and a presence SYNC will be initiated implicitly if a channel enters the ATTACHED state") { + it("if a channel enters the ATTACHED state then all queued presence messages will be sent immediately and a presence SYNC may be initiated") { let options = AblyTests.commonAppSetup() let client1 = AblyTests.newRealtime(options) defer { client1.dispose(); client1.close() } @@ -475,6 +539,441 @@ class RealtimeClientPresence: QuickSpec { expect(channel2.presence.syncComplete).toEventually(beTrue(), timeout: testTimeout) expect(channel2.presenceMap.members).to(haveCount(2)) } + + // RTP5c + context("when a channel becomes ATTACHED") { + + // RTP5c1 + it("if the resumed flag is true and no SYNC is initiated as part of the attach then do nothing, PresenceMap is not affected and no members need to be re-entered") { + let options = AblyTests.commonAppSetup() + + var clientMembers: ARTRealtime? + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 3, options: options) { + done() + } + } + defer { clientMembers?.dispose(); clientMembers?.close() } + + options.disconnectedRetryTimeout = 1.0 + options.tokenDetails = getTestTokenDetails(key: options.key!, ttl: 5.0) + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + var originalMembers: [ARTPresenceMessage]? + waitUntil(timeout: testTimeout) { done in + channel.presence.get { members, error in + expect(error).to(beNil()) + expect(members).to(haveCount(3)) + originalMembers = members + done() + } + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(3, done: done) + client.connection.once(.Disconnected) { stateChange in + // Token expired + partialDone() + } + client.connection.once(.Connected) { stateChange in + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + // TODO: use sandbox to reproduce this + let attached = ARTProtocolMessage() + attached.action = .Attached + attached.channel = channel.name + attached.flags = Int64(ARTProtocolMessageFlag.Resumed.rawValue) + client.transport?.receive(attached) + partialDone() + } + channel.once(.Update) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStageChange is nil"); done(); return + } + // No loss of continuity + expect(stateChange.resumed).to(beTrue()) + expect(stateChange.reason).to(beNil()) + expect(stateChange.current).to(equal(ARTRealtimeChannelState.Attached)) + expect(stateChange.previous).to(equal(ARTRealtimeChannelState.Attached)) + partialDone() + } + } + + waitUntil(timeout: testTimeout) { done in + channel.presence.get { members, error in + expect(error).to(beNil()) + expect(members).to(equal(originalMembers)) + done() + } + } + } + + // RTP5c2 + context("all members not present in the PresenceMap but present in the internal PresenceMap must be re-entered automatically") { + + it("when SYNC is initiated as part of the attach and the SYNC is complete") { + let options = AblyTests.commonAppSetup() + + var clientMembers: ARTRealtime? + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 3, options: options) { + done() + } + } + defer { clientMembers?.dispose(); clientMembers?.close() } + + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + } + + guard let connectionId = client.connection.id else { + fail("Should have a connection ID"); return + } + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(4, done: done) + let localMember = ARTPresenceMessage(clientId: "local1", action: .Enter, connectionId: connectionId, id: "\(connectionId):1:1", timestamp: NSDate()) + + channel.once(.Attaching) { stateChange in + // Local member + channel.presenceMap.add(localMember) + partialDone() + } + channel.attach() { error in + expect(error).to(beNil()) + partialDone() + } + + transport.beforeProcessingReceivedMessage = { protocolMessage in + if protocolMessage.action == .Attached { + // Expect a Sync + expect(protocolMessage.hasPresence).to(beTrue()) + expect(protocolMessage.resumed).to(beFalse()) + } + else if protocolMessage.action == .Sync { + transport.beforeProcessingReceivedMessage = nil + partialDone() + } + } + + // Before the sync ends + channel.presenceMap.testSuite_injectIntoMethodBefore(#selector(ARTPresenceMap.endSync)) { + expect(channel.presenceMap.members).to(haveCount(4)) + expect(channel.presenceMap.localMembers).to(haveCount(1)) + + transport.beforeProcessingSentMessage = { protocolMessage in + if protocolMessage.action == .Presence && protocolMessage.presence?.first?.action == .Enter { + expect(channel.presenceMap.localMembers).to(beEmpty()) + transport.beforeProcessingSentMessage = nil + } + } + // Re-entered automatically + channel.presence.subscribe(.Enter) { enter in + // The members re-entered automatically must be removed from the internal PresenceMap, + //so it must be a different object + expect(enter).toNot(beIdenticalTo(localMember)) + expect(enter.clientId) == localMember.clientId + expect(enter.connectionId) == localMember.connectionId + partialDone() + } + } + } + + waitUntil(timeout: testTimeout) { done in + channel.presence.get { members, error in + expect(error).to(beNil()) + expect(members).to(haveCount(4)) + done() + } + } + } + + it("when resumed flag is false and a SYNC is not expected") { + let options = AblyTests.commonAppSetup() + + var clientMembers: ARTRealtime? + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 3, options: options) { + done() + } + } + defer { clientMembers?.dispose(); clientMembers?.close() } + + options.clientId = "local1" + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.presence.get { members, error in + expect(error).to(beNil()) + expect(members).to(haveCount(3)) + partialDone() + } + channel.presence.enter(nil) { error in + expect(error).to(beNil()) + partialDone() + } + } + + expect(channel.presenceMap.members).to(haveCount(4)) + expect(channel.presenceMap.localMembers).to(haveCount(1)) + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(3, done: done) + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + transport.beforeProcessingReceivedMessage = { protocolMessage in + if protocolMessage.action == .Attached { + expect(protocolMessage.hasPresence).to(beFalse()) + expect(protocolMessage.resumed).to(beFalse()) + transport.beforeProcessingReceivedMessage = nil + partialDone() + } + } + transport.beforeProcessingSentMessage = { protocolMessage in + if protocolMessage.action == .Presence && protocolMessage.presence?.first?.action == .Enter { + expect(protocolMessage.presence?.first?.clientId).to(equal("local1")) + expect(channel.presenceMap.localMembers).to(beEmpty()) + transport.beforeProcessingSentMessage = nil + partialDone() + } + } + + channel.presence.subscribe(.Leave) { leave in + // Members will leave the PresenceMap due to the ATTACHED without Presence + expect(leave.clientId).to(satisfyAnyOf(equal("local1"), equal("user1"), equal("user2"), equal("user3"))) + } + + // Re-entered automatically + channel.presence.subscribe(.Update) { update in + expect(update.clientId) == "local1" + partialDone() + } + + channel.presence.subscribe(.Enter) { enter in + fail("Members already being present so the client should receive UPDATE events"); done(); return + } + + // Inject ATTACHED message + let attached = ARTProtocolMessage() + attached.action = .Attached + attached.channel = channel.name + attached.flags = 0 //no presence, no resume + transport.receive(attached) + } + + channel.presence.unsubscribe() + waitUntil(timeout: testTimeout) { done in + channel.presence.get { members, error in + expect(error).to(beNil()) + expect(members).to(haveCount(4)) + done() + } + } + } + } + + // RTP5c3 + it("if any of the automatic ENTER presence messages published fail then an UPDATE event should be emitted on the channel") { + let options = AblyTests.commonAppSetup() + + var clientMembers: ARTRealtime? + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 3, options: options) { + done() + } + } + defer { clientMembers?.dispose(); clientMembers?.close() } + + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + } + + guard let connectionId = client.connection.id else { + fail("Should have a connection ID"); return + } + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(3, done: done) + let localMember = ARTPresenceMessage(clientId: "local1", action: .Enter, connectionId: connectionId, id: "\(connectionId):1:1", timestamp: NSDate()) + + channel.once(.Attaching) { stateChange in + // Local member + channel.presenceMap.add(localMember) + partialDone() + } + channel.attach() + + // Before the sync ends + channel.presenceMap.testSuite_injectIntoMethodBefore(#selector(ARTPresenceMap.endSync)) { + expect(channel.presenceMap.members).to(haveCount(4)) + expect(channel.presenceMap.localMembers).to(haveCount(1)) + + // Time out + let reEnterError = ARTErrorInfo.createWithCode(50003, message: "timed out") + transport.replaceAcksWithNacks(reEnterError) { _ in } + + // Re-entered automatically should fail + channel.presence.subscribe(.Enter) { enter in + fail("Should not Enter the local member") + } + partialDone() + } + + channel.once(.Update) { stateChange in + guard let stateChange = stateChange else { + fail("ChannelStateChange is nil"); partialDone(); return + } + guard let reason = stateChange.reason else { + fail("Reason from ChannelStateChange is nil"); partialDone(); return + } + expect(reason.code) == 91004 + expect(reason.message).to(contain(localMember.clientId!)) + expect(reason.message).to(contain("timed out")) + expect(stateChange.resumed).to(beTrue()) + partialDone() + } + } + + waitUntil(timeout: testTimeout) { done in + channel.presence.get { members, error in + expect(error).to(beNil()) + expect(members).to(haveCount(3)) + done() + } + } + } + + } + + // RTP5f + context("channel enters the SUSPENDED state") { + + it("all queued presence messages should fail immediately") { + let options = AblyTests.commonAppSetup() + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(3, done: done) + channel.once(.Attaching) { stateChange in + expect(stateChange?.reason).to(beNil()) + expect(channel.queuedMessages.count) == 1 + channel.setSuspended(ARTStatus.state(.Error, info: ARTErrorInfo.createWithCode(1234, message: "unknown error"))) + partialDone() + } + channel.once(.Suspended) { stateChange in + // All queued presence messages will fail immediately + expect(channel.queuedMessages.count) == 0 + partialDone() + } + channel.presence.enterClient("tester", data: nil) { error in + guard let error = error else { + fail("Error is nil"); partialDone(); return + } + expect(error.code) == 1234 + expect(error.message).to(contain("unknown error")) + partialDone() + } + } + } + + it("should maintain the PresenceMap and any members present before and after the sync should not emit presence events") { + let options = AblyTests.commonAppSetup() + + var clientMembers: ARTRealtime? + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 3, options: options) { + done() + } + } + defer { clientMembers?.dispose(); clientMembers?.close() } + + options.clientId = "tester" + options.tokenDetails = getTestTokenDetails(key: options.key!, ttl: 5.0, clientId: options.clientId) + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.presence.enter(nil) { error in + expect(error).to(beNil()) + partialDone() + } + channel.presence.get { members, error in + expect(error).to(beNil()) + expect(members).to(haveCount(3)) + partialDone() + } + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(4, done: done) + channel.presence.subscribe { presence in + expect(presence.action).to(equal(ARTPresenceAction.Leave)) + expect(presence.clientId).to(equal("tester")) + partialDone() + } + channel.once(.Suspended) { stateChange in + expect(channel.presenceMap.members).to(haveCount(4)) + expect(channel.presenceMap.localMembers).to(haveCount(1)) + partialDone() + } + channel.once(.Attaching) { stateChange in + expect(stateChange?.reason).to(beNil()) + channel.presence.leave(nil) { error in + expect(error).to(beNil()) + partialDone() + } + expect(channel.queuedMessages.count) == 1 + } + channel.once(.Attached) { stateChange in + expect(stateChange?.reason).to(beNil()) + partialDone() + } + channel.setSuspended(ARTStatus.state(.Ok)) + } + + channel.presence.unsubscribe() + waitUntil(timeout: testTimeout) { done in + channel.presence.get { members, error in + expect(error).to(beNil()) + expect(members).to(haveCount(3)) + expect(channel.presenceMap.members).to(haveCount(3)) + expect(channel.presenceMap.localMembers).to(beEmpty()) + done() + } + } + } + + } + } // RTP8 diff --git a/Spec/RestClient.swift b/Spec/RestClient.swift index 2040e14b7..b4d867b8a 100644 --- a/Spec/RestClient.swift +++ b/Spec/RestClient.swift @@ -234,17 +234,17 @@ class RestClient: QuickSpec { let options = ARTClientOptions(key: "xxxx:xxxx") options.restHost = "10.255.255.1" //non-routable IP address expect(options.httpRequestTimeout).to(equal(15.0)) //Seconds - options.httpRequestTimeout = 0.5 + options.httpRequestTimeout = 1.0 let client = ARTRest(options: options) let channel = client.channels.get("test") waitUntil(timeout: testTimeout) { done in let start = NSDate() channel.publish(nil, data: "message") { error in let end = NSDate() - expect(end.timeIntervalSinceDate(start)).to(beCloseTo(options.httpRequestTimeout, within: 0.1)) + expect(end.timeIntervalSinceDate(start)).to(beCloseTo(options.httpRequestTimeout, within: 0.5)) expect(error).toNot(beNil()) if let error = error { - expect(error.code).to(equal(-1001)) + expect(error.code).to(satisfyAnyOf(equal(-1001 /*Timed Out*/), equal(-1004 /*Cannot Connect To Host*/))) } done() } @@ -278,7 +278,7 @@ class RestClient: QuickSpec { it("max elapsed time in which fallback host retries for HTTP requests will be attempted") { let options = ARTClientOptions(key: "xxxx:xxxx") expect(options.httpMaxRetryDuration).to(equal(10.0)) //Seconds - options.httpMaxRetryDuration = 0.2 + options.httpMaxRetryDuration = 1.0 let client = ARTRest(options: options) client.httpExecutor = testHTTPExecutor testHTTPExecutor.http = MockHTTP(network: .RequestTimeout(timeout: 0.1)) @@ -287,7 +287,7 @@ class RestClient: QuickSpec { let start = NSDate() channel.publish(nil, data: "nil") { _ in let end = NSDate() - expect(end.timeIntervalSinceDate(start)).to(beCloseTo(options.httpMaxRetryDuration, within: 0.5)) + expect(end.timeIntervalSinceDate(start)).to(beCloseTo(options.httpMaxRetryDuration, within: 0.9)) done() } } diff --git a/Spec/TestUtilities.swift b/Spec/TestUtilities.swift index 71ead971d..55c7f7e51 100644 --- a/Spec/TestUtilities.swift +++ b/Spec/TestUtilities.swift @@ -786,7 +786,7 @@ class TestProxyTransport: ARTWebSocketTransport { } override func receive(msg: ARTProtocolMessage) { - if msg.action == .Ack { + if msg.action == .Ack || msg.action == .Presence { if let error = replacingAcksWithNacks { msg.action = .Nack msg.error = error @@ -810,9 +810,9 @@ class TestProxyTransport: ARTWebSocketTransport { super.receiveWithData(data) } - func replaceAcksWithNacks(error: ARTErrorInfo, block: (() -> ()) -> ()) { + func replaceAcksWithNacks(error: ARTErrorInfo, block: (doneReplacing: () -> Void) -> Void) { replacingAcksWithNacks = error - block({ self.replacingAcksWithNacks = nil }) + block(doneReplacing: { self.replacingAcksWithNacks = nil }) } func simulateTransportSuccess() { From 16b8f20ad4c80a29041aad5e8c0654ae2c7f1e3d Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Tue, 7 Feb 2017 12:23:33 +0000 Subject: [PATCH 39/43] RTP19 (#568) * RTP19 * RTP19a * Fix: if channel resumed successfully then do not start sync * Enhance RTP5: presence get should not have Absent members --- Source/ARTPresenceMap.m | 1 + Source/ARTRealtimeChannel.m | 8 +- Spec/RealtimeClientPresence.swift | 145 +++++++++++++++++++++++++++++- 3 files changed, 148 insertions(+), 6 deletions(-) diff --git a/Source/ARTPresenceMap.m b/Source/ARTPresenceMap.m index 97e0472ea..653316a9a 100644 --- a/Source/ARTPresenceMap.m +++ b/Source/ARTPresenceMap.m @@ -173,6 +173,7 @@ - (void)reenterLocalMembersMissingFromSync { [self internalRemove:localMember]; [self.delegate map:self shouldReenterLocalMember:reenter]; } + [self cleanUpAbsentMembers]; } - (void)reset { diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index a5de1019b..4540fdc3d 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -434,9 +434,11 @@ - (void)setAttached:(ARTProtocolMessage *)message { [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p PresenceMap Sync started", _realtime, self]; } else if ([self.presenceMap.members count] > 0 || [self.presenceMap.localMembers count] > 0) { - // When an ATTACHED message is received without a HAS_PRESENCE flag and PresenceMap has existing members - [self.presenceMap startSync]; - [self.presenceMap endSync]; + if (!message.resumed) { + // When an ATTACHED message is received without a HAS_PRESENCE flag and PresenceMap has existing members + [self.presenceMap startSync]; + [self.presenceMap endSync]; + } } if (self.state == ARTRealtimeChannelAttached) { diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index d87ca1d7b..0b7b6c7ee 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -282,6 +282,122 @@ class RealtimeClientPresence: QuickSpec { } + // RTP19 + context("PresenceMap has existing members when a SYNC is started") { + + it("should ensure that members no longer present on the channel are removed from the local PresenceMap once the sync is complete") { + let options = AblyTests.commonAppSetup() + + var clientMembers: ARTRealtime? + defer { clientMembers?.dispose(); clientMembers?.close() } + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 2, options: options) { + done() + } + } + + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + waitUntil(timeout: testTimeout) { done in + channel.presence.get { members, error in + expect(error).to(beNil()) + expect(members).to(haveCount(2)) //synced + done() + } + } + + expect(channel.presenceMap.members).to(haveCount(2)) + // Inject a local member + let localMember = ARTPresenceMessage(clientId: NSUUID().UUIDString, action: .Enter, connectionId: "another", id: "another:0:0") + channel.presenceMap.add(localMember) + expect(channel.presenceMap.members).to(haveCount(3)) + expect(channel.presenceMap.members.filter{ clientId, _ in clientId == localMember.clientId }).to(haveCount(1)) + + waitUntil(timeout: testTimeout) { done in + channel.presence.get { members, error in + expect(error).to(beNil()) + guard let members = members where members.count == 3 else { + fail("Should at least have 3 members"); done(); return + } + expect(members.filter{ $0.clientId == localMember.clientId }).to(haveCount(1)) + done() + } + } + + waitUntil(timeout: testTimeout) { done in + channel.presence.subscribe(.Leave) { leave in + expect(channel.presence.syncComplete).to(beFalse()) + expect(leave.clientId).to(equal(localMember.clientId)) + done() + } + + // Request a sync + let syncMessage = ARTProtocolMessage() + syncMessage.action = .Sync + syncMessage.channel = channel.name + client.transport?.send(syncMessage) + } + + waitUntil(timeout: testTimeout) { done in + channel.presence.get { members, error in + expect(error).to(beNil()) + guard let members = members where members.count == 2 else { + fail("Should at least have 2 members"); done(); return + } + expect(members.filter{ $0.clientId == localMember.clientId }).to(beEmpty()) + done() + } + } + } + + // RTP19a + it("should emit a LEAVE event for each existing member if the PresenceMap has existing members when an ATTACHED message is received without a HAS_PRESENCE flag") { + let options = AblyTests.commonAppSetup() + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") + + // Inject local members + channel.presenceMap.add(ARTPresenceMessage(clientId: "tester1", action: .Enter, connectionId: "another", id: "another:0:0")) + channel.presenceMap.add(ARTPresenceMessage(clientId: "tester2", action: .Enter, connectionId: "another", id: "another:0:1")) + + guard let transport = client.transport as? TestProxyTransport else { + fail("TestProxyTransport is not set"); return + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(4, done: done) + transport.afterProcessingReceivedMessage = { protocolMessage in + if protocolMessage.action == .Attached { + expect(protocolMessage.hasPresence).to(beFalse()) + partialDone() + } + } + channel.presence.subscribe(.Leave) { leave in + expect(leave.clientId?.hasPrefix("tester")).to(beTrue()) + expect(leave.action).to(equal(ARTPresenceAction.Leave)) + expect(leave.timestamp).to(beCloseTo(NSDate(), within: 0.5)) + expect(leave.id).to(beNil()) + partialDone() //2 times + } + channel.attach { error in + expect(error).to(beNil()) + partialDone() + } + } + + waitUntil(timeout: testTimeout) { done in + channel.presence.get { members, error in + expect(error).to(beNil()) + expect(members).to(beEmpty()) + done() + } + } + } + + } + // RTP4 it("should receive all 250 members") { let options = AblyTests.commonAppSetup() @@ -605,7 +721,11 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in channel.presence.get { members, error in expect(error).to(beNil()) + guard let members = members else { + fail("Members is nil"); done(); return + } expect(members).to(equal(originalMembers)) + expect(members).to(allPass({ (member: ARTPresenceMessage?) in member!.action != .Absent })) done() } } @@ -694,7 +814,11 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in channel.presence.get { members, error in expect(error).to(beNil()) + guard let members = members else { + fail("Members is nil"); done(); return + } expect(members).to(haveCount(4)) + expect(members).to(allPass({ (member: ARTPresenceMessage?) in member!.action != .Absent })) done() } } @@ -748,6 +872,7 @@ class RealtimeClientPresence: QuickSpec { } transport.beforeProcessingSentMessage = { protocolMessage in if protocolMessage.action == .Presence && protocolMessage.presence?.first?.action == .Enter { + // Re-enter expect(protocolMessage.presence?.first?.clientId).to(equal("local1")) expect(channel.presenceMap.localMembers).to(beEmpty()) transport.beforeProcessingSentMessage = nil @@ -782,7 +907,11 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in channel.presence.get { members, error in expect(error).to(beNil()) - expect(members).to(haveCount(4)) + guard let members = members else { + fail("Members is nil"); done(); return + } + expect(members).to(haveCount(1)) + expect(members.first?.clientId).to(equal("local1")) done() } } @@ -863,7 +992,11 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in channel.presence.get { members, error in expect(error).to(beNil()) + guard let members = members else { + fail("Members is nil"); done(); return + } expect(members).to(haveCount(3)) + expect(members).to(allPass({ (member: ARTPresenceMessage?) in member!.action != .Absent })) done() } } @@ -964,7 +1097,11 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in channel.presence.get { members, error in expect(error).to(beNil()) + guard let members = members else { + fail("Members is nil"); done(); return + } expect(members).to(haveCount(3)) + expect(members).to(allPass({ (member: ARTPresenceMessage?) in member!.action != .Absent })) expect(channel.presenceMap.members).to(haveCount(3)) expect(channel.presenceMap.localMembers).to(beEmpty()) done() @@ -1628,7 +1765,7 @@ class RealtimeClientPresence: QuickSpec { var clientMembers: ARTRealtime? defer { clientMembers?.dispose(); clientMembers?.close() } waitUntil(timeout: testTimeout) { done in - clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 100, options: options) { + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 101, options: options) { done() } } @@ -1692,6 +1829,7 @@ class RealtimeClientPresence: QuickSpec { fail("Members is nil"); done(); return } expect(members).to(haveCount(102)) //100 initial members + "b" + "c", client "a" is discarded + expect(members).to(allPass({ (member: ARTPresenceMessage?) in member!.action != .Absent })) expect(members.filter{ $0.clientId == "a" }).to(beEmpty()) expect(members.filter{ $0.clientId == "b" }).to(haveCount(1)) expect(members.filter{ $0.clientId == "b" }.first?.timestamp).to(equal(now)) @@ -1711,7 +1849,7 @@ class RealtimeClientPresence: QuickSpec { var clientMembers: ARTRealtime? defer { clientMembers?.dispose(); clientMembers?.close() } waitUntil(timeout: testTimeout) { done in - clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 100, options: options) { + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 101, options: options) { done() } } @@ -1775,6 +1913,7 @@ class RealtimeClientPresence: QuickSpec { fail("Members is nil"); done(); return } expect(members).to(haveCount(102)) //100 initial members + "b" + "c", client "a" is discarded + expect(members).to(allPass({ (member: ARTPresenceMessage?) in member!.action != .Absent })) expect(members.filter{ $0.clientId == "a" }).to(beEmpty()) expect(members.filter{ $0.clientId == "b" }).to(haveCount(1)) expect(members.filter{ $0.clientId == "b" }.first?.timestamp).to(equal(now)) From ab02ee0d44cb9d5182e4a1dc26a2766aeb79726f Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 22 Feb 2017 11:08:22 +0000 Subject: [PATCH 40/43] Fix race condition --- Spec/RealtimeClient.swift | 4 +++- Spec/RestClient.swift | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index 4dc712b24..02e87b1ab 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -1215,9 +1215,11 @@ class RealtimeClient: QuickSpec { // https://github.com/ably/ably-ios/issues/577 it("background behaviour") { let options = AblyTests.commonAppSetup() + options.autoConnect = false + let realtime = ARTRealtime(options: options) waitUntil(timeout: testTimeout) { done in NSURLSession.sharedSession().dataTaskWithURL(NSURL(string:"https://ably.io")!) { _ in - let realtime = ARTRealtime(options: options) + realtime.connect() realtime.channels.get("foo").attach { error in expect(error).to(beNil()) done() diff --git a/Spec/RestClient.swift b/Spec/RestClient.swift index 9a5a38d3c..26ab43c6c 100644 --- a/Spec/RestClient.swift +++ b/Spec/RestClient.swift @@ -1312,9 +1312,9 @@ class RestClient: QuickSpec { // https://github.com/ably/ably-ios/issues/577 it("background behaviour") { let options = AblyTests.commonAppSetup() + let rest = ARTRest(options: options) waitUntil(timeout: testTimeout) { done in NSURLSession.sharedSession().dataTaskWithURL(NSURL(string:"https://ably.io")!) { _ in - let rest = ARTRest(options: options) rest.channels.get("foo").history { _ in done() } From 9722811477dedeee39f08cf4434c0e5af6ab4ffb Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Wed, 22 Feb 2017 21:14:31 +0000 Subject: [PATCH 41/43] Fix #583: update httpRequestTimeout and httpMaxRetryDuration --- Source/ARTClientOptions.m | 4 ++-- Spec/RestClient.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/ARTClientOptions.m b/Source/ARTClientOptions.m index 1ff1f0ace..b1cf75246 100644 --- a/Source/ARTClientOptions.m +++ b/Source/ARTClientOptions.m @@ -39,8 +39,8 @@ - (instancetype)initDefaults { _suspendedRetryTimeout = 30.0; //Seconds _channelRetryTimeout = 15.0; //Seconds _httpOpenTimeout = 4.0; //Seconds - _httpRequestTimeout = 15.0; //Seconds - _httpMaxRetryDuration = 10.0; //Seconds + _httpRequestTimeout = 10.0; //Seconds + _httpMaxRetryDuration = 15.0; //Seconds _httpMaxRetryCount = 3; _fallbackHosts = nil; _fallbackHostsUseDefault = false; diff --git a/Spec/RestClient.swift b/Spec/RestClient.swift index 26ab43c6c..e257c03ca 100644 --- a/Spec/RestClient.swift +++ b/Spec/RestClient.swift @@ -233,7 +233,7 @@ class RestClient: QuickSpec { it("timeout for any single HTTP request and response") { let options = ARTClientOptions(key: "xxxx:xxxx") options.restHost = "10.255.255.1" //non-routable IP address - expect(options.httpRequestTimeout).to(equal(15.0)) //Seconds + expect(options.httpRequestTimeout).to(equal(10.0)) //Seconds options.httpRequestTimeout = 1.0 let client = ARTRest(options: options) let channel = client.channels.get("test") @@ -277,7 +277,7 @@ class RestClient: QuickSpec { it("max elapsed time in which fallback host retries for HTTP requests will be attempted") { let options = ARTClientOptions(key: "xxxx:xxxx") - expect(options.httpMaxRetryDuration).to(equal(10.0)) //Seconds + expect(options.httpMaxRetryDuration).to(equal(15.0)) //Seconds options.httpMaxRetryDuration = 1.0 let client = ARTRest(options: options) client.httpExecutor = testHTTPExecutor From d51283556efa5747b10d1c3eae9ed03dd15fa269 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Fri, 24 Feb 2017 09:17:08 +0000 Subject: [PATCH 42/43] EventEmitter: use @synchronized because NSMutableArray are not thread safe --- Source/ARTEventEmitter+Private.h | 4 ++-- Source/ARTEventEmitter.m | 20 +++++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Source/ARTEventEmitter+Private.h b/Source/ARTEventEmitter+Private.h index 65a187fc7..cacc783c6 100644 --- a/Source/ARTEventEmitter+Private.h +++ b/Source/ARTEventEmitter+Private.h @@ -22,8 +22,8 @@ ART_ASSUME_NONNULL_BEGIN @interface __GENERIC(ARTEventEmitter, EventType, ItemType) () -@property (readwrite, nonatomic) __GENERIC(NSMutableDictionary, EventType, __GENERIC(NSMutableArray, __GENERIC(ARTEventEmitterEntry, ItemType) *) *) *listeners; -@property (readwrite, nonatomic) __GENERIC(NSMutableArray, __GENERIC(ARTEventEmitterEntry, ItemType) *) *anyListeners; +@property (readwrite, atomic) __GENERIC(NSMutableDictionary, EventType, __GENERIC(NSMutableArray, __GENERIC(ARTEventEmitterEntry, ItemType) *) *) *listeners; +@property (readwrite, atomic) __GENERIC(NSMutableArray, __GENERIC(ARTEventEmitterEntry, ItemType) *) *anyListeners; @end diff --git a/Source/ARTEventEmitter.m b/Source/ARTEventEmitter.m index ef96e5471..96a3e6ac6 100644 --- a/Source/ARTEventEmitter.m +++ b/Source/ARTEventEmitter.m @@ -204,7 +204,9 @@ - (void)emit:(id)event with:(id)data { [entry.listener off]; } for (ARTEventEmitterEntry *entry in toRemoveFromTotalListeners) { - [self.anyListeners removeObject:entry]; + @synchronized(self.anyListeners) { + [self.anyListeners removeObject:entry]; + } [entry.listener off]; } for (ARTEventEmitterEntry *entry in toCall) { @@ -227,9 +229,13 @@ - (void)removeObject:(id)obj fromArrayWithKey:(id)key inDictionary:(NSMutableDic if (array == nil) { return; } - [array removeObject:obj]; + @synchronized(array) { + [array removeObject:obj]; + } if ([array count] == 0) { - [dict removeObjectForKey:key]; + @synchronized(dict) { + [dict removeObjectForKey:key]; + } } } @@ -238,9 +244,13 @@ - (void)removeObject:(id)obj fromArrayWithKey:(id)key inDictionary:(NSMutableDic if (array == nil) { return; } - [array artRemoveWhere:cond]; + @synchronized(array) { + [array artRemoveWhere:cond]; + } if ([array count] == 0) { - [dict removeObjectForKey:key]; + @synchronized(dict) { + [dict removeObjectForKey:key]; + } } } From a31ad3a0e8478fb7e0e11abfc7249e1c722491d3 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 23 Mar 2017 14:14:05 +0000 Subject: [PATCH 43/43] Thread safety (#586) * New EventEmitter (using NSNotificationCenter) - In most ways that matter NSNotificationCenter is thread safe. You can add/remove observers from any thread and you can post notifications from any thread. * Events for the EventEmitter * Fix: should cancel timers when connection times out * Fix: new state change can occur before receiving publishing acknowledgement * Test suite: async forced transitions * Test suite: ack order * Test suite: stop when there's no internet * Fix: instance objects released to soon * Performed a static analysis from Xcode * fixup! Test suite: ack order * Memory leak: call session invalidate to dispose of its strong reference to the delegate * fixup! Test suite: ack order * Fix RTN19a: guarantee of a new transport (check transport reference) * Fix: ACK or NACK has not yet been received for a message, the client should consider the delivery of those messages as failed * Enhance RTN14b: better timings * Fix: REST and Realtime, wait for last operation to release the object * fixup! Test suite: ack order * Fix: cancel timers when a connection gets closed * fixup! Test suite: ack order * Test suite: timings * fixup! Enhance RTN14b: better timings * Test suite: close connections * Fix: turn off immediately reachability when close occurs * Fix RTC1d: wait for host is not reachable error * fixup! Test suite: ack order * Travis update * Fix RTN19a --- .travis.yml | 2 +- Examples/Tests/TestsTests/TestsTests.swift | 6 +- Source/ARTAuth+Private.h | 11 +- Source/ARTAuth.m | 32 +- Source/ARTChannels+Private.h | 2 +- Source/ARTConnection+Private.h | 2 +- Source/ARTConnection.h | 7 + Source/ARTConnection.m | 36 ++- Source/ARTConnectionDetails.m | 2 +- Source/ARTCrypto+Private.h | 2 +- Source/ARTDataEncoder.h | 2 +- Source/ARTEventEmitter+Private.h | 20 +- Source/ARTEventEmitter.h | 85 +++-- Source/ARTEventEmitter.m | 341 ++++++++++++--------- Source/ARTFallback.h | 2 +- Source/ARTFallback.m | 4 +- Source/ARTGCD.h | 4 +- Source/ARTGCD.m | 14 +- Source/ARTHttp.h | 6 +- Source/ARTHttp.m | 6 +- Source/ARTJsonLikeEncoder.m | 3 + Source/ARTOSReachability.m | 5 +- Source/ARTPresenceMap.m | 50 ++- Source/ARTPresenceMessage.h | 8 + Source/ARTPresenceMessage.m | 14 + Source/ARTQueuedMessage.m | 4 + Source/ARTRealtime+Private.h | 8 +- Source/ARTRealtime.m | 211 +++++++------ Source/ARTRealtimeChannel+Private.h | 7 +- Source/ARTRealtimeChannel.h | 19 +- Source/ARTRealtimeChannel.m | 154 ++++++---- Source/ARTRealtimePresence.h | 12 +- Source/ARTRealtimePresence.m | 16 +- Source/ARTRealtimeTransport.h | 4 +- Source/ARTRest+Private.h | 4 +- Source/ARTRestPresence.h | 4 +- Source/ARTStats.h | 20 +- Source/ARTStatus.h | 2 +- Source/ARTStatus.m | 4 +- Source/ARTTokenParams.h | 6 +- Source/ARTTypes.h | 4 + Source/ARTTypes.m | 10 + Source/ARTURLSessionServerTrust.h | 2 + Source/ARTURLSessionServerTrust.m | 4 + Source/ARTWebSocketTransport.h | 1 - Source/ARTWebSocketTransport.m | 6 + Spec/RealtimeClient.swift | 49 +-- Spec/RealtimeClientChannel.swift | 46 ++- Spec/RealtimeClientConnection.swift | 304 ++++++++++-------- Spec/RealtimeClientPresence.swift | 4 +- Spec/RestClient.swift | 2 +- Spec/TestUtilities.swift | 6 +- Spec/Utilities.swift | 27 +- Tests/ARTRealtimeMessageTest.m | 7 +- Tests/ARTRealtimePresenceTest.m | 38 ++- Tests/ARTRealtimeRecoverTest.m | 3 +- 56 files changed, 1012 insertions(+), 642 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3df771a21..963bcf974 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,5 +15,5 @@ script: # Use `travis_wait` when a long running command or compile step regularly takes longer than 10 minutes without producing any output. # It writes a short line to the build log every minute for 20 minutes, extending the amount of time your command has to finish. # Prefix `travis_wait` with a greater number to extend the wait time. - - travis_wait 30 scan --scheme "Ably" --open_report false + - scan --scheme "Ably" --open_report false --devices "iPhone 6s" - bash ./Scripts/run_examples_tests.sh diff --git a/Examples/Tests/TestsTests/TestsTests.swift b/Examples/Tests/TestsTests/TestsTests.swift index e6cff7f7f..8a7f405f3 100644 --- a/Examples/Tests/TestsTests/TestsTests.swift +++ b/Examples/Tests/TestsTests/TestsTests.swift @@ -64,8 +64,9 @@ class TestsTests: XCTestCase { self.waitForExpectationsWithTimeout(10, handler: nil) let backgroundRealtimeExpectation = self.expectationWithDescription("Realtime in a Background Queue") + var realtime: ARTRealtime! //strong reference NSURLSession.sharedSession().dataTaskWithURL(NSURL(string:"https://ably.io")!) { _ in - let realtime = ARTRealtime(key: key as String) + realtime = ARTRealtime(key: key as String) realtime.channels.get("foo").attach { _ in defer { backgroundRealtimeExpectation.fulfill() } } @@ -73,8 +74,9 @@ class TestsTests: XCTestCase { self.waitForExpectationsWithTimeout(10, handler: nil) let backgroundRestExpectation = self.expectationWithDescription("Rest in a Background Queue") + var rest: ARTRest! //strong reference NSURLSession.sharedSession().dataTaskWithURL(NSURL(string:"https://ably.io")!) { _ in - let rest = ARTRest(key: key as String) + rest = ARTRest(key: key as String) rest.channels.get("foo").history { _ in defer { backgroundRestExpectation.fulfill() } } diff --git a/Source/ARTAuth+Private.h b/Source/ARTAuth+Private.h index d94470ef5..6c7645b1c 100644 --- a/Source/ARTAuth+Private.h +++ b/Source/ARTAuth+Private.h @@ -18,7 +18,7 @@ ART_ASSUME_NONNULL_BEGIN /// Messages related to the ARTAuth @protocol ARTAuthDelegate -@property (nonatomic, readonly) __GENERIC(ARTEventEmitter, NSNumber * /*ARTAuthorizationState*/, id) *authorizationEmitter; +@property (nonatomic, readonly) ARTEventEmitter *authorizationEmitter; - (void)auth:(ARTAuth *)auth didAuthorize:(ARTTokenDetails *)tokenDetails; @end @@ -42,7 +42,7 @@ ART_ASSUME_NONNULL_BEGIN - (ARTTokenParams *)mergeParams:(ARTTokenParams *)customParams; - (NSURL *)buildURL:(ARTAuthOptions *)options withParams:(ARTTokenParams *)params; -- (NSMutableURLRequest *)buildRequest:(ARTAuthOptions *)options withParams:(ARTTokenParams *)params; +- (NSMutableURLRequest *)buildRequest:(nullable ARTAuthOptions *)options withParams:(nullable ARTTokenParams *)params; // Execute the received ARTTokenRequest - (void)executeTokenRequest:(ARTTokenRequest *)tokenRequest callback:(void (^)(ARTTokenDetails *__art_nullable tokenDetails, NSError *__art_nullable error))callback; @@ -70,4 +70,11 @@ ART_ASSUME_NONNULL_BEGIN @end +#pragma mark - ARTEvent + +@interface ARTEvent (AuthorizationState) +- (instancetype)initWithAuthorizationState:(ARTAuthorizationState)value; ++ (instancetype)newWithAuthorizationState:(ARTAuthorizationState)value; +@end + ART_ASSUME_NONNULL_END diff --git a/Source/ARTAuth.m b/Source/ARTAuth.m index 04edfcb03..916faad2f 100644 --- a/Source/ARTAuth.m +++ b/Source/ARTAuth.m @@ -377,11 +377,11 @@ - (void)authorize:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOp if (lastDelegate) { // Only the last request should remain [lastDelegate.authorizationEmitter off]; - [lastDelegate.authorizationEmitter once:[NSNumber numberWithInt:ARTAuthorizationSucceeded] callback:^(id null) { + [lastDelegate.authorizationEmitter once:[ARTEvent newWithAuthorizationState:ARTAuthorizationSucceeded] callback:^(id null) { successBlock(_tokenDetails); [lastDelegate.authorizationEmitter off]; }]; - [lastDelegate.authorizationEmitter once:[NSNumber numberWithInt:ARTAuthorizationFailed] callback:^(NSError *error) { + [lastDelegate.authorizationEmitter once:[ARTEvent newWithAuthorizationState:ARTAuthorizationFailed] callback:^(NSError *error) { failureBlock(error); [lastDelegate.authorizationEmitter off]; }]; @@ -402,7 +402,10 @@ - (void)authorize:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOp _tokenDetails = tokenDetails; _method = ARTAuthMethodToken; - if (lastDelegate) { + if (!tokenDetails) { + failureBlock([ARTErrorInfo createWithCode:0 message:@"Token details are empty"]); + } + else if (lastDelegate) { [lastDelegate auth:self didAuthorize:tokenDetails]; } else { @@ -506,3 +509,26 @@ - (void)toTokenDetails:(ARTAuth *)auth callback:(void (^)(ARTTokenDetails * _Nul } @end + +NSString *ARTAuthorizationStateToStr(ARTAuthorizationState state) { + switch (state) { + case ARTAuthorizationSucceeded: + return @"Succeeded"; //0 + case ARTAuthorizationFailed: + return @"Failed"; //1 + } +} + +#pragma mark - ARTEvent + +@implementation ARTEvent (AuthorizationState) + +- (instancetype)initWithAuthorizationState:(ARTAuthorizationState)value { + return [self initWithString:[NSString stringWithFormat:@"ARTAuthorizationState%@", ARTAuthorizationStateToStr(value)]]; +} + ++ (instancetype)newWithAuthorizationState:(ARTAuthorizationState)value { + return [[self alloc] initWithAuthorizationState:value]; +} + +@end diff --git a/Source/ARTChannels+Private.h b/Source/ARTChannels+Private.h index eb37ccbfb..7e53f5d9c 100644 --- a/Source/ARTChannels+Private.h +++ b/Source/ARTChannels+Private.h @@ -17,7 +17,7 @@ extern NSString* (^__art_nullable ARTChannels_getChannelNamePrefix)(); @protocol ARTChannelsDelegate -- (id)makeChannel:(NSString *)channel options:(ARTChannelOptions *)options; +- (id)makeChannel:(NSString *)channel options:(nullable ARTChannelOptions *)options; @end diff --git a/Source/ARTConnection+Private.h b/Source/ARTConnection+Private.h index d991b96c4..e713d9ab4 100644 --- a/Source/ARTConnection+Private.h +++ b/Source/ARTConnection+Private.h @@ -17,7 +17,7 @@ ART_ASSUME_NONNULL_BEGIN @interface ARTConnection () -@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSNumber *, ARTConnectionStateChange *) *eventEmitter; +@property (readonly, strong, nonatomic) ARTEventEmitter *eventEmitter; @property(weak, nonatomic) ARTRealtime* realtime; @end diff --git a/Source/ARTConnection.h b/Source/ARTConnection.h index 4bec48dab..7802ccbf3 100644 --- a/Source/ARTConnection.h +++ b/Source/ARTConnection.h @@ -35,4 +35,11 @@ ART_EMBED_INTERFACE_EVENT_EMITTER(ARTRealtimeConnectionEvent, ARTConnectionState @end +#pragma mark - ARTEvent + +@interface ARTEvent (ConnectionEvent) +- (instancetype)initWithConnectionEvent:(ARTRealtimeConnectionEvent)value; ++ (instancetype)newWithConnectionEvent:(ARTRealtimeConnectionEvent)value; +@end + ART_ASSUME_NONNULL_END diff --git a/Source/ARTConnection.m b/Source/ARTConnection.m index 376bca888..b8a571dbe 100644 --- a/Source/ARTConnection.m +++ b/Source/ARTConnection.m @@ -20,7 +20,7 @@ @implementation ARTConnection { } - (instancetype)initWithRealtime:(ARTRealtime *)realtime { - if (self == [super init]) { + if (self = [super init]) { _queue = dispatch_queue_create("io.ably.realtime.connection", DISPATCH_QUEUE_SERIAL); _eventEmitter = [[ARTEventEmitter alloc] initWithQueue:_queue]; _realtime = realtime; @@ -78,39 +78,49 @@ - (NSString *)getRecoveryKey { } } -- (__GENERIC(ARTEventListener, ARTConnectionStateChange *) *)on:(ARTRealtimeConnectionEvent)event callback:(void (^)(ARTConnectionStateChange *))cb { - return [_eventEmitter on:[NSNumber numberWithInt:event] callback:cb]; +- (ARTEventListener *)on:(ARTRealtimeConnectionEvent)event callback:(void (^)(ARTConnectionStateChange *))cb { + return [_eventEmitter on:[ARTEvent newWithConnectionEvent:event] callback:cb]; } -- (__GENERIC(ARTEventListener, ARTConnectionStateChange *) *)on:(void (^)(ARTConnectionStateChange *))cb { +- (ARTEventListener *)on:(void (^)(ARTConnectionStateChange *))cb { return [_eventEmitter on:cb]; } -- (__GENERIC(ARTEventListener, ARTConnectionStateChange *) *)once:(ARTRealtimeConnectionEvent)event callback:(void (^)(ARTConnectionStateChange *))cb { - return [_eventEmitter once:[NSNumber numberWithInt:event] callback:cb]; +- (ARTEventListener *)once:(ARTRealtimeConnectionEvent)event callback:(void (^)(ARTConnectionStateChange *))cb { + return [_eventEmitter once:[ARTEvent newWithConnectionEvent:event] callback:cb]; } -- (__GENERIC(ARTEventListener, ARTConnectionStateChange *) *)once:(void (^)(ARTConnectionStateChange *))cb { +- (ARTEventListener *)once:(void (^)(ARTConnectionStateChange *))cb { return [_eventEmitter once:cb]; } - (void)off { [_eventEmitter off]; } -- (void)off:(ARTRealtimeConnectionEvent)event listener:listener { - [_eventEmitter off:[NSNumber numberWithInt:event] listener:listener]; +- (void)off:(ARTRealtimeConnectionEvent)event listener:(ARTEventListener *)listener { + [_eventEmitter off:[ARTEvent newWithConnectionEvent:event] listener:listener]; } -- (void)off:(__GENERIC(ARTEventListener, ARTConnectionStateChange *) *)listener { +- (void)off:(ARTEventListener *)listener { [_eventEmitter off:listener]; } - (void)emit:(ARTRealtimeConnectionEvent)event with:(ARTConnectionStateChange *)data { - [_eventEmitter emit:[NSNumber numberWithInt:event] with:data]; + [_eventEmitter emit:[ARTEvent newWithConnectionEvent:event] with:data]; } -- (ARTEventListener *)timed:(ARTEventListener *)listener deadline:(NSTimeInterval)deadline onTimeout:(void (^)())onTimeout { - return [_eventEmitter timed:listener deadline:deadline onTimeout:onTimeout]; +@end + +#pragma mark - ARTEvent + +@implementation ARTEvent (ConnectionEvent) + +- (instancetype)initWithConnectionEvent:(ARTRealtimeConnectionEvent)value { + return [self initWithString:[NSString stringWithFormat:@"ARTRealtimeConnectionEvent%@", ARTRealtimeConnectionEventToStr(value)]]; +} + ++ (instancetype)newWithConnectionEvent:(ARTRealtimeConnectionEvent)value { + return [[self alloc] initWithConnectionEvent:value]; } @end diff --git a/Source/ARTConnectionDetails.m b/Source/ARTConnectionDetails.m index db7e45f1d..6489c6528 100644 --- a/Source/ARTConnectionDetails.m +++ b/Source/ARTConnectionDetails.m @@ -17,7 +17,7 @@ - (instancetype)initWithClientId:(NSString *__art_nullable)clientId maxInboundRate:(NSInteger)maxInboundRate connectionStateTtl:(NSTimeInterval)connectionStateTtl serverId:(NSString *)serverId { - if (self == [super init]) { + if (self = [super init]) { _clientId = clientId; _connectionKey = connectionKey; _maxMessageSize = maxMessageSize; diff --git a/Source/ARTCrypto+Private.h b/Source/ARTCrypto+Private.h index 395bb5368..b9b86157d 100644 --- a/Source/ARTCrypto+Private.h +++ b/Source/ARTCrypto+Private.h @@ -50,7 +50,7 @@ ART_ASSUME_NONNULL_BEGIN + (int)defaultKeyLength; + (int)defaultBlockLength; -+ (NSData *)generateSecureRandomData:(size_t)length; ++ (nullable NSData *)generateSecureRandomData:(size_t)length; + (id)cipherWithParams:(ARTCipherParams *)params; diff --git a/Source/ARTDataEncoder.h b/Source/ARTDataEncoder.h index e9e886790..01d1ca750 100644 --- a/Source/ARTDataEncoder.h +++ b/Source/ARTDataEncoder.h @@ -37,7 +37,7 @@ ART_ASSUME_NONNULL_BEGIN + (NSString *)artAddEncoding:(NSString *)encoding toString:(NSString *__art_nullable)s; - (NSString *)artLastEncoding; -- (NSString *)artRemoveLastEncoding; +- (nullable NSString *)artRemoveLastEncoding; @end diff --git a/Source/ARTEventEmitter+Private.h b/Source/ARTEventEmitter+Private.h index cacc783c6..66ae2f10e 100644 --- a/Source/ARTEventEmitter+Private.h +++ b/Source/ARTEventEmitter+Private.h @@ -7,24 +7,18 @@ // #include "ARTEventEmitter.h" -#include "CompatibilityMacros.h" -ART_ASSUME_NONNULL_BEGIN +NS_ASSUME_NONNULL_BEGIN -@interface __GENERIC(ARTEventEmitterEntry, ItemType) : NSObject +@interface ARTEventEmitter () -@property (readwrite, strong, nonatomic) __GENERIC(ARTEventListener, ItemType) *listener; -@property (readwrite, nonatomic) BOOL once; +@property (nonatomic, readonly) NSNotificationCenter *notificationCenter; +@property (nonatomic, readonly) dispatch_queue_t queue; -- (instancetype)initWithListener:(__GENERIC(ARTEventListener, ItemType) *)listener once:(BOOL)once; +@property (readonly, atomic) NSMutableDictionary *> *listeners; +@property (readonly, atomic) NSMutableArray *anyListeners; @end -@interface __GENERIC(ARTEventEmitter, EventType, ItemType) () +NS_ASSUME_NONNULL_END -@property (readwrite, atomic) __GENERIC(NSMutableDictionary, EventType, __GENERIC(NSMutableArray, __GENERIC(ARTEventEmitterEntry, ItemType) *) *) *listeners; -@property (readwrite, atomic) __GENERIC(NSMutableArray, __GENERIC(ARTEventEmitterEntry, ItemType) *) *anyListeners; - -@end - -ART_ASSUME_NONNULL_END diff --git a/Source/ARTEventEmitter.h b/Source/ARTEventEmitter.h index f9e6cd4fe..50877ff54 100644 --- a/Source/ARTEventEmitter.h +++ b/Source/ARTEventEmitter.h @@ -7,36 +7,60 @@ // #import -#import "ARTTypes.h" @class ARTRealtime; +@class ARTEventEmitter; -ART_ASSUME_NONNULL_BEGIN +NS_ASSUME_NONNULL_BEGIN -@interface __GENERIC(ARTEventListener, ItemType) : NSObject +@protocol ARTEventIdentification +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; +- (NSString *)identification; +@end + +@interface ARTEvent : NSObject + +- (instancetype)initWithString:(NSString *)value; ++ (instancetype)newWithString:(NSString *)value; + +@end + +#pragma mark - ARTEventListener + +@interface ARTEventListener : NSObject -- (void)call:(ItemType)argument; +@property (nonatomic, readonly) NSString *eventId; +@property (weak, nonatomic, readonly) id token; +@property (nonatomic, readonly) NSUInteger count; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithId:(NSString *)eventId token:(id)token handler:(ARTEventEmitter *)eventHandler center:(NSNotificationCenter *)center; + +- (ARTEventListener *)setTimer:(NSTimeInterval)timeoutDeadline onTimeout:(void (^)())timeoutBlock; +- (void)startTimer; +- (void)stopTimer; @end -@interface __GENERIC(ARTEventEmitter, EventType, ItemType) : NSObject +#pragma mark - ARTEventEmitter + +@interface ARTEventEmitter, ItemType> : NSObject - (instancetype)init; - (instancetype)initWithQueue:(dispatch_queue_t)queue; -- (__GENERIC(ARTEventListener, ItemType) *)on:(EventType)event callback:(void (^)(ItemType __art_nullable))cb; -- (__GENERIC(ARTEventListener, ItemType) *)on:(void (^)(ItemType __art_nullable))cb; +- (ARTEventListener *)on:(EventType)event callback:(void (^)(ItemType __art_nullable))cb; +- (ARTEventListener *)on:(void (^)(ItemType __art_nullable))cb; -- (__GENERIC(ARTEventListener, ItemType) *)once:(EventType)event callback:(void (^)(ItemType __art_nullable))cb; -- (__GENERIC(ARTEventListener, ItemType) *)once:(void (^)(ItemType __art_nullable))cb; +- (ARTEventListener *)once:(EventType)event callback:(void (^)(ItemType __art_nullable))cb; +- (ARTEventListener *)once:(void (^)(ItemType __art_nullable))cb; -- (void)off:(EventType)event listener:(__GENERIC(ARTEventListener, ItemType) *)listener; -- (void)off:(__GENERIC(ARTEventListener, ItemType) *)listener; +- (void)off:(EventType)event listener:(ARTEventListener *)listener; +- (void)off:(ARTEventListener *)listener; - (void)off; -- (__GENERIC(ARTEventListener, ItemType) *)timed:(__GENERIC(ARTEventListener, ItemType) *)listener deadline:(NSTimeInterval)deadline onTimeout:(void (^__art_nullable)())onTimeout; - -- (void)emit:(EventType)event with:(ItemType __art_nullable)data; +- (void)emit:(nullable EventType)event with:(nullable ItemType)data; @end @@ -44,54 +68,49 @@ ART_ASSUME_NONNULL_BEGIN // This way you can automatically "implement the EventEmitter pattern" for a class // as the spec says. It's supposed to be used together with ART_EMBED_IMPLEMENTATION_EVENT_EMITTER // in the implementation of the class. -#define ART_EMBED_INTERFACE_EVENT_EMITTER(EventType, ItemType) - (__GENERIC(ARTEventListener, ItemType) *)on:(EventType)event callback:(void (^)(ItemType __art_nullable))cb;\ -- (__GENERIC(ARTEventListener, ItemType) *)on:(void (^)(ItemType __art_nullable))cb;\ +#define ART_EMBED_INTERFACE_EVENT_EMITTER(EventType, ItemType) - (ARTEventListener *)on:(EventType)event callback:(void (^)(ItemType __art_nullable))cb;\ +- (ARTEventListener *)on:(void (^)(ItemType __art_nullable))cb;\ \ -- (__GENERIC(ARTEventListener, ItemType) *)once:(EventType)event callback:(void (^)(ItemType __art_nullable))cb;\ -- (__GENERIC(ARTEventListener, ItemType) *)once:(void (^)(ItemType __art_nullable))cb;\ +- (ARTEventListener *)once:(EventType)event callback:(void (^)(ItemType __art_nullable))cb;\ +- (ARTEventListener *)once:(void (^)(ItemType __art_nullable))cb;\ \ -- (void)off:(EventType)event listener:(__GENERIC(ARTEventListener, ItemType) *)listener;\ -- (void)off:(__GENERIC(ARTEventListener, ItemType) *)listener;\ -- (void)off;\ -\ -- (__GENERIC(ARTEventListener, ItemType) *)timed:(__GENERIC(ARTEventListener, ItemType) *)listener deadline:(NSTimeInterval)deadline onTimeout:(void (^__art_nullable)())onTimeout; +- (void)off:(EventType)event listener:(ARTEventListener *)listener;\ +- (void)off:(ARTEventListener *)listener;\ +- (void)off; // This macro adds methods to a class implementation that just bridge calls to an internal // instance variable, which must be called _eventEmitter, of type ARTEventEmitter *. // It's supposed to be used together with ART_EMBED_IMPLEMENTATION_EVENT_EMITTER in the // header file of the class. -#define ART_EMBED_IMPLEMENTATION_EVENT_EMITTER(EventType, ItemType) - (__GENERIC(ARTEventListener, ItemType) *)on:(EventType)event callback:(void (^)(ItemType __art_nullable))cb {\ +#define ART_EMBED_IMPLEMENTATION_EVENT_EMITTER(EventType, ItemType) - (ARTEventListener *)on:(EventType)event callback:(void (^)(ItemType __art_nullable))cb {\ return [_eventEmitter on:event callback:cb];\ }\ \ -- (__GENERIC(ARTEventListener, ItemType) *)on:(void (^)(ItemType __art_nullable))cb {\ +- (ARTEventListener *)on:(void (^)(ItemType __art_nullable))cb {\ return [_eventEmitter on:cb];\ }\ \ -- (__GENERIC(ARTEventListener, ItemType) *)once:(EventType)event callback:(void (^)(ItemType __art_nullable))cb {\ +- (ARTEventListener *)once:(EventType)event callback:(void (^)(ItemType __art_nullable))cb {\ return [_eventEmitter once:event callback:cb];\ }\ \ -- (__GENERIC(ARTEventListener, ItemType) *)once:(void (^)(ItemType __art_nullable))cb {\ +- (ARTEventListener *)once:(void (^)(ItemType __art_nullable))cb {\ return [_eventEmitter once:cb];\ }\ \ -- (void)off:(EventType)event listener:listener {\ +- (void)off:(EventType)event listener:(ARTEventListener *)listener {\ [_eventEmitter off:event listener:listener];\ }\ \ -- (void)off:(__GENERIC(ARTEventListener, ItemType) *)listener {\ +- (void)off:(ARTEventListener *)listener {\ [_eventEmitter off:listener];\ }\ - (void)off {\ [_eventEmitter off];\ }\ -- (__GENERIC(ARTEventListener, ItemType) *)timed:(__GENERIC(ARTEventListener, ItemType) *)listener deadline:(NSTimeInterval)deadline onTimeout:(void (^)())onTimeout {\ -return [_eventEmitter timed:listener deadline:deadline onTimeout:onTimeout];\ -}\ \ - (void)emit:(EventType)event with:(ItemType)data {\ [_eventEmitter emit:event with:data];\ } -ART_ASSUME_NONNULL_END +NS_ASSUME_NONNULL_END diff --git a/Source/ARTEventEmitter.m b/Source/ARTEventEmitter.m index f76068778..48c3948fc 100644 --- a/Source/ARTEventEmitter.m +++ b/Source/ARTEventEmitter.m @@ -11,6 +11,7 @@ #import "ARTRealtime.h" #import "ARTRealtime+Private.h" #import "ARTRealtimeChannel.h" +#import "ARTGCD.h" @implementation NSMutableArray (AsSet) @@ -29,77 +30,112 @@ - (void)artRemoveWhere:(BOOL (^)(id))cond { @end -@interface ARTEventListener () +#pragma mark - ARTEvent -- (instancetype)initWithBlock:(void (^)(id __art_nonnull))block queue:(dispatch_queue_t)queue; -- (void)setTimerWithDeadline:(NSTimeInterval)deadline onTimeout:(void (^)())onTimeout; -- (void)off; +@implementation ARTEvent { + NSString *_value; +} +- (instancetype)initWithString:(NSString *)value { + if (self = [super init]) { + _value = value; + } + return self; +} + ++ (instancetype)newWithString:(NSString *)value { + return [[self alloc] initWithString:value]; +} + +- (NSString *)identification { + return _value; +} + +@end + +#pragma mark - ARTEventListener + +@interface ARTEventListener () +@property (readonly) BOOL timerIsRunning; +@property (readonly) BOOL hasTimer; @end @implementation ARTEventListener { - void (^_block)(id __art_nonnull); - _Nonnull dispatch_queue_t _queue; - _Nullable dispatch_block_t _timerBlock; + __weak NSNotificationCenter *_center; + __weak ARTEventEmitter *_eventHandler; + NSTimeInterval _timeoutDeadline; + void (^_timeoutBlock)(); + dispatch_block_t _work; } -- (instancetype)initWithBlock:(void (^)(id __art_nonnull))block queue:(dispatch_queue_t)queue { - self = [self init]; - if (self) { - _block = block; - _queue = queue; +- (instancetype)initWithId:(NSString *)eventId token:(id)token handler:(ARTEventEmitter *)eventHandler center:(NSNotificationCenter *)center { + if (self = [super init]) { + _eventId = eventId; + _token = token; + _center = center; + _eventHandler = eventHandler; + _timeoutDeadline = 0; + _timeoutBlock = nil; + _timerIsRunning = false; } return self; } -- (void)call:(id)argument { - [self cancelTimer]; - _block(argument); +- (void)dealloc { + [self removeObserver]; } -- (void)setTimerWithDeadline:(NSTimeInterval)deadline onTimeout:(void (^)())onTimeout { - [self cancelTimer]; - _timerBlock = dispatch_block_create(0, ^{ - onTimeout(); - }); - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, deadline * NSEC_PER_SEC), _queue, _timerBlock); +- (void)removeObserver { + [self stopTimer]; + [_center removeObserver:_token]; } -- (void)cancelTimer { - if (_timerBlock) { - dispatch_block_cancel(_timerBlock); - } +- (BOOL)handled { + return _count++ > 0; } -- (void)off { - [self cancelTimer]; +- (ARTEventListener *)setTimer:(NSTimeInterval)timeoutDeadline onTimeout:(void (^)())timeoutBlock { + if (_timeoutBlock) { + NSAssert(false, @"timer is already set"); + } + _timeoutBlock = timeoutBlock; + _timeoutDeadline = timeoutDeadline; + return self; } -@end +- (void)timeout { + [_eventHandler off:self]; + if (_timeoutBlock) { + _timeoutBlock(); + } +} -@implementation ARTEventEmitterEntry +- (BOOL)hasTimer { + return _timeoutBlock != nil; +} -- (instancetype)initWithListener:(ARTEventListener *)listener once:(BOOL)once { - self = [self init]; - if (self) { - _listener = listener; - _once = once; +- (void)startTimer { + if (_timerIsRunning) { + NSAssert(false, @"timer is already running"); } - return self; + _timerIsRunning = true; + __weak typeof(self) weakSelf = self; + _work = artDispatchScheduled(_timeoutDeadline, [_eventHandler queue], ^{ + [weakSelf timeout]; + }); } -- (BOOL)isEqual:(id)object { - if ([object isKindOfClass:[self class]]) { - return self == object || self.listener == ((ARTEventEmitterEntry *)object).listener; - } - return self.listener == object; +- (void)stopTimer { + artDispatchCancel(nil); + artDispatchCancel(_work); + _timerIsRunning = false; } @end -@implementation ARTEventEmitter { - _Nonnull dispatch_queue_t _queue; -} +#pragma mark - ARTEventEmitter + +@implementation ARTEventEmitter - (instancetype)init { return [self initWithQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)]; @@ -108,59 +144,98 @@ - (instancetype)init { - (instancetype)initWithQueue:(dispatch_queue_t)queue { self = [super init]; if (self) { + _notificationCenter = [[NSNotificationCenter alloc] init]; _queue = queue; [self resetListeners]; } return self; } -- (ARTEventListener *)on:(id)event callback:(void (^)(id __art_nonnull))cb { - ARTEventListener *listener = [[ARTEventListener alloc] initWithBlock:cb queue:_queue]; - [self addOnEntry:[[ARTEventEmitterEntry alloc] initWithListener:listener once:false] event:event]; - return listener; -} - -- (ARTEventListener *)once:(id)event callback:(void (^)(id __art_nonnull))cb { - ARTEventListener *listener = [[ARTEventListener alloc] initWithBlock:cb queue:_queue]; - [self addOnEntry:[[ARTEventEmitterEntry alloc] initWithListener:listener once:true] event:event]; - return listener; +- (ARTEventListener *)on:(id)event callback:(void (^)(id __art_nonnull))cb { + NSString *eventId = [NSString stringWithFormat:@"%p-%@", self, [event identification]]; + __weak __block ARTEventListener *weakListener; + id observerToken = [_notificationCenter addObserverForName:eventId object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { + if (weakListener == nil) return; + if ([weakListener hasTimer] && ![weakListener timerIsRunning]) return; + [weakListener stopTimer]; + cb(note.object); + }]; + ARTEventListener *eventToken = [[ARTEventListener alloc] initWithId:eventId token:observerToken handler:self center:_notificationCenter]; + weakListener = eventToken; + [self addObject:eventToken toArrayWithKey:eventToken.eventId inDictionary:self.listeners]; + return eventToken; } -- (void)addOnEntry:(ARTEventEmitterEntry *)entry event:(id)event { - [self addObject:entry toArrayWithKey:event inDictionary:self.listeners]; +- (ARTEventListener *)once:(id)event callback:(void (^)(id __art_nonnull))cb { + NSString *eventId = [NSString stringWithFormat:@"%p-%@", self, [event identification]]; + __weak __block ARTEventListener *weakListener; + __weak typeof(self) weakSelf = self; + id observerToken = [_notificationCenter addObserverForName:eventId object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { + if (weakListener == nil) return; + if ([weakListener hasTimer] && ![weakListener timerIsRunning]) return; + if ([weakListener handled]) return; + [weakListener removeObserver]; + [weakSelf removeObject:weakListener fromArrayWithKey:[weakListener eventId] inDictionary:[weakSelf listeners]]; + cb(note.object); + }]; + ARTEventListener *eventToken = [[ARTEventListener alloc] initWithId:eventId token:observerToken handler:self center:_notificationCenter]; + weakListener = eventToken; + [self addObject:eventToken toArrayWithKey:eventToken.eventId inDictionary:self.listeners]; + return eventToken; } - (ARTEventListener *)on:(void (^)(id __art_nonnull))cb { - ARTEventListener *listener = [[ARTEventListener alloc] initWithBlock:cb queue:_queue]; - [self addOnAllEntry:[[ARTEventEmitterEntry alloc] initWithListener:listener once:false]]; - return listener; + NSString *eventId = [NSString stringWithFormat:@"%p", self]; + __weak __block ARTEventListener *weakListener; + id observerToken = [_notificationCenter addObserverForName:eventId object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { + if (weakListener == nil) return; + if ([weakListener hasTimer] && ![weakListener timerIsRunning]) return; + [weakListener stopTimer]; + cb(note.object); + }]; + ARTEventListener *eventToken = [[ARTEventListener alloc] initWithId:eventId token:observerToken handler:self center:_notificationCenter]; + weakListener = eventToken; + [self.anyListeners addObject:eventToken]; + return eventToken; } - (ARTEventListener *)once:(void (^)(id __art_nonnull))cb { - ARTEventListener *listener = [[ARTEventListener alloc] initWithBlock:cb queue:_queue]; - [self addOnAllEntry:[[ARTEventEmitterEntry alloc] initWithListener:listener once:true]]; - return listener; -} - -- (void)addOnAllEntry:(ARTEventEmitterEntry *)entry { - [self.anyListeners addObject:entry]; + NSString *eventId = [NSString stringWithFormat:@"%p", self]; + __weak __block ARTEventListener *weakListener; + __weak typeof(self) weakSelf = self; + id observerToken = [_notificationCenter addObserverForName:eventId object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { + if (weakListener == nil) return; + if ([weakListener hasTimer] && ![weakListener timerIsRunning]) return; + if ([weakListener handled]) return; + [weakListener removeObserver]; + [[weakSelf anyListeners] removeObject:weakListener]; + cb(note.object); + }]; + ARTEventListener *eventToken = [[ARTEventListener alloc] initWithId:eventId token:observerToken handler:self center:_notificationCenter]; + weakListener = eventToken; + [self.anyListeners addObject:eventToken]; + return eventToken; } -- (void)off:(id)event listener:(ARTEventListener *)listener { - [listener off]; - [self removeObject:listener fromArrayWithKey:event inDictionary:self.listeners where:^BOOL(id entry) { - return ((ARTEventEmitterEntry *)entry).listener == listener; - }]; +- (void)off:(id)event listener:(ARTEventListener *)listener { + NSString *eventId = [NSString stringWithFormat:@"%p-%@", self, [event identification]]; + if (![eventId isEqualToString:listener.eventId]) return; + [listener removeObserver]; + @synchronized (_listeners) { + [self.listeners[listener.eventId] removeObject:listener]; + if ([self.listeners[listener.eventId] firstObject] == nil) { + [self.listeners removeObjectForKey:listener.eventId]; + } + } } - (void)off:(ARTEventListener *)listener { - [listener off]; - BOOL (^cond)(id) = ^BOOL(id entry) { - return ((ARTEventEmitterEntry *)entry).listener == listener; - }; - [self.anyListeners artRemoveWhere:cond]; - for (id event in [self.listeners allKeys]) { - [self removeObject:listener fromArrayWithKey:event inDictionary:self.listeners where:cond]; + [listener removeObserver]; + @synchronized (_listeners) { + [self.listeners[listener.eventId] removeObject:listener]; + } + @synchronized (_anyListeners) { + [self.anyListeners removeObject:listener]; } } @@ -169,101 +244,75 @@ - (void)off { } - (void)resetListeners { - for (NSArray *entries in [_listeners allValues]) { - for (ARTEventEmitterEntry *entry in entries) { - [entry.listener off]; + @synchronized (_listeners) { + for (NSArray *items in [_listeners allValues]) { + for (ARTEventListener *item in items) { + [item removeObserver]; + } } - } - for (ARTEventEmitterEntry *entry in _anyListeners) { - [entry.listener off]; + [_listeners removeAllObjects]; } _listeners = [[NSMutableDictionary alloc] init]; + + @synchronized (_anyListeners) { + for (ARTEventListener *item in _anyListeners) { + [item removeObserver]; + } + [_anyListeners removeAllObjects]; + } _anyListeners = [[NSMutableArray alloc] init]; } -- (void)emit:(id)event with:(id)data { - NSMutableArray *toCall = [[NSMutableArray alloc] init]; - NSMutableArray *toRemoveFromListeners = [[NSMutableArray alloc] init]; - NSMutableArray *toRemoveFromTotalListeners = [[NSMutableArray alloc] init]; - @try { - for (ARTEventEmitterEntry *entry in [self.listeners objectForKey:event]) { - if (entry.once) { - [toRemoveFromListeners addObject:entry]; - } - [toCall addObject:entry]; - } - - for (ARTEventEmitterEntry *entry in self.anyListeners) { - if (entry.once) { - [toRemoveFromTotalListeners addObject:entry]; - } - [toCall addObject:entry]; - } +- (void)emit:(id)event with:(id)data { + NSString *eventId; + if (event) { + eventId = [NSString stringWithFormat:@"%p-%@", self, [event identification]]; + [self.notificationCenter postNotificationName:eventId object:data]; + [self.notificationCenter postNotificationName:[NSString stringWithFormat:@"%p", self] object:data]; } - @finally { - for (ARTEventEmitterEntry *entry in toRemoveFromListeners) { - [self removeObject:entry fromArrayWithKey:event inDictionary:self.listeners]; - [entry.listener off]; - } - for (ARTEventEmitterEntry *entry in toRemoveFromTotalListeners) { - @synchronized(self.anyListeners) { - [self.anyListeners removeObject:entry]; - } - [entry.listener off]; - } - for (ARTEventEmitterEntry *entry in toCall) { - [entry.listener call:data]; - } + else { + eventId = [NSString stringWithFormat:@"%p", self]; + [self.notificationCenter postNotificationName:eventId object:data]; } } - (void)addObject:(id)obj toArrayWithKey:(id)key inDictionary:(NSMutableDictionary *)dict { - NSMutableArray *array = [dict objectForKey:key]; - if (array == nil) { - array = [[NSMutableArray alloc] init]; - [dict setObject:array forKey:key]; + @synchronized (dict) { + NSMutableArray *array = [dict objectForKey:key]; + if (array == nil) { + array = [[NSMutableArray alloc] init]; + [dict setObject:array forKey:key]; + } + if ([array indexOfObject:obj] == NSNotFound) { + [array addObject:obj]; + } } - [array addObject:obj]; } - (void)removeObject:(id)obj fromArrayWithKey:(id)key inDictionary:(NSMutableDictionary *)dict { - NSMutableArray *array = [dict objectForKey:key]; - if (array == nil) { - return; - } - @synchronized(array) { + @synchronized (dict) { + NSMutableArray *array = [dict objectForKey:key]; + if (array == nil) { + return; + } [array removeObject:obj]; - } - if ([array count] == 0) { - @synchronized(dict) { + if ([array count] == 0) { [dict removeObjectForKey:key]; } } } - (void)removeObject:(id)obj fromArrayWithKey:(id)key inDictionary:(NSMutableDictionary *)dict where:(BOOL(^)(id))cond { - NSMutableArray *array = [dict objectForKey:key]; - if (array == nil) { - return; - } - @synchronized(array) { + @synchronized (dict) { + NSMutableArray *array = [dict objectForKey:key]; + if (array == nil) { + return; + } [array artRemoveWhere:cond]; - } - if ([array count] == 0) { - @synchronized(dict) { + if ([array count] == 0) { [dict removeObjectForKey:key]; } } } -- (ARTEventListener *)timed:(ARTEventListener *)listener deadline:(NSTimeInterval)deadline onTimeout:(void (^)())onTimeout { - __weak ARTEventEmitter *s = self; - __weak ARTEventListener *weakListener = listener; - [listener setTimerWithDeadline:deadline onTimeout:^void() { - [s off:weakListener]; - if (onTimeout) onTimeout(); - }]; - return listener; -} - @end diff --git a/Source/ARTFallback.h b/Source/ARTFallback.h index 4301fe4a0..10e2483ed 100644 --- a/Source/ARTFallback.h +++ b/Source/ARTFallback.h @@ -29,7 +29,7 @@ ART_ASSUME_NONNULL_BEGIN /** returns a random fallback host, returns null when all hosts have been popped. */ --(NSString *) popFallbackHost; +- (nullable NSString *)popFallbackHost; @end diff --git a/Source/ARTFallback.m b/Source/ARTFallback.m index 9d76151fe..5aeb283e4 100644 --- a/Source/ARTFallback.m +++ b/Source/ARTFallback.m @@ -53,10 +53,10 @@ - (instancetype)init { } - (NSString *)popFallbackHost { - if([self.hosts count] ==0) { + if ([self.hosts count] ==0) { return nil; } - NSString *host= [self.hosts lastObject]; + NSString *host = [self.hosts lastObject]; [self.hosts removeLastObject]; return host; } diff --git a/Source/ARTGCD.h b/Source/ARTGCD.h index ddcb5498b..d43a8d57f 100644 --- a/Source/ARTGCD.h +++ b/Source/ARTGCD.h @@ -14,7 +14,9 @@ void artDispatchSpecifyMainQueue(); void artDispatchMainQueue(dispatch_block_t block); void artDispatchGlobalQueue(dispatch_block_t block); -dispatch_block_t artDispatchScheduled(NSTimeInterval seconds, dispatch_block_t block); +dispatch_block_t artDispatchScheduledOnMainQueue(NSTimeInterval seconds, dispatch_block_t block); +dispatch_block_t artDispatchScheduledOnGlobalQueue(NSTimeInterval seconds, dispatch_block_t block); +dispatch_block_t artDispatchScheduled(NSTimeInterval seconds, dispatch_queue_t queue, dispatch_block_t block); void artDispatchCancel(dispatch_block_t block); #endif /* ARTGCD_h */ diff --git a/Source/ARTGCD.m b/Source/ARTGCD.m index 94798a4cc..2faf4ce86 100644 --- a/Source/ARTGCD.m +++ b/Source/ARTGCD.m @@ -30,12 +30,20 @@ void artDispatchGlobalQueue(dispatch_block_t block) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block); } -dispatch_block_t artDispatchScheduled(NSTimeInterval seconds, dispatch_block_t block) { +dispatch_block_t artDispatchScheduledOnMainQueue(NSTimeInterval seconds, dispatch_block_t block) { + return artDispatchScheduled(seconds, dispatch_get_main_queue(), block); +} + +dispatch_block_t artDispatchScheduledOnGlobalQueue(NSTimeInterval seconds, dispatch_block_t block) { + return artDispatchScheduled(seconds, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block); +} + +dispatch_block_t artDispatchScheduled(NSTimeInterval seconds, dispatch_queue_t queue, dispatch_block_t block) { dispatch_block_t work = dispatch_block_create(0, block); - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC * seconds)), dispatch_get_main_queue(), work); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC * seconds)), queue, work); return work; } void artDispatchCancel(dispatch_block_t block) { - dispatch_block_cancel(block); + if (block) dispatch_block_cancel(block); } diff --git a/Source/ARTHttp.h b/Source/ARTHttp.h index a87dfea67..6cf6db6d1 100644 --- a/Source/ARTHttp.h +++ b/Source/ARTHttp.h @@ -38,9 +38,9 @@ ART_ASSUME_NONNULL_BEGIN @interface ARTHttpResponse : NSObject @property (readonly, assign, nonatomic) int status; -@property (readwrite, strong, nonatomic) ARTErrorInfo *error; -@property (art_nullable, readonly, strong, nonatomic) NSDictionary *headers; -@property (art_nullable, readonly, strong, nonatomic) NSData *body; +@property (nullable, readwrite, nonatomic) ARTErrorInfo *error; +@property (nullable, readonly, nonatomic) NSDictionary *headers; +@property (nullable, readonly, nonatomic) NSData *body; - (instancetype)init; - (instancetype)initWithStatus:(int)status headers:(art_nullable NSDictionary *)headers body:(art_nullable NSData *)body; diff --git a/Source/ARTHttp.m b/Source/ARTHttp.m index 4c0700c8d..efc69488e 100644 --- a/Source/ARTHttp.m +++ b/Source/ARTHttp.m @@ -163,6 +163,10 @@ - (instancetype)init { return self; } +- (void)dealloc { + [_urlSession finishTasksAndInvalidate]; +} + - (instancetype)initWithBaseUrl:(NSURL *)baseUrl { self = [self init]; if (self) { @@ -216,7 +220,7 @@ - (void)executeRequest:(NSMutableURLRequest *)request completion:(void (^)(NSHTT request.HTTPBody = artRequest.body; [self.logger debug:@"ARTHttp: makeRequest %@", [request allHTTPHeaderFields]]; - [self.urlSession get:request completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { + [_urlSession get:request completion:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; [self.logger verbose:@"ARTHttp: Got response %@, err %@", response, error]; diff --git a/Source/ARTJsonLikeEncoder.m b/Source/ARTJsonLikeEncoder.m index 5614be074..49a67ec00 100644 --- a/Source/ARTJsonLikeEncoder.m +++ b/Source/ARTJsonLikeEncoder.m @@ -668,6 +668,9 @@ - (ARTStatsResourceCount *)statsResourceCountFromDictionary:(NSDictionary *)inpu - (NSError *)decodeError:(NSData *)error { NSDictionary *decodedError = [[self decodeDictionary:error] valueForKey:@"error"]; + if (!decodedError) { + return nil; + } NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"", NSLocalizedFailureReasonErrorKey: decodedError[@"message"], diff --git a/Source/ARTOSReachability.m b/Source/ARTOSReachability.m index 2aebbe1e9..411d3a857 100644 --- a/Source/ARTOSReachability.m +++ b/Source/ARTOSReachability.m @@ -25,8 +25,7 @@ @implementation ARTOSReachability { } - (instancetype)initWithLogger:(ARTLog *)logger { - self = [super self]; - if (self) { + if (self = [super init]) { _logger = logger; if (ARTOSReachability_instances == nil) { _instances = [[NSMutableDictionary alloc] init]; @@ -84,4 +83,4 @@ - (void)dealloc { } } -@end \ No newline at end of file +@end diff --git a/Source/ARTPresenceMap.m b/Source/ARTPresenceMap.m index 0b8bc4c65..963f4e6cb 100644 --- a/Source/ARTPresenceMap.m +++ b/Source/ARTPresenceMap.m @@ -19,9 +19,33 @@ typedef NS_ENUM(NSUInteger, ARTPresenceSyncState) { ARTPresenceSyncFailed //ItemType: ARTErrorInfo* }; +NSString *ARTPresenceSyncStateToStr(ARTPresenceSyncState state) { + switch (state) { + case ARTPresenceSyncInitialized: + return @"Initialized"; //0 + case ARTPresenceSyncStarted: + return @"Started"; //1 + case ARTPresenceSyncEnded: + return @"Ended"; //2 + case ARTPresenceSyncFailed: + return @"Failed"; //3 + } +} + +#pragma mark - ARTEvent + +@interface ARTEvent (PresenceSyncState) + +- (instancetype)initWithPresenceSyncState:(ARTPresenceSyncState)value; ++ (instancetype)newWithPresenceSyncState:(ARTPresenceSyncState)value; + +@end + +#pragma mark - ARTPresenceMap + @interface ARTPresenceMap () { ARTPresenceSyncState _syncState; - ARTEventEmitter *_syncEventEmitter; + ARTEventEmitter *_syncEventEmitter; NSMutableDictionary *_members; NSMutableSet *_localMembers; } @@ -184,7 +208,7 @@ - (void)reset { - (void)startSync { _syncSessionId++; _syncState = ARTPresenceSyncStarted; - [_syncEventEmitter emit:[NSNumber numberWithInt:ARTPresenceSyncStarted] with:nil]; + [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:_syncState] with:nil]; } - (void)endSync { @@ -192,20 +216,20 @@ - (void)endSync { [self leaveMembersNotPresentInSync]; _syncState = ARTPresenceSyncEnded; [self reenterLocalMembersMissingFromSync]; - [_syncEventEmitter emit:[NSNumber numberWithInt:ARTPresenceSyncEnded] with:[_members allValues]]; + [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncEnded] with:[_members allValues]]; [_syncEventEmitter off]; } - (void)failsSync:(ARTErrorInfo *)error { [self reset]; _syncState = ARTPresenceSyncFailed; - [_syncEventEmitter emit:[NSNumber numberWithInt:ARTPresenceSyncFailed] with:error]; + [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncFailed] with:error]; [_syncEventEmitter off]; } - (void)onceSyncEnds:(void (^)(NSArray *))callback { if (self.syncInProgress) { - [_syncEventEmitter once:[NSNumber numberWithInt:ARTPresenceSyncEnded] callback:callback]; + [_syncEventEmitter once:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncEnded] callback:callback]; } else { callback([_members allValues]); @@ -214,7 +238,7 @@ - (void)onceSyncEnds:(void (^)(NSArray *))callback { - (void)onceSyncFails:(void (^)(ARTErrorInfo *))callback { if (self.syncInProgress) { - [_syncEventEmitter once:[NSNumber numberWithInt:ARTPresenceSyncFailed] callback:callback]; + [_syncEventEmitter once:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncFailed] callback:callback]; } } @@ -233,3 +257,17 @@ - (NSString *)memberKey:(ARTPresenceMessage *) message { } @end + +#pragma mark - ARTEvent + +@implementation ARTEvent (PresenceSyncState) + +- (instancetype)initWithPresenceSyncState:(ARTPresenceSyncState)value { + return [self initWithString:[NSString stringWithFormat:@"ARTPresenceSyncState%@", ARTPresenceSyncStateToStr(value)]]; +} + ++ (instancetype)newWithPresenceSyncState:(ARTPresenceSyncState)value { + return [[self alloc] initWithPresenceSyncState:value]; +} + +@end diff --git a/Source/ARTPresenceMessage.h b/Source/ARTPresenceMessage.h index c0a044ee6..93150533c 100644 --- a/Source/ARTPresenceMessage.h +++ b/Source/ARTPresenceMessage.h @@ -7,6 +7,7 @@ // #import "ARTBaseMessage.h" +#import "ARTEventEmitter.h" /// Presence action type typedef NS_ENUM(NSUInteger, ARTPresenceAction) { @@ -32,4 +33,11 @@ ART_ASSUME_NONNULL_BEGIN @end +#pragma mark - ARTEvent + +@interface ARTEvent (PresenceAction) +- (instancetype)initWithPresenceAction:(ARTPresenceAction)value; ++ (instancetype)newWithPresenceAction:(ARTPresenceAction)value; +@end + ART_ASSUME_NONNULL_END diff --git a/Source/ARTPresenceMessage.m b/Source/ARTPresenceMessage.m index a3d51041c..145ade075 100644 --- a/Source/ARTPresenceMessage.m +++ b/Source/ARTPresenceMessage.m @@ -86,3 +86,17 @@ - (NSUInteger)hash { return @"Update"; //4 } } + +#pragma mark - ARTEvent + +@implementation ARTEvent (PresenceAction) + +- (instancetype)initWithPresenceAction:(ARTPresenceAction)value { + return [self initWithString:[NSString stringWithFormat:@"ARTPresenceAction%@", ARTPresenceActionToStr(value)]]; +} + ++ (instancetype)newWithPresenceAction:(ARTPresenceAction)value { + return [[self alloc] initWithPresenceAction:value]; +} + +@end diff --git a/Source/ARTQueuedMessage.m b/Source/ARTQueuedMessage.m index 693061003..d97c1eb29 100644 --- a/Source/ARTQueuedMessage.m +++ b/Source/ARTQueuedMessage.m @@ -25,6 +25,10 @@ - (instancetype)initWithProtocolMessage:(ARTProtocolMessage *)msg callback:(void return self; } +- (NSString *)description { + return [self.msg description]; +} + - (BOOL)mergeFrom:(ARTProtocolMessage *)msg callback:(void (^)(ARTStatus *))cb { if ([self.msg mergeFrom:msg]) { if (cb) { diff --git a/Source/ARTRealtime+Private.h b/Source/ARTRealtime+Private.h index fa907b64d..d193ad387 100644 --- a/Source/ARTRealtime+Private.h +++ b/Source/ARTRealtime+Private.h @@ -25,8 +25,8 @@ ART_ASSUME_NONNULL_BEGIN @interface ARTRealtime () -@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSNumber *, ARTConnectionStateChange *) *internalEventEmitter; -@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSNull *, NSNull *) *connectedEventEmitter; +@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, ARTEvent *, ARTConnectionStateChange *) *internalEventEmitter; +@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, ARTEvent *, NSNull *) *connectedEventEmitter; // State properties - (BOOL)shouldSendEvents; @@ -43,7 +43,7 @@ ART_ASSUME_NONNULL_BEGIN @interface ARTRealtime () @property (readwrite, strong, nonatomic) ARTRest *rest; -@property (readonly, getter=getTransport, art_nullable) id transport; +@property (readonly, nullable) id transport; @property (readonly, strong, nonatomic, art_nonnull) id reachability; @property (readonly, getter=getLogger) ARTLog *logger; @property (nonatomic) NSTimeInterval connectionStateTtl; @@ -55,7 +55,7 @@ ART_ASSUME_NONNULL_BEGIN @property (readwrite, strong, nonatomic) __GENERIC(NSMutableArray, ARTQueuedMessage*) *queuedMessages; /// List of pending messages waiting for ACK/NACK action to confirm the success receipt and acceptance. -@property (readonly, strong, nonatomic) __GENERIC(NSMutableArray, ARTQueuedMessage*) *pendingMessages; +@property (readwrite, strong, nonatomic) __GENERIC(NSMutableArray, ARTQueuedMessage*) *pendingMessages; /// First `msgSerial` pending message. @property (readwrite, assign, nonatomic) int64_t pendingMessageStartSerial; diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index 33282754d..318614124 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -46,13 +46,17 @@ - (void)setRetryIn:(NSTimeInterval)retryIn; @implementation ARTRealtime { BOOL _resuming; BOOL _renewingToken; - __GENERIC(ARTEventEmitter, NSNull *, ARTErrorInfo *) *_pingEventEmitter; + __GENERIC(ARTEventEmitter, ARTEvent *, ARTErrorInfo *) *_pingEventEmitter; NSDate *_startedReconnection; Class _transportClass; Class _reachabilityClass; id _transport; ARTFallback *_fallbacks; _Nonnull dispatch_queue_t _eventQueue; + __weak ARTEventListener *_connectionRetryFromSuspendedListener; + __weak ARTEventListener *_connectionRetryFromDisconnectedListener; + __weak ARTEventListener *_connectingTimeoutListener; + dispatch_block_t _authenitcatingTimeoutWork; } @synthesize authorizationEmitter = _authorizationEmitter; @@ -119,6 +123,8 @@ - (void)auth:(ARTAuth *)auth didAuthorize:(ARTTokenDetails *)tokenDetails { // 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 = [[_transportClass alloc] initWithRest:self.rest options:self.options resumeKey:_transport.resumeKey connectionSerial:_transport.connectionSerial]; + _transport.delegate = self; [_transport connectWithToken:tokenDetails.token]; } break; @@ -138,7 +144,7 @@ - (void)auth:(ARTAuth *)auth didAuthorize:(ARTTokenDetails *)tokenDetails { } } -- (id)getTransport { +- (id)transport { return _transport; } @@ -186,11 +192,6 @@ - (void)dealloc { [_internalEventEmitter off]; } - if (_transport) { - _transport.delegate = nil; - [_transport close]; - } - _transport = nil; self.rest.prioritizedHost = nil; } @@ -203,6 +204,9 @@ - (void)connect { } - (void)close { + [_reachability off]; + [self cancelTimers]; + switch (self.connection.state) { case ARTRealtimeInitialized: case ARTRealtimeClosing: @@ -247,9 +251,9 @@ - (void)ping:(void (^)(ARTErrorInfo *)) cb { }]; return; } - [_pingEventEmitter timed:[_pingEventEmitter once:cb] deadline:[ARTDefault realtimeRequestTimeout] onTimeout:^{ + [[[_pingEventEmitter once:cb] setTimer:[ARTDefault realtimeRequestTimeout] onTimeout:^{ cb([ARTErrorInfo createWithCode:ARTCodeErrorConnectionTimedOut status:ARTStateConnectionFailed message:@"timed out"]); - }]; + }] startTimer]; [self.transport sendPing]; } } @@ -276,14 +280,11 @@ - (void)transition:(ARTRealtimeConnectionState)state withErrorInfo:(ARTErrorInfo [self.connection setErrorReason:errorInfo]; } - dispatch_semaphore_t waitingForCurrentEventSemaphore = [self transitionSideEffects:stateChange]; + ARTEventListener *stateChangeEventListener = [self transitionSideEffects:stateChange]; - [_internalEventEmitter emit:[NSNumber numberWithInteger:state] with:stateChange]; + [_internalEventEmitter emit:[ARTEvent newWithConnectionEvent:(ARTRealtimeConnectionEvent)state] with:stateChange]; - if (waitingForCurrentEventSemaphore) { - // Current event is handled. Start running timeouts. - dispatch_semaphore_signal(waitingForCurrentEventSemaphore); - } + [stateChangeEventListener startTimer]; } - (void)updateWithErrorInfo:(art_nullable ARTErrorInfo *)errorInfo { @@ -296,25 +297,24 @@ - (void)updateWithErrorInfo:(art_nullable ARTErrorInfo *)errorInfo { ARTConnectionStateChange *stateChange = [[ARTConnectionStateChange alloc] initWithCurrent:self.connection.state previous:self.connection.state event:ARTRealtimeConnectionEventUpdate reason:errorInfo retryIn:0]; - dispatch_semaphore_t semaphore = [self transitionSideEffects:stateChange]; + ARTEventListener *stateChangeEventListener = [self transitionSideEffects:stateChange]; - if (semaphore) { - dispatch_semaphore_signal(semaphore); - } + [stateChangeEventListener startTimer]; } -- (_Nullable dispatch_semaphore_t)transitionSideEffects:(ARTConnectionStateChange *)stateChange { +- (ARTEventListener *)transitionSideEffects:(ARTConnectionStateChange *)stateChange { ARTStatus *status = nil; - dispatch_semaphore_t waitingForCurrentEventSemaphore = nil; + ARTEventListener *stateChangeEventListener = nil; // Do not increase the reference count (avoid retain cycles): // i.e. the `unlessStateChangesBefore` is setting a timer and if the `ARTRealtime` instance is released before that timer, then it could create a leak. __weak __typeof(self) weakSelf = self; switch (stateChange.current) { case ARTRealtimeConnecting: { - waitingForCurrentEventSemaphore = [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ + stateChangeEventListener = [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ [weakSelf onConnectionTimeOut]; }]; + _connectingTimeoutListener = stateChangeEventListener; if (!_reachability) { _reachability = [[_reachabilityClass alloc] initWithLogger:self.logger]; @@ -357,12 +357,11 @@ - (_Nullable dispatch_semaphore_t)transitionSideEffects:(ARTConnectionStateChang } }]; } - break; } case ARTRealtimeClosing: { [_reachability off]; - waitingForCurrentEventSemaphore = [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ + stateChangeEventListener = [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ [weakSelf transition:ARTRealtimeClosed]; }]; [self.transport sendClose]; @@ -371,20 +370,18 @@ - (_Nullable dispatch_semaphore_t)transitionSideEffects:(ARTConnectionStateChang case ARTRealtimeClosed: [_reachability off]; [self.transport close]; - self.transport.delegate = nil; _connection.key = nil; _connection.id = nil; _transport = nil; self.rest.prioritizedHost = nil; - [_authorizationEmitter emit:[NSNumber numberWithInt:ARTAuthorizationFailed] with:[ARTErrorInfo createWithCode:ARTStateAuthorizationFailed message:@"Connection has been closed"]]; + [_authorizationEmitter emit:[ARTEvent newWithAuthorizationState:ARTAuthorizationFailed] with:[ARTErrorInfo createWithCode:ARTStateAuthorizationFailed message:@"Connection has been closed"]]; break; case ARTRealtimeFailed: 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]; + [_authorizationEmitter emit:[ARTEvent newWithAuthorizationState:ARTAuthorizationFailed] with:stateChange.reason]; break; case ARTRealtimeDisconnected: { if (!_startedReconnection) { @@ -396,43 +393,45 @@ - (_Nullable dispatch_semaphore_t)transitionSideEffects:(ARTConnectionStateChang }]; } if ([[NSDate date] timeIntervalSinceDate:_startedReconnection] >= _connectionStateTtl) { - [self transition:ARTRealtimeSuspended withErrorInfo:stateChange.reason]; + artDispatchScheduled(0, _eventQueue, ^{ + [self transition:ARTRealtimeSuspended withErrorInfo:stateChange.reason]; + }); return nil; } [self.transport close]; - self.transport.delegate = nil; _transport = nil; [stateChange setRetryIn:self.options.disconnectedRetryTimeout]; - waitingForCurrentEventSemaphore = [self unlessStateChangesBefore:stateChange.retryIn do:^{ + stateChangeEventListener = [self unlessStateChangesBefore:stateChange.retryIn do:^{ [weakSelf transition:ARTRealtimeConnecting]; + _connectionRetryFromDisconnectedListener = nil; }]; + _connectionRetryFromDisconnectedListener = stateChangeEventListener; break; } case ARTRealtimeSuspended: { [self.transport close]; - self.transport.delegate = nil; _transport = nil; [stateChange setRetryIn:self.options.suspendedRetryTimeout]; - waitingForCurrentEventSemaphore = [self unlessStateChangesBefore:stateChange.retryIn do:^{ + stateChangeEventListener = [self unlessStateChangesBefore:stateChange.retryIn do:^{ [weakSelf transition:ARTRealtimeConnecting]; + _connectionRetryFromSuspendedListener = nil; }]; - [_authorizationEmitter emit:[NSNumber numberWithInt:ARTAuthorizationFailed] with:[ARTErrorInfo createWithCode:ARTStateAuthorizationFailed message:@"Connection has been suspended"]]; + _connectionRetryFromSuspendedListener = stateChangeEventListener; + [_authorizationEmitter emit:[ARTEvent newWithAuthorizationState:ARTAuthorizationFailed] with:[ARTErrorInfo createWithCode:ARTStateAuthorizationFailed message:@"Connection has been suspended"]]; break; } case ARTRealtimeConnected: { _fallbacks = nil; - __GENERIC(NSArray, ARTQueuedMessage *) *pending = self.pendingMessages; - _pendingMessages = [[NSMutableArray alloc] init]; - for (ARTQueuedMessage *queued in pending) { - [self send:queued.msg callback:^(ARTStatus *__art_nonnull status) { - for (id cb in queued.cbs) { - ((void(^)(ARTStatus *__art_nonnull))cb)(status); - } - }]; + if (stateChange.reason) { + ARTStatus *status = [ARTStatus state:ARTStateError info:[stateChange.reason copy]]; + [self failPendingMessages:status]; + } + else { + [self resendPendingMessages]; } - [_connectedEventEmitter emit:[NSNull null] with:nil]; - [_authorizationEmitter emit:[NSNumber numberWithInt:ARTAuthorizationSucceeded] with:nil]; + [_connectedEventEmitter emit:nil with:nil]; + [_authorizationEmitter emit:[ARTEvent newWithAuthorizationState:ARTAuthorizationSucceeded] with:nil]; break; } case ARTRealtimeInitialized: @@ -448,59 +447,40 @@ - (_Nullable dispatch_semaphore_t)transitionSideEffects:(ARTConnectionStateChang } } } else if (![self shouldQueueEvents]) { - [self failQueuedMessages:status]; ARTStatus *channelStatus = status; if (!channelStatus) { channelStatus = [self defaultError]; } + [self failQueuedMessages:channelStatus]; // For every Channel - for (ARTRealtimeChannel* channel in self.channels) { - switch (channel.state) { - case ARTRealtimeChannelInitialized: - case ARTRealtimeChannelAttaching: - case ARTRealtimeChannelAttached: - case ARTRealtimeChannelFailed: - if (stateChange.current == ARTRealtimeClosing) { - //do nothing. Closed state is coming. - } - else if (stateChange.current == ARTRealtimeClosed) { - [channel detachChannel:[ARTStatus state:ARTStateOk]]; - } - else if (stateChange.current == ARTRealtimeSuspended) { - [channel setSuspended:channelStatus]; - } - else { - [channel setFailed:channelStatus]; - } - break; - default: - [channel setSuspended:channelStatus]; - break; + for (ARTRealtimeChannel *channel in self.channels) { + if (stateChange.current == ARTRealtimeClosing) { + //do nothing. Closed state is coming. + } + else if (stateChange.current == ARTRealtimeClosed) { + [channel detachChannel:[ARTStatus state:ARTStateOk]]; + } + else if (stateChange.current == ARTRealtimeSuspended) { + [channel setSuspended:channelStatus]; + } + else { + [channel setFailed:channelStatus]; } } } [self.connection emit:stateChange.event with:stateChange]; - return waitingForCurrentEventSemaphore; -} - -- (_Nonnull dispatch_semaphore_t)unlessStateChangesBefore:(NSTimeInterval)deadline do:(void(^)())callback __attribute__((warn_unused_result)) { - // Defer until next event loop execution so that any event emitted in the current one doesn't cancel the timeout. - ARTRealtimeConnectionState state = self.connection.state; - // Timeout should be dispatched after current event. - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0), _eventQueue, ^{ - // Wait until the current event is done. - dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); - if (state != self.connection.state) { - // Already changed; Ignore the timer. - return; + return stateChangeEventListener; +} + +- (ARTEventListener *)unlessStateChangesBefore:(NSTimeInterval)deadline do:(void(^)())callback __attribute__((warn_unused_result)) { + return [[_internalEventEmitter once:^(ARTConnectionStateChange *change) { + // Any state change cancels the timeout. + }] setTimer:deadline onTimeout:^{ + if (callback) { + callback(); } - [_internalEventEmitter timed:[_internalEventEmitter once:^(ARTConnectionStateChange *change) { - // Any state change cancels the timeout. - }] deadline:deadline onTimeout:callback]; - }); - return semaphore; + }]; } - (void)onHeartbeat { @@ -509,7 +489,7 @@ - (void)onHeartbeat { NSString *msg = [NSString stringWithFormat:@"ARTRealtime received a ping when in state %@", ARTRealtimeConnectionStateToStr(self.connection.state)]; [self.logger warn:@"R:%p %@", self, msg]; } - [_pingEventEmitter emit:[NSNull null] with:nil]; + [_pingEventEmitter emit:nil with:nil]; } - (void)onConnected:(ARTProtocolMessage *)message { @@ -592,7 +572,7 @@ - (void)onClosed { [self transition:ARTRealtimeClosed]; break; default: - NSAssert(false, @"Invalid Realtime state transitioning to Closed: expected Closing or Closed"); + NSAssert(false, @"Invalid Realtime state transitioning to Closed: expected Closing or Closed, has %@", ARTRealtimeConnectionStateToStr(self.connection.state)); break; } } @@ -606,7 +586,7 @@ - (void)onAuth { [self transportReconnectWithRenewedToken]; break; default: - [self.logger error:@"Invalid Realtime state: expected Connecting or Connected"]; + [self.logger error:@"Invalid Realtime state: expected Connecting or Connected, has %@", ARTRealtimeConnectionStateToStr(self.connection.state)]; break; } } @@ -626,7 +606,27 @@ - (void)onError:(ARTProtocolMessage *)message { } } +- (void)cancelTimers { + [_connectionRetryFromSuspendedListener stopTimer]; + _connectionRetryFromSuspendedListener = nil; + [_connectionRetryFromDisconnectedListener stopTimer]; + _connectionRetryFromDisconnectedListener = nil; + // Cancel connecting scheduled work + [_connectingTimeoutListener stopTimer]; + _connectingTimeoutListener = nil; + // Cancel auth scheduled work + artDispatchCancel(_authenitcatingTimeoutWork); + _authenitcatingTimeoutWork = nil; +} + - (void)onConnectionTimeOut { + // Cancel connecting scheduled work + [_connectingTimeoutListener stopTimer]; + _connectingTimeoutListener = nil; + // Cancel auth scheduled work + artDispatchCancel(_authenitcatingTimeoutWork); + _authenitcatingTimeoutWork = nil; + ARTErrorInfo *error; if (self.auth.authorizing && (self.options.authUrl || self.options.authCallback)) { error = [ARTErrorInfo createWithCode:ARTCodeErrorAuthConfiguredProviderFailure status:ARTStateConnectionFailed message:@"timed out"]; @@ -684,8 +684,10 @@ - (void)transportConnectForcingNewToken:(BOOL)forceNewToken { // Transport instance couldn't exist anymore when `authorize` completes or reaches time out. __weak __typeof(self) weakSelf = self; - dispatch_block_t work = artDispatchScheduled([ARTDefault realtimeRequestTimeout], ^{ + // Schedule timeout handler + _authenitcatingTimeoutWork = artDispatchScheduled([ARTDefault realtimeRequestTimeout], _eventQueue, ^{ [weakSelf onConnectionTimeOut]; + // FIXME: should cancel the auth request as well. }); // Deactivate use of `ARTAuthDelegate`: `authorize` should complete without waiting for a CONNECTED state. @@ -694,7 +696,9 @@ - (void)transportConnectForcingNewToken:(BOOL)forceNewToken { @try { [self.auth authorize:nil options:options callback:^(ARTTokenDetails *tokenDetails, NSError *error) { // Cancel scheduled work - artDispatchCancel(work); + artDispatchCancel(_authenitcatingTimeoutWork); + _authenitcatingTimeoutWork = nil; + // It's still valid? switch ([[weakSelf connection] state]) { case ARTRealtimeClosing: @@ -715,7 +719,7 @@ - (void)transportConnectForcingNewToken:(BOOL)forceNewToken { _transport = [[_transportClass alloc] initWithRest:self.rest options:self.options resumeKey:_transport.resumeKey connectionSerial:_transport.connectionSerial]; _transport.delegate = self; } - [[weakSelf getTransport] connectWithToken:tokenDetails.token]; + [[weakSelf transport] connectWithToken:tokenDetails.token]; }]; } @finally { @@ -837,6 +841,24 @@ - (void)send:(ARTProtocolMessage *)msg callback:(void (^)(ARTStatus *))cb { } } +- (void)resendPendingMessages { + NSArray *pms = self.pendingMessages; + self.pendingMessages = [NSMutableArray array]; + for (ARTQueuedMessage *pendingMessage in pms) { + [self send:pendingMessage.msg callback:^(ARTStatus *status) { + pendingMessage.cb(status); + }]; + } +} + +- (void)failPendingMessages:(ARTStatus *)status { + NSArray *pms = self.pendingMessages; + self.pendingMessages = [NSMutableArray array]; + for (ARTQueuedMessage *pendingMessage in pms) { + pendingMessage.cb(status); + } +} + - (void)sendQueuedMessages { NSArray *qms = self.queuedMessages; self.queuedMessages = [NSMutableArray array]; @@ -846,11 +868,11 @@ - (void)sendQueuedMessages { } } -- (void)failQueuedMessages:(ARTStatus *)error { +- (void)failQueuedMessages:(ARTStatus *)status { NSArray *qms = self.queuedMessages; self.queuedMessages = [NSMutableArray array]; for (ARTQueuedMessage *message in qms) { - message.cb(error); + message.cb(status); } } @@ -917,7 +939,6 @@ - (void)nack:(ARTProtocolMessage *)message { // we can handle it gracefully by only processing the // relevant portion of the response count -= (int)(self.pendingMessageStartSerial - serial); - serial = self.pendingMessageStartSerial; } NSRange nackRange; diff --git a/Source/ARTRealtimeChannel+Private.h b/Source/ARTRealtimeChannel+Private.h index e20f4ad4b..dce8d5f4e 100644 --- a/Source/ARTRealtimeChannel+Private.h +++ b/Source/ARTRealtimeChannel+Private.h @@ -23,9 +23,10 @@ ART_ASSUME_NONNULL_BEGIN @property (readwrite, strong, nonatomic) NSMutableArray *queuedMessages; @property (readwrite, strong, nonatomic, art_nullable) NSString *attachSerial; @property (readonly, getter=getClientId) NSString *clientId; -@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSNumber *, ARTChannelStateChange *) *statesEventEmitter; -@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSString *, ARTMessage *) *messagesEventEmitter; -@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, NSNumber *, ARTPresenceMessage *) *presenceEventEmitter; +@property (readonly, strong, nonatomic) ARTEventEmitter *internalEventEmitter; +@property (readonly, strong, nonatomic) ARTEventEmitter *statesEventEmitter; +@property (readonly, strong, nonatomic) ARTEventEmitter, ARTMessage *> *messagesEventEmitter; +@property (readonly, strong, nonatomic) ARTEventEmitter *presenceEventEmitter; @property (readwrite, strong, nonatomic) ARTPresenceMap *presenceMap; @property (readwrite, assign, nonatomic) ARTPresenceAction lastPresenceAction; diff --git a/Source/ARTRealtimeChannel.h b/Source/ARTRealtimeChannel.h index 2d972d350..896b9735d 100644 --- a/Source/ARTRealtimeChannel.h +++ b/Source/ARTRealtimeChannel.h @@ -31,14 +31,14 @@ ART_ASSUME_NONNULL_BEGIN - (void)detach; - (void)detach:(art_nullable void (^)(ARTErrorInfo *__art_nullable))callback; -- (__GENERIC(ARTEventListener, ARTMessage *) *__art_nullable)subscribe:(void (^)(ARTMessage *message))callback; -- (__GENERIC(ARTEventListener, ARTMessage *) *__art_nullable)subscribeWithAttachCallback:(art_nullable void (^)(ARTErrorInfo *__art_nullable))onAttach callback:(void (^)(ARTMessage *message))cb; -- (__GENERIC(ARTEventListener, ARTMessage *) *__art_nullable)subscribe:(NSString *)name callback:(void (^)(ARTMessage *message))cb; -- (__GENERIC(ARTEventListener, ARTMessage *) *__art_nullable)subscribe:(NSString *)name onAttach:(art_nullable void (^)(ARTErrorInfo *__art_nullable))onAttach callback:(void (^)(ARTMessage *message))cb; +- (ARTEventListener *__art_nullable)subscribe:(void (^)(ARTMessage *message))callback; +- (ARTEventListener *__art_nullable)subscribeWithAttachCallback:(art_nullable void (^)(ARTErrorInfo *__art_nullable))onAttach callback:(void (^)(ARTMessage *message))cb; +- (ARTEventListener *__art_nullable)subscribe:(NSString *)name callback:(void (^)(ARTMessage *message))cb; +- (ARTEventListener *__art_nullable)subscribe:(NSString *)name onAttach:(art_nullable void (^)(ARTErrorInfo *__art_nullable))onAttach callback:(void (^)(ARTMessage *message))cb; - (void)unsubscribe; -- (void)unsubscribe:(__GENERIC(ARTEventListener, ARTMessage *) *__art_nullable)listener; -- (void)unsubscribe:(NSString *)name listener:(__GENERIC(ARTEventListener, ARTMessage *) *__art_nullable)listener; +- (void)unsubscribe:(ARTEventListener *__art_nullable)listener; +- (void)unsubscribe:(NSString *)name listener:(ARTEventListener *__art_nullable)listener; - (BOOL)history:(ARTRealtimeHistoryQuery *__art_nullable)query callback:(void(^)(__GENERIC(ARTPaginatedResult, ARTMessage *) *__art_nullable result, ARTErrorInfo *__art_nullable error))callback error:(NSError *__art_nullable *__art_nullable)errorPtr; @@ -46,4 +46,11 @@ ART_EMBED_INTERFACE_EVENT_EMITTER(ARTChannelEvent, ARTChannelStateChange *) @end +#pragma mark - ARTEvent + +@interface ARTEvent (ChannelEvent) +- (instancetype)initWithChannelEvent:(ARTChannelEvent)value; ++ (instancetype)newWithChannelEvent:(ARTChannelEvent)value; +@end + ART_ASSUME_NONNULL_END diff --git a/Source/ARTRealtimeChannel.m b/Source/ARTRealtimeChannel.m index d03584674..2f893055f 100644 --- a/Source/ARTRealtimeChannel.m +++ b/Source/ARTRealtimeChannel.m @@ -26,13 +26,14 @@ #import "ARTDefault.h" #import "ARTRest.h" #import "ARTClientOptions.h" +#import "ARTTypes.h" @interface ARTRealtimeChannel () { ARTRealtimePresence *_realtimePresence; CFRunLoopTimerRef _attachTimer; CFRunLoopTimerRef _detachTimer; - __GENERIC(ARTEventEmitter, NSNull *, ARTErrorInfo *) *_attachedEventEmitter; - __GENERIC(ARTEventEmitter, NSNull *, ARTErrorInfo *) *_detachedEventEmitter; + __GENERIC(ARTEventEmitter, ARTEvent *, ARTErrorInfo *) *_attachedEventEmitter; + __GENERIC(ARTEventEmitter, ARTEvent *, ARTErrorInfo *) *_detachedEventEmitter; } @end @@ -58,6 +59,7 @@ - (instancetype)initWithRealtime:(ARTRealtime *)realtime andName:(NSString *)nam _presenceEventEmitter = [[ARTEventEmitter alloc] initWithQueue:_eventQueue]; _attachedEventEmitter = [[ARTEventEmitter alloc] initWithQueue:_eventQueue]; _detachedEventEmitter = [[ARTEventEmitter alloc] initWithQueue:_eventQueue]; + _internalEventEmitter = [[ARTEventEmitter alloc] initWithQueue:_eventQueue]; } return self; } @@ -199,7 +201,7 @@ - (void)publishProtocolMessage:(ARTProtocolMessage *)pm callback:(void (^)(ARTSt } else { [self addToQueue:pm callback:queuedCallback]; - [self.realtime.internalEventEmitter once:[NSNumber numberWithInteger:ARTRealtimeConnected] callback:^(ARTConnectionStateChange *__art_nullable change) { + [self.realtime.internalEventEmitter once:[ARTEvent newWithConnectionEvent:ARTRealtimeConnectionEventConnected] callback:^(ARTConnectionStateChange *__art_nullable change) { [weakSelf sendQueuedMessages]; }]; } @@ -223,16 +225,27 @@ - (void)addToQueue:(ARTProtocolMessage *)msg callback:(void (^)(ARTStatus *))cb } - (void)sendMessage:(ARTProtocolMessage *)pm callback:(void (^)(ARTStatus *))cb { - __block BOOL gotFailure = false; NSString *oldConnectionId = self.realtime.connection.id; + ARTProtocolMessage *pmSent = (ARTProtocolMessage *)[pm copy]; + + __block BOOL connectionStateHasChanged = false; __block ARTEventListener *listener = [self.realtime.internalEventEmitter on:^(ARTConnectionStateChange *stateChange) { - if (!(stateChange.current == ARTRealtimeClosed || stateChange.current == ARTRealtimeFailed - || (stateChange.current == ARTRealtimeConnected && ![oldConnectionId isEqual:self.realtime.connection.id] /* connection state lost */))) { + if (!(stateChange.current == ARTRealtimeClosed || + stateChange.current == ARTRealtimeFailed || + (stateChange.current == ARTRealtimeConnected && ![oldConnectionId isEqual:self.realtime.connection.id] /* connection state lost */))) { + // Ok return; } - gotFailure = true; + connectionStateHasChanged = true; [self.realtime.internalEventEmitter off:listener]; if (!cb) return; + + if (stateChange.current == ARTRealtimeClosed && stateChange.reason == nil && pmSent.action == ARTProtocolMessageClose) { + // No ack/nack is expected. + cb([ARTStatus state:ARTStateOk]); + return; + } + ARTErrorInfo *reason = stateChange.reason ? stateChange.reason : [ARTErrorInfo createWithCode:0 message:@"connection broken before receiving publishing acknowledgement."]; cb([ARTStatus state:ARTStateError info:reason]); }]; @@ -242,8 +255,9 @@ - (void)sendMessage:(ARTProtocolMessage *)pm callback:(void (^)(ARTStatus *))cb } [self.realtime send:pm callback:^(ARTStatus *status) { + // New state change can occur before receiving publishing acknowledgement. [self.realtime.internalEventEmitter off:listener]; - if (cb && !gotFailure) cb(status); + if (cb && !connectionStateHasChanged) cb(status); }]; } @@ -257,11 +271,11 @@ - (void)throwOnDisconnectedOrFailed { } } -- (ARTEventListener *)subscribe:(void (^)(ARTMessage * _Nonnull))callback { +- (ARTEventListener *)subscribe:(void (^)(ARTMessage * _Nonnull))callback { return [self subscribeWithAttachCallback:nil callback:callback]; } -- (ARTEventListener *)subscribeWithAttachCallback:(void (^)(ARTErrorInfo * _Nullable))onAttach callback:(void (^)(ARTMessage * _Nonnull))cb { +- (ARTEventListener *)subscribeWithAttachCallback:(void (^)(ARTErrorInfo * _Nullable))onAttach callback:(void (^)(ARTMessage * _Nonnull))cb { if (self.state == ARTRealtimeChannelFailed) { if (onAttach) onAttach([ARTErrorInfo createWithCode:0 message:@"attempted to subscribe while channel is in Failed state."]); return nil; @@ -270,11 +284,11 @@ - (void)throwOnDisconnectedOrFailed { return [self.messagesEventEmitter on:cb]; } -- (ARTEventListener *)subscribe:(NSString *)name callback:(void (^)(ARTMessage * _Nonnull))cb { +- (ARTEventListener *)subscribe:(NSString *)name callback:(void (^)(ARTMessage * _Nonnull))cb { return [self subscribe:name onAttach:nil callback:cb]; } -- (ARTEventListener *)subscribe:(NSString *)name onAttach:(void (^)(ARTErrorInfo * _Nullable))onAttach callback:(void (^)(ARTMessage * _Nonnull))cb { +- (ARTEventListener *)subscribe:(NSString *)name onAttach:(void (^)(ARTErrorInfo * _Nullable))onAttach callback:(void (^)(ARTMessage * _Nonnull))cb { if (self.state == ARTRealtimeChannelFailed) { if (onAttach) onAttach([ARTErrorInfo createWithCode:0 message:@"attempted to subscribe while channel is in Failed state."]); return nil; @@ -287,47 +301,45 @@ - (void)unsubscribe { [self.messagesEventEmitter off]; } -- (void)unsubscribe:(ARTEventListener *)listener { +- (void)unsubscribe:(ARTEventListener *)listener { [self.messagesEventEmitter off:listener]; } -- (void)unsubscribe:(NSString *)name listener:(ARTEventListener *)listener { +- (void)unsubscribe:(NSString *)name listener:(ARTEventListener *)listener { [self.messagesEventEmitter off:name listener:listener]; } -- (__GENERIC(ARTEventListener, ARTChannelStateChange *) *)on:(ARTChannelEvent)event callback:(void (^)(ARTChannelStateChange *))cb { - return [self.statesEventEmitter on:[NSNumber numberWithInt:event] callback:cb]; +- (ARTEventListener *)on:(ARTChannelEvent)event callback:(void (^)(ARTChannelStateChange *))cb { + return [self.statesEventEmitter on:[ARTEvent newWithChannelEvent:event] callback:cb]; } -- (__GENERIC(ARTEventListener, ARTChannelStateChange *) *)on:(void (^)(ARTChannelStateChange *))cb { +- (ARTEventListener *)on:(void (^)(ARTChannelStateChange *))cb { return [self.statesEventEmitter on:cb]; } -- (__GENERIC(ARTEventListener, ARTChannelStateChange *) *)once:(ARTChannelEvent)event callback:(void (^)(ARTChannelStateChange *))cb { - return [self.statesEventEmitter once:[NSNumber numberWithInt:event] callback:cb]; +- (ARTEventListener *)once:(ARTChannelEvent)event callback:(void (^)(ARTChannelStateChange *))cb { + return [self.statesEventEmitter once:[ARTEvent newWithChannelEvent:event] callback:cb]; } -- (__GENERIC(ARTEventListener, ARTChannelStateChange *) *)once:(void (^)(ARTChannelStateChange *))cb { +- (ARTEventListener *)once:(void (^)(ARTChannelStateChange *))cb { return [self.statesEventEmitter once:cb]; } - (void)off { [self.statesEventEmitter off]; } + - (void)off:(ARTChannelEvent)event listener:listener { - [self.statesEventEmitter off:[NSNumber numberWithInt:event] listener:listener]; + [self.statesEventEmitter off:[ARTEvent newWithChannelEvent:event] listener:listener]; } -- (void)off:(__GENERIC(ARTEventListener, ARTChannelStateChange *) *)listener { +- (void)off:(ARTEventListener *)listener { [self.statesEventEmitter off:listener]; } - (void)emit:(ARTChannelEvent)event with:(ARTChannelStateChange *)data { - [self.statesEventEmitter emit:[NSNumber numberWithInt:event] with:data]; -} - -- (ARTEventListener *)timed:(ARTEventListener *)listener deadline:(NSTimeInterval)deadline onTimeout:(void (^)())onTimeout { - return [self.statesEventEmitter timed:listener deadline:deadline onTimeout:onTimeout]; + [self.statesEventEmitter emit:[ARTEvent newWithChannelEvent:event] with:data]; + [self.internalEventEmitter emit:[ARTEvent newWithChannelEvent:event] with:data]; } - (void)transition:(ARTRealtimeChannelState)state status:(ARTStatus *)status { @@ -341,14 +353,14 @@ - (void)transition:(ARTRealtimeChannelState)state status:(ARTStatus *)status { switch (state) { case ARTRealtimeChannelSuspended: - [_attachedEventEmitter emit:[NSNull null] with:status.errorInfo]; + [_attachedEventEmitter emit:nil with:status.errorInfo]; break; case ARTRealtimeChannelDetached: [self.presenceMap failsSync:status.errorInfo]; break; case ARTRealtimeChannelFailed: - [_attachedEventEmitter emit:[NSNull null] with:status.errorInfo]; - [_detachedEventEmitter emit:[NSNull null] with:status.errorInfo]; + [_attachedEventEmitter emit:nil with:status.errorInfo]; + [_detachedEventEmitter emit:nil with:status.errorInfo]; [self.presenceMap failsSync:status.errorInfo]; break; default: @@ -359,25 +371,18 @@ - (void)transition:(ARTRealtimeChannelState)state status:(ARTStatus *)status { } - (void)dealloc { - if (self.statesEventEmitter) { - [self.statesEventEmitter off]; - } + [_statesEventEmitter off]; + [_internalEventEmitter off]; } -- (void)unlessStateChangesBefore:(NSTimeInterval)deadline do:(void(^)())callback { - // Defer until next event loop execution so that any event emitted in the current - // one doesn't cancel the timeout. - ARTRealtimeChannelState state = self.state; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0), _eventQueue, ^{ - if (state != self.state) { - // Already changed; do nothing. - return; +- (ARTEventListener *)unlessStateChangesBefore:(NSTimeInterval)deadline do:(void(^)())callback { + return [[self.internalEventEmitter once:^(ARTChannelStateChange *stateChange) { + // Any state change cancels the timeout. + }] setTimer:deadline onTimeout:^{ + if (callback) { + callback(); } - // FIXME: should not use the global listener for internal purpose - [self timed:[self once:^(ARTChannelStateChange *stateChange) { - // Any state change cancels the timeout. - }] deadline:deadline onTimeout:callback]; - }); + }]; } /** @@ -425,9 +430,15 @@ - (ARTRealtimeChannelState)state { } - (void)setAttached:(ARTProtocolMessage *)message { - if (self.state == ARTRealtimeChannelFailed) { - return; + switch (self.state) { + case ARTRealtimeChannelDetaching: + case ARTRealtimeChannelFailed: + // Ignore + return; + default: + break; } + self.attachSerial = message.channelSerial; if (message.hasPresence) { @@ -455,7 +466,7 @@ - (void)setAttached:(ARTProtocolMessage *)message { ARTStatus *status = message.error ? [ARTStatus state:ARTStateError info:message.error] : [ARTStatus state:ARTStateOk]; [self transition:ARTRealtimeChannelAttached status:status]; - [_attachedEventEmitter emit:[NSNull null] with:nil]; + [_attachedEventEmitter emit:nil with:nil]; } - (void)setDetached:(ARTProtocolMessage *)message { @@ -483,7 +494,7 @@ - (void)setDetached:(ARTProtocolMessage *)message { ARTErrorInfo *errorInfo = message.error ? message.error : [ARTErrorInfo createWithCode:0 message:@"channel has detached"]; ARTStatus *reason = [ARTStatus state:ARTStateNotAttached info:errorInfo]; [self detachChannel:reason]; - [_detachedEventEmitter emit:[NSNull null] with:nil]; + [_detachedEventEmitter emit:nil with:nil]; } - (void)detachChannel:(ARTStatus *)status { @@ -504,11 +515,12 @@ - (void)setSuspended:(ARTStatus *)status retryIn:(NSTimeInterval)retryTimeout { [self failQueuedMessages:status]; [self transition:ARTRealtimeChannelSuspended status:status]; __weak __typeof(self) weakSelf = self; - [self unlessStateChangesBefore:retryTimeout do:^{ + [[self unlessStateChangesBefore:retryTimeout do:^{ [weakSelf reattach:^(ARTErrorInfo *errorInfo) { - [weakSelf setSuspended:[ARTStatus state:ARTStateError info:errorInfo]]; + ARTStatus *status = [ARTStatus state:ARTStateError info:errorInfo]; + [weakSelf setSuspended:status]; } withReason:nil]; - }]; + }] startTimer]; } - (void)onMessage:(ARTProtocolMessage *)message { @@ -594,7 +606,7 @@ - (void)onSync:(ARTProtocolMessage *)message { } - (void)broadcastPresence:(ARTPresenceMessage *)pm { - [self.presenceEventEmitter emit:[NSNumber numberWithUnsignedInteger:pm.action] with:pm]; + [self.presenceEventEmitter emit:[ARTEvent newWithPresenceAction:pm.action] with:pm]; } - (void)onError:(ARTProtocolMessage *)msg { @@ -673,16 +685,14 @@ - (void)attachAfterChecks:(void (^)(ARTErrorInfo * _Nullable))callback { attachMessage.action = ARTProtocolMessageAttach; attachMessage.channel = self.name; - __block BOOL timeouted = false; - [self.realtime send:attachMessage callback:nil]; - [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ - timeouted = true; + [[self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ + // Timeout ARTErrorInfo *errorInfo = [ARTErrorInfo createWithCode:ARTStateAttachTimedOut message:@"attach timed out"]; ARTStatus *status = [ARTStatus state:ARTStateAttachTimedOut info:errorInfo]; [self setSuspended:status]; - }]; + }] startTimer]; if (![self.realtime shouldQueueEvents]) { ARTEventListener *reconnectedListener = [self.realtime.connectedEventEmitter once:^(NSNull *n) { @@ -750,21 +760,19 @@ - (void)detachAfterChecks:(void (^)(ARTErrorInfo * _Nullable))callback { detachMessage.action = ARTProtocolMessageDetach; detachMessage.channel = self.name; - __block BOOL timeouted = false; - [self.realtime send:detachMessage callback:nil]; - [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ - timeouted = true; + [[self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ + // Timeout ARTErrorInfo *errorInfo = [ARTErrorInfo createWithCode:ARTStateDetachTimedOut message:@"detach timed out"]; ARTStatus *status = [ARTStatus state:ARTStateDetachTimedOut info:errorInfo]; [self transition:ARTRealtimeChannelAttached status:status]; - [_detachedEventEmitter emit:[NSNull null] with:errorInfo]; - }]; + [_detachedEventEmitter emit:nil with:errorInfo]; + }] startTimer]; if (![self.realtime shouldQueueEvents]) { ARTEventListener *reconnectedListener = [self.realtime.connectedEventEmitter once:^(NSNull *n) { - // Disconnected and connected while attaching, re-detach. + // Disconnected and connected while detaching, re-detach. [self detachAfterChecks:callback]; }]; [_detachedEventEmitter once:^(ARTErrorInfo *err) { @@ -843,3 +851,17 @@ - (void)map:(ARTPresenceMap *)map shouldReenterLocalMember:(ARTPresenceMessage * } @end + +#pragma mark - ARTEvent + +@implementation ARTEvent (ChannelEvent) + +- (instancetype)initWithChannelEvent:(ARTChannelEvent)value { + return [self initWithString:[NSString stringWithFormat:@"ARTChannelEvent%@",ARTChannelEventToStr(value)]]; +} + ++ (instancetype)newWithChannelEvent:(ARTChannelEvent)value { + return [[self alloc] initWithChannelEvent:value]; +} + +@end diff --git a/Source/ARTRealtimePresence.h b/Source/ARTRealtimePresence.h index 7d8d7c8da..1a67e42f0 100644 --- a/Source/ARTRealtimePresence.h +++ b/Source/ARTRealtimePresence.h @@ -45,14 +45,14 @@ ART_ASSUME_NONNULL_BEGIN - (void)leaveClient:(NSString *)clientId data:(id __art_nullable)data; - (void)leaveClient:(NSString *)clientId data:(id __art_nullable)data callback:(art_nullable void (^)(ARTErrorInfo *__art_nullable))cb; -- (__GENERIC(ARTEventListener, ARTPresenceMessage *) *__art_nullable)subscribe:(void (^)(ARTPresenceMessage *message))callback; -- (__GENERIC(ARTEventListener, ARTPresenceMessage *) *__art_nullable)subscribeWithAttachCallback:(art_nullable void (^)(ARTErrorInfo *__art_nullable))onAttach callback:(void (^)(ARTPresenceMessage *message))cb; -- (__GENERIC(ARTEventListener, ARTPresenceMessage *) *__art_nullable)subscribe:(ARTPresenceAction)action callback:(void (^)(ARTPresenceMessage *message))cb; -- (__GENERIC(ARTEventListener, ARTPresenceMessage *) *__art_nullable)subscribe:(ARTPresenceAction)action onAttach:(art_nullable void (^)(ARTErrorInfo *__art_nullable))onAttach callback:(void (^)(ARTPresenceMessage *message))cb; +- (ARTEventListener *__art_nullable)subscribe:(void (^)(ARTPresenceMessage *message))callback; +- (ARTEventListener *__art_nullable)subscribeWithAttachCallback:(art_nullable void (^)(ARTErrorInfo *__art_nullable))onAttach callback:(void (^)(ARTPresenceMessage *message))cb; +- (ARTEventListener *__art_nullable)subscribe:(ARTPresenceAction)action callback:(void (^)(ARTPresenceMessage *message))cb; +- (ARTEventListener *__art_nullable)subscribe:(ARTPresenceAction)action onAttach:(art_nullable void (^)(ARTErrorInfo *__art_nullable))onAttach callback:(void (^)(ARTPresenceMessage *message))cb; - (void)unsubscribe; -- (void)unsubscribe:(__GENERIC(ARTEventListener, ARTPresenceMessage *) *)listener; -- (void)unsubscribe:(ARTPresenceAction)action listener:(__GENERIC(ARTEventListener, ARTPresenceMessage *) *)listener; +- (void)unsubscribe:(ARTEventListener *)listener; +- (void)unsubscribe:(ARTPresenceAction)action listener:(ARTEventListener *)listener; - (void)history:(void(^)(__GENERIC(ARTPaginatedResult, ARTPresenceMessage *) *__art_nullable result, ARTErrorInfo *__art_nullable error))callback; - (BOOL)history:(ARTRealtimeHistoryQuery *__art_nullable)query callback:(void(^)(__GENERIC(ARTPaginatedResult, ARTPresenceMessage *) *__art_nullable result, ARTErrorInfo *__art_nullable error))callback error:(NSError *__art_nullable *__art_nullable)errorPtr; diff --git a/Source/ARTRealtimePresence.m b/Source/ARTRealtimePresence.m index 91c47c230..67c4998dd 100644 --- a/Source/ARTRealtimePresence.m +++ b/Source/ARTRealtimePresence.m @@ -196,11 +196,11 @@ - (BOOL)getSyncComplete { return _channel.presenceMap.syncComplete; } -- (ARTEventListener *)subscribe:(void (^)(ARTPresenceMessage * _Nonnull))callback { +- (ARTEventListener *)subscribe:(void (^)(ARTPresenceMessage * _Nonnull))callback { return [self subscribeWithAttachCallback:nil callback:callback]; } -- (ARTEventListener *)subscribeWithAttachCallback:(void (^)(ARTErrorInfo * _Nullable))onAttach callback:(void (^)(ARTPresenceMessage * _Nonnull))cb { +- (ARTEventListener *)subscribeWithAttachCallback:(void (^)(ARTErrorInfo * _Nullable))onAttach callback:(void (^)(ARTPresenceMessage * _Nonnull))cb { if (_channel.state == ARTRealtimeChannelFailed) { if (onAttach) onAttach([ARTErrorInfo createWithCode:0 message:@"attempted to subscribe while channel is in Failed state."]); return nil; @@ -209,29 +209,29 @@ - (BOOL)getSyncComplete { return [_channel.presenceEventEmitter on:cb]; } -- (ARTEventListener *)subscribe:(ARTPresenceAction)action callback:(void (^)(ARTPresenceMessage * _Nonnull))cb { +- (ARTEventListener *)subscribe:(ARTPresenceAction)action callback:(void (^)(ARTPresenceMessage * _Nonnull))cb { return [self subscribe:action onAttach:nil callback:cb]; } -- (ARTEventListener *)subscribe:(ARTPresenceAction)action onAttach:(void (^)(ARTErrorInfo * _Nullable))onAttach callback:(void (^)(ARTPresenceMessage * _Nonnull))cb { +- (ARTEventListener *)subscribe:(ARTPresenceAction)action onAttach:(void (^)(ARTErrorInfo * _Nullable))onAttach callback:(void (^)(ARTPresenceMessage * _Nonnull))cb { if (_channel.state == ARTRealtimeChannelFailed) { if (onAttach) onAttach([ARTErrorInfo createWithCode:0 message:@"attempted to subscribe while channel is in Failed state."]); return nil; } [_channel attach:onAttach]; - return [_channel.presenceEventEmitter on:[NSNumber numberWithUnsignedInteger:action] callback:cb]; + return [_channel.presenceEventEmitter on:[ARTEvent newWithPresenceAction:action] callback:cb]; } - (void)unsubscribe { [_channel.presenceEventEmitter off]; } -- (void)unsubscribe:(ARTEventListener *)listener { +- (void)unsubscribe:(ARTEventListener *)listener { [_channel.presenceEventEmitter off:listener]; } -- (void)unsubscribe:(ARTPresenceAction)action listener:(ARTEventListener *)listener { - [_channel.presenceEventEmitter off:[NSNumber numberWithUnsignedInteger:action] listener:listener]; +- (void)unsubscribe:(ARTPresenceAction)action listener:(ARTEventListener *)listener { + [_channel.presenceEventEmitter off:[ARTEvent newWithPresenceAction:action] listener:listener]; } @end diff --git a/Source/ARTRealtimeTransport.h b/Source/ARTRealtimeTransport.h index 0da99049c..dc8731c08 100644 --- a/Source/ARTRealtimeTransport.h +++ b/Source/ARTRealtimeTransport.h @@ -66,12 +66,12 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeTransportState) { @protocol ARTRealtimeTransport -- (instancetype)initWithRest:(ARTRest *)rest options:(ARTClientOptions *)options resumeKey:(NSString *)resumeKey connectionSerial:(NSNumber *)connectionSerial; +- (instancetype)initWithRest:(ARTRest *)rest options:(ARTClientOptions *)options resumeKey:(nullable NSString *)resumeKey connectionSerial:(nullable NSNumber *)connectionSerial; @property (readonly, strong, nonatomic) NSString *resumeKey; @property (readonly, strong, nonatomic) NSNumber *connectionSerial; @property (readonly, assign, nonatomic) ARTRealtimeTransportState state; -@property (readwrite, weak, nonatomic) id delegate; +@property (nullable, readwrite, strong, nonatomic) id delegate; - (void)send:(ARTProtocolMessage *)msg; - (void)receive:(ARTProtocolMessage *)msg; diff --git a/Source/ARTRest+Private.h b/Source/ARTRest+Private.h index c99f65b2f..2de208be9 100644 --- a/Source/ARTRest+Private.h +++ b/Source/ARTRest+Private.h @@ -25,7 +25,7 @@ ART_ASSUME_NONNULL_BEGIN // Private prioritized host for testing only (overrides the current `restHost`) @property (readwrite, strong, nonatomic, art_nullable) NSString *prioritizedHost; -@property (nonatomic, strong) id httpExecutor; +@property (nonatomic, weak) id httpExecutor; @property (nonatomic, readonly, getter=getBaseUrl) NSURL *baseUrl; @@ -44,7 +44,7 @@ ART_ASSUME_NONNULL_BEGIN - (void)executeRequest:(NSMutableURLRequest *)request withAuthOption:(ARTAuthentication)authOption completion:(void (^)(NSHTTPURLResponse *__art_nullable, NSData *__art_nullable, NSError *__art_nullable))callback; -- (id)internetIsUp:(void (^)(BOOL isUp))cb; +- (nullable id)internetIsUp:(void (^)(BOOL isUp))cb; @end diff --git a/Source/ARTRestPresence.h b/Source/ARTRestPresence.h index d7c9150a2..71efa5e39 100644 --- a/Source/ARTRestPresence.h +++ b/Source/ARTRestPresence.h @@ -17,8 +17,8 @@ ART_ASSUME_NONNULL_BEGIN @interface ARTPresenceQuery : NSObject @property (nonatomic, readwrite) NSUInteger limit; -@property (nonatomic, strong, readwrite) NSString *clientId; -@property (nonatomic, strong, readwrite) NSString *connectionId; +@property (nullable, nonatomic, strong, readwrite) NSString *clientId; +@property (nullable, nonatomic, strong, readwrite) NSString *connectionId; - (instancetype)init; - (instancetype)initWithClientId:(NSString *__art_nullable)clientId connectionId:(NSString *__art_nullable)connectionId; diff --git a/Source/ARTStats.h b/Source/ARTStats.h index 60670f090..4806d60b9 100644 --- a/Source/ARTStats.h +++ b/Source/ARTStats.h @@ -44,9 +44,9 @@ typedef NS_ENUM(NSUInteger, ARTStatsGranularity) { @property (readonly, strong, nonatomic) ARTStatsMessageCount *presence; - (instancetype)init UNAVAILABLE_ATTRIBUTE; -- (instancetype)initWithAll:(ARTStatsMessageCount *)all - messages:(ARTStatsMessageCount *)messages - presence:(ARTStatsMessageCount *)presence; +- (instancetype)initWithAll:(nullable ARTStatsMessageCount *)all + messages:(nullable ARTStatsMessageCount *)messages + presence:(nullable ARTStatsMessageCount *)presence; + (instancetype)empty; @@ -60,10 +60,10 @@ typedef NS_ENUM(NSUInteger, ARTStatsGranularity) { @property (readonly, strong, nonatomic) ARTStatsMessageTypes *webhook; - (instancetype)init UNAVAILABLE_ATTRIBUTE; -- (instancetype)initWithAll:(ARTStatsMessageTypes *)all - realtime:(ARTStatsMessageTypes *)realtime - rest:(ARTStatsMessageTypes *)rest - webhook:(ARTStatsMessageTypes *)webhook; +- (instancetype)initWithAll:(nullable ARTStatsMessageTypes *)all + realtime:(nullable ARTStatsMessageTypes *)realtime + rest:(nullable ARTStatsMessageTypes *)rest + webhook:(nullable ARTStatsMessageTypes *)webhook; + (instancetype)empty; @@ -95,9 +95,9 @@ typedef NS_ENUM(NSUInteger, ARTStatsGranularity) { @property (readonly, strong, nonatomic) ARTStatsResourceCount *tls; - (instancetype)init UNAVAILABLE_ATTRIBUTE; -- (instancetype)initWithAll:(ARTStatsResourceCount *)all - plain:(ARTStatsResourceCount *)plain - tls:(ARTStatsResourceCount *)tls; +- (instancetype)initWithAll:(nullable ARTStatsResourceCount *)all + plain:(nullable ARTStatsResourceCount *)plain + tls:(nullable ARTStatsResourceCount *)tls; + (instancetype)empty; diff --git a/Source/ARTStatus.h b/Source/ARTStatus.h index 7e7c53c38..fb3b8f1fe 100644 --- a/Source/ARTStatus.h +++ b/Source/ARTStatus.h @@ -39,7 +39,7 @@ typedef CF_ENUM(NSUInteger, ARTCodeError) { // FIXME: check hard coded errors ARTCodeErrorAPIKeyMissing = 80001, ARTCodeErrorConnectionTimedOut = 80014, - ARTCodeErrorAuthConfiguredProviderFailure = 80019 + ARTCodeErrorAuthConfiguredProviderFailure = 80019, }; ART_ASSUME_NONNULL_BEGIN diff --git a/Source/ARTStatus.m b/Source/ARTStatus.m index c852d5c1b..d262d7ebb 100644 --- a/Source/ARTStatus.m +++ b/Source/ARTStatus.m @@ -51,7 +51,7 @@ - (NSInteger)getStatus { } - (NSString *)description { - return [NSString stringWithFormat:@"ARTErrorInfo with code %ld, message: %@", (long)self.statusCode, self.message]; + return [NSString stringWithFormat:@"ARTErrorInfo with code %ld, message: %@", (long)self.code, self.message]; } @end @@ -92,4 +92,4 @@ -(void) setErrorInfo:(ARTErrorInfo *)errorInfo { _errorInfo = errorInfo; } -@end \ No newline at end of file +@end diff --git a/Source/ARTTokenParams.h b/Source/ARTTokenParams.h index b53127967..16e170cd0 100644 --- a/Source/ARTTokenParams.h +++ b/Source/ARTTokenParams.h @@ -32,14 +32,14 @@ ART_ASSUME_NONNULL_BEGIN /** A clientId to associate with this token. */ -@property (art_nullable, nonatomic, copy, readwrite) NSString *clientId; +@property (nullable, nonatomic, copy, readwrite) NSString *clientId; /** Timestamp (in millis since the epoch) of this request. Timestamps, in conjunction with the nonce, are used to prevent n requests from being replayed. */ -@property (art_nullable, nonatomic, copy, readwrite) NSDate *timestamp; +@property (nullable, nonatomic, copy, readwrite) NSDate *timestamp; -@property (nonatomic, readonly, strong) NSString *nonce; +@property (nullable, nonatomic, readonly, strong) NSString *nonce; - (instancetype)init; - (instancetype)initWithClientId:(NSString *__art_nullable)clientId; diff --git a/Source/ARTTypes.h b/Source/ARTTypes.h index 29c6b1a48..42b7c083c 100644 --- a/Source/ARTTypes.h +++ b/Source/ARTTypes.h @@ -9,6 +9,7 @@ #import #import "CompatibilityMacros.h" #import "ARTStatus.h" +#import "ARTEventEmitter.h" @class ARTStatus; @class ARTHttpResponse; @@ -179,6 +180,9 @@ NSString *generateNonce(); - (NSDictionary *__art_nullable)toJSON:(NSError *__art_nullable *__art_nullable)error; @end +@interface NSString (ARTEventIdentification) +@end + @interface NSString (ARTJsonCompatible) @end diff --git a/Source/ARTTypes.m b/Source/ARTTypes.m index e8c048e1c..700e788b7 100644 --- a/Source/ARTTypes.m +++ b/Source/ARTTypes.m @@ -142,6 +142,16 @@ - (NSString *)description { @end +#pragma mark - ARTEventIdentification + +@implementation NSString (ARTEventIdentification) + +- (NSString *)identification { + return self; +} + +@end + #pragma mark - ARTJsonCompatible @implementation NSString (ARTJsonCompatible) diff --git a/Source/ARTURLSessionServerTrust.h b/Source/ARTURLSessionServerTrust.h index a5ab366e6..2a4c3e225 100644 --- a/Source/ARTURLSessionServerTrust.h +++ b/Source/ARTURLSessionServerTrust.h @@ -15,6 +15,8 @@ ART_ASSUME_NONNULL_BEGIN - (void)get:(NSURLRequest *)request completion:(void (^)(NSHTTPURLResponse *__art_nullable, NSData *__art_nullable, NSError *__art_nullable))callback; +- (void)finishTasksAndInvalidate; + @end ART_ASSUME_NONNULL_END diff --git a/Source/ARTURLSessionServerTrust.m b/Source/ARTURLSessionServerTrust.m index 5bbde01d5..93a97058e 100644 --- a/Source/ARTURLSessionServerTrust.m +++ b/Source/ARTURLSessionServerTrust.m @@ -23,6 +23,10 @@ - (instancetype)init { return self; } +- (void)finishTasksAndInvalidate { + [_session finishTasksAndInvalidate]; +} + - (void)get:(NSURLRequest *)request completion:(void (^)(NSHTTPURLResponse *__art_nullable, NSData *__art_nullable, NSError *__art_nullable))callback { NSURLSessionDataTask *task = [_session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { callback((NSHTTPURLResponse *)response, data, error); diff --git a/Source/ARTWebSocketTransport.h b/Source/ARTWebSocketTransport.h index baa222ea4..73b36a52f 100644 --- a/Source/ARTWebSocketTransport.h +++ b/Source/ARTWebSocketTransport.h @@ -23,7 +23,6 @@ ART_ASSUME_NONNULL_BEGIN @property (readonly, strong, nonatomic) NSString *resumeKey; @property (readonly, strong, nonatomic) NSNumber *connectionSerial; -@property (readwrite, weak, nonatomic) id delegate; @end diff --git a/Source/ARTWebSocketTransport.m b/Source/ARTWebSocketTransport.m index f53dde6d5..d5da8dcbf 100644 --- a/Source/ARTWebSocketTransport.m +++ b/Source/ARTWebSocketTransport.m @@ -37,6 +37,7 @@ }; @implementation ARTWebSocketTransport { + id _delegate; ARTRealtimeTransportState _state; /** A dispatch queue for firing the events. @@ -44,6 +45,8 @@ @implementation ARTWebSocketTransport { _Nonnull dispatch_queue_t _workQueue; } +@synthesize delegate = _delegate; + - (instancetype)initWithRest:(ARTRest *)rest options:(ARTClientOptions *)options resumeKey:(NSString *)resumeKey connectionSerial:(NSNumber *)connectionSerial { self = [super init]; if (self) { @@ -65,6 +68,7 @@ - (void)dealloc { [self.logger verbose:__FILE__ line:__LINE__ message:@"R:%p WS:%p dealloc", _delegate, self]; self.websocket.delegate = nil; self.websocket = nil; + self.delegate = nil; } - (void)send:(ARTProtocolMessage *)msg { @@ -185,6 +189,7 @@ - (void)sendPing { } - (void)close { + self.delegate = nil; if (!_websocket) return; self.websocket.delegate = nil; [self.websocket closeWithCode:ARTWsCloseNormal reason:@"Normal Closure"]; @@ -192,6 +197,7 @@ - (void)close { } - (void)abort:(ARTStatus *)reason { + self.delegate = nil; if (!_websocket) return; self.websocket.delegate = nil; if (reason.errorInfo) { diff --git a/Spec/RealtimeClient.swift b/Spec/RealtimeClient.swift index 02e87b1ab..f051d5aab 100644 --- a/Spec/RealtimeClient.swift +++ b/Spec/RealtimeClient.swift @@ -49,6 +49,7 @@ class RealtimeClient: QuickSpec { options.clientId = "client_string" let client = ARTRealtime(options: options) + defer { client.close() } waitUntil(timeout: testTimeout) { done in client.connection.on { stateChange in @@ -68,7 +69,6 @@ class RealtimeClient: QuickSpec { } } } - client.close() } //RTC1a @@ -90,6 +90,7 @@ class RealtimeClient: QuickSpec { // First connection let client = ARTRealtime(options: options) + defer { client.close() } waitUntil(timeout: testTimeout) { done in client.connection.on { stateChange in @@ -113,6 +114,7 @@ class RealtimeClient: QuickSpec { // New connection let newClient = ARTRealtime(options: options) + defer { newClient.close() } waitUntil(timeout: testTimeout) { done in newClient.connection.on { stateChange in @@ -131,8 +133,6 @@ class RealtimeClient: QuickSpec { } } } - newClient.close() - client.close() } //RTC1d @@ -141,8 +141,10 @@ class RealtimeClient: QuickSpec { options.realtimeHost = "fake.ably.io" options.autoConnect = false let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } - waitUntil(timeout: testTimeout) { done in + waitUntil(timeout: testTimeout * 2) { done in + let partialDone = AblyTests.splitDone(2, done: done) client.connection.once(.Connecting) { _ in guard let webSocketTransport = client.transport as? ARTWebSocketTransport else { fail("Transport should be of type ARTWebSocketTransport"); done() @@ -150,11 +152,18 @@ class RealtimeClient: QuickSpec { } expect(webSocketTransport.websocketURL).toNot(beNil()) expect(webSocketTransport.websocketURL?.host).to(equal("fake.ably.io")) - done() + partialDone() + } + client.connection.once(.Failed) { stateChange in + guard let reason = stateChange?.reason else { + fail("Reason is nil"); done(); return + } + expect(reason.code) == Int(CFNetworkErrors.CFHostErrorUnknown.rawValue) + expect(reason.message).to(contain("kCFErrorDomainCFNetwork")) + partialDone() } client.connect() } - expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Disconnected), timeout: testTimeout) } //RTC1e @@ -195,7 +204,6 @@ class RealtimeClient: QuickSpec { }) expect(client.channels.get("test")).toNot(beNil()) - client.close() } context("Auth object") { @@ -204,9 +212,8 @@ class RealtimeClient: QuickSpec { it("should provide access to the Auth object") { let options = AblyTests.commonAppSetup() let client = ARTRealtime(options: options) - + defer { client.close() } expect(client.auth.options.key).to(equal(options.key)) - client.close() } // RTC4a @@ -214,6 +221,7 @@ class RealtimeClient: QuickSpec { let options = AblyTests.commonAppSetup() options.clientId = "client_string" let client = ARTRealtime(options: options) + defer { client.close() } waitUntil(timeout: testTimeout) { done in client.connection.on { stateChange in @@ -233,7 +241,6 @@ class RealtimeClient: QuickSpec { } } } - client.close() } } @@ -244,6 +251,7 @@ class RealtimeClient: QuickSpec { // RTC5a it("should present an async interface") { let client = ARTRealtime(options: AblyTests.commonAppSetup()) + defer { client.close() } // Async waitUntil(timeout: testTimeout) { done in // Proxy from `client.rest.stats` @@ -252,12 +260,12 @@ class RealtimeClient: QuickSpec { done() }) } - client.close() } // RTC5b it("should accept all the same params as RestClient") { let client = ARTRealtime(options: AblyTests.commonAppSetup()) + defer { client.close() } var paginatedResult: ARTPaginatedResult? // Realtime @@ -287,7 +295,6 @@ class RealtimeClient: QuickSpec { expect(paginated.items.count).to(equal(paginatedResult!.items.count)) }) } - client.close() } } @@ -295,6 +302,7 @@ class RealtimeClient: QuickSpec { // RTC6a it("should present an async interface") { let client = ARTRealtime(options: AblyTests.commonAppSetup()) + defer { client.close() } // Async waitUntil(timeout: testTimeout) { done in // Proxy from `client.rest.time` @@ -303,7 +311,6 @@ class RealtimeClient: QuickSpec { done() }) } - client.close() } } @@ -313,6 +320,7 @@ class RealtimeClient: QuickSpec { options.suspendedRetryTimeout = 6.0 let client = ARTRealtime(options: options) + defer { client.close() } var start: NSDate? var endInterval: UInt? @@ -336,7 +344,9 @@ class RealtimeClient: QuickSpec { if start == nil { // Force - client.onSuspended() + delay(0) { + client.onSuspended() + } } case .Suspended: start = NSDate() @@ -345,7 +355,6 @@ class RealtimeClient: QuickSpec { } } } - client.close() if let secs = endInterval { expect(secs).to(beLessThanOrEqualTo(UInt(options.suspendedRetryTimeout))) @@ -943,7 +952,9 @@ class RealtimeClient: QuickSpec { } let hook = client.auth.testSuite_injectIntoMethodAfter(#selector(client.auth.authorize(_:options:callback:))) { - client.close() + delay(0) { + client.close() + } } defer { hook.remove() } @@ -1214,14 +1225,12 @@ class RealtimeClient: QuickSpec { // https://github.com/ably/ably-ios/issues/577 it("background behaviour") { - let options = AblyTests.commonAppSetup() - options.autoConnect = false - let realtime = ARTRealtime(options: options) waitUntil(timeout: testTimeout) { done in NSURLSession.sharedSession().dataTaskWithURL(NSURL(string:"https://ably.io")!) { _ in - realtime.connect() + let realtime = ARTRealtime(options: AblyTests.commonAppSetup()) realtime.channels.get("foo").attach { error in expect(error).to(beNil()) + realtime.close() done() } }.resume() diff --git a/Spec/RealtimeClientChannel.swift b/Spec/RealtimeClientChannel.swift index b020d3634..7111ad263 100644 --- a/Spec/RealtimeClientChannel.swift +++ b/Spec/RealtimeClientChannel.swift @@ -558,12 +558,15 @@ class RealtimeClientChannel: QuickSpec { } } - client.simulateSuspended(beforeSuspension: { done in + waitUntil(timeout: testTimeout) { done in channel.once(.Suspended) { stateChange in expect(stateChange?.reason).to(beNil()) done() } - }) + delay(0) { + client.onSuspended() + } + } expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) @@ -2074,15 +2077,20 @@ class RealtimeClientChannel: QuickSpec { let channel = client.channels.get("test") var resultClientId: String? - channel.subscribe() { message in - resultClientId = message.clientId - } let message = ARTMessage(name: nil, data: "message") message.clientId = "client_string" - channel.publish([message]) { errorInfo in - expect(errorInfo).to(beNil()) + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.subscribe() { message in + resultClientId = message.clientId + partialDone() + } + channel.publish([message]) { errorInfo in + expect(errorInfo).to(beNil()) + partialDone() + } } expect(resultClientId).toEventually(equal(message.clientId), timeout: testTimeout) @@ -2302,12 +2310,17 @@ class RealtimeClientChannel: QuickSpec { let message = ARTMessage(name: nil, data: "message", clientId: options.clientId!) var resultClientId: String? - channel.subscribe() { message in - resultClientId = message.clientId - } - channel.publish([message]) { error in - expect(error).to(beNil()) + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + channel.subscribe() { message in + resultClientId = message.clientId + partialDone() + } + channel.publish([message]) { error in + expect(error).to(beNil()) + partialDone() + } } expect(resultClientId).toEventually(equal(message.clientId), timeout: testTimeout) @@ -2347,12 +2360,14 @@ class RealtimeClientChannel: QuickSpec { let channel = client.channels.get("test") let message = ARTMessage(name: nil, data: "message", clientId: "john") waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) channel.subscribe() { received in expect(received.clientId).to(equal(message.clientId)) - done() + partialDone() } channel.publish([message]) { error in expect(error).to(beNil()) + partialDone() } } } @@ -2385,15 +2400,16 @@ class RealtimeClientChannel: QuickSpec { let channel = client.channels.get("test") waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) channel.subscribe { message in expect(message.name).to(equal("event")) expect(message.data as? NSObject).to(equal("data")) expect(message.clientId).to(equal("foo")) - done() + partialDone() } - channel.publish("event", data: "data", clientId: "foo") { errorInfo in expect(errorInfo).to(beNil()) + partialDone() } } } diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index 669d99fa5..cad4b5909 100644 --- a/Spec/RealtimeClientConnection.swift +++ b/Spec/RealtimeClientConnection.swift @@ -257,12 +257,16 @@ class RealtimeClientConnection: QuickSpec { } case .Connected: if alreadyClosed { - client.onSuspended() + delay(0) { + client.onSuspended() + } } else if alreadyDisconnected { client.close() } else { events += [state] - client.onDisconnected() + delay(0) { + client.onDisconnected() + } } case .Disconnected: events += [state] @@ -537,13 +541,6 @@ class RealtimeClientConnection: QuickSpec { } } - class TotalReach { - // Easy way to create an atomic var - static var shared = 0 - // This prevents others from using the default '()' initializer - private init() {} - } - // RTN5 it("basic operations should work simultaneously") { let options = AblyTests.commonAppSetup() @@ -577,11 +574,12 @@ class RealtimeClientConnection: QuickSpec { } } + var i = 0 waitUntil(timeout: testTimeout) { done in // Sends 50 messages from different clients to the same channel // 50 messages for 50 clients = 50*50 total messages // echo is off, so we need to subtract one message per client - let partialDone = AblyTests.splitDone(max*max - max, done: done) + let total = max*max - max for client in disposable { let channel = client.channels.get(channelName) expect(channel.state).to(equal(ARTRealtimeChannelState.Attached)) @@ -589,7 +587,10 @@ class RealtimeClientConnection: QuickSpec { channel.subscribe { message in expect(message.data as? String).to(equal("message_string")) sync.lock() - partialDone() + i += 1 + if i == total { + done() + } sync.unlock() } @@ -890,7 +891,6 @@ class RealtimeClientConnection: QuickSpec { expect(error).toNot(beNil()) partialDone() } - } expect(client.msgSerial) == 5 @@ -1054,8 +1054,11 @@ class RealtimeClientConnection: QuickSpec { waitUntil(timeout: testTimeout) { done in channel.attach() { error in expect(error).to(beNil()) - channel.publish(nil, data: "message", callback: { errorInfo in - expect(errorInfo).toNot(beNil()) + channel.publish(nil, data: "message", callback: { error in + guard let error = error else { + fail("Error is nil"); done(); return + } + expect(error.message).to(contain("connection broken before receiving publishing acknowledgement")) done() }) // Wait until the message is pushed to Ably first @@ -1064,6 +1067,13 @@ class RealtimeClientConnection: QuickSpec { } } } + + // This verifies that the pending message as been released and the publish callback is called only once! + waitUntil(timeout: testTimeout) { done in + delay(1.0) { + done() + } + } } it("connection state enters FAILED") { @@ -1111,26 +1121,38 @@ class RealtimeClientConnection: QuickSpec { let transport = client.transport as! TestProxyTransport transport.actionsIgnored += [.Ack, .Nack] - channel.attach() - expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) - - var gotPublishedCallback = false - channel.publish(nil, data: "message", callback: { errorInfo in - expect(errorInfo).toNot(beNil()) - gotPublishedCallback = true - }) - - let oldConnectionId = client.connection.id! - // Wait until the message is pushed to Ably first waitUntil(timeout: testTimeout) { done in - delay(1.0) { done() } + channel.attach() { _ in + done() + } } - client.simulateLostConnectionAndState() - expect(gotPublishedCallback).to(beFalse()) - expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) - expect(client.connection.id).toNot(equal(oldConnectionId)) - expect(gotPublishedCallback).to(beTrue()) + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(3, done: done) + + channel.publish(nil, data: "message") { error in + guard let error = error else { + fail("Error is nil"); return + } + expect(error.code) == 80008 + expect(error.message).to(contain("Unable to recover connection")) + partialDone() + } + + let oldConnectionId = client.connection.id! + + // Wait until the message is pushed to Ably first + delay(1.0) { + client.connection.once(.Disconnected) { _ in + partialDone() + } + client.connection.once(.Connected) { stateChange in + expect(client.connection.id).toNot(equal(oldConnectionId)) + partialDone() + } + client.simulateLostConnectionAndState() + } + } } } @@ -1181,6 +1203,7 @@ class RealtimeClientConnection: QuickSpec { } var ids = [String]() let max = 25 + let sync = NSLock() waitUntil(timeout: testTimeout) { done in for _ in 1...max { @@ -1197,11 +1220,13 @@ class RealtimeClientConnection: QuickSpec { return } expect(ids).toNot(contain(connectionId)) - ids.append(connectionId) + sync.lock() + ids.append(connectionId) if ids.count == max { done() } + sync.unlock() currentConnection.off() currentConnection.close() @@ -1845,9 +1870,11 @@ class RealtimeClientConnection: QuickSpec { let options = AblyTests.commonAppSetup() options.autoConnect = false options.authCallback = { tokenParams, callback in - callback(getTestTokenDetails(key: options.key, capability: tokenParams.capability, ttl: tokenParams.ttl), nil) + delay(0) { + callback(getTestTokenDetails(key: options.key, capability: tokenParams.capability, ttl: tokenParams.ttl), nil) + } } - let tokenTtl = 1.0 + let tokenTtl = 3.0 options.token = getTestToken(key: options.key, ttl: tokenTtl) let client = ARTRealtime(options: options) @@ -1857,61 +1884,45 @@ class RealtimeClientConnection: QuickSpec { client.close() } - // Let the token expire waitUntil(timeout: testTimeout) { done in - delay(tokenTtl) { - done() - } - } - - var transport: TestProxyTransport! + // Let the token expire + client.connection.once(.Disconnected) { stateChange in + guard let reason = stateChange?.reason else { + fail("Token error is missing"); done(); return + } + expect(reason.code) == 40142 - waitUntil(timeout: testTimeout) { done in - client.connection.on { stateChange in - let stateChange = stateChange! - let state = stateChange.current - let errorInfo = stateChange.reason - switch state { - case .Connected: - expect(errorInfo).to(beNil()) - // New token - expect(client.auth.tokenDetails!.token).toNot(equal(options.token)) - done() - case .Failed, .Disconnected, .Suspended: - fail("Should not emit error (\(errorInfo))") - done() - default: - break + client.connection.on { stateChange in + let stateChange = stateChange! + let state = stateChange.current + let errorInfo = stateChange.reason + switch state { + case .Connected: + expect(errorInfo).to(beNil()) + // New token + expect(client.auth.tokenDetails!.token).toNot(equal(options.token)) + done() + case .Failed, .Suspended: + fail("Should not emit error (\(errorInfo))") + done() + default: + break + } } } client.connect() - transport = client.transport as! TestProxyTransport } - - let failures = transport.protocolMessagesReceived.filter({ $0.action == .Error }) - - if failures.count != 1 { - fail("Should have only one connection request fail") - return - } - - expect(failures[0].error!.code).to(equal(40142)) //Token expired } it("should transition to Failed when the token renewal fails") { let options = AblyTests.commonAppSetup() options.autoConnect = false - let tokenTtl = 1.0 + let tokenTtl = 3.0 let tokenDetails = getTestTokenDetails(key: options.key, capability: nil, ttl: tokenTtl)! options.token = tokenDetails.token options.authCallback = { tokenParams, callback in - callback(tokenDetails, nil) // Return the same expired token again. - } - - // Let the token expire - waitUntil(timeout: testTimeout) { done in - delay(tokenTtl) { - done() + delay(0) { + callback(tokenDetails, nil) // Return the same expired token again. } } @@ -1922,41 +1933,28 @@ class RealtimeClientConnection: QuickSpec { client.close() } - client.connect() - let firstTransport = client.transport as! TestProxyTransport - expect(client.transport).toEventuallyNot(beIdenticalTo(firstTransport), timeout: testTimeout) - let newTransport = client.transport as! TestProxyTransport - waitUntil(timeout: testTimeout) { done in - client.connection.on { stateChange in - let stateChange = stateChange! - let state = stateChange.current - let errorInfo = stateChange.reason - switch state { - case .Connected: - fail("Should not be connected") - done() - case .Failed, .Disconnected, .Suspended: - guard let errorInfo = errorInfo else { - fail("ErrorInfo is nil"); done(); return - } - expect(errorInfo.code).to(equal(40142)) //Token expired - done() - default: - break + let partialDone = AblyTests.splitDone(3, done: done) + client.connection.once(.Connected) { stateChange in + expect(stateChange?.reason).to(beNil()) + partialDone() + } + client.connection.once(.Disconnected) { stateChange in + guard let reason = stateChange?.reason else { + fail("Reason is nil"); done(); return; } + expect(reason.code) == 40142 + partialDone() } + client.connection.once(.Failed) { stateChange in + guard let reason = stateChange?.reason else { + fail("Reason is nil"); done(); return; + } + expect(reason.code) == 40142 + partialDone() + } + client.connect() } - - let failures = firstTransport.protocolMessagesReceived.filter({ $0.action == .Error }) + newTransport.protocolMessagesReceived.filter({ $0.action == .Error }) - - if failures.count != 2 { - fail("Should have two connection request fail") - return - } - - expect(failures[0].error!.code).to(equal(40142)) - expect(failures[1].error!.code).to(equal(40142)) } it("should transition to Failed state because the token is invalid and not renewable") { @@ -2052,6 +2050,10 @@ class RealtimeClientConnection: QuickSpec { options.autoConnect = false let expectedTime = 3.0 + options.authCallback = { tokenParams, completion in + // Ignore `completion` closure to force a time out + } + let previousConnectionStateTtl = ARTDefault.connectionStateTtl() defer { ARTDefault.setConnectionStateTtl(previousConnectionStateTtl) } ARTDefault.setConnectionStateTtl(expectedTime) @@ -2099,11 +2101,51 @@ class RealtimeClientConnection: QuickSpec { // RTN14e it("connection state has been in the DISCONNECTED state for more than the default connectionStateTtl should change the state to SUSPENDED") { let options = AblyTests.commonAppSetup() - options.realtimeHost = "10.255.255.1" //non-routable IP address options.disconnectedRetryTimeout = 0.1 options.suspendedRetryTimeout = 0.5 options.autoConnect = false - let expectedTime = 1.0 + let expectedTime: NSTimeInterval = 1.0 + + options.authCallback = { _ in + // Force a timeout + } + + let previousConnectionStateTtl = ARTDefault.connectionStateTtl() + defer { ARTDefault.setConnectionStateTtl(previousConnectionStateTtl) } + ARTDefault.setConnectionStateTtl(expectedTime) + + let previousRealtimeRequestTimeout = ARTDefault.realtimeRequestTimeout() + defer { ARTDefault.setRealtimeRequestTimeout(previousRealtimeRequestTimeout) } + ARTDefault.setRealtimeRequestTimeout(0.1) + + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + + waitUntil(timeout: testTimeout) { done in + client.connection.on(.Suspended) { stateChange in + expect(client.connection.errorReason!.message).to(contain("timed out")) + + let start = NSDate() + client.connection.once(.Connecting) { stateChange in + let end = NSDate() + expect(end.timeIntervalSinceDate(start)).to(beCloseTo(options.suspendedRetryTimeout, within: 0.5)) + done() + } + } + client.connect() + } + } + + it("on CLOSE the connection should stop connection retries") { + let options = AblyTests.commonAppSetup() + options.disconnectedRetryTimeout = 0.1 + options.suspendedRetryTimeout = 0.5 + options.autoConnect = false + let expectedTime: NSTimeInterval = 1.0 + + options.authCallback = { _ in + // Force a timeout + } let previousConnectionStateTtl = ARTDefault.connectionStateTtl() defer { ARTDefault.setConnectionStateTtl(previousConnectionStateTtl) } @@ -2129,6 +2171,18 @@ class RealtimeClientConnection: QuickSpec { } client.connect() } + + client.close() + + // Check if the connection gets closed + waitUntil(timeout: testTimeout) { done in + client.connection.once(.Connecting) { stateChange in + fail("Should be closing the connection"); done(); return + } + delay(2.0) { + done() + } + } } } @@ -2173,22 +2227,20 @@ class RealtimeClientConnection: QuickSpec { } waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) client1.connection.once(.Connecting) { _ in expect(client1.resuming).to(beTrue()) - done() + partialDone() } - } - - waitUntil(timeout: testTimeout) { done in client1.connection.once(.Connected) { _ in expect(client1.resuming).to(beFalse()) expect(client1.connection.id).toNot(equal(firstConnection.id)) expect(client1.connection.key).toNot(equal(firstConnection.key)) - done() + partialDone() } } - - expect(states).to(equal([.Connecting, .Connected, .Disconnected, .Connecting, .Connected])) + + expect(states).toEventually(equal([.Connecting, .Connected, .Disconnected, .Connecting, .Connected]), timeout: testTimeout) } // RTN15b @@ -2541,7 +2593,7 @@ class RealtimeClientConnection: QuickSpec { // RTN15f it("ACK and NACK responses for published messages can only ever be received on the transport connection on which those messages were sent") { let options = AblyTests.commonAppSetup() - options.disconnectedRetryTimeout = 0.5 + options.disconnectedRetryTimeout = 1.5 let client = AblyTests.newRealtime(options) defer { client.dispose(); client.close() } let channel = client.channels.get("test") @@ -2562,7 +2614,9 @@ class RealtimeClientConnection: QuickSpec { fail("Shouldn't be called") } } - client.onDisconnected() + delay(0) { + client.onDisconnected() + } client.connection.once(.Connected) { _ in resumed = true channel.testSuite_injectIntoMethodBefore(#selector(channel.sendQueuedMessages)) { @@ -2764,18 +2818,20 @@ class RealtimeClientConnection: QuickSpec { defer { client.dispose(); client.close() } let channel = client.channels.get("test") waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) client.connection.once(.Connected) { _ in expect(client.connection.serial).to(equal(-1)) expect(client.connection.recoveryKey).to(equal("\(client.connection.key!):\(client.connection.serial)")) } channel.publish(nil, data: "message") { error in expect(error).to(beNil()) + partialDone() } channel.subscribe { message in expect(message.data as? String).to(equal("message")) expect(client.connection.serial).to(equal(0)) channel.unsubscribe() - done() + partialDone() } } } @@ -3338,34 +3394,30 @@ class RealtimeClientConnection: QuickSpec { // RTN19a it("should resend any ProtocolMessage that is awaiting a ACK/NACK") { let options = AblyTests.commonAppSetup() - options.logLevel = .Debug options.disconnectedRetryTimeout = 0.1 let client = AblyTests.newRealtime(options) defer { client.dispose(); client.close() } let channel = client.channels.get("test") let transport = client.transport as! TestProxyTransport - expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) - waitUntil(timeout: testTimeout) { done in channel.attach { _ in done() } } waitUntil(timeout: testTimeout) { done in - transport.ignoreSends = true channel.publish(nil, data: "message") { error in expect(error).to(beNil()) guard let newTransport = client.transport as? TestProxyTransport else { fail("Transport is nil"); done(); return } + expect(newTransport).toNot(beIdenticalTo(transport)) + expect(transport.protocolMessagesSent.filter{ $0.action == .Message }).to(haveCount(1)) expect(transport.protocolMessagesReceived.filter{ $0.action == .Connected }).to(haveCount(1)) expect(newTransport.protocolMessagesReceived.filter{ $0.action == .Connected }).to(haveCount(1)) - expect(transport.protocolMessagesSent.filter{ $0.action == .Message }).to(haveCount(0)) - expect(transport.protocolMessagesSentIgnored.filter{ $0.action == .Message }).to(haveCount(1)) + expect(transport.protocolMessagesReceived.filter{ $0.action == .Connected }).to(haveCount(1)) expect(newTransport.protocolMessagesSent.filter{ $0.action == .Message }).to(haveCount(1)) done() } - transport.ignoreSends = false client.onDisconnected() } } @@ -3404,7 +3456,7 @@ class RealtimeClientConnection: QuickSpec { // RTN19b it("should resent the DETACH message if there are any pending channels") { let options = AblyTests.commonAppSetup() - options.disconnectedRetryTimeout = 0.1 + options.disconnectedRetryTimeout = 1.0 let client = AblyTests.newRealtime(options) defer { client.dispose(); client.close() } let channel = client.channels.get("test") diff --git a/Spec/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index 9ae631b62..05fdcc216 100644 --- a/Spec/RealtimeClientPresence.swift +++ b/Spec/RealtimeClientPresence.swift @@ -626,10 +626,12 @@ class RealtimeClientPresence: QuickSpec { let channel2 = client2.channels.get(channel1.name) waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) channel2.presence.enterClient("Client 2", data: nil) { error in expect(error).to(beNil()) expect(channel2.queuedMessages).to(haveCount(0)) expect(channel2.state).to(equal(ARTRealtimeChannelState.Attached)) + partialDone() } channel2.presence.subscribe(.Enter) { _ in if channel2.presence.syncComplete { @@ -639,7 +641,7 @@ class RealtimeClientPresence: QuickSpec { expect(channel2.presenceMap.members).to(haveCount(1)) } channel2.presence.unsubscribe() - done() + partialDone() } expect(channel2.queuedMessages).to(haveCount(1)) diff --git a/Spec/RestClient.swift b/Spec/RestClient.swift index e257c03ca..1076ee176 100644 --- a/Spec/RestClient.swift +++ b/Spec/RestClient.swift @@ -1312,9 +1312,9 @@ class RestClient: QuickSpec { // https://github.com/ably/ably-ios/issues/577 it("background behaviour") { let options = AblyTests.commonAppSetup() - let rest = ARTRest(options: options) waitUntil(timeout: testTimeout) { done in NSURLSession.sharedSession().dataTaskWithURL(NSURL(string:"https://ably.io")!) { _ in + let rest = ARTRest(options: options) rest.channels.get("foo").history { _ in done() } diff --git a/Spec/TestUtilities.swift b/Spec/TestUtilities.swift index 55c7f7e51..e671f1e17 100644 --- a/Spec/TestUtilities.swift +++ b/Spec/TestUtilities.swift @@ -111,8 +111,7 @@ class AblyTests { let (responseData, responseError, _) = NSURLSessionServerTrustSync().get(request) if let error = responseError { - XCTFail(error.localizedDescription) - return options + fatalError(error.localizedDescription) } testApplication = JSON(data: responseData!) @@ -279,6 +278,9 @@ class NSURLSessionServerTrustSync: NSObject, NSURLSessionDelegate, NSURLSessionT responseError = error httpResponse = response } + else if let error = error { + responseError = error + } requestCompleted = true } task.resume() diff --git a/Spec/Utilities.swift b/Spec/Utilities.swift index e5e0beb3b..02db3d62c 100644 --- a/Spec/Utilities.swift +++ b/Spec/Utilities.swift @@ -22,8 +22,8 @@ class Utilities: QuickSpec { var receivedBarOnce: Int? var receivedAll: Int? var receivedAllOnce: Int? - var listenerFoo1: ARTEventListener? - var listenerAll: ARTEventListener? + weak var listenerFoo1: ARTEventListener? + weak var listenerAll: ARTEventListener? beforeEach { eventEmitter = ARTEventEmitter() @@ -57,7 +57,7 @@ class Utilities: QuickSpec { eventEmitter.emit("qux", with:789) - expect(receivedAll).to(equal(789)) + expect(receivedAll).toEventually(equal(789), timeout: testTimeout) } it("should only call once listeners once for its event") { @@ -98,9 +98,9 @@ class Utilities: QuickSpec { } it("should remove the timeout") { - eventEmitter.timed(listenerFoo1!, deadline: 0.1, onTimeout: { + listenerFoo1!.setTimer(0.1, onTimeout: { fail("onTimeout callback shouldn't have been called") - }) + }).startTimer() eventEmitter.off(listenerFoo1!) waitUntil(timeout: 0.3) { done in delay(0.15) { @@ -118,7 +118,7 @@ class Utilities: QuickSpec { expect(receivedFoo1).to(equal(111)) expect(receivedAll).to(equal(111)) } - + it("should stop receive events if off matches the listener's criteria") { eventEmitter.off("foo", listener: listenerFoo1!) eventEmitter.emit("foo", with: 111) @@ -152,12 +152,12 @@ class Utilities: QuickSpec { } it("should remove all timeouts") { - eventEmitter.timed(listenerFoo1!, deadline: 0.1, onTimeout: { + listenerFoo1!.setTimer(0.1, onTimeout: { fail("onTimeout callback shouldn't have been called") - }) - eventEmitter.timed(listenerAll!, deadline: 0.1, onTimeout: { + }).startTimer() + listenerAll!.setTimer(0.1, onTimeout: { fail("onTimeout callback shouldn't have been called") - }) + }).startTimer() eventEmitter.off() waitUntil(timeout: 0.3) { done in delay(0.15) { @@ -169,7 +169,7 @@ class Utilities: QuickSpec { context("the timed method") { it("should not call onTimeout if the deadline isn't reached") { - eventEmitter.timed(listenerFoo1!, deadline: 0.2, onTimeout: { + weak var timer = listenerFoo1!.setTimer(0.2, onTimeout: { fail("onTimeout callback shouldn't have been called") }) waitUntil(timeout: 0.4) { done in @@ -180,16 +180,17 @@ class Utilities: QuickSpec { done() } } + timer?.startTimer() } } it("should call onTimeout and off the listener if the deadline is reached") { var calledOnTimeout = false let beforeEmitting = NSDate() - eventEmitter.timed(listenerFoo1!, deadline: 0.3, onTimeout: { + listenerFoo1!.setTimer(0.3, onTimeout: { calledOnTimeout = true expect(NSDate()).to(beCloseTo(beforeEmitting.dateByAddingTimeInterval(0.3), within: 0.2)) - }) + }).startTimer() waitUntil(timeout: 0.5) { done in delay(0.35) { expect(calledOnTimeout).to(beTrue()) diff --git a/Tests/ARTRealtimeMessageTest.m b/Tests/ARTRealtimeMessageTest.m index 28c41baba..f9008de8e 100644 --- a/Tests/ARTRealtimeMessageTest.m +++ b/Tests/ARTRealtimeMessageTest.m @@ -145,19 +145,21 @@ - (void)testEchoMessagesDefault { NSString *message2 = @"message2"; __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; + void(^partialDone)() = [ARTTestUtil splitFulfillFrom:self expectation:expectation in:3]; ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtime *realtime2 = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel = [realtime.channels get:channelName]; __block bool gotMessage1 = false; [channel subscribe:^(ARTMessage *message) { - if([[message data] isEqualToString:message1]) { + if ([[message data] isEqualToString:message1]) { gotMessage1 = true; + partialDone(); } else { XCTAssertTrue(gotMessage1); XCTAssertEqualObjects([message data], message2); - [expectation fulfill]; + partialDone(); } }]; [channel publish:nil data:message1 callback:^(ARTErrorInfo *errorInfo) { @@ -165,6 +167,7 @@ - (void)testEchoMessagesDefault { ARTRealtimeChannel *channel2 = [realtime2.channels get:channelName]; [channel2 publish:nil data:message2 callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); + partialDone(); }]; }]; [self waitForExpectationsWithTimeout:[ARTTestUtil timeout] handler:nil]; diff --git a/Tests/ARTRealtimePresenceTest.m b/Tests/ARTRealtimePresenceTest.m index b6924a37b..21374fb27 100644 --- a/Tests/ARTRealtimePresenceTest.m +++ b/Tests/ARTRealtimePresenceTest.m @@ -763,22 +763,36 @@ - (void)testEnterClient { NSString *clientId = @"otherClientId"; NSString *clientId2 = @"yetAnotherClientId"; __weak XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"%s", __FUNCTION__]]; + void(^partialFulfill)() = [ARTTestUtil splitFulfillFrom:self expectation:expectation in:4]; ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *channel = [realtime.channels get:@"channelName"]; [channel.presence enterClient:clientId data:nil callback:^(ARTErrorInfo *errorInfo) { XCTAssertNil(errorInfo); - [channel.presence enterClient:clientId2 data:nil callback:^(ARTErrorInfo *errorInfo) { - XCTAssertNil(errorInfo); - [channel.presence get:^(NSArray *members, ARTErrorInfo *error) { - XCTAssert(!error); - XCTAssertEqual(2, members.count); - ARTPresenceMessage *m0 = [members objectAtIndex:0]; - XCTAssertEqualObjects(m0.clientId, clientId2); - ARTPresenceMessage *m1 = [members objectAtIndex:1]; - XCTAssertEqualObjects(m1.clientId, clientId); - [expectation fulfill]; - }]; - }]; + partialFulfill(); + }]; + [channel.presence enterClient:clientId2 data:nil callback:^(ARTErrorInfo *errorInfo) { + XCTAssertNil(errorInfo); + partialFulfill(); + }]; + [channel.presence subscribe:^(ARTPresenceMessage *message) { + partialFulfill(); + }]; + [self waitForExpectationsWithTimeout:[ARTTestUtil timeout] handler:nil]; + + __weak XCTestExpectation *expectationPresenceGet = [self expectationWithDescription:[NSString stringWithFormat:@"%s-PresenceGet", __FUNCTION__]]; + [channel.presence get:^(NSArray *members, ARTErrorInfo *error) { + XCTAssert(!error); + XCTAssertEqual(2, members.count); + ARTPresenceMessage *m0 = [members objectAtIndex:0]; + // cannot guarantee the order + if (![m0.clientId isEqualToString:clientId2] && ![m0.clientId isEqualToString:clientId]) { + XCTFail(@"clientId1 is different from what's expected"); + } + ARTPresenceMessage *m1 = [members objectAtIndex:1]; + if (![m1.clientId isEqualToString:clientId] && ![m1.clientId isEqualToString:clientId2]) { + XCTFail(@"clientId2 is different from what's expected"); + } + [expectationPresenceGet fulfill]; }]; [self waitForExpectationsWithTimeout:[ARTTestUtil timeout] handler:nil]; [realtime testSuite_waitForConnectionToClose:self]; diff --git a/Tests/ARTRealtimeRecoverTest.m b/Tests/ARTRealtimeRecoverTest.m index 47f099ffa..8b79b81da 100644 --- a/Tests/ARTRealtimeRecoverTest.m +++ b/Tests/ARTRealtimeRecoverTest.m @@ -53,9 +53,10 @@ - (void)testRecoverDisconnected { [self waitForExpectationsWithTimeout:[ARTTestUtil timeout] handler:nil]; __weak XCTestExpectation *expectation2 = [self expectationWithDescription:[NSString stringWithFormat:@"%s-2", __FUNCTION__]]; + __block ARTRealtime *realtimeNonRecovered; [realtime.connection once:ARTRealtimeConnectionEventDisconnected callback:^(ARTConnectionStateChange *stateChange) { options.recover = nil; - ARTRealtime *realtimeNonRecovered = [[ARTRealtime alloc] initWithOptions:options]; + realtimeNonRecovered = [[ARTRealtime alloc] initWithOptions:options]; ARTRealtimeChannel *c2 = [realtimeNonRecovered.channels get:channelName]; // Sending other message to the same channel to check if the recovered connection receives it [c2 publish:nil data:c2Message callback:^(ARTErrorInfo *errorInfo) {