From 85fd3b53f84a5dd7ba6ea7e9d2f09df897397008 Mon Sep 17 00:00:00 2001 From: Ricardo Pereira Date: Thu, 23 Mar 2017 17:16:06 +0000 Subject: [PATCH] 0.9 master (#588) * Start 0.9 version * 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 * Update RSC15a for 0.9 (#515) * Start 0.9 version * Update RSC15a: default fallback hosts * Default fallbackHosts as Array of Strings * 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 * Update RSC15e (#514) * Remove specs RSA10c and RSA10d (#522) * RSA10l (#524) * Auth: deprecate `authorise` in favor of `authorize`. * RSA10l * Use `authorize` instead of `authorise` (close #496) * 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 * Update RSC15b for 0.9 (#516) * Update RSC15b * Fix: REST fallback should only apply when the default host is used * Update RSA10a for 0.9 (#520) * Update RSA10a * Fix: authorize should change auth method to Token for future requests * Update RSA10j for 0.9 (#521) * Update RSA10j * Fix: ttl when omitted should set the default value * Fix RTN15h * 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 * 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 * 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 * RTN22 (#537) * RTN22 * RTN22a * Fix: realtime transport can be nil * Fix: realtime should renew token by transitioning to CONNECTING * Update RTL3 for 0.9 (#544) * Update RTL3a * Update RTL3b and RTL3c * RTL3d * RTL3e * RTL14 (#550) * 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 * 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 * 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 * 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 * Update RTL6c for 0.9 (#547) * Update RTL6c2 * Update RTL6c4 * RTL6c3 for Detached * Update RTL6c4 for channel Failed * Better code completion * Add ChannelStateChange.event property (#561) * Update RTN4 for 0.9 (#560) * Update RTN4e * Update RTN4f * Add ConnectionStateChange.event property * 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 * 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) * 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 * Remove pending tests (#542) * Fix: RTP6c pending test * RSL1g4: remove pending * RTP2: remove pending * Fix RTC8 * Update RTP3 (#566) * Update RTP8d (#572) * RTP17 (#571) * PresenceMap: list of internal members * RTP17 * Add ARTPresenceActionToStr method * RTP17: pending * 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 * RTP18 (#567) * Test suite: ARTPresenceMessage convenience initializer * RTP18 * RTP18a * RTP18b * RTP18c * Update RSA9h for 0.9 (#574) * Auth: optional arguments * Update RSA9h * Update RSA8e for 0.9 (#573) * Update RSA8e * Auth: optional arguments * Comments * Update RSA10g for 0.9 (#575) * Auth: optional arguments * Update RSA10g * Auth: subsequent authorizations with stored values * 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 * RTP19 (#568) * RTP19 * RTP19a * Fix: if channel resumed successfully then do not start sync * Enhance RTP5: presence get should not have Absent members * Fix race condition * Fix #583: update httpRequestTimeout and httpMaxRetryDuration * EventEmitter: use @synchronized because NSMutableArray are not thread safe * 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 +- Ably.podspec | 2 +- Ably.xcodeproj/project.pbxproj | 28 +- Examples/Tests/Podfile.lock | 2 +- Examples/Tests/TestsTests/TestsTests.swift | 6 +- Podfile.lock | 1 + README.md | 6 +- Source/ARTAuth+Private.h | 33 +- Source/ARTAuth.h | 8 +- Source/ARTAuth.m | 166 +- Source/ARTAuthDetails.h | 23 + Source/ARTAuthDetails.m | 30 + Source/ARTAuthOptions.h | 7 - Source/ARTAuthOptions.m | 21 +- Source/ARTBaseMessage.h | 2 +- Source/ARTChannels+Private.h | 2 +- Source/ARTClientOptions.h | 13 + Source/ARTClientOptions.m | 46 +- Source/ARTConnection+Private.h | 4 +- Source/ARTConnection.h | 9 +- Source/ARTConnection.m | 38 +- Source/ARTConnectionDetails.m | 2 +- Source/ARTCrypto+Private.h | 2 +- Source/ARTDataEncoder.h | 2 +- Source/ARTDataEncoder.m | 4 +- Source/ARTDefault.h | 3 +- Source/ARTDefault.m | 6 +- Source/ARTEventEmitter+Private.h | 20 +- Source/ARTEventEmitter.h | 85 +- Source/ARTEventEmitter.m | 341 ++-- Source/ARTFallback+Private.h | 25 + Source/ARTFallback.h | 12 +- Source/ARTFallback.m | 40 +- Source/ARTGCD.h | 5 + Source/ARTGCD.m | 26 +- Source/ARTHttp.h | 6 +- Source/ARTHttp.m | 6 +- Source/ARTJsonLikeEncoder.m | 23 +- Source/ARTLog.h | 1 + Source/ARTLog.m | 8 + Source/ARTOSReachability.m | 5 +- Source/ARTPaginatedResult.m | 2 +- Source/ARTPresenceMap.h | 31 +- Source/ARTPresenceMap.m | 250 ++- Source/ARTPresenceMessage+Private.h | 15 + Source/ARTPresenceMessage.h | 18 +- Source/ARTPresenceMessage.m | 63 +- Source/ARTProtocolMessage+Private.h | 11 +- Source/ARTProtocolMessage.h | 27 +- Source/ARTProtocolMessage.m | 53 +- Source/ARTQueuedMessage.m | 4 + Source/ARTRealtime+Private.h | 14 +- Source/ARTRealtime.m | 539 ++++-- Source/ARTRealtimeChannel+Private.h | 17 +- Source/ARTRealtimeChannel.h | 21 +- Source/ARTRealtimeChannel.m | 433 +++-- Source/ARTRealtimePresence.h | 12 +- Source/ARTRealtimePresence.m | 23 +- Source/ARTRealtimeTransport.h | 20 +- Source/ARTRealtimeTransport.m | 2 - Source/ARTRest+Private.h | 8 +- Source/ARTRest.m | 142 +- Source/ARTRestChannel.m | 2 +- Source/ARTRestPresence.h | 4 +- Source/ARTStats.h | 20 +- Source/ARTStatus.h | 28 +- Source/ARTStatus.m | 12 +- Source/ARTTokenParams.h | 6 +- Source/ARTTokenParams.m | 3 +- Source/ARTTokenRequest.m | 3 +- Source/ARTTypes.h | 74 +- Source/ARTTypes.m | 129 +- Source/ARTURLSessionServerTrust.h | 2 + Source/ARTURLSessionServerTrust.m | 4 + Source/ARTWebSocketTransport+Private.h | 5 +- Source/ARTWebSocketTransport.h | 9 - Source/ARTWebSocketTransport.m | 104 +- Source/Ably.h | 1 + Source/Ably.modulemap | 2 + Source/Info.plist | 2 +- Spec/Auth.swift | 999 +++++++++-- Spec/RealtimeClient.swift | 907 +++++++++- Spec/RealtimeClientChannel.swift | 1375 +++++++++++--- Spec/RealtimeClientConnection.swift | 1107 ++++++++---- Spec/RealtimeClientPresence.swift | 1885 ++++++++++++++++++-- Spec/RestClient.swift | 218 ++- Spec/RestClientChannel.swift | 12 +- Spec/RestClientPresence.swift | 40 +- Spec/TestUtilities.swift | 205 ++- Spec/Utilities.swift | 27 +- Tests/ARTFallbackTest.m | 2 + Tests/ARTRealtime+TestSuite.m | 2 +- Tests/ARTRealtimeAttachTest.m | 102 +- Tests/ARTRealtimeChannelTest.m | 64 +- Tests/ARTRealtimeMessageTest.m | 29 +- Tests/ARTRealtimePresenceHistoryTest.m | 30 +- Tests/ARTRealtimePresenceTest.m | 123 +- Tests/ARTRealtimeRecoverTest.m | 9 +- Tests/ARTRealtimeResumeTest.m | 8 +- Tests/ARTRestCapabilityTest.m | 2 +- 100 files changed, 8249 insertions(+), 2083 deletions(-) create mode 100644 Source/ARTAuthDetails.h create mode 100644 Source/ARTAuthDetails.m create mode 100644 Source/ARTFallback+Private.h create mode 100644 Source/ARTPresenceMessage+Private.h 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/Ably.podspec b/Ably.podspec index 0cf1571ef..3b0b6c427 100644 --- a/Ably.podspec +++ b/Ably.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Ably" - s.version = "0.8.10" + 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/Ably.xcodeproj/project.pbxproj b/Ably.xcodeproj/project.pbxproj index 05e199b8d..0877537c3 100644 --- a/Ably.xcodeproj/project.pbxproj +++ b/Ably.xcodeproj/project.pbxproj @@ -94,10 +94,12 @@ 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 */; }; + 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 */; }; @@ -129,6 +131,7 @@ D75A3F1B1DDE5B62002A4AAD /* ARTGCD.h in Headers */ = {isa = PBXBuildFile; fileRef = D75A3F191DDE5B62002A4AAD /* ARTGCD.h */; settings = {ATTRIBUTES = (Public, ); }; }; 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 +159,8 @@ 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, ); }; }; + 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 */; }; EB20F8D71C653F2300EF3978 /* ARTPresence+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EB20F8D61C653F1E00EF3978 /* ARTPresence+Private.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -164,7 +168,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, ); }; }; @@ -332,6 +336,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 = ""; }; @@ -364,6 +370,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 = ""; }; @@ -393,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 = ""; }; @@ -668,18 +676,20 @@ D746AE331BBC29FF003ECEF8 /* Types */ = { isa = PBXGroup; children = ( - EB8AC6421C6515ED002ABA92 /* ARTTokenParams+Private.h */, - EB0505FB1C5BD7C4006BA7E2 /* ARTBaseMessage+Private.h */, 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 */, + 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,12 +703,14 @@ D77394021C6F6FFE00F5478F /* ARTProtocolMessage+Private.h */, 96E408421A38939E00087F77 /* ARTProtocolMessage.m */, 96BF61621A35CDE1004CF2B3 /* ARTBaseMessage.h */, + EB0505FB1C5BD7C4006BA7E2 /* ARTBaseMessage+Private.h */, 96BF61631A35CDE1004CF2B3 /* ARTBaseMessage.m */, D746AE361BBC3201003ECEF8 /* ARTMessage.h */, D746AE371BBC3201003ECEF8 /* ARTMessage.m */, D746AE261BBB61C9003ECEF8 /* ARTPresence.h */, D746AE271BBB61C9003ECEF8 /* ARTPresence.m */, 96A5079F1A377AA50077CDF8 /* ARTPresenceMessage.h */, + D7F2B8B11E42410D00B65151 /* ARTPresenceMessage+Private.h */, 96A507A01A377AA50077CDF8 /* ARTPresenceMessage.m */, 1C2B0FFB1B136A6D00E3633C /* ARTPresenceMap.h */, 1C2B0FFC1B136A6D00E3633C /* ARTPresenceMap.m */, @@ -710,7 +722,6 @@ 1C55427C1B148306003068DB /* ARTStatus.m */, 96BF615C1A35C1C8004CF2B3 /* ARTTypes.h */, 96BF615D1A35C1C8004CF2B3 /* ARTTypes.m */, - EB503C871C7E4A090053AF00 /* ARTClientOptions+Private.h */, ); name = Types; sourceTree = ""; @@ -763,6 +774,7 @@ D746AE2E1BBBE7D7003ECEF8 /* ARTPaginatedResult+Private.h */, 850BFB4B1B79323C009D0ADD /* ARTPaginatedResult.m */, 1C578E1D1B3435CA00EF46EC /* ARTFallback.h */, + D77F02A71DAF8099001B3FF9 /* ARTFallback+Private.h */, 1C578E1E1B3435CA00EF46EC /* ARTFallback.m */, ); name = HTTP; @@ -837,6 +849,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 */, @@ -848,9 +861,11 @@ 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 */, + D73691FF1DB788C40062C150 /* ARTAuthDetails.h in Headers */, 1C05CF201AC1D7EB00687AC9 /* ARTRealtime+Private.h in Headers */, D7F1D37A1BF4E33A001A4B5E /* ARTRestChannel+Private.h in Headers */, 85B2C2191B6FE8DE00EA5254 /* CompatibilityMacros.h in Headers */, @@ -1177,6 +1192,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/Examples/Tests/Podfile.lock b/Examples/Tests/Podfile.lock index e2b81b3a6..6d0cd2e23 100644 --- a/Examples/Tests/Podfile.lock +++ b/Examples/Tests/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - Ably (0.8.10): + - Ably (0.9.0): - msgpack (= 0.1.8) - SocketRocket (= 0.5.1) - msgpack (0.1.8) 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/Podfile.lock b/Podfile.lock index bb47b7e8e..551bb9744 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -25,3 +25,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: c59fef6e348f90fbaa529d13373c2958f7b9b642 COCOAPODS: 1.1.1 + diff --git a/README.md b/README.md index 3d2ff2070..cbd2f4d21 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/ARTAuth+Private.h b/Source/ARTAuth+Private.h index aa9679ae7..6c7645b1c 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) ARTEventEmitter *authorizationEmitter; +- (void)auth:(ARTAuth *)auth didAuthorize:(ARTTokenDetails *)tokenDetails; +@end + @interface ARTAuth () @property (nonatomic, readonly, strong) ARTClientOptions *options; @@ -19,6 +31,9 @@ ART_ASSUME_NONNULL_BEGIN @property (art_nullable, nonatomic, readonly, strong) ARTTokenDetails *tokenDetails; @property (nonatomic, readonly, assign) NSTimeInterval timeOffset; +@property (art_nullable, weak) id delegate; +@property (readonly, assign) BOOL authorizing; + @end @interface ARTAuth (Private) @@ -27,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; @@ -38,6 +53,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; @@ -46,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.h b/Source/ARTAuth.h index a2a6bd6f4..96223689b 100644 --- a/Source/ARTAuth.h +++ b/Source/ARTAuth.h @@ -43,11 +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; +- (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:(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 ab6326779..916faad2f 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; } @@ -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,16 +183,41 @@ - (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:(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 { - // 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]; - 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; } @@ -317,64 +341,81 @@ - (void)executeTokenRequest:(ARTTokenRequest *)tokenRequest callback:(void (^)(A } - (void)authorise:(ARTTokenParams *)tokenParams options:(ARTAuthOptions *)authOptions callback:(void (^)(ARTTokenDetails *, NSError *))callback { - BOOL requestNewToken = NO; + [self authorize:tokenParams options:authOptions callback:callback]; +} - ARTAuthOptions *replacedOptions; - if ([authOptions isOnlyForceTrue]) { - replacedOptions = [self.options copy]; - replacedOptions.force = YES; - } - else { - replacedOptions = [authOptions copy] ? : [self.options copy]; - } +- (void)authorize:(void (^)(ARTTokenDetails *, NSError *))callback { + [self authorize:_options.defaultTokenParams 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]; 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; + // Success + void (^successBlock)(ARTTokenDetails *) = ^(ARTTokenDetails *tokenDetails) { + [self.logger verbose:@"RS:%p ARTAuth: token request succeeded: %@", _rest, tokenDetails]; + if (callback) { + callback(self.tokenDetails, nil); } - else { - [self.logger verbose:@"RS:%p ARTAuth: current token has expired. Requesting new token.", _rest]; - requestNewToken = YES; + _authorizing = false; + }; + + // Failure + void (^failureBlock)(NSError *) = ^(NSError *error) { + [self.logger verbose:@"RS:%p ARTAuth: token request failed: %@", _rest, error]; + if (callback) { + callback(nil, error); } - } - 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; + _authorizing = false; + }; + + __weak id lastDelegate = self.delegate; + if (lastDelegate) { + // Only the last request should remain + [lastDelegate.authorizationEmitter off]; + [lastDelegate.authorizationEmitter once:[ARTEvent newWithAuthorizationState:ARTAuthorizationSucceeded] callback:^(id null) { + successBlock(_tokenDetails); + [lastDelegate.authorizationEmitter off]; + }]; + [lastDelegate.authorizationEmitter once:[ARTEvent newWithAuthorizationState:ARTAuthorizationFailed] callback:^(NSError *error) { + failureBlock(error); + [lastDelegate.authorizationEmitter off]; + }]; } - 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]; + _authorizing = true; + [self requestToken:currentTokenParams withOptions:replacedOptions callback:^(ARTTokenDetails *tokenDetails, NSError *error) { + if (error) { + failureBlock(error); + if (lastDelegate) { + [lastDelegate.authorizationEmitter off]; } - }]; - } else { - if (callback) { - callback(self.tokenDetails, nil); + return; } - } + + _tokenDetails = tokenDetails; + _method = ARTAuthMethodToken; + + if (!tokenDetails) { + failureBlock([ARTErrorInfo createWithCode:0 message:@"Token details are empty"]); + } + else if (lastDelegate) { + [lastDelegate auth:self didAuthorize:tokenDetails]; + } + else { + successBlock(tokenDetails); + } + }]; +} + +- (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 { @@ -468,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/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/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..e878defea 100644 --- a/Source/ARTAuthOptions.m +++ b/Source/ARTAuthOptions.m @@ -70,14 +70,12 @@ - (id)copyWithZone:(NSZone *)zone { options.authParams = self.authParams; options.queryTime = self.queryTime; options.useTokenAuth = self.useTokenAuth; - options.force = self.force; return options; } - (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 { @@ -120,9 +118,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 +130,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/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/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/ARTClientOptions.h b/Source/ARTClientOptions.h index 4708b47f3..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. */ @@ -75,11 +81,18 @@ 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; - (BOOL)hasCustomRestHost; +- (BOOL)hasDefaultRestHost; - (BOOL)hasCustomRealtimeHost; +- (BOOL)hasDefaultRealtimeHost; @end diff --git a/Source/ARTClientOptions.m b/Source/ARTClientOptions.m index 7d41df2e7..b1cf75246 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; @@ -36,13 +37,20 @@ - (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 + _httpRequestTimeout = 10.0; //Seconds + _httpMaxRetryDuration = 15.0; //Seconds _httpMaxRetryCount = 3; + _fallbackHosts = nil; + _fallbackHostsUseDefault = false; return self; } +- (NSString *)description { + return [NSString stringWithFormat:@"%@\n\t clientId: %@;", [super description], self.clientId]; +} + - (NSString*)getRestHost { if (_restHost) { return _restHost; @@ -89,8 +97,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; @@ -102,11 +110,13 @@ - (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; options.httpRequestTimeout = self.httpRequestTimeout; - options.fallbackHosts = self.fallbackHosts; + options->_fallbackHosts = self.fallbackHosts; //ignore setter + options->_fallbackHostsUseDefault = self.fallbackHostsUseDefault; //ignore setter return options; } @@ -122,11 +132,33 @@ - (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 { + 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 { diff --git a/Source/ARTConnection+Private.h b/Source/ARTConnection+Private.h index 1634b26fb..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 @@ -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..7802ccbf3 100644 --- a/Source/ARTConnection.h +++ b/Source/ARTConnection.h @@ -31,8 +31,15 @@ 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 +#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 f410c833b..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:(ARTRealtimeConnectionState)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:(ARTRealtimeConnectionState)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:(ARTRealtimeConnectionState)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:(ARTRealtimeConnectionState)event with:(ARTConnectionStateChange *)data { - [_eventEmitter emit:[NSNumber numberWithInt:event] with:data]; +- (void)emit:(ARTRealtimeConnectionEvent)event with:(ARTConnectionStateChange *)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/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/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/ARTDefault.m b/Source/ARTDefault.m index ce7783de5..1d3e33ceb 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.10"; +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 NSTimeInterval _realtimeRequestTimeout = 10.0; 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 b0cb42289..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,99 +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; - } - [array artRemoveWhere:cond]; - if ([array count] == 0) { - @synchronized(dict) { + @synchronized (dict) { + NSMutableArray *array = [dict objectForKey:key]; + if (array == nil) { + return; + } + [array artRemoveWhere:cond]; + 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+Private.h b/Source/ARTFallback+Private.h new file mode 100644 index 000000000..487553b58 --- /dev/null +++ b/Source/ARTFallback+Private.h @@ -0,0 +1,25 @@ +// +// 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; + ++ (BOOL)restShouldFallback:(NSURL *)host withOptions:(ARTClientOptions *)options; ++ (BOOL)realtimeShouldFallback:(NSURL *)host withOptions:(ARTClientOptions *)options; + +@end + +ART_ASSUME_NONNULL_END diff --git a/Source/ARTFallback.h b/Source/ARTFallback.h index f2e78fb6c..10e2483ed 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. @@ -29,7 +29,7 @@ extern int (^ARTFallback_getRandomHostIndex)(int count); /** 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 2444f7ec0..5aeb283e4 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,17 +41,48 @@ - (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]; } - (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; } ++ (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/ARTGCD.h b/Source/ARTGCD.h index 128f144b6..d43a8d57f 100644 --- a/Source/ARTGCD.h +++ b/Source/ARTGCD.h @@ -13,5 +13,10 @@ void artDispatchSpecifyMainQueue(); void artDispatchMainQueue(dispatch_block_t block); +void artDispatchGlobalQueue(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 e13803740..2faf4ce86 100644 --- a/Source/ARTGCD.m +++ b/Source/ARTGCD.m @@ -22,8 +22,28 @@ 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 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)), queue, work); + return work; +} + +void artDispatchCancel(dispatch_block_t 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 87f5a576c..49a67ec00 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]; @@ -363,6 +375,10 @@ - (NSDictionary *)protocolMessageToDictionary:(ARTProtocolMessage *)message { 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; } @@ -652,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"], @@ -684,7 +703,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; } @@ -706,7 +725,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/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/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/ARTPresenceMap.h b/Source/ARTPresenceMap.h index ed8ac3f26..f9e99b234 100644 --- a/Source/ARTPresenceMap.h +++ b/Source/ARTPresenceMap.h @@ -9,32 +9,51 @@ #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) 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)initWithQueue:(_Nonnull dispatch_queue_t)queue; +- (instancetype)initWithQueue:(_Nonnull dispatch_queue_t)queue logger:(ARTLog *)logger; -- (void)put:(ARTPresenceMessage *)message; -- (void)clean; +- (BOOL)add:(ARTPresenceMessage *)message; +- (void)reset; - (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 8c21b917a..963f4e6cb 100644 --- a/Source/ARTPresenceMap.m +++ b/Source/ARTPresenceMap.m @@ -8,74 +8,246 @@ #import "ARTPresenceMap.h" #import "ARTPresenceMessage.h" +#import "ARTPresenceMessage+Private.h" #import "ARTEventEmitter.h" +#import "ARTLog.h" -@interface ARTPresenceMap () { - BOOL _syncStarted; - __GENERIC(ARTEventEmitter, NSNull *, __GENERIC(NSArray, ARTPresenceMessage *) *) *_syncEndedEventEmitter; +typedef NS_ENUM(NSUInteger, ARTPresenceSyncState) { + ARTPresenceSyncInitialized, + ARTPresenceSyncStarted, //ItemType: nil + ARTPresenceSyncEnded, //ItemType: NSArray* + 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 + } } -@property (readwrite, strong, atomic) __GENERIC(NSMutableDictionary, NSString *, ARTPresenceMessage *) *recentMembers; +#pragma mark - ARTEvent + +@interface ARTEvent (PresenceSyncState) + +- (instancetype)initWithPresenceSyncState:(ARTPresenceSyncState)value; ++ (instancetype)newWithPresenceSyncState:(ARTPresenceSyncState)value; + +@end + +#pragma mark - ARTPresenceMap + +@interface ARTPresenceMap () { + ARTPresenceSyncState _syncState; + ARTEventEmitter *_syncEventEmitter; + NSMutableDictionary *_members; + NSMutableSet *_localMembers; +} @end -@implementation ARTPresenceMap +@implementation ARTPresenceMap { + __weak ARTLog *_logger; +} -- (instancetype)initWithQueue:(_Nonnull dispatch_queue_t)queue { +- (instancetype)initWithQueue:(_Nonnull dispatch_queue_t)queue logger:(ARTLog *)logger { self = [super init]; if(self) { - _recentMembers = [NSMutableDictionary dictionary]; - _syncStarted = false; - _syncComplete = false; - _syncEndedEventEmitter = [[ARTEventEmitter alloc] initWithQueue:queue]; + _logger = logger; + [self reset]; + _syncSessionId = 0; + _syncState = ARTPresenceSyncInitialized; + _syncEventEmitter = [[ARTEventEmitter alloc] initWithQueue:queue]; } return self; } -- (__GENERIC(NSDictionary, NSString *, ARTPresenceMessage *) *)getMembers { - return self.recentMembers; +- (NSDictionary *)members { + return _members; +} + +- (NSMutableSet *)localMembers { + return _localMembers; +} + +- (BOOL)add:(ARTPresenceMessage *)message { + 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: + [self internalRemove:messageCopy]; + break; + default: + break; + } + 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; + } + + 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; + } + + [_logger debug:__FILE__ line:__LINE__ message:@"Presence member \"%@\" with action %@ has been ignored", received.memberKey, ARTPresenceActionToStr(received.action)]; + return NO; + } + + return !received.timestamp || + [latest.timestamp timeIntervalSince1970] <= [received.timestamp timeIntervalSince1970]; } -- (void)put:(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]; +- (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)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)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]; + } + [self cleanUpAbsentMembers]; +} + +- (void)reset { + _members = [NSMutableDictionary dictionary]; + _localMembers = [NSMutableSet set]; +} + - (void)startSync { - _recentMembers = [NSMutableDictionary dictionary]; - _syncStarted = true; - _syncComplete = false; + _syncSessionId++; + _syncState = ARTPresenceSyncStarted; + [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:_syncState] with:nil]; } - (void)endSync { - [self clean]; - _syncStarted = false; - _syncComplete = true; - [_syncEndedEventEmitter emit:[NSNull null] with:[self.recentMembers allValues]]; + [self cleanUpAbsentMembers]; + [self leaveMembersNotPresentInSync]; + _syncState = ARTPresenceSyncEnded; + [self reenterLocalMembersMissingFromSync]; + [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncEnded] with:[_members allValues]]; + [_syncEventEmitter off]; +} + +- (void)failsSync:(ARTErrorInfo *)error { + [self reset]; + _syncState = ARTPresenceSyncFailed; + [_syncEventEmitter emit:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncFailed] with:error]; + [_syncEventEmitter off]; } - (void)onceSyncEnds:(void (^)(NSArray *))callback { if (self.syncInProgress) { - [_syncEndedEventEmitter once:callback]; + [_syncEventEmitter once:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncEnded] callback:callback]; } else { - callback([self.recentMembers allValues]); + callback([_members allValues]); + } +} + +- (void)onceSyncFails:(void (^)(ARTErrorInfo *))callback { + if (self.syncInProgress) { + [_syncEventEmitter once:[ARTEvent newWithPresenceSyncState:ARTPresenceSyncFailed] callback:callback]; } } -- (BOOL)getSyncInProgress { - return _syncStarted && !_syncComplete; +- (BOOL)syncComplete { + return !(_syncState == ARTPresenceSyncInitialized || _syncState == ARTPresenceSyncStarted); +} + +- (BOOL)syncInProgress { + return _syncState == ARTPresenceSyncStarted; } #pragma mark private @@ -85,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+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 69c3629d4..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) { @@ -17,11 +18,26 @@ typedef NS_ENUM(NSUInteger, ARTPresenceAction) { ARTPresenceUpdate }; +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:(nonnull ARTPresenceMessage *)presence; @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 774ba3ede..145ade075 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; } @@ -38,4 +41,62 @@ - (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) { + 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 + } +} + +#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/ARTProtocolMessage+Private.h b/Source/ARTProtocolMessage+Private.h index f9464f860..5812232e0 100644 --- a/Source/ARTProtocolMessage+Private.h +++ b/Source/ARTProtocolMessage+Private.h @@ -6,6 +6,13 @@ // Copyright (c) 2014 Ably. All rights reserved. // +/// ARTProtocolMessageFlag bitmask +typedef NS_OPTIONS(NSUInteger, ARTProtocolMessageFlag) { + ARTProtocolMessageFlagHasPresence = (1UL << 0), //1 + ARTProtocolMessageFlagHasBacklog = (1UL << 1), //2 + ARTProtocolMessageFlagResumed = (1UL << 2) //4 +}; + ART_ASSUME_NONNULL_BEGIN @interface ARTProtocolMessage () @@ -13,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.h b/Source/ARTProtocolMessage.h index b93d9c2ec..f1c4a9529 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,8 +35,11 @@ typedef NS_ENUM(NSUInteger, ARTProtocolMessageAction) { ARTProtocolMessagePresence = 14, ARTProtocolMessageMessage = 15, ARTProtocolMessageSync = 16, + ARTProtocolMessageAuth = 17, }; +NSString *__art_nonnull ARTProtocolMessageActionToStr(ARTProtocolMessageAction action); + ART_ASSUME_NONNULL_BEGIN /** @@ -47,19 +51,20 @@ ART_ASSUME_NONNULL_BEGIN @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 (nullable, readwrite, strong, nonatomic) ARTErrorInfo *error; +@property (nullable, readwrite, strong, nonatomic) NSString *id; +@property (nullable, readwrite, strong, nonatomic) NSString *channel; +@property (nullable, readwrite, strong, nonatomic) NSString *channelSerial; +@property (nullable, readwrite, strong, nonatomic) NSString *connectionId; +@property (nullable, readwrite, strong, nonatomic, getter=getConnectionKey) NSString *connectionKey; @property (readwrite, assign, nonatomic) int64_t connectionSerial; -@property (art_nullable, readwrite, assign, nonatomic) NSNumber *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 (nullable, readwrite, assign, nonatomic) NSNumber *msgSerial; +@property (nullable, readwrite, strong, nonatomic) NSDate *timestamp; +@property (nullable, readwrite, strong, nonatomic) __GENERIC(NSArray, ARTMessage *) *messages; +@property (nullable, readwrite, strong, nonatomic) __GENERIC(NSArray, ARTPresenceMessage *) *presence; @property (readwrite, assign, nonatomic) int64_t flags; -@property (art_nullable, readwrite, nonatomic) ARTConnectionDetails *connectionDetails; +@property (nullable, readwrite, nonatomic) ARTConnectionDetails *connectionDetails; +@property (nullable, nonatomic) ARTAuthDetails *auth; @end diff --git a/Source/ARTProtocolMessage.m b/Source/ARTProtocolMessage.m index 0fbdedd52..17ad66e2b 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 { @@ -116,3 +124,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/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 8c4b983d0..d193ad387 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,12 +23,10 @@ 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; - -+ (NSString *)protocolStr:(ARTProtocolMessageAction)action; +@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, ARTEvent *, ARTConnectionStateChange *) *internalEventEmitter; +@property (readonly, strong, nonatomic) __GENERIC(ARTEventEmitter, ARTEvent *, NSNull *) *connectedEventEmitter; // State properties - (BOOL)shouldSendEvents; @@ -44,9 +43,10 @@ ART_ASSUME_NONNULL_BEGIN @interface ARTRealtime () @property (readwrite, strong, nonatomic) ARTRest *rest; -@property (readonly, getter=getTransport) 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; /// Current protocol `msgSerial`. Starts at zero. @property (readwrite, assign, nonatomic) int64_t msgSerial; @@ -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 1b82e9128..318614124 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" @@ -24,13 +25,15 @@ #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" +#import "ARTGCD.h" @interface ARTConnectionStateChange () @@ -43,16 +46,21 @@ - (void)setRetryIn:(NSTimeInterval)retryIn; @implementation ARTRealtime { BOOL _resuming; BOOL _renewingToken; - __GENERIC(ARTEventEmitter, NSNull *, ARTErrorInfo *) *_pingEventEmitter; + __GENERIC(ARTEventEmitter, ARTEvent *, ARTErrorInfo *) *_pingEventEmitter; NSDate *_startedReconnection; - NSTimeInterval _connectionStateTtl; 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; + - (instancetype)initWithKey:(NSString *)key { return [self initWithOptions:[[ARTClientOptions alloc] initWithKey:key]]; } @@ -81,12 +89,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.logger verbose:__FILE__ line:__LINE__ message:@"R:%p initialized with RS:%p", self, _rest]; self.rest.prioritizedHost = nil; - + if (options.autoConnect) { [self connect]; } @@ -94,7 +105,46 @@ - (instancetype)initWithOptions:(ARTClientOptions *)options { return self; } -- (id)getTransport { +- (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 = [[_transportClass alloc] initWithRest:self.rest options:self.options resumeKey:_transport.resumeKey connectionSerial:_transport.connectionSerial]; + _transport.delegate = self; + [_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)transport { return _transport; } @@ -111,7 +161,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 { @@ -119,7 +182,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]; @@ -129,11 +192,6 @@ - (void)dealloc { [_internalEventEmitter off]; } - if (_transport) { - _transport.delegate = nil; - [_transport close]; - } - _transport = nil; self.rest.prioritizedHost = nil; } @@ -146,6 +204,9 @@ - (void)connect { } - (void)close { + [_reachability off]; + [self cancelTimers]; + switch (self.connection.state) { case ARTRealtimeInitialized: case ARTRealtimeClosing: @@ -179,7 +240,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: @@ -190,9 +251,9 @@ - (void)ping:(void (^)(ARTErrorInfo *)) cb { }]; return; } - [_pingEventEmitter timed:[_pingEventEmitter once:cb] deadline:[ARTDefault realtimeRequestTimeout] onTimeout:^{ - cb([ARTErrorInfo createWithCode:0 status:ARTStateConnectionFailed message:@"timed out"]); - }]; + [[[_pingEventEmitter once:cb] setTimer:[ARTDefault realtimeRequestTimeout] onTimeout:^{ + cb([ARTErrorInfo createWithCode:ARTCodeErrorConnectionTimedOut status:ARTStateConnectionFailed message:@"timed out"]); + }] startTimer]; [self.transport sendPing]; } } @@ -210,35 +271,50 @@ - (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 realtime state transitions to %tu - %@", self, state, 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]; } - dispatch_semaphore_t waitingForCurrentEventSemaphore = [self transitionSideEffects:stateChange]; + ARTEventListener *stateChangeEventListener = [self transitionSideEffects:stateChange]; - [_internalEventEmitter emit:[NSNumber numberWithInteger:state] with:stateChange]; - [self.connection emit: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 { + [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 event:ARTRealtimeConnectionEventUpdate reason:errorInfo retryIn:0]; + + ARTEventListener *stateChangeEventListener = [self transitionSideEffects:stateChange]; + + [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:^{ - [self transition:ARTRealtimeDisconnected withErrorInfo:[ARTErrorInfo createWithCode:0 status:ARTStateConnectionFailed message:@"timed out"]]; + stateChangeEventListener = [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ + [weakSelf onConnectionTimeOut]; }]; + _connectingTimeoutListener = stateChangeEventListener; if (!_reachability) { _reachability = [[_reachabilityClass alloc] initWithLogger:self.logger]; @@ -247,32 +323,32 @@ - (_Nullable dispatch_semaphore_t)transitionSideEffects:(ARTConnectionStateChang 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; } _transport = [[_transportClass alloc] initWithRest:self.rest options:self.options resumeKey:resumeKey connectionSerial:connectionSerial]; _transport.delegate = self; - [_transport connect]; + [self transportConnectForcingNewToken:_renewingToken]; } - 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) { + 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: @@ -281,13 +357,12 @@ - (_Nullable dispatch_semaphore_t)transitionSideEffects:(ARTConnectionStateChang } }]; } - break; } case ARTRealtimeClosing: { [_reachability off]; - waitingForCurrentEventSemaphore = [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ - [self transition:ARTRealtimeClosed]; + stateChangeEventListener = [self unlessStateChangesBefore:[ARTDefault realtimeRequestTimeout] do:^{ + [weakSelf transition:ARTRealtimeClosed]; }]; [self.transport sendClose]; break; @@ -295,18 +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:[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:[ARTEvent newWithAuthorizationState:ARTAuthorizationFailed] with:stateChange.reason]; break; case ARTRealtimeDisconnected: { if (!_startedReconnection) { @@ -318,41 +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:^{ - [self transition:ARTRealtimeConnecting]; + 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:^{ - [self transition:ARTRealtimeConnecting]; + stateChangeEventListener = [self unlessStateChangesBefore:stateChange.retryIn do:^{ + [weakSelf transition:ARTRealtimeConnecting]; + _connectionRetryFromSuspendedListener = nil; }]; + _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]; + [_connectedEventEmitter emit:nil with:nil]; + [_authorizationEmitter emit:[ARTEvent newWithAuthorizationState:ARTAuthorizationSucceeded] with:nil]; break; } case ARTRealtimeInitialized: @@ -361,63 +440,56 @@ - (_Nullable dispatch_semaphore_t)transitionSideEffects:(ARTConnectionStateChang 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; if (!channelStatus) { channelStatus = [self defaultError]; } + [self failQueuedMessages:channelStatus]; // 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]; - } + for (ARTRealtimeChannel *channel in self.channels) { + if (stateChange.current == ARTRealtimeClosing) { + //do nothing. Closed state is coming. } - else { + else if (stateChange.current == ARTRealtimeClosed) { + [channel detachChannel:[ARTStatus state:ARTStateOk]]; + } + else if (stateChange.current == ARTRealtimeSuspended) { [channel setSuspended:channelStatus]; } + else { + [channel setFailed:channelStatus]; + } } } - return waitingForCurrentEventSemaphore; + [self.connection emit:stateChange.event with:stateChange]; + return stateChangeEventListener; } -- (_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; +- (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 { [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]; + [_pingEventEmitter emit:nil with:nil]; } - (void)onConnected:(ARTProtocolMessage *)message { @@ -431,12 +503,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) { @@ -451,6 +525,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; } @@ -461,15 +536,13 @@ - (void)onConnected:(ARTProtocolMessage *)message { break; case ARTRealtimeConnected: { // Renewing token. - dispatch_semaphore_t semaphore = [self transitionSideEffects:[[ARTConnectionStateChange alloc] initWithCurrent:ARTRealtimeConnected previous:ARTRealtimeConnected reason:nil]]; - [self transition:ARTRealtimeConnected withErrorInfo:message.error]; - if (semaphore) { - dispatch_semaphore_signal(semaphore); - } + [self updateWithErrorInfo:message.error]; } default: break; } + + _resuming = false; } - (void)onDisconnected { @@ -478,14 +551,12 @@ - (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]) { - [self connectWithRenewedToken]; + ARTErrorInfo *error = message.error; + if ([self shouldRenewToken:&error]) { [self transition:ARTRealtimeDisconnected withErrorInfo:error]; [self.connection setErrorReason:nil]; + _renewingToken = true; + [self transition:ARTRealtimeConnecting withErrorInfo:nil]; return; } [self transition:ARTRealtimeDisconnected withErrorInfo:error]; @@ -501,36 +572,186 @@ - (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; + } +} + +- (void)onAuth { + [self.logger info:@"R:%p server has requested an authorise", self]; + switch (self.connection.state) { + case ARTRealtimeConnecting: + case ARTRealtimeConnected: + _resuming = true; + [self transportReconnectWithRenewedToken]; + break; + default: + [self.logger error:@"Invalid Realtime state: expected Connecting or Connected, has %@", ARTRealtimeConnectionStateToStr(self.connection.state)]; 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 (!_renewingToken && error && error.statusCode == 401 && error.code >= 40140 && error.code < 40150 && [self isTokenRenewable]) { - [self connectWithRenewedToken]; + if ([self shouldRenewToken:&error]) { + [self.transport close]; + [self transportReconnectWithRenewedToken]; return; } [self.connection setId:nil]; - [self transition:ARTRealtimeFailed withErrorInfo:message.error]; + [self transition:ARTRealtimeFailed withErrorInfo:error]; } } -- (BOOL)isTokenRenewable { - return self.options.authCallback || self.options.authUrl || self.options.key; +- (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)connectWithRenewedToken { +- (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"]; + } + 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) { + if ([self.auth tokenIsRenewable]) { + return YES; + } + *errorPtr = [ARTErrorInfo createWithCode:ARTStateRequestTokenFailed message:ARTAblyMessageNoMeansToRenewToken]; + } + return NO; +} + +- (void)transportReconnectWithHost:(NSString *)host { + [self.transport setHost:host]; + [self transportConnectForcingNewToken:false]; +} + +- (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)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; + + // 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. + 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(_authenitcatingTimeoutWork); + _authenitcatingTimeoutWork = nil; + + // 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 transport] 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 { @@ -542,7 +763,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]; } @@ -618,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]; @@ -627,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); } } @@ -698,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; @@ -727,8 +967,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; @@ -755,47 +994,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 - default: - return [NSString stringWithFormat: @"unknown protocol state %d", (int)action]; - } -} - #pragma mark - ARTRealtimeTransportDelegate implementation - (void)realtimeTransport:(id)transport didReceiveMessage:(ARTProtocolMessage *)message { @@ -804,7 +1002,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]; @@ -843,6 +1041,9 @@ - (void)realtimeTransport:(id)transport didReceiveMessage:(ARTProtocolMessage *) case ARTProtocolMessageClosed: [self onClosed]; break; + case ARTProtocolMessageAuth: + [self onAuth]; + break; default: [self onChannelMessage:message]; break; @@ -872,7 +1073,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; @@ -881,7 +1082,7 @@ - (void)realtimeTransportDisconnected:(id)transport { if (self.connection.state == ARTRealtimeClosing) { [self transition:ARTRealtimeClosed]; } else { - [self transition:ARTRealtimeDisconnected]; + [self transition:ARTRealtimeDisconnected withErrorInfo:[ARTErrorInfo createFromNSError:error.error]]; } } @@ -897,8 +1098,8 @@ - (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 != nil) ? [self reconnectWithFallback] : [self transition:ARTRealtimeFailed withErrorInfo:[ARTErrorInfo createWithNSError:error.error]]; + _fallbacks = [[ARTFallback alloc] initWithOptions:[self getClientOptions]]; + (_fallbacks != nil) ? [self reconnectWithFallback] : [self transition:ARTRealtimeFailed withErrorInfo:[ARTErrorInfo createFromNSError:error.error]]; }]; return; } else if (_fallbacks && [self reconnectWithFallback]) { @@ -909,7 +1110,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/ARTRealtimeChannel+Private.h b/Source/ARTRealtimeChannel+Private.h index 36031f42f..dce8d5f4e 100644 --- a/Source/ARTRealtimeChannel+Private.h +++ b/Source/ARTRealtimeChannel+Private.h @@ -9,23 +9,24 @@ #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; @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, 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; @@ -56,12 +57,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.h b/Source/ARTRealtimeChannel.h index 2b20f4446..896b9735d 100644 --- a/Source/ARTRealtimeChannel.h +++ b/Source/ARTRealtimeChannel.h @@ -31,19 +31,26 @@ 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; -ART_EMBED_INTERFACE_EVENT_EMITTER(ARTChannelEvent, ARTErrorInfo *) +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 5d0fa0901..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 @@ -50,13 +51,15 @@ - (instancetype)initWithRealtime:(ARTRealtime *)realtime andName:(NSString *)nam _state = ARTRealtimeChannelInitialized; _queuedMessages = [NSMutableArray array]; _attachSerial = nil; - _presenceMap = [[ARTPresenceMap alloc] initWithQueue:_eventQueue]; + _presenceMap = [[ARTPresenceMap alloc] initWithQueue:_eventQueue logger:self.logger]; + _presenceMap.delegate = self; _lastPresenceAction = ARTPresenceAbsent; _statesEventEmitter = [[ARTEventEmitter alloc] initWithQueue:_eventQueue]; _messagesEventEmitter = [[ARTEventEmitter alloc] initWithQueue:_eventQueue]; _presenceEventEmitter = [[ARTEventEmitter alloc] initWithQueue:_eventQueue]; _attachedEventEmitter = [[ARTEventEmitter alloc] initWithQueue:_eventQueue]; _detachedEventEmitter = [[ARTEventEmitter alloc] initWithQueue:_eventQueue]; + _internalEventEmitter = [[ARTEventEmitter alloc] initWithQueue:_eventQueue]; } return self; } @@ -65,6 +68,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]; @@ -86,8 +93,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 = [NSNumber numberWithLongLong:self.presenceMap.syncMsgSerial]; @@ -137,34 +144,53 @@ - (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 addToQueue:pm callback:queuedCallback]; [self attach]; - // intentional fall-through + break; case ARTRealtimeChannelAttaching: - { - [self addToQueue:pm callback:cb]; + [self addToQueue:pm callback:queuedCallback]; break; - } + case ARTRealtimeChannelSuspended: case ARTRealtimeChannelDetaching: case ARTRealtimeChannelDetached: 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,16 +199,14 @@ - (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]; + [self.realtime.internalEventEmitter once:[ARTEvent newWithConnectionEvent:ARTRealtimeConnectionEventConnected] callback:^(ARTConnectionStateChange *__art_nullable change) { + [weakSelf sendQueuedMessages]; }]; } break; } - default: - NSAssert(NO, @"Invalid State"); } } @@ -201,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]); }]; @@ -220,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); }]; } @@ -235,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; @@ -248,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; @@ -265,85 +301,88 @@ - (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, ARTErrorInfo *) *)on:(ARTChannelEvent)event callback:(void (^)(ARTErrorInfo *))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, ARTErrorInfo *) *)on:(void (^)(ARTErrorInfo *))cb { +- (ARTEventListener *)on:(void (^)(ARTChannelStateChange *))cb { return [self.statesEventEmitter on:cb]; } -- (__GENERIC(ARTEventListener, ARTErrorInfo *) *)once:(ARTChannelEvent)event callback:(void (^)(ARTErrorInfo *))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, ARTErrorInfo *) *)once:(void (^)(ARTErrorInfo *))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, ARTErrorInfo *) *)listener { +- (void)off:(ARTEventListener *)listener { [self.statesEventEmitter off:listener]; } -- (void)emit:(ARTChannelEvent)event with:(ARTErrorInfo *)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]; +- (void)emit:(ARTChannelEvent)event with:(ARTChannelStateChange *)data { + [self.statesEventEmitter emit:[ARTEvent newWithChannelEvent:event] with:data]; + [self.internalEventEmitter emit:[ARTEvent newWithChannelEvent:event] with:data]; } - (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; - _errorReason = status.errorInfo; - if (state == ARTRealtimeChannelFailed) { - [_attachedEventEmitter emit:[NSNull null] with:status.errorInfo]; - [_detachedEventEmitter emit:[NSNull null] with:status.errorInfo]; + if (status.storeErrorInfo) { + _errorReason = 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]]; + + switch (state) { + case ARTRealtimeChannelSuspended: + [_attachedEventEmitter emit:nil with:status.errorInfo]; + break; + case ARTRealtimeChannelDetached: + [self.presenceMap failsSync:status.errorInfo]; + break; + case ARTRealtimeChannelFailed: + [_attachedEventEmitter emit:nil with:status.errorInfo]; + [_detachedEventEmitter emit:nil with:status.errorInfo]; + [self.presenceMap failsSync:status.errorInfo]; + break; + default: + break; } - [self emit:(ARTChannelEvent)state with:status.errorInfo]; + [self emit:stateChange.event with:stateChange]; } - (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(); } - [self timed:[self once:^(ARTErrorInfo *errorInfo) { - // Any state change cancels the timeout. - }] deadline:deadline onTimeout:callback]; - }); + }]; } /** @@ -359,6 +398,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]; @@ -390,66 +430,97 @@ - (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) { + [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) { + 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) { if (message.error != nil) { _errorReason = message.error; - [self emit:ARTChannelEventError with: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]) { - [self.presenceMap startSync]; - [self.logger debug:__FILE__ line:__LINE__ message:@"R:%p C:%p PresenceMap Sync started", _realtime, self]; - } - [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]]; - } - [_attachedEventEmitter emit:[NSNull null] with:nil]; + ARTStatus *status = message.error ? [ARTStatus state:ARTStateError info:message.error] : [ARTStatus state:ARTStateOk]; + [self transition:ARTRealtimeChannelAttached status:status]; + [_attachedEventEmitter emit:nil 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]; + [_detachedEventEmitter emit:nil with:nil]; +} + +- (void)detachChannel:(ARTStatus *)status { + [self failQueuedMessages:status]; + [self transition:ARTRealtimeChannelDetached status:status]; } -- (void)detachChannel:(ARTStatus *)error { - [self failQueuedMessages:error]; - [self transition:ARTRealtimeChannelDetached status:error]; +- (void)setFailed:(ARTStatus *)status { + [self failQueuedMessages:status]; + [self transition:ARTRealtimeChannelFailed status:status]; } -- (void)setFailed:(ARTStatus *)error { - [self failQueuedMessages:error]; - [self transition:ARTRealtimeChannelFailed status:error]; +- (void)setSuspended:(ARTStatus *)status { + [self setSuspended:status retryIn:self.realtime.options.channelRetryTimeout]; } -- (void)setSuspended:(ARTStatus *)error { - [self failQueuedMessages:error]; - [self transition:ARTRealtimeChannelDetached status:error]; +- (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) { + ARTStatus *status = [ARTStatus state:ARTStateError info:errorInfo]; + [weakSelf setSuspended:status]; + } withReason:nil]; + }] startTimer]; } - (void)onMessage:(ARTProtocolMessage *)message { @@ -464,7 +535,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:ARTChannelEventError with:errorInfo]; + ARTChannelStateChange *stateChange = [[ARTChannelStateChange alloc] initWithCurrent:self.state previous:self.state event:ARTChannelEventUpdate reason:errorInfo]; + [self emit:stateChange.event with:stateChange]; } } @@ -482,6 +554,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) { @@ -503,11 +576,9 @@ - (void)onPresence:(ARTProtocolMessage *)message { presence.id = [NSString stringWithFormat:@"%@:%d", message.id, i]; } - [self.presenceMap put:presence]; - if (!self.presenceMap.syncInProgress) { - [self.presenceMap clean]; + if ([self.presenceMap add:presence]) { + [self broadcastPresence:presence]; } - [self broadcastPresence:presence]; ++i; } @@ -517,13 +588,15 @@ - (void)onSync:(ARTProtocolMessage *)message { self.presenceMap.syncMsgSerial = [message.msgSerial longLongValue]; 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]; - [self.presenceMap put:presence]; - [self broadcastPresence:presence]; + if ([self.presenceMap add:presence]) { + [self broadcastPresence:presence]; + } } if ([self isLastChannelSerial:message.channelSerial]) { @@ -533,11 +606,11 @@ - (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 { - [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]]; } @@ -545,29 +618,53 @@ - (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]; + [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: + 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]); + [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; } - 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"]); @@ -576,7 +673,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]; } @@ -586,18 +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]; - _errorReason = errorInfo; - [self transition:ARTRealtimeChannelFailed status:status]; - [_attachedEventEmitter emit:[NSNull null] with:errorInfo]; - }]; + [self setSuspended:status]; + }] startTimer]; if (![self.realtime shouldQueueEvents]) { ARTEventListener *reconnectedListener = [self.realtime.connectedEventEmitter once:^(NSNull *n) { @@ -614,8 +709,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]; @@ -624,6 +729,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"]); @@ -650,28 +760,29 @@ - (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]; - _errorReason = errorInfo; - [self transition:ARTRealtimeChannelFailed status:status]; - [_detachedEventEmitter emit:[NSNull null] with:errorInfo]; - }]; + [self transition:ARTRealtimeChannelAttached status:status]; + [_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) { [self.realtime.connectedEventEmitter off:reconnectedListener]; }]; } + + if (self.presenceMap.syncInProgress) { + [self.presenceMap failsSync:[ARTErrorInfo createWithCode:90000 message:@"channel is being DETACHED"]]; + } } - (void)detach { @@ -715,4 +826,42 @@ - (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 + +#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 10b4def56..67c4998dd 100644 --- a/Source/ARTRealtimePresence.m +++ b/Source/ARTRealtimePresence.m @@ -62,10 +62,15 @@ - (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); } @@ -191,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; @@ -204,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 9f8faf265..dc8731c08 100644 --- a/Source/ARTRealtimeTransport.h +++ b/Source/ARTRealtimeTransport.h @@ -24,10 +24,16 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeTransportErrorType) { ARTRealtimeTransportErrorTypeNoInternet, ARTRealtimeTransportErrorTypeTimeout, ARTRealtimeTransportErrorTypeBadResponse, - ARTRealtimeTransportErrorTypeAuth, ARTRealtimeTransportErrorTypeOther }; +typedef NS_ENUM(NSUInteger, ARTRealtimeTransportState) { + ARTRealtimeTransportStateOpening, + ARTRealtimeTransportStateOpened, + ARTRealtimeTransportStateClosing, + ARTRealtimeTransportStateClosed, +}; + @interface ARTRealtimeTransportError : NSObject @property (nonatomic, strong) NSError *error; @@ -50,7 +56,7 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeTransportErrorType) { - (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; @@ -60,22 +66,24 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeTransportErrorType) { @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 (nullable, readwrite, strong, nonatomic) id delegate; -@property (readwrite, weak, nonatomic) id delegate; - (void)send:(ARTProtocolMessage *)msg; - (void)receive:(ARTProtocolMessage *)msg; -- (void)connect; -- (void)connectForcingNewToken:(BOOL)forceNewToken; +- (void)connectWithKey:(NSString *)key; +- (void)connectWithToken:(NSString *)token; - (void)sendClose; - (void)sendPing; - (void)close; - (void)abort:(ARTStatus *)reason; - (NSString *)host; - (void)setHost:(NSString *)host; +- (ARTRealtimeTransportState)state; @end 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+Private.h b/Source/ARTRest+Private.h index be2f284ab..2de208be9 100644 --- a/Source/ARTRest+Private.h +++ b/Source/ARTRest+Private.h @@ -21,9 +21,11 @@ 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; +@property (nonatomic, weak) id httpExecutor; @property (nonatomic, readonly, getter=getBaseUrl) NSURL *baseUrl; @@ -42,9 +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; -- (void)prepareAuthorisationHeader:(ARTAuthMethod)method completion:(void (^)(NSString *__art_nonnull authorization, NSError *__art_nullable error))callback; - -- (id)internetIsUp:(void (^)(BOOL isUp))cb; +- (nullable id)internetIsUp:(void (^)(BOOL isUp))cb; @end diff --git a/Source/ARTRest.m b/Source/ARTRest.m index 0c7bd4dbf..6ad47e3ef 100644 --- a/Source/ARTRest.m +++ b/Source/ARTRest.m @@ -25,16 +25,21 @@ #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" #import "ARTTokenParams.h" #import "ARTTokenDetails.h" #import "ARTDefault.h" -#import "ARTFallback.h" #import "ARTGCD.h" +@interface ARTRest () { + __block NSUInteger _tokenErrorRetries; +} + +@end + @implementation ARTRest - (instancetype)initWithOptions:(ARTClientOptions *)options { @@ -57,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; @@ -69,11 +74,12 @@ - (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]; - [self.logger debug:__FILE__ line:__LINE__ message:@"RS:%p initialized", self]; + [self.logger verbose:__FILE__ line:__LINE__ message:@"RS:%p initialized", self]; } return self; } @@ -87,7 +93,24 @@ - (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 { + 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 { @@ -98,9 +121,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: @@ -114,15 +143,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 { @@ -143,11 +194,14 @@ - (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]; - [self executeRequest:request withAuthOption:ARTAuthenticationNewToken completion:callback]; - return; + 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 + 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) { @@ -157,8 +211,8 @@ - (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]; + if (!blockFallbacks && [ARTFallback restShouldFallback:request.URL withOptions:_options]) { + blockFallbacks = [[ARTFallback alloc] initWithOptions:_options]; } if (blockFallbacks) { NSString *host = [blockFallbacks popFallbackHost]; @@ -180,6 +234,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; @@ -193,32 +258,25 @@ - (BOOL)shouldRetryWithFallback:(NSMutableURLRequest *)request response:(NSHTTPU return NO; } -- (void)prepareAuthorisationHeader:(ARTAuthMethod)method completion:(void (^)(NSString *authorization, NSError *error))callback { - [self prepareAuthorisationHeader:method force:NO completion:callback]; +- (NSString *)currentHost { + if (_prioritizedHost) { + // Test purpose only + return _prioritizedHost; + } + return self.options.restHost; } -- (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 authorise: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 *)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]; +} + +- (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/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/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 3f37407d9..fb3b8f1fe 100644 --- a/Source/ARTStatus.h +++ b/Source/ARTStatus.h @@ -26,25 +26,39 @@ typedef NS_ENUM(NSUInteger, ARTState) { ARTStateNoClientId, ARTStateMismatchedClientId, ARTStateRequestTokenFailed, + ARTStateAuthorizationFailed, ARTStateAuthUrlIncompatibleContent, ARTStateBadConnectionState, ARTStateError = 99999 }; /** - ARTCodeErrors - The list of all public error codes returned under the error domain ARTAblyErrorDomain */ typedef CF_ENUM(NSUInteger, ARTCodeError) { // FIXME: check hard coded errors - ARTCodeErrorAPIKeyMissing = 80001 + ARTCodeErrorAPIKeyMissing = 80001, + ARTCodeErrorConnectionTimedOut = 80014, + ARTCodeErrorAuthConfiguredProviderFailure = 80019, }; ART_ASSUME_NONNULL_BEGIN FOUNDATION_EXPORT NSString *const ARTAblyErrorDomain; +/** + Ably client exception names + */ +FOUNDATION_EXPORT NSString *const ARTFallbackIncompatibleOptionsException; + +/** + Ably client error messages + */ +FOUNDATION_EXPORT NSString *const ARTAblyMessageNoMeansToRenewToken; + +/** + Ably client error class + */ @interface ARTErrorInfo : NSError @property (readonly, getter=getMessage) NSString *message; @@ -52,18 +66,20 @@ FOUNDATION_EXPORT NSString *const ARTAblyErrorDomain; + (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 *)createFromNSError:(NSError *)error; + (ARTErrorInfo *)wrap:(ARTErrorInfo *)error prepend:(NSString *)prepend; - (NSString *)description; @end - +/** + Ably client status class + */ @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 4444650ff..d262d7ebb 100644 --- a/Source/ARTStatus.m +++ b/Source/ARTStatus.m @@ -13,6 +13,10 @@ // Reverse-DNS style domain NSString *const ARTAblyErrorDomain = @"io.ably.cocoa"; +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; } @@ -27,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; } @@ -47,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 @@ -59,6 +63,7 @@ - (instancetype)init { if (self) { _state = ARTStateOk; _errorInfo = nil; + _storeErrorInfo = false; } return self; } @@ -72,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; } @@ -86,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/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/Source/ARTTypes.h b/Source/ARTTypes.h index e416d4086..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; @@ -25,7 +26,8 @@ typedef NS_ENUM(NSUInteger, ARTAuthentication) { ARTAuthenticationOff, ARTAuthenticationOn, ARTAuthenticationUseBasic, - ARTAuthenticationNewToken + ARTAuthenticationNewToken, + ARTAuthenticationTokenRetry }; typedef NS_ENUM(NSUInteger, ARTAuthMethod) { @@ -33,6 +35,9 @@ typedef NS_ENUM(NSUInteger, ARTAuthMethod) { ARTAuthMethodToken }; + +#pragma mark - ARTRealtimeConnectionState + typedef NS_ENUM(NSUInteger, ARTRealtimeConnectionState) { ARTRealtimeInitialized, ARTRealtimeConnecting, @@ -44,8 +49,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, @@ -53,19 +77,29 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeChannelState) { ARTRealtimeChannelAttached, ARTRealtimeChannelDetaching, ARTRealtimeChannelDetached, + ARTRealtimeChannelSuspended, ARTRealtimeChannelFailed }; +NSString *__art_nonnull ARTRealtimeChannelStateToStr(ARTRealtimeChannelState state); + + +#pragma mark - ARTChannelEvent + typedef NS_ENUM(NSUInteger, ARTChannelEvent) { ARTChannelEventInitialized, ARTChannelEventAttaching, ARTChannelEventAttached, ARTChannelEventDetaching, ARTChannelEventDetached, + ARTChannelEventSuspended, ARTChannelEventFailed, - ARTChannelEventError + ARTChannelEventUpdate }; +NSString *__art_nonnull ARTChannelEventToStr(ARTChannelEvent event); + + typedef NS_ENUM(NSInteger, ARTDataQueryError) { ARTDataQueryErrorLimit = 1, ARTDataQueryErrorTimestampRange = 2, @@ -100,28 +134,62 @@ 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; @end +#pragma mark - ARTChannelStateChange + +@interface ARTChannelStateChange : NSObject + +- (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; + +@end + +#pragma mark - ARTJsonCompatible + @protocol ARTJsonCompatible - (NSDictionary *__art_nullable)toJSON:(NSError *__art_nullable *__art_nullable)error; @end +@interface NSString (ARTEventIdentification) +@end + @interface NSString (ARTJsonCompatible) @end @interface NSDictionary (ARTJsonCompatible) @end +@interface NSURL (ARTLog) +@end + ART_ASSUME_NONNULL_END diff --git a/Source/ARTTypes.m b/Source/ARTTypes.m index d2a955b9d..700e788b7 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; } @@ -62,12 +63,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 +90,70 @@ - (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 + } +} + +#pragma mark - ARTChannelStateChange + +@implementation ARTChannelStateChange + +- (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 event:(ARTChannelEvent)event reason:(ARTErrorInfo *)reason resumed:(BOOL)resumed { + self = [self init]; + if (self) { + _current = current; + _previous = previous; + _event = event; + _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 - ARTEventIdentification + +@implementation NSString (ARTEventIdentification) + +- (NSString *)identification { + return self; } @end +#pragma mark - ARTJsonCompatible + @implementation NSString (ARTJsonCompatible) - (NSDictionary *)toJSON:(NSError *__art_nullable *__art_nullable)error { @@ -128,3 +187,51 @@ - (NSDictionary *)toJSON:(NSError *__art_nullable *__art_nullable)error { } @end + +@implementation NSURL (ARTLog) + +- (NSString *)description { + return [NSString stringWithFormat:@"%@", self.absoluteString]; +} + +@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 ARTRealtimeChannelSuspended: + return @"Suspended"; //5 + case ARTRealtimeChannelFailed: + return @"Failed"; //6 + } +} + +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 ARTChannelEventSuspended: + return @"Suspended"; //5 + case ARTChannelEventFailed: + return @"Failed"; //6 + case ARTChannelEventUpdate: + return @"Update"; //7 + } +} 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+Private.h b/Source/ARTWebSocketTransport+Private.h index dc7887808..9c7bf5a39 100644 --- a/Source/ARTWebSocketTransport+Private.h +++ b/Source/ARTWebSocketTransport+Private.h @@ -22,7 +22,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; @@ -31,6 +30,10 @@ ART_ASSUME_NONNULL_BEGIN - (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; + +- (void)setState:(ARTRealtimeTransportState)state; + @end ART_ASSUME_NONNULL_END diff --git a/Source/ARTWebSocketTransport.h b/Source/ARTWebSocketTransport.h index abf0cd621..73b36a52f 100644 --- a/Source/ARTWebSocketTransport.h +++ b/Source/ARTWebSocketTransport.h @@ -23,15 +23,6 @@ ART_ASSUME_NONNULL_BEGIN @property (readonly, strong, nonatomic) NSString *resumeKey; @property (readonly, strong, nonatomic) NSNumber *connectionSerial; -@property (readwrite, weak, nonatomic) id delegate; - -@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 diff --git a/Source/ARTWebSocketTransport.m b/Source/ARTWebSocketTransport.m index 01f0e8ae4..d5da8dcbf 100644 --- a/Source/ARTWebSocketTransport.m +++ b/Source/ARTWebSocketTransport.m @@ -10,7 +10,6 @@ #import "ARTRest.h" #import "ARTRest+Private.h" -#import "ARTAuth.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, @@ -37,45 +37,50 @@ }; @implementation ARTWebSocketTransport { + id _delegate; + ARTRealtimeTransportState _state; /** A dispatch queue for firing the events. */ _Nonnull dispatch_queue_t _workQueue; } -// FIXME: Realtime sould be extending from RestClient +@synthesize delegate = _delegate; + - (instancetype)initWithRest:(ARTRest *)rest options:(ARTClientOptions *)options resumeKey:(NSString *)resumeKey connectionSerial:(NSNumber *)connectionSerial { self = [super init]; if (self) { _workQueue = dispatch_queue_create("io.ably.transport.websocket", DISPATCH_QUEUE_SERIAL); _websocket = nil; - _closing = NO; + _state = ARTRealtimeTransportStateClosed; _encoder = rest.defaultEncoder; _logger = rest.logger; - _auth = rest.auth; _options = [options copy]; _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; + self.delegate = 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]; } - (void)sendWithData:(NSData *)data { - [self.websocket send:data]; + if (self.websocket.readyState == SR_OPEN) { + [self.websocket send:data]; + } } - (void)receive:(ARTProtocolMessage *)msg { @@ -87,49 +92,22 @@ - (void)receiveWithData:(NSData *)data { [self receive:pm]; } -- (void)connect { - [self connectForcingNewToken:false]; -} - -- (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; - } - 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]; - } - else { - [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.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]; - }]; - } +- (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]; + // Connect + [self.websocket open]; } -- (BOOL)getIsConnected { - return self.websocket.readyState == SR_OPEN; +- (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]; + // Connect + [self.websocket open]; } - (NSURL *)setupWebSocket:(__GENERIC(NSArray, NSURLQueryItem *) *)params withOptions:(ARTClientOptions *)options resumeKey:(NSString *)resumeKey connectionSerial:(NSNumber *)connectionSerial { @@ -198,19 +176,20 @@ - (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 { + self.delegate = nil; if (!_websocket) return; self.websocket.delegate = nil; [self.websocket closeWithCode:ARTWsCloseNormal reason:@"Normal Closure"]; @@ -218,6 +197,7 @@ - (void)close { } - (void)abort:(ARTStatus *)reason { + self.delegate = nil; if (!_websocket) return; self.websocket.delegate = nil; if (reason.errorInfo) { @@ -237,6 +217,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 { @@ -263,7 +254,7 @@ - (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reas switch (code) { case ARTWsCloseNormal: - if (s.closing) { + if (_state == ARTRealtimeTransportStateClosing) { // OK [s.delegate realtimeTransportClosed:s]; } @@ -275,7 +266,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: @@ -298,6 +289,8 @@ - (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reas NSAssert(true, @"WebSocket close: unknown code"); break; } + + s.state = ARTRealtimeTransportStateClosed; }); } @@ -310,6 +303,7 @@ - (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error { if (s) { [s.delegate realtimeTransportFailed:s withError:[self classifyError:error]]; } + s.state = ARTRealtimeTransportStateClosed; }); } diff --git a/Source/Ably.h b/Source/Ably.h index ec9c3a8d9..a8dccfad4 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/Source/Ably.modulemap b/Source/Ably.modulemap index d02c1269e..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" @@ -31,5 +32,6 @@ framework module Ably { header "ARTLog+Private.h" header "ARTRealtimePresence+Private.h" header "ARTRestPresence+Private.h" + header "ARTFallback+Private.h" } } diff --git a/Source/Info.plist b/Source/Info.plist index bb86c85b2..3df65d7b1 100644 --- a/Source/Info.plist +++ b/Source/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 0.8.10 + 0.9.0 CFBundleSignature ???? CFBundleVersion diff --git a/Spec/Auth.swift b/Spec/Auth.swift index 8a02b807f..9fdb54722 100644 --- a/Spec/Auth.swift +++ b/Spec/Auth.swift @@ -185,6 +185,149 @@ 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() + } + } + } + + // 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 @@ -205,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 @@ -214,7 +678,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) @@ -222,11 +687,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() } } @@ -247,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() @@ -289,19 +754,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() } } @@ -310,16 +773,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 @@ -483,7 +942,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 @@ -511,7 +970,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 @@ -542,8 +1001,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 @@ -551,10 +1011,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() } @@ -577,7 +1036,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 @@ -598,7 +1057,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("*")) @@ -663,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 @@ -1124,7 +1643,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() } @@ -1210,7 +1729,6 @@ class Auth : QuickSpec { tokenParams.clientId = nil let authOptions = ARTAuthOptions() - authOptions.force = true authOptions.queryTime = true authOptions.key = options.key @@ -1290,7 +1808,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() } @@ -1318,7 +1836,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() } @@ -1343,6 +1861,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()) @@ -1614,113 +2181,126 @@ 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)) - - // Reuse the valid token - rest.auth.authorise(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)) - - publishTestMessage(rest, completion: { error in - expect(error).to(beNil()) - done() - }) - }) - }) + done() + } } - } - // RSA10b - it("should supports all TokenParams and AuthOptions") { - let rest = ARTRest(options: AblyTests.commonAppSetup()) + // 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()) waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(ARTTokenParams(), options: ARTAuthOptions(), callback: { tokenDetails, error in - guard let error = error else { - fail("Error is nil"); done(); return - } - expect(error.description).to(contain("no means to renew the token is provided")) + channel.publish(nil, data: "second check") { error in + expect(error).to(beNil()) done() - }) + } } - } - - // 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()) + // 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 - // Create token - rest.auth.authorise(tokenParams, options: nil) { tokenDetails, error in + rest.auth.authorize(nil, options: nil, callback: { tokenDetails, error in expect(error).to(beNil()) - expect(tokenDetails).toNot(beNil()) - expect(tokenDetails?.token).toNot(beEmpty()) + guard let tokenDetails = tokenDetails else { + XCTFail("TokenDetails is nil"); done(); return + } + // Check that token has changed + expect(tokenDetails.token).toNot(equal(firstTokenDetails.token)) - 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() + 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() } - } + }) } } - // RSA10d - it("should issue a new token even if an existing token exists when AuthOption.force is true") { + // 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() - options.clientId = "defClientId" + let testToken = getTestToken() + options.token = testToken let rest = ARTRest(options: options) - let authOptions = ARTAuthOptions() - authOptions.key = options.key - authOptions.force = true - - // Current token + expect(rest.auth.tokenDetails?.token).toNot(beNil()) waitUntil(timeout: testTimeout) { done in - rest.auth.authorise(nil, options: nil) { tokenDetails, error in - expect(error).to(beNil()) - expect(tokenDetails?.token).toNot(beNil()) + 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)) - let currentToken = tokenDetails?.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() + }) + }) + } + } - rest.auth.authorise(nil, options: authOptions) { tokenDetails, error in + // 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()) - guard let tokenDetails = tokenDetails else { - XCTFail("TokenDetails is nil"); done(); return - } - expect(tokenDetails.clientId).to(equal("defClientId")) - expect(tokenDetails.token).toNot(equal(currentToken)) + 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()) + + waitUntil(timeout: testTimeout) { done in + rest.auth.authorize(ARTTokenParams(), options: ARTAuthOptions(), callback: { tokenDetails, error in + guard let error = error else { + fail("Error is nil"); done(); return } - } + expect(error.description).to(contain("no means to renew the token is provided")) + done() + }) } } @@ -1740,7 +2320,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 @@ -1760,7 +2340,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 @@ -1790,11 +2370,10 @@ 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 - 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 { @@ -1802,14 +2381,13 @@ 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 { 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)) @@ -1835,7 +2413,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 @@ -1843,7 +2421,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()) @@ -1868,7 +2446,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()) @@ -1876,7 +2454,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()) @@ -1896,7 +2474,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() @@ -1905,7 +2483,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 @@ -1919,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 @@ -1927,7 +2543,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 @@ -1941,7 +2557,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 @@ -1966,7 +2582,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 @@ -2000,7 +2616,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 @@ -2018,7 +2634,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() @@ -2048,7 +2664,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 } @@ -2063,7 +2679,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() @@ -2082,7 +2698,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() @@ -2094,7 +2710,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() @@ -2118,7 +2734,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 @@ -2136,7 +2752,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 } @@ -2147,7 +2763,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 } @@ -2176,7 +2792,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 @@ -2194,7 +2810,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 } @@ -2208,7 +2824,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 } @@ -2222,7 +2838,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 } @@ -2237,7 +2853,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 } @@ -2263,7 +2879,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()) @@ -2274,7 +2890,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()) @@ -2286,7 +2902,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 } @@ -2299,7 +2915,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 } @@ -2324,7 +2940,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 } @@ -2350,7 +2966,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 @@ -2367,7 +2983,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 @@ -2381,6 +2997,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.authorize(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.authorize(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 @@ -2405,7 +3074,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 @@ -2421,7 +3090,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 @@ -2479,7 +3148,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 @@ -2498,7 +3167,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 @@ -2554,7 +3223,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)) @@ -2585,6 +3254,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") { @@ -2642,15 +3319,12 @@ class Auth : QuickSpec { } } - let authOptions = ARTAuthOptions() - authOptions.force = true - let tokenParams = ARTTokenParams() tokenParams.capability = "{\"\(channel.name)\":[\"*\"]}" tokenParams.clientId = "tester" waitUntil(timeout: testTimeout) { done in - realtime.auth.authorise(tokenParams, options: authOptions) { tokenDetails, error in + realtime.auth.authorize(tokenParams, options: nil) { tokenDetails, error in expect(error).to(beNil()) expect(tokenDetails).toNot(beNil()) done() @@ -2662,8 +3336,7 @@ class Auth : QuickSpec { waitUntil(timeout: testTimeout) { done in channel.attach { error in - // Not implemented on v0.8 - //expect(error).to(beNil()) + expect(error).to(beNil()) done() } } @@ -2688,15 +3361,12 @@ class Auth : QuickSpec { fail("TokenDetails is nil"); return } - let authOptions = ARTAuthOptions() - authOptions.force = true - let tokenParams = ARTTokenParams() tokenParams.capability = "{\"restricted\":[\"*\"]}" tokenParams.clientId = "secret" waitUntil(timeout: testTimeout) { done in - realtime.auth.authorise(tokenParams, options: authOptions) { tokenDetails, error in + realtime.auth.authorize(tokenParams, options: nil) { tokenDetails, error in guard let error = error else { fail("Error is nil"); done(); return } @@ -2706,8 +3376,7 @@ class Auth : QuickSpec { } } - // Not implemented on v0.8 - //expect(realtime.connection.state).toEventually(equal(ARTRealtimeConnectionState.Failed), timeout: testTimeout) + 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)) } @@ -2719,7 +3388,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/RealtimeClient.swift b/Spec/RealtimeClient.swift index 534405b59..f051d5aab 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() } } @@ -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,21 +355,882 @@ class RealtimeClient: QuickSpec { } } } - client.close() if let secs = endInterval { expect(secs).to(beLessThanOrEqualTo(UInt(options.suspendedRetryTimeout))) } } + // 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 + 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 + } + 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.connection.errorReason).to(beNil()) + } + + 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) { 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")) + done() + } + channel.attach() + } + + waitUntil(timeout: testTimeout) { done in + let partialDone = AblyTests.splitDone(2, done: done) + + client.connection.once(.Update) { 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(.Connected) { _ in + fail("Already connected") + } + 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) { _ in + fail("Should not reach Failed state"); done(); return + } + channel.once(.Attached) { stateChange in + expect(stateChange?.reason).to(beNil()) + done() + } + channel.attach() + } + + expect(client.auth.tokenDetails?.token).toNot(equal(testToken)) + } + + // 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) { stateChange in + guard let error = stateChange?.reason 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:))) { + delay(0) { + 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() + } + } + } + } + // https://github.com/ably/ably-ios/issues/577 it("background behaviour") { - let options = AblyTests.commonAppSetup() waitUntil(timeout: testTimeout) { done in NSURLSession.sharedSession().dataTaskWithURL(NSURL(string:"https://ably.io")!) { _ in - let realtime = ARTRealtime(options: options) + 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 9217dfcd2..7111ad263 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,16 +68,16 @@ 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 - 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,27 @@ 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.event).to(equal(ARTChannelEvent.Attached)) + expect(stateChange.reason).to(beNil()) channel.detach() case .Detached: - channel.onError(AblyTests.newErrorProtocolMessage()) - case .Failed: + expect(stateChange.event).to(equal(ARTChannelEvent.Detached)) + 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 +131,123 @@ 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.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() + 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(stateChange.event).to(equal(ARTChannelEvent.Suspended)) + 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.event).to(equal(ARTChannelEvent.Update)) + 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 +269,135 @@ 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) + } + } + + // 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) } } @@ -193,19 +425,18 @@ 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.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(error) + client.onError(pmError) } + expect(channel.state).to(equal(ARTRealtimeChannelState.Failed)) } @@ -218,26 +449,25 @@ 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.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(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 +476,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,52 +497,134 @@ 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() + } + delay(0) { + client.onSuspended() + } + } + + 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") { + let client = AblyTests.newRealtime(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("ChannelStateChange is nil"); done(); return + } + expect(stateChange.reason).to(beNil()) + done() + } + }) + } + + // 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") + } + defer { + channel.off() + } + + 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() + } + } } } @@ -347,53 +661,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") { @@ -472,7 +791,7 @@ class RealtimeClientChannel: QuickSpec { } - // RTL4h + // RTL4i context("happens when connection is CONNECTED if it's currently") { it("INITIALIZED") { let options = AblyTests.commonAppSetup() @@ -568,47 +887,62 @@ 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.errorReason!.code).to(equal(40160)) + expect(channel.state).to(equal(ARTRealtimeChannelState.Failed)) } // 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") - let start = NSDate() waitUntil(timeout: testTimeout) { done in channel.attach { errorInfo in expect(errorInfo).toNot(beNil()) expect(errorInfo).to(equal(channel.errorReason)) - callbackCalled = true done() } } - 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.9)) + + 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") { @@ -659,11 +993,141 @@ 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") { // 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() } @@ -684,13 +1148,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 @@ -702,6 +1159,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()) @@ -804,10 +1359,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(1.5) + it("if a DETACHED is 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) @@ -819,22 +1371,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(1.5)).to(beCloseTo(end, within: 0.5)) + expect(start.dateByAddingTimeInterval(1.0)).to(beCloseTo(end, within: 0.5)) } // RTL5g @@ -956,6 +1515,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 @@ -1020,8 +1615,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() @@ -1047,8 +1645,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 { @@ -1083,8 +1684,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 { @@ -1098,8 +1702,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 { @@ -1143,6 +1750,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! @@ -1204,6 +1812,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 @@ -1229,8 +1870,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(.Attached) { 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("invalid channel state")) + 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 @@ -1257,7 +1923,7 @@ class RealtimeClientChannel: QuickSpec { } } - it("SUSPENDED") { + it("connection is SUSPENDED") { client.connect() expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) client.onSuspended() @@ -1267,7 +1933,7 @@ class RealtimeClientChannel: QuickSpec { } } - it("CLOSING") { + it("connection is CLOSING") { client.connect() expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) client.close() @@ -1277,7 +1943,7 @@ class RealtimeClientChannel: QuickSpec { } } - it("CLOSED") { + it("connection is CLOSED") { client.connect() expect(client.connection.state).toEventually(equal(ARTRealtimeConnectionState.Connected), timeout: testTimeout) client.close() @@ -1287,7 +1953,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()) @@ -1297,7 +1963,7 @@ class RealtimeClientChannel: QuickSpec { } } - it("DETACHING") { + it("channel is DETACHING") { client.connect() channel.attach() expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) @@ -1308,7 +1974,7 @@ class RealtimeClientChannel: QuickSpec { } } - it("DETACHED") { + it("channel is DETACHED") { client.connect() channel.attach() expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Attached), timeout: testTimeout) @@ -1318,6 +1984,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) + } + } } } @@ -1388,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) @@ -1616,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) @@ -1661,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() } } } @@ -1699,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() } } } @@ -1866,6 +2568,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))) @@ -1877,13 +2580,12 @@ class RealtimeClientChannel: QuickSpec { partlyDone() } - channel.on(.Error) { 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() } @@ -2291,6 +2993,9 @@ class RealtimeClientChannel: QuickSpec { channel.on(.Attached) { _ in fail("Should not be called") } + defer { + channel.off() + } var hook: AspectToken? waitUntil(timeout: testTimeout) { done in @@ -2302,9 +3007,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()) @@ -2315,19 +3019,311 @@ class RealtimeClientChannel: QuickSpec { attachedMessageWithError.action = .Attached attachedMessageWithError.channel = "test" - channel.once(.Error) { 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.event).to(equal(ARTChannelEvent.Update)) + 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)) } + // 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()) + 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" + + channel.once(.Failed) { stateChange in + guard let error = stateChange?.reason else { + fail("Reason error is nil"); done(); return + } + expect(error).to(beIdenticalTo(errorProtocolMessage.error)) + expect(channel.errorReason).to(beIdenticalTo(error)) + done() + } + + client.transport?.receive(errorProtocolMessage) + } + + expect(channel.state).to(equal(ARTRealtimeChannelState.Failed)) + } + } context("crypto") { @@ -2416,31 +3412,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.realtimeTransport(client.transport, didReceiveMessage: 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()) - } } } } diff --git a/Spec/RealtimeClientConnection.swift b/Spec/RealtimeClientConnection.swift index 1f18c7f09..cad4b5909 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.10")) + expect(query).to(haveParam("lib", withValue: "ios-0.9.0")) } else { XCTFail("MockTransport isn't working") @@ -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] @@ -304,6 +308,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() @@ -414,7 +453,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 @@ -438,13 +477,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: @@ -455,13 +498,47 @@ class RealtimeClientConnection: QuickSpec { expect(errorInfo).toNot(beNil()) } - } - class TotalReach { - // Easy way to create an atomic var - static var shared = 0 - // This prevents others from using the default '()' initializer - private init() {} + // 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 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() + } + + let connectedMessageWithError = originalConnectedMessage + connectedMessageWithError.error = ARTErrorInfo.createWithCode(1234, message: "fabricated error") + client.transport?.receive(connectedMessageWithError) + } + } } // RTN5 @@ -479,6 +556,7 @@ class RealtimeClientConnection: QuickSpec { client.close() } } + waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(max, done: done) for _ in 1...max { @@ -496,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)) @@ -508,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() } @@ -546,7 +628,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") @@ -612,15 +694,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() } } } @@ -683,15 +763,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() } } } @@ -780,6 +858,181 @@ 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 @@ -799,19 +1052,27 @@ 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: { 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 + delay(1.0) { + transport.simulateIncomingNormalClose() } } - channel.attach() + } + + // 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() + } } } @@ -829,19 +1090,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() } } @@ -862,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() + } + } } } @@ -932,6 +1203,7 @@ class RealtimeClientConnection: QuickSpec { } var ids = [String]() let max = 25 + let sync = NSLock() waitUntil(timeout: testTimeout) { done in for _ in 1...max { @@ -948,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() @@ -1596,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) @@ -1608,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. } } @@ -1673,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") { @@ -1745,7 +1992,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 @@ -1803,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) @@ -1850,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) } @@ -1880,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() + } + } } } @@ -1924,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 @@ -2048,9 +2349,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)) @@ -2292,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") @@ -2313,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)) { @@ -2384,7 +2687,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() @@ -2516,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() } } } @@ -2643,9 +2947,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() } @@ -2672,12 +2980,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() } @@ -2708,15 +3020,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 @@ -2747,12 +3063,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() } @@ -2802,9 +3122,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() } @@ -2831,10 +3155,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() } @@ -2874,9 +3202,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() } @@ -2894,7 +3226,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)) } @@ -2916,12 +3248,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 @@ -2956,12 +3292,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 @@ -2987,13 +3327,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 @@ -3012,140 +3356,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() @@ -3184,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() } } @@ -3250,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") @@ -3396,6 +3602,227 @@ 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(.Update) { 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(_:options:callback:))) { + 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() + } + + } + + // 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 @@ -3412,8 +3839,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)) @@ -3618,6 +4044,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/RealtimeClientPresence.swift b/Spec/RealtimeClientPresence.swift index 92438b5d2..05fdcc216 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") { @@ -34,7 +40,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()) } @@ -51,9 +57,9 @@ class RealtimeClientPresence: QuickSpec { } 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 @@ -72,7 +78,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) @@ -91,7 +97,7 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in clientSecondary = AblyTests.addMembersSequentiallyToChannel("test", members: 150, options: options) { done() - }.first + } } let client = AblyTests.newRealtime(options) @@ -101,7 +107,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,17 +120,282 @@ 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 guard let transport = client.transport as? TestProxyTransport else { - fail("Transport is nil"); return + 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(transport.protocolMessagesSent.filter{ $0.action == .Sync }).toEventually(haveCount(1), timeout: testTimeout) - expect(transport.protocolMessagesSent.filter{ $0.action == .Sync }.first!.channelSerial).to(equal(lastSyncSerial)) + 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() + } + } + } + + // 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() + } + } + } + + } + + // 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 @@ -134,7 +407,7 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in clientSource = AblyTests.addMembersSequentiallyToChannel("test", members: 250, options: options) { done() - }.first + } } let clientTarget = ARTRealtime(options: options) @@ -232,45 +505,110 @@ class RealtimeClientPresence: QuickSpec { 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) } - } + 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)) - // 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") + channel.subscribe() { _ in + fail("Shouldn't receive any presence event") + } + defer { channel.off() } - waitUntil(timeout: testTimeout) { done in - channel.presence.enterClient("user", data: nil) { error in - expect(error).toNot(beNil()) - done() + 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()) } - channel.detach() } + } - } + // 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() } @@ -288,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 { @@ -301,7 +641,7 @@ class RealtimeClientPresence: QuickSpec { expect(channel2.presenceMap.members).to(haveCount(1)) } channel2.presence.unsubscribe() - done() + partialDone() } expect(channel2.queuedMessages).to(haveCount(1)) @@ -318,30 +658,486 @@ class RealtimeClientPresence: QuickSpec { expect(channel2.presence.syncComplete).toEventually(beTrue(), timeout: testTimeout) expect(channel2.presenceMap.members).to(haveCount(2)) } - } - - // RTP8 - context("enter") { - - // RTP8a - it("should enter the current client, optionally with the data provided") { - let options = AblyTests.commonAppSetup() - options.clientId = "john" - let client1 = ARTRealtime(options: options) - defer { client1.close() } - let channel1 = client1.channels.get("test") + // RTP5c + context("when a channel becomes ATTACHED") { - let client2 = ARTRealtime(options: options) - defer { client2.close() } - let channel2 = client2.channels.get("test") + // 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() - waitUntil(timeout: testTimeout) { done in - channel1.attach { err in - expect(err).to(beNil()) - channel1.presence.subscribe(.Enter) { member in - expect(member.clientId).to(equal(options.clientId)) - expect(member.data as? NSObject).to(equal("online")) + 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()) + 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() + } + } + } + + // 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()) + 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() + } + } + } + + 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 { + // Re-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()) + guard let members = members else { + fail("Members is nil"); done(); return + } + expect(members).to(haveCount(1)) + expect(members.first?.clientId).to(equal("local1")) + 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()) + 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() + } + } + } + + } + + // 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()) + 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() + } + } + } + + } + + } + + // RTP8 + context("enter") { + + // RTP8a + it("should enter the current client, optionally with the data provided") { + let options = AblyTests.commonAppSetup() + options.clientId = "john" + + let client1 = ARTRealtime(options: options) + defer { client1.close() } + let channel1 = client1.channels.get("test") + + let client2 = ARTRealtime(options: options) + defer { client2.close() } + let channel2 = client2.channels.get("test") + + waitUntil(timeout: testTimeout) { done in + channel1.attach { err in + expect(err).to(beNil()) + channel1.presence.subscribe(.Enter) { member in + expect(member.clientId).to(equal(options.clientId)) + expect(member.data as? NSObject).to(equal("online")) done() } channel2.presence.enter("online") @@ -775,137 +1571,683 @@ class RealtimeClientPresence: QuickSpec { } } - let transport = client.transport as! TestProxyTransport - let sent = transport.protocolMessagesSent.filter({ $0.action == .Presence })[1].presence![0] - 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] - expect(received.action).to(equal(ARTPresenceAction.Update)) - expect(received.clientId).to(equal("john")) + let transport = client.transport as! TestProxyTransport + let sent = transport.protocolMessagesSent.filter({ $0.action == .Presence })[1].presence![0] + expect(sent.action).to(equal(ARTPresenceAction.Update)) + expect(sent.clientId).to(beNil()) + + 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")) + } + + } + + // RTP10 + context("leave") { + + // RTP10a + it("should leave the current client from the channel and the data will be updated with the value provided") { + 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 + channel.presence.subscribe(.Enter) { member in + expect(member.data as? NSObject).to(equal("online")) + done() + } + channel.presence.enter("online") + } + + expect(channel.presenceMap.members).toEventually(haveCount(1), timeout: testTimeout) + + waitUntil(timeout: testTimeout) { done in + channel.presence.subscribe(.Leave) { member in + expect(member.data as? NSObject).to(equal("offline")) + done() + } + channel.presence.leave("offline") + } + + expect(channel.presenceMap.members).toEventually(haveCount(0), timeout: testTimeout) + } + + // RTP10a + it("should leave the current client with no data") { + 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 + channel.presence.subscribe(.Enter) { member in + expect(member.data as? NSObject).to(equal("online")) + done() + } + channel.presence.enter("online") + } + + waitUntil(timeout: testTimeout) { done in + channel.presence.subscribe(.Leave) { member in + expect(member.data as? NSObject).to(equal("online")) + done() + } + channel.presence.leave(nil) + } + } + + } + + // RTP2 + it("should be used a PresenceMap to maintain a list of members") { + let options = AblyTests.commonAppSetup() + var clientSecondary: ARTRealtime! + defer { clientSecondary.dispose(); clientSecondary.close() } + + waitUntil(timeout: testTimeout) { done in + clientSecondary = AblyTests.addMembersSequentiallyToChannel("test", members: 100, options: options) { + done() + } + } + + let client = AblyTests.newRealtime(options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("test") + + var user50LeaveTimestamp: NSDate? + channel.presence.subscribe(.Leave) { member in + expect(member.clientId).to(equal("user50")) + user50LeaveTimestamp = member.timestamp + } + + var user50PresentTimestamp: NSDate? + 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 + } + } + + waitUntil(timeout: testTimeout) { done in + 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 { + let msg = AblyTests.newPresenceProtocolMessage("test", action: .Leave, clientId: "user50") + // Ensure it happens "later" than the PRESENT message. + msg.timestamp = NSDate().dateByAddingTimeInterval(1.0) + 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()) + 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() + } + } + + 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: 101, 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).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)) + 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: 101, 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).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)) + 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()) } - } - - // RTP10 - context("leave") { - - // RTP10a - it("should leave the current client from the channel and the data will be updated with the value provided") { + // 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() - options.clientId = "john" - 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 - channel.presence.subscribe(.Enter) { member in - expect(member.data as? NSObject).to(equal("online")) + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 1, options: options) { done() } - channel.presence.enter("online") } - expect(channel.presenceMap.members).toEventually(haveCount(1), timeout: testTimeout) + let client = ARTRealtime(options: options) + defer { client.dispose(); client.close() } + let channel = client.channels.get("foo") waitUntil(timeout: testTimeout) { done in - channel.presence.subscribe(.Leave) { member in - expect(member.data as? NSObject).to(equal("offline")) - done() + 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() } - channel.presence.leave("offline") } - expect(channel.presenceMap.members).toEventually(beEmpty(), timeout: testTimeout) + expect(channel.presenceMap.members).to(haveCount(1)) } - // RTP10a - it("should leave the current client with no data") { + // 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() - options.clientId = "john" - 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 - channel.presence.subscribe(.Enter) { member in - expect(member.data as? NSObject).to(equal("online")) + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 20, options: options) { done() } - channel.presence.enter("online") } + 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 - channel.presence.subscribe(.Leave) { member in - expect(member.data as? NSObject).to(equal("online")) - done() + transport.afterProcessingReceivedMessage = { protocolMessage in + if protocolMessage.action == .Sync { + done() + } } - channel.presence.leave(nil) } - } - } + expect(channel.presenceMap.syncInProgress).toEventually(beFalse(), timeout: testTimeout) - // RTP2 - it("should be used a PresenceMap to maintain a list of members") { - let options = AblyTests.commonAppSetup() - var clientSecondary: ARTRealtime! - defer { clientSecondary.dispose(); clientSecondary.close() } + guard let user11MemberKey = channel.presenceMap.members["user11"]?.memberKey() else { + fail("user11 memberKey is not present"); return + } - waitUntil(timeout: testTimeout) { done in - clientSecondary = AblyTests.addMembersSequentiallyToChannel("test", members: 100, options: options) { - done() - }.first + 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()) } - let client = AblyTests.newRealtime(options) - defer { client.dispose(); client.close() } - let channel = client.channels.get("test") + // 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 user50LeaveTimestamp: NSDate? - channel.presence.subscribe(.Leave) { member in - expect(member.clientId).to(equal("user50")) - user50LeaveTimestamp = member.timestamp - } + var clientMembers: ARTRealtime? + defer { clientMembers?.dispose(); clientMembers?.close() } + waitUntil(timeout: testTimeout) { done in + clientMembers = AblyTests.addMembersSequentiallyToChannel("foo", members: 20, options: options) { + done() + } + } - var user50PresentTimestamp: NSDate? - channel.presenceMap.testSuite_getArgumentFrom(#selector(ARTPresenceMap.put(_:)), atIndex: 0) { arg0 in - let member = arg0 as! ARTPresenceMessage - if member.clientId == "user50" && member.action == .Present { - user50PresentTimestamp = member.timestamp + 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 - channel.attach() { _ in - let transport = client.transport as! TestProxyTransport - 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 { - let msg = AblyTests.newPresenceProtocolMessage("test", action: .Leave, clientId: "user50") - // Ensure it happens "later" than the PRESENT message. - msg.timestamp = NSDate().dateByAddingTimeInterval(1.0) - client.onChannelMessage(msg) - done() + 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)) } - 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)) - done() + // 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()) } - expect(user50LeaveTimestamp).to(beGreaterThan(user50PresentTimestamp)) } // RTP8 @@ -959,10 +2301,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() } } @@ -980,7 +2326,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() } } @@ -997,7 +2346,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() } } @@ -1011,7 +2363,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() } } @@ -1104,11 +2459,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")) } @@ -1158,6 +2511,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 @@ -1399,6 +2782,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() @@ -1701,9 +3242,9 @@ class RealtimeClientPresence: QuickSpec { 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) @@ -1836,10 +3377,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() + } + } + waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(2, done: done) channel.presence.get() { members, error in @@ -1856,7 +3407,7 @@ class RealtimeClientPresence: QuickSpec { } } - expect(channel.state).to(equal(ARTRealtimeChannelState.Detached)) + expect(channel.state).toEventually(equal(ARTRealtimeChannelState.Detached), timeout: testTimeout) } // RTP11c @@ -1871,7 +3422,7 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in clientSecondary = AblyTests.addMembersSequentiallyToChannel("test", members: 150, options: options) { done() - }.first + } } let client = AblyTests.newRealtime(options) @@ -1909,7 +3460,7 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in clientSecondary = AblyTests.addMembersSequentiallyToChannel("test", members: 150, options: options) { done() - }.first + } } let client = AblyTests.newRealtime(options) @@ -2002,7 +3553,7 @@ class RealtimeClientPresence: QuickSpec { waitUntil(timeout: testTimeout) { done in clientSecondary = AblyTests.addMembersSequentiallyToChannel("test", members: 150, data: expectedData, options: options) { done() - }.first + } } let client = ARTRealtime(options: options) @@ -2130,9 +3681,9 @@ class RealtimeClientPresence: QuickSpec { } 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) @@ -2145,9 +3696,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() @@ -2181,9 +3732,9 @@ class RealtimeClientPresence: QuickSpec { } 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/RestClient.swift b/Spec/RestClient.swift index f6410eb48..1076ee176 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() } } @@ -233,18 +233,18 @@ 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 - options.httpRequestTimeout = 0.5 + expect(options.httpRequestTimeout).to(equal(10.0)) //Seconds + 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() } @@ -277,8 +277,8 @@ 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 + expect(options.httpMaxRetryDuration).to(equal(15.0)) //Seconds + 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() } } @@ -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.authorise(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() } } @@ -573,6 +581,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") @@ -588,6 +669,8 @@ class RestClient: QuickSpec { done() } } + + expect(testHTTPExecutor.requests).to(haveCount(1)) } // RSC15b @@ -654,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") { @@ -713,7 +850,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) @@ -739,9 +876,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: "//rest.ably.io")).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(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: "//\(client.options.restHost)")).to(beTrue()) } // RSC15a @@ -766,6 +938,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) @@ -792,7 +972,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..()) -> [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,17 +207,18 @@ class AblyTests { } } } - return [client] + + 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) } } } @@ -278,6 +278,9 @@ class NSURLSessionServerTrustSync: NSObject, NSURLSessionDelegate, NSURLSessionT responseError = error httpResponse = response } + else if let error = error { + responseError = error + } requestCompleted = true } task.resume() @@ -324,7 +327,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 @@ -356,14 +359,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 } @@ -408,7 +411,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 +423,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 +437,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 } @@ -577,9 +584,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 +628,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 +654,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. @@ -635,9 +682,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)) { @@ -663,12 +750,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() } } @@ -701,7 +788,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 @@ -725,9 +812,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() { @@ -862,6 +949,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 { @@ -908,13 +1003,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() } } @@ -932,7 +1027,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) } @@ -969,34 +1064,52 @@ 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 ARTRealtimeStateToStr(self) + return ARTRealtimeConnectionStateToStr(self) + } +} + +extension ARTRealtimeConnectionEvent : CustomStringConvertible { + public var description : String { + return ARTRealtimeConnectionEventToStr(self) } } extension ARTProtocolMessageAction : CustomStringConvertible { public var description : String { - return ARTRealtime.protocolStr(self) + return ARTProtocolMessageActionToStr(self) } } 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) + } +} + +extension ARTPresenceAction : CustomStringConvertible { + public var description : String { + return ARTPresenceActionToStr(self) } } 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/ARTFallbackTest.m b/Tests/ARTFallbackTest.m index eed3fbb92..f930bb71c 100644 --- a/Tests/ARTFallbackTest.m +++ b/Tests/ARTFallbackTest.m @@ -8,7 +8,9 @@ #import #import "ARTFallback.h" +#import "ARTFallback+Private.h" #import "ARTDefault.h" + @interface ARTFallbackTest : XCTestCase @end 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]; }]; diff --git a/Tests/ARTRealtimeAttachTest.m b/Tests/ARTRealtimeAttachTest.m index 27ad2dd2f..6247fcbab 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]; } @@ -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:^(ARTErrorInfo *errorInfo) { - if (channel.state == ARTRealtimeChannelAttached) { - XCTFail(@"Should not have made it to attached"); - } - else if( channel.state == ARTRealtimeChannelAttaching) { - [channel detach]; - } - else if(channel.state == ARTRealtimeChannelDetaching) { - [channel off]; - [expectation fulfill]; - } - else if(channel.state == 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__]]; @@ -184,15 +158,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 +185,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 +226,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 +243,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 +262,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 +281,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]; } @@ -342,9 +315,8 @@ - (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]; }]; [realtime.connection on:^(ARTConnectionStateChange *stateChange) { 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..f9008de8e 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++; } }]; @@ -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]; @@ -208,9 +211,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 +313,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 14fa97008..21374fb27 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); }]; @@ -513,8 +512,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) { @@ -535,11 +534,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]; @@ -603,8 +602,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); }]; @@ -675,11 +674,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]; } @@ -695,11 +694,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]; } @@ -764,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]; @@ -967,7 +980,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]; @@ -1000,15 +1013,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/ARTRealtimeRecoverTest.m b/Tests/ARTRealtimeRecoverTest.m index 4cdbc364a..8b79b81da 100644 --- a/Tests/ARTRealtimeRecoverTest.m +++ b/Tests/ARTRealtimeRecoverTest.m @@ -37,7 +37,7 @@ - (void)testRecoverDisconnected { ARTRealtime *realtime = [[ARTRealtime alloc] initWithOptions:options]; __block NSString *firstConnectionId = nil; void (^splitDone)() = [ARTTestUtil splitFulfillFrom:self expectation:expectation in:2]; - [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 @@ -53,9 +53,10 @@ - (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) { + __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) { @@ -76,7 +77,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]; 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); 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"];