From aa8a2295ae41c040718936e9776b9409537e48b5 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Thu, 27 Nov 2025 15:11:27 +0000 Subject: [PATCH 1/4] Avoid direct use of options host for web socket. --- Source/ARTClientOptions.m | 8 ++++++-- Source/ARTWebSocketTransport.m | 9 ++++++--- Source/PrivateHeaders/Ably/ARTClientOptions+Private.h | 1 + 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Source/ARTClientOptions.m b/Source/ARTClientOptions.m index 8f24d10af..bdc3051be 100644 --- a/Source/ARTClientOptions.m +++ b/Source/ARTClientOptions.m @@ -105,14 +105,18 @@ - (NSURL*)restUrl { return [self restUrlComponents].URL; } -- (NSURL*)realtimeUrl { +- (NSURL*)realtimeUrlForHost:(NSString *)host { NSURLComponents *components = [[NSURLComponents alloc] init]; components.scheme = self.tls ? @"wss" : @"ws"; - components.host = self.realtimeHost; + components.host = host; components.port = [NSNumber numberWithInteger:(self.tls ? self.tlsPort : self.port)]; return components.URL; } +- (NSURL*)realtimeUrl { + return [self realtimeUrlForHost:self.realtimeHost]; +} + - (id)copyWithZone:(NSZone *)zone { ARTClientOptions *options = [super copyWithZone:zone]; diff --git a/Source/ARTWebSocketTransport.m b/Source/ARTWebSocketTransport.m index ad41ac772..41c37e387 100644 --- a/Source/ARTWebSocketTransport.m +++ b/Source/ARTWebSocketTransport.m @@ -65,6 +65,8 @@ The important thing here is that it has a fixed QoS class (of `QOS_CLASS_DEFAULT - the calls to `-[NSStream open]` (it's not clear to me what exactly is blocking here but it triggers an Xcode warning so let's avoid it) */ _Nonnull dispatch_queue_t _websocketOpenQueue; + + NSString *_host; } @synthesize delegate = _delegate; @@ -73,6 +75,7 @@ The important thing here is that it has a fixed QoS class (of `QOS_CLASS_DEFAULT - (instancetype)initWithRest:(ARTRestInternal *)rest options:(ARTClientOptions *)options resumeKey:(NSString *)resumeKey logger:(ARTInternalLog *)logger webSocketFactory:(id)webSocketFactory { self = [super init]; if (self) { + _host = options.realtimeHost; _workQueue = rest.queue; _websocketOpenQueue = dispatch_queue_create("io.ably.websocketOpen", dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT, 0)); _websocket = nil; @@ -190,7 +193,7 @@ - (NSURL *)setupWebSocket:(NSDictionary *)params w // URL NSURLComponents *urlComponents = [NSURLComponents componentsWithString:@"/"]; urlComponents.queryItems = [queryItems allValues]; - NSURL *url = [urlComponents URLRelativeToURL:[options realtimeUrl]]; + NSURL *url = [urlComponents URLRelativeToURL:[options realtimeUrlForHost:self.host]]; ARTLogDebug(_logger, @"R:%p WS:%p url %@", _delegate, self, url); @@ -245,11 +248,11 @@ - (void)abort:(ARTStatus *)reason { } - (void)setHost:(NSString *)host { - self.options.realtimeHost = host; + _host = host; } - (NSString *)host { - return self.options.realtimeHost; + return _host; } - (ARTRealtimeTransportState)state { diff --git a/Source/PrivateHeaders/Ably/ARTClientOptions+Private.h b/Source/PrivateHeaders/Ably/ARTClientOptions+Private.h index 2cc564ede..a2d00f49d 100644 --- a/Source/PrivateHeaders/Ably/ARTClientOptions+Private.h +++ b/Source/PrivateHeaders/Ably/ARTClientOptions+Private.h @@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN + (void)setDefaultEnvironment:(nullable NSString *)environment; + (BOOL)getDefaultIdempotentRestPublishingForVersion:(NSString *)version; - (NSURLComponents *)restUrlComponents; +- (NSURL*)realtimeUrlForHost:(NSString *)host; // helps obtain url with an alternative host without changing other params // MARK: - Plugins From 4059131adbf426a01d9d30a9461d127cd36efc81 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Thu, 27 Nov 2025 15:34:25 +0000 Subject: [PATCH 2/4] Implement endpoint support in ARTClientOptions and related classes (ADR-119). --- .github/workflows/integration-test.yaml | 2 +- Ably.xcodeproj/project.pbxproj | 32 +- Examples/Tests/TestsTests/TestsTests.swift | 10 +- Scripts/log-environment-information.sh | 6 +- Source/ARTClientOptions.m | 185 +++-- Source/ARTDefault.m | 81 ++- Source/ARTDomainSelector.m | 218 ++++++ Source/ARTFallbackHosts.m | 29 - Source/ARTRealtime.m | 28 +- Source/ARTRest.m | 17 +- Source/ARTWebSocketTransport.m | 3 +- Source/Ably.modulemap | 2 +- .../Ably/ARTClientOptions+Private.h | 16 +- .../PrivateHeaders/Ably/ARTDefault+Private.h | 4 +- .../PrivateHeaders/Ably/ARTDomainSelector.h | 42 ++ Source/PrivateHeaders/Ably/ARTFallbackHosts.h | 13 - Source/include/Ably/ARTClientOptions.h | 20 +- Source/include/Ably/ARTDefault.h | 5 +- Source/include/module.modulemap | 2 +- .../Test Utilities/TestUtilities.swift | 20 +- .../Tests/ARTDomainSelectorTests.swift | 441 ++++++++++++ Test/AblyTests/Tests/PushAdminTests.swift | 2 +- .../Tests/RealtimeClientConnectionTests.swift | 246 ++++--- .../AblyTests/Tests/RealtimeClientTests.swift | 19 +- Test/AblyTests/Tests/RestClientTests.swift | 668 +++++++----------- 25 files changed, 1341 insertions(+), 770 deletions(-) create mode 100644 Source/ARTDomainSelector.m delete mode 100644 Source/ARTFallbackHosts.m create mode 100644 Source/PrivateHeaders/Ably/ARTDomainSelector.h delete mode 100644 Source/PrivateHeaders/Ably/ARTFallbackHosts.h create mode 100644 Test/AblyTests/Tests/ARTDomainSelectorTests.swift diff --git a/.github/workflows/integration-test.yaml b/.github/workflows/integration-test.yaml index 53f06d54f..50b80945d 100644 --- a/.github/workflows/integration-test.yaml +++ b/.github/workflows/integration-test.yaml @@ -32,7 +32,7 @@ jobs: env: LC_CTYPE: en_US.UTF-8 LANG: en_US.UTF-8 - ABLY_ENV: sandbox + ABLY_ENV: nonprod:sandbox steps: - name: Check out repo diff --git a/Ably.xcodeproj/project.pbxproj b/Ably.xcodeproj/project.pbxproj index aaa03eac1..a95e595d1 100644 --- a/Ably.xcodeproj/project.pbxproj +++ b/Ably.xcodeproj/project.pbxproj @@ -357,6 +357,12 @@ 840FCE552C875B8A001163E1 /* ARTDevicePushDetails+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 840FCE522C875B8A001163E1 /* ARTDevicePushDetails+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 8412FDE72661AC37001FE9E6 /* AblyDeltaCodec.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8412FDE12661AC37001FE9E6 /* AblyDeltaCodec.xcframework */; }; 8412FDED2661AC37001FE9E6 /* msgpack.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8412FDE32661AC37001FE9E6 /* msgpack.xcframework */; }; + 842433FA2ED0A65800FAE2FF /* ARTDomainSelector.m in Sources */ = {isa = PBXBuildFile; fileRef = 842433F92ED0A65800FAE2FF /* ARTDomainSelector.m */; }; + 842433FB2ED0A65800FAE2FF /* ARTDomainSelector.m in Sources */ = {isa = PBXBuildFile; fileRef = 842433F92ED0A65800FAE2FF /* ARTDomainSelector.m */; }; + 842433FC2ED0A65800FAE2FF /* ARTDomainSelector.m in Sources */ = {isa = PBXBuildFile; fileRef = 842433F92ED0A65800FAE2FF /* ARTDomainSelector.m */; }; + 842433FE2ED0A67D00FAE2FF /* ARTDomainSelector.h in Headers */ = {isa = PBXBuildFile; fileRef = 842433FD2ED0A67D00FAE2FF /* ARTDomainSelector.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 842433FF2ED0A67D00FAE2FF /* ARTDomainSelector.h in Headers */ = {isa = PBXBuildFile; fileRef = 842433FD2ED0A67D00FAE2FF /* ARTDomainSelector.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 842434002ED0A67D00FAE2FF /* ARTDomainSelector.h in Headers */ = {isa = PBXBuildFile; fileRef = 842433FD2ED0A67D00FAE2FF /* ARTDomainSelector.h */; settings = {ATTRIBUTES = (Private, ); }; }; 844B9CCF2C807BC400A260E8 /* ARTDeviceDetails+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 844B9CCE2C807BC400A260E8 /* ARTDeviceDetails+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 844B9CD02C807BC400A260E8 /* ARTDeviceDetails+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 844B9CCE2C807BC400A260E8 /* ARTDeviceDetails+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 844B9CD12C807BC400A260E8 /* ARTDeviceDetails+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 844B9CCE2C807BC400A260E8 /* ARTDeviceDetails+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -878,12 +884,6 @@ D72C67DF201AB74000978EBB /* ARTPushActivationStateMachine+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D72C67DE201AB74000978EBB /* ARTPushActivationStateMachine+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 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 */; }; - D737F826263AF4CE0064FA05 /* ARTFallbackHosts.h in Headers */ = {isa = PBXBuildFile; fileRef = D737F824263AF4CE0064FA05 /* ARTFallbackHosts.h */; settings = {ATTRIBUTES = (Private, ); }; }; - D737F827263AF4CE0064FA05 /* ARTFallbackHosts.h in Headers */ = {isa = PBXBuildFile; fileRef = D737F824263AF4CE0064FA05 /* ARTFallbackHosts.h */; settings = {ATTRIBUTES = (Private, ); }; }; - D737F828263AF4CE0064FA05 /* ARTFallbackHosts.h in Headers */ = {isa = PBXBuildFile; fileRef = D737F824263AF4CE0064FA05 /* ARTFallbackHosts.h */; settings = {ATTRIBUTES = (Private, ); }; }; - D737F829263AF4CE0064FA05 /* ARTFallbackHosts.m in Sources */ = {isa = PBXBuildFile; fileRef = D737F825263AF4CE0064FA05 /* ARTFallbackHosts.m */; }; - D737F82A263AF4CE0064FA05 /* ARTFallbackHosts.m in Sources */ = {isa = PBXBuildFile; fileRef = D737F825263AF4CE0064FA05 /* ARTFallbackHosts.m */; }; - D737F82B263AF4CE0064FA05 /* ARTFallbackHosts.m in Sources */ = {isa = PBXBuildFile; fileRef = D737F825263AF4CE0064FA05 /* ARTFallbackHosts.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 */; }; @@ -1221,6 +1221,8 @@ 8412FDE12661AC37001FE9E6 /* AblyDeltaCodec.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = AblyDeltaCodec.xcframework; path = Carthage/Build/AblyDeltaCodec.xcframework; sourceTree = ""; }; 8412FDE32661AC37001FE9E6 /* msgpack.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = msgpack.xcframework; path = Carthage/Build/msgpack.xcframework; sourceTree = ""; }; 8412FDF42661AC7B001FE9E6 /* Nimble.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Nimble.xcframework; path = Carthage/Build/Nimble.xcframework; sourceTree = ""; }; + 842433F92ED0A65800FAE2FF /* ARTDomainSelector.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARTDomainSelector.m; sourceTree = ""; }; + 842433FD2ED0A67D00FAE2FF /* ARTDomainSelector.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ARTDomainSelector.h; path = PrivateHeaders/Ably/ARTDomainSelector.h; sourceTree = ""; }; 844B9CCE2C807BC400A260E8 /* ARTDeviceDetails+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "ARTDeviceDetails+Private.h"; path = "PrivateHeaders/Ably/ARTDeviceDetails+Private.h"; sourceTree = ""; }; 84557E7A2E91B1D700596CC6 /* ARTRestAnnotations.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARTRestAnnotations.m; sourceTree = ""; }; 84557E7E2E91B20300596CC6 /* ARTRestAnnotations.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ARTRestAnnotations.h; path = include/Ably/ARTRestAnnotations.h; sourceTree = ""; }; @@ -1314,8 +1316,6 @@ D72C67DE201AB74000978EBB /* ARTPushActivationStateMachine+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "ARTPushActivationStateMachine+Private.h"; path = "PrivateHeaders/Ably/ARTPushActivationStateMachine+Private.h"; sourceTree = ""; }; D73691FD1DB788C40062C150 /* ARTAuthDetails.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARTAuthDetails.h; path = include/Ably/ARTAuthDetails.h; sourceTree = ""; }; D73691FE1DB788C40062C150 /* ARTAuthDetails.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTAuthDetails.m; sourceTree = ""; }; - D737F824263AF4CE0064FA05 /* ARTFallbackHosts.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ARTFallbackHosts.h; path = PrivateHeaders/Ably/ARTFallbackHosts.h; sourceTree = ""; }; - D737F825263AF4CE0064FA05 /* ARTFallbackHosts.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARTFallbackHosts.m; sourceTree = ""; }; D746AE1A1BBB5207003ECEF8 /* ARTDataQuery.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ARTDataQuery.h; path = include/Ably/ARTDataQuery.h; sourceTree = ""; }; D746AE1B1BBB5207003ECEF8 /* ARTDataQuery+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "ARTDataQuery+Private.h"; path = "PrivateHeaders/Ably/ARTDataQuery+Private.h"; sourceTree = ""; }; D746AE1C1BBB5207003ECEF8 /* ARTDataQuery.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARTDataQuery.m; sourceTree = ""; }; @@ -1990,6 +1990,8 @@ 2124B79A29DB14C000AD8361 /* ARTLogAdapter.m */, 21276CB929EF323100107B5F /* ARTContinuousClock.h */, 21276CBD29EF34AE00107B5F /* ARTContinuousClock.m */, + 842433FD2ED0A67D00FAE2FF /* ARTDomainSelector.h */, + 842433F92ED0A65800FAE2FF /* ARTDomainSelector.m */, ); name = Utilities; sourceTree = ""; @@ -2015,8 +2017,6 @@ 1C578E1D1B3435CA00EF46EC /* ARTFallback.h */, D77F02A71DAF8099001B3FF9 /* ARTFallback+Private.h */, 1C578E1E1B3435CA00EF46EC /* ARTFallback.m */, - D737F824263AF4CE0064FA05 /* ARTFallbackHosts.h */, - D737F825263AF4CE0064FA05 /* ARTFallbackHosts.m */, D74CBC01212EB58700D090E4 /* ARTNSHTTPURLResponse+ARTPaginated.h */, D74CBC02212EB58700D090E4 /* ARTNSHTTPURLResponse+ARTPaginated.m */, D74CBC05212EB5B900D090E4 /* ARTNSMutableURLRequest+ARTPaginated.h */, @@ -2139,6 +2139,7 @@ D7D5A69A1CA3D9040071BD6D /* ARTAuthOptions+Private.h in Headers */, 2114D4312D4BCAE20032839A /* ARTRealtime+WrapperSDKProxy.h in Headers */, 96A507B51A37881C0077CDF8 /* ARTNSDate+ARTUtil.h in Headers */, + 842433FE2ED0A67D00FAE2FF /* ARTDomainSelector.h in Headers */, 848B1D562DF0947600A1AE5B /* ARTWrapperSDKProxyRealtimeAnnotations.h in Headers */, D5BB211926AA9A9F00AA5F3E /* ARTNSMutableDictionary+ARTDictionaryUtil.h in Headers */, 215924D92D636E04004A235C /* ARTWrapperSDKProxyRealtimePresence+Private.h in Headers */, @@ -2221,7 +2222,6 @@ EBFFAC191E97919C003E7326 /* ARTLocalDevice+Private.h in Headers */, 21B4A7BD2E560F8000687F68 /* ARTErrorInfo+Private.h in Headers */, 211A60DB29D726F800D169C5 /* ARTConnectionStateChangeParams.h in Headers */, - D737F826263AF4CE0064FA05 /* ARTFallbackHosts.h in Headers */, D746AE4F1BBD84E7003ECEF8 /* ARTChannelOptions.h in Headers */, 848B1D4D2DF093BD00A1AE5B /* ARTWrapperSDKProxyRealtimeAnnotations+Private.h in Headers */, D7588AF31BFF91B800BB8279 /* ARTURLSessionServerTrust.h in Headers */, @@ -2315,6 +2315,7 @@ D710D51C21949C42008F54AD /* ARTLocalDevice.h in Headers */, D710D4DA21949BF9008F54AD /* ARTPendingMessage.h in Headers */, 2104EFAD2A4CC33300CC1184 /* ARTAttachRetryState.h in Headers */, + 842433FF2ED0A67D00FAE2FF /* ARTDomainSelector.h in Headers */, 84557E852E91B21F00596CC6 /* ARTRestAnnotations+Private.h in Headers */, D710D4B321949B47008F54AD /* ARTRestChannel+Private.h in Headers */, D710D61C21949DEC008F54AD /* ARTPaginatedResult+Private.h in Headers */, @@ -2422,7 +2423,6 @@ D710D48921949A85008F54AD /* ARTConstants.h in Headers */, D710D60921949DA9008F54AD /* ARTURLSessionServerTrust.h in Headers */, D710D51621949C42008F54AD /* ARTPush.h in Headers */, - D737F827263AF4CE0064FA05 /* ARTFallbackHosts.h in Headers */, 213AEA302D36E9D30067FD5F /* ARTWrapperSDKProxyRealtime+Private.h in Headers */, D710D4D521949BF9008F54AD /* ARTRealtimeChannel.h in Headers */, 840FCE542C875B8A001163E1 /* ARTDevicePushDetails+Private.h in Headers */, @@ -2524,6 +2524,7 @@ D710D52E21949C44008F54AD /* ARTLocalDevice.h in Headers */, D710D4EA21949BFB008F54AD /* ARTPendingMessage.h in Headers */, 2104EFAE2A4CC33300CC1184 /* ARTAttachRetryState.h in Headers */, + 842434002ED0A67D00FAE2FF /* ARTDomainSelector.h in Headers */, 84557E832E91B21F00596CC6 /* ARTRestAnnotations+Private.h in Headers */, D710D4B921949B48008F54AD /* ARTRestChannel+Private.h in Headers */, D710D62821949DED008F54AD /* ARTPaginatedResult+Private.h in Headers */, @@ -2631,7 +2632,6 @@ D710D48B21949A86008F54AD /* ARTConstants.h in Headers */, D710D60B21949DAA008F54AD /* ARTURLSessionServerTrust.h in Headers */, D710D52821949C44008F54AD /* ARTPush.h in Headers */, - D737F828263AF4CE0064FA05 /* ARTFallbackHosts.h in Headers */, 213AEA312D36E9D30067FD5F /* ARTWrapperSDKProxyRealtime+Private.h in Headers */, D710D4E521949BFB008F54AD /* ARTRealtimeChannel.h in Headers */, 840FCE552C875B8A001163E1 /* ARTDevicePushDetails+Private.h in Headers */, @@ -2935,7 +2935,6 @@ EB2D5A911CC941A700AD1A67 /* ARTRealtimeTransport.m in Sources */, D746AE391BBC3201003ECEF8 /* ARTMessage.m in Sources */, 211A60FF29D8ABF100D169C5 /* ARTChannelStateChangeParams.m in Sources */, - D737F829263AF4CE0064FA05 /* ARTFallbackHosts.m in Sources */, D7E0FEB9211DE94700659FAA /* ARTNSMutableRequest+ARTRest.m in Sources */, D7B621991E4A762A00684474 /* ARTPushChannel.m in Sources */, EB89D40B1C61C6EA007FA5B7 /* ARTRealtimeChannels.m in Sources */, @@ -2964,6 +2963,7 @@ D746AE541BBD85C5003ECEF8 /* ARTChannels.m in Sources */, 96BF61541A35B39C004CF2B3 /* ARTRest.m in Sources */, D7D8F82C1BC2C706009718F2 /* ARTTokenRequest.m in Sources */, + 842433FB2ED0A65800FAE2FF /* ARTDomainSelector.m in Sources */, D7D8F8261BC2C691009718F2 /* ARTTokenDetails.m in Sources */, D75A3F1C1DDE5B62002A4AAD /* ARTGCD.m in Sources */, 5CC1D9CD2E7C263F005DC3ED /* ARTMessageVersion.m in Sources */, @@ -3079,7 +3079,6 @@ D710D4F021949C0D008F54AD /* ARTRealtimePresence.m in Sources */, D710D4F321949C0D008F54AD /* ARTRealtimeChannels.m in Sources */, 211A610029D8ABF100D169C5 /* ARTChannelStateChangeParams.m in Sources */, - D737F82A263AF4CE0064FA05 /* ARTFallbackHosts.m in Sources */, D710D53521949C54008F54AD /* ARTDevicePushDetails.m in Sources */, D710D62F21949E03008F54AD /* ARTDataQuery.m in Sources */, D710D53121949C54008F54AD /* ARTPush.m in Sources */, @@ -3108,6 +3107,7 @@ D710D63321949E03008F54AD /* ARTNSHTTPURLResponse+ARTPaginated.m in Sources */, D710D5D021949D78008F54AD /* ARTAuthOptions.m in Sources */, D710D5E221949D78008F54AD /* ARTTypes.m in Sources */, + 842433FC2ED0A65800FAE2FF /* ARTDomainSelector.m in Sources */, D710D5DF21949D78008F54AD /* ARTDataEncoder.m in Sources */, D710D4EE21949C0D008F54AD /* ARTConnection.m in Sources */, 5CC1D9CA2E7C263F005DC3ED /* ARTMessageVersion.m in Sources */, @@ -3221,7 +3221,6 @@ 217D1859254222F900DFF07E /* ARTSRWebSocket.m in Sources */, D710D50021949C0E008F54AD /* ARTRealtimePresence.m in Sources */, D710D50321949C0E008F54AD /* ARTRealtimeChannels.m in Sources */, - D737F82B263AF4CE0064FA05 /* ARTFallbackHosts.m in Sources */, 211A610129D8ABF100D169C5 /* ARTChannelStateChangeParams.m in Sources */, D710D54721949C55008F54AD /* ARTDevicePushDetails.m in Sources */, D710D63F21949E04008F54AD /* ARTDataQuery.m in Sources */, @@ -3252,6 +3251,7 @@ D710D5F621949D79008F54AD /* ARTAuthOptions.m in Sources */, D710D60821949D79008F54AD /* ARTTypes.m in Sources */, D710D60521949D79008F54AD /* ARTDataEncoder.m in Sources */, + 842433FA2ED0A65800FAE2FF /* ARTDomainSelector.m in Sources */, D710D4FE21949C0E008F54AD /* ARTConnection.m in Sources */, 215F75FD2922B1DB009E0E76 /* ARTClientInformation.m in Sources */, 5CC1D9CB2E7C263F005DC3ED /* ARTMessageVersion.m in Sources */, diff --git a/Examples/Tests/TestsTests/TestsTests.swift b/Examples/Tests/TestsTests/TestsTests.swift index d932d186c..603da0c6e 100644 --- a/Examples/Tests/TestsTests/TestsTests.swift +++ b/Examples/Tests/TestsTests/TestsTests.swift @@ -18,7 +18,7 @@ class TestsTests: XCTestCase { var responseData: Data? let postAppExpectation = self.expectation(description: "POST app to sandbox") - let request = NSMutableURLRequest(url: URL(string: "https://sandbox-rest.ably.io:443/apps")!) + let request = NSMutableURLRequest(url: URL(string: "https://sandbox.realtime.ably-nonprod.net:443/apps")!) request.httpMethod = "POST" request.httpBody = "{\"keys\":[{}]}".data(using: String.Encoding.utf8) request.allHTTPHeaderFields = [ @@ -47,7 +47,7 @@ class TestsTests: XCTestCase { } let options = ARTClientOptions(key: key as String) - options.environment = "sandbox" + options.endpoint = "nonprod:sandbox" let client = ARTRealtime(options: options) let receiveExpectation = self.expectation(description: "message received") @@ -64,17 +64,17 @@ class TestsTests: XCTestCase { let backgroundRealtimeExpectation = self.expectation(description: "Realtime in a Background Queue") var realtime: ARTRealtime! //strong reference - URLSession.shared.dataTask(with: URL(string: "https://ably.io")!) { _,_,_ in + URLSession.shared.dataTask(with: URL(string: "https://ably.com")!) { _,_,_ in realtime = ARTRealtime(key: key as String) realtime.channels.get("foo").attach { _ in do { backgroundRealtimeExpectation.fulfill() } } - } .resume() + }.resume() self.waitForExpectations(timeout: 10, handler: nil) let backgroundRestExpectation = self.expectation(description: "Rest in a Background Queue") var rest: ARTRest! //strong reference - URLSession.shared.dataTask(with: URL(string: "https://ably.io")!) { _,_,_ in + URLSession.shared.dataTask(with: URL(string: "https://ably.com")!) { _,_,_ in rest = ARTRest(key: key as String) rest.channels.get("foo").history { result, error in do { backgroundRestExpectation.fulfill() } diff --git a/Scripts/log-environment-information.sh b/Scripts/log-environment-information.sh index 1d465c801..b4e9fe27f 100755 --- a/Scripts/log-environment-information.sh +++ b/Scripts/log-environment-information.sh @@ -1,11 +1,11 @@ #!/bin/bash -# Prints the public IP address of the host machine, and the result of resolving sandbox-realtime.ably.io. Useful information to have in a CI environment. +# Prints the public IP address of the host machine, and the result of resolving sandbox.realtime.ably-nonprod.net. Useful information to have in a CI environment. set -e ip=$(curl -s https://api.ipify.org) echo "Public IP address is: $ip" -echo "Output of \`dig sandbox-realtime.ably.io\`:" -dig sandbox-realtime.ably.io +echo "Output of \`dig sandbox.realtime.ably-nonprod.net\`:" +dig sandbox.realtime.ably-nonprod.net diff --git a/Source/ARTClientOptions.m b/Source/ARTClientOptions.m index bdc3051be..c4f1239c2 100644 --- a/Source/ARTClientOptions.m +++ b/Source/ARTClientOptions.m @@ -2,12 +2,15 @@ #import "ARTClientOptions+TestConfiguration.h" #import "ARTAuthOptions+Private.h" +#import "ARTDefault.h" #import "ARTDefault+Private.h" #import "ARTStatus.h" #import "ARTTokenParams.h" #import "ARTStringifiable.h" #import "ARTNSString+ARTUtil.h" #import "ARTTestClientOptions.h" +#import "ARTNSArray+ARTFunctional.h" +#import "ARTDomainSelector.h" #ifdef ABLY_SUPPORTS_PLUGINS @import _AblyPluginSupportPrivate; @@ -16,7 +19,7 @@ const ARTPluginName ARTPluginNameLiveObjects = @"LiveObjects"; -NSString *ARTDefaultEnvironment = nil; +NSString *ARTDefaultEndpoint = nil; @interface ARTClientOptions () @@ -26,7 +29,13 @@ - (instancetype)initDefaults; @end -@implementation ARTClientOptions +@implementation ARTClientOptions { + NSString *_endpoint; + NSString *_restHost; + NSString *_realtimeHost; + NSString *_environment; + ARTDomainSelector *_domainSelector; +} - (instancetype)initDefaults { self = [super initDefaults]; @@ -36,9 +45,12 @@ - (instancetype)initDefaults { [ARTPluginAPI registerSelf]; #endif + _endpoint = ARTDefaultEndpoint; _port = [ARTDefault port]; _tlsPort = [ARTDefault tlsPort]; - _environment = ARTDefaultEnvironment; + _restHost = nil; + _realtimeHost = nil; + _environment = nil; _queueMessages = YES; _echoMessages = YES; _useBinaryProtocol = true; @@ -65,6 +77,7 @@ - (instancetype)initDefaults { _pushRegistererDelegate = nil; _testOptions = [[ARTTestClientOptions alloc] init]; _pluginData = [[NSMutableDictionary alloc] init]; + _connectivityCheckUrl = [ARTDefault connectivityCheckUrl]; return self; } @@ -72,36 +85,120 @@ - (NSString *)description { return [NSString stringWithFormat:@"%@\n\t clientId: %@;", [super description], self.clientId]; } -- (NSString*)restHost { - if (_restHost != nil) { - return _restHost; +// MARK: - Endpoint support + +- (void)setEndpoint:(NSString *)endpoint { + // REC1b1: endpoint cannot be used with deprecated options + if (self.hasEnvironment || self.hasCustomRestHost || self.hasCustomRealtimeHost) { + [NSException raise:NSInvalidArgumentException + format:@"The `endpoint` option cannot be used in conjunction with the `environment`, `restHost`, or `realtimeHost` options."]; + } + _endpoint = endpoint; + // Reset domain selector when endpoint changes + _domainSelector = nil; +} + +- (NSString *)endpoint { + return _endpoint; +} + +- (ARTDomainSelector *)domainSelector { + if (!_domainSelector) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + _domainSelector = [[ARTDomainSelector alloc] initWithEndpointClientOption:_endpoint + fallbackHostsClientOption:_fallbackHosts + environmentClientOption:_environment + restHostClientOption:_restHost + realtimeHostClientOption:_realtimeHost + fallbackHostsUseDefault:_fallbackHostsUseDefault]; +#pragma clang diagnostic pop + } + return _domainSelector; +} + +- (NSString *)primaryDomain { + return self.domainSelector.primaryDomain; +} + +- (NSArray *)fallbackDomains { + return self.domainSelector.fallbackDomains; +} + +// MARK: - Legacy hosts support + +- (void)setEnvironment:(NSString *)environment { + // REC1b1: endpoint cannot be used with deprecated options + if (self.endpoint && [self.endpoint isNotEmptyString]) { + [NSException raise:NSInvalidArgumentException + format:@"The `endpoint` option cannot be used in conjunction with the `environment`, `restHost`, or `realtimeHost` options."]; } - if ([_environment isEqualToString:ARTDefaultProduction]) { - return [ARTDefault restHost]; + + // REC1c1: environment cannot be used with host options + if (self.hasCustomRestHost || self.hasCustomRealtimeHost) { + [NSException raise:NSInvalidArgumentException + format:@"The `environment` option cannot be used in conjunction with the `restHost`, or `realtimeHost` options."]; } - return self.hasEnvironment ? [self host:[ARTDefault restHost] forEnvironment:_environment] : [ARTDefault restHost]; + _environment = environment; + // Reset domain selector when environment changes + _domainSelector = nil; } -- (NSString*)realtimeHost { - if (_realtimeHost != nil) { - return _realtimeHost; +- (NSString *)environment { + return _environment; +} + +- (void)setRestHost:(NSString *)host { + // REC1b1: endpoint cannot be used with deprecated options + if (self.endpoint && [self.endpoint isNotEmptyString]) { + [NSException raise:NSInvalidArgumentException + format:@"The `endpoint` option cannot be used in conjunction with the `environment`, `restHost`, or `realtimeHost` options."]; + } + + // REC1c1: environment cannot be used with host options + if (self.hasEnvironment) { + [NSException raise:NSInvalidArgumentException + format:@"The `environment` option cannot be used in conjunction with the `restHost`, or `realtimeHost` options."]; } - if ([_environment isEqualToString:ARTDefaultProduction]) { - return [ARTDefault realtimeHost]; + _restHost = host; + // Reset domain selector when restHost changes + _domainSelector = nil; +} + +- (NSString *)restHost { + return _restHost ?: self.domainSelector.primaryDomain; +} + +- (void)setRealtimeHost:(NSString *)host { + // REC1b1: endpoint cannot be used with deprecated options + if (self.endpoint && [self.endpoint isNotEmptyString]) { + [NSException raise:NSInvalidArgumentException + format:@"The `endpoint` option cannot be used in conjunction with the `environment`, `restHost`, or `realtimeHost` options."]; } - return self.hasEnvironment ? [self host:[ARTDefault realtimeHost] forEnvironment:_environment] : [ARTDefault realtimeHost]; + // REC1c1: environment cannot be used with host options + if (self.hasEnvironment) { + [NSException raise:NSInvalidArgumentException + format:@"The `environment` option cannot be used in conjunction with the `restHost`, or `realtimeHost` options."]; + } + _realtimeHost = host; + // Reset domain selector when realtimeHost changes + _domainSelector = nil; +} + +- (NSString *)realtimeHost { + return _realtimeHost ?: self.domainSelector.primaryDomain; } - (NSURLComponents *)restUrlComponents { NSURLComponents *components = [[NSURLComponents alloc] init]; components.scheme = self.tls ? @"https" : @"http"; - components.host = self.restHost; + components.host = self.primaryDomain; components.port = [NSNumber numberWithInteger:(self.tls ? self.tlsPort : self.port)]; return components; } -- (NSURL*)restUrl { +- (NSURL *)restUrl { return [self restUrlComponents].URL; } @@ -114,23 +211,21 @@ - (NSURL*)realtimeUrlForHost:(NSString *)host { } - (NSURL*)realtimeUrl { - return [self realtimeUrlForHost:self.realtimeHost]; + return [self realtimeUrlForHost:self.primaryDomain]; } - (id)copyWithZone:(NSZone *)zone { ARTClientOptions *options = [super copyWithZone:zone]; options.clientId = self.clientId; + options.endpoint = self.endpoint; options.port = self.port; options.tlsPort = self.tlsPort; - 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; options.useBinaryProtocol = self.useBinaryProtocol; options.autoConnect = self.autoConnect; - options.environment = self.environment; options.tls = self.tls; options.logLevel = self.logLevel; options.logHandler = self.logHandler; @@ -141,11 +236,15 @@ - (id)copyWithZone:(NSZone *)zone { options.httpMaxRetryDuration = self.httpMaxRetryDuration; options.httpOpenTimeout = self.httpOpenTimeout; options.fallbackRetryTimeout = self.fallbackRetryTimeout; + options.connectivityCheckUrl = self.connectivityCheckUrl; options->_fallbackHosts = self.fallbackHosts; //ignore setter #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" options->_fallbackHostsUseDefault = self.fallbackHostsUseDefault; //ignore setter + if (self->_restHost) options.restHost = self.restHost; + if (self->_realtimeHost) options.realtimeHost = self.realtimeHost; + if (self->_environment) options.environment = self.environment; #pragma clang diagnostic pop options.httpRequestTimeout = self.httpRequestTimeout; @@ -175,27 +274,11 @@ - (BOOL)isBasicAuth { } - (BOOL)hasCustomRestHost { - return (_restHost && ![_restHost isEqualToString:[ARTDefault restHost]]) || (self.hasEnvironment && !self.isProductionEnvironment); -} - -- (BOOL)hasDefaultRestHost { - return ![self hasCustomRestHost]; + return _restHost != nil; } - (BOOL)hasCustomRealtimeHost { - return (_realtimeHost && ![_realtimeHost isEqualToString:[ARTDefault realtimeHost]]) || (self.hasEnvironment && !self.isProductionEnvironment); -} - -- (BOOL)hasDefaultRealtimeHost { - return ![self hasCustomRealtimeHost]; -} - -- (BOOL)hasCustomPort { - return self.port && self.port != [ARTDefault port]; -} - -- (BOOL)hasCustomTlsPort { - return self.tlsPort && self.tlsPort != [ARTDefault tlsPort]; + return _realtimeHost != nil; } - (void)setFallbackHosts:(nullable NSArray *)value { @@ -203,6 +286,8 @@ - (void)setFallbackHosts:(nullable NSArray *)value { [ARTException raise:ARTFallbackIncompatibleOptionsException format:@"Could not setup custom fallback hosts because it is currently configured to use default fallback hosts."]; } _fallbackHosts = value; + // Reset domain selector when fallbackHosts changes + _domainSelector = nil; } - (void)setFallbackHostsUseDefault:(BOOL)value { @@ -210,10 +295,12 @@ - (void)setFallbackHostsUseDefault:(BOOL)value { [ARTException raise:ARTFallbackIncompatibleOptionsException format:@"Could not configure options to use default fallback hosts because a custom fallback host list is being used."]; } _fallbackHostsUseDefault = value; + // Reset domain selector when fallbackHostsUseDefault changes + _domainSelector = nil; } -+ (void)setDefaultEnvironment:(NSString *)environment { - ARTDefaultEnvironment = environment; ++ (void)setDefaultEndpoint:(NSString *)endpoint { + ARTDefaultEndpoint = endpoint; } - (void)setDefaultTokenParams:(ARTTokenParams *)value { @@ -230,19 +317,17 @@ + (BOOL)getDefaultIdempotentRestPublishingForVersion:(NSString *)version { } - (BOOL)isProductionEnvironment { - return [[self.environment lowercaseString] isEqualToString:[ARTDefaultProduction lowercaseString]]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [[self.environment lowercaseString] isEqualToString:[ARTDefaultProductionEnvironment lowercaseString]]; +#pragma clang diagnostic pop } - (BOOL)hasEnvironment { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" return self.environment != nil && [self.environment isNotEmptyString]; -} - -- (BOOL)hasEnvironmentDifferentThanProduction { - return self.hasEnvironment && !self.isProductionEnvironment; -} - -- (NSString *)host:(NSString *)host forEnvironment:(NSString *)environment { - return [NSString stringWithFormat:@"%@-%@", environment, host]; +#pragma clang diagnostic pop } // MARK: - Plugins diff --git a/Source/ARTDefault.m b/Source/ARTDefault.m index 94fe3fa36..5f3f169df 100644 --- a/Source/ARTDefault.m +++ b/Source/ARTDefault.m @@ -1,53 +1,56 @@ #import "Ably.h" #import "ARTDefault+Private.h" -#import "ARTNSArray+ARTFunctional.h" #import "ARTClientInformation+Private.h" +#import "ARTDomainSelector.h" -static NSString *const ARTDefault_apiVersion = @"4"; // CSV2 +NSString *const ARTDefaultAPIVersion = @"4"; // CSV2 +NSString *const ARTDefaultProductionEnvironment = @"production"; -NSString *const ARTDefaultProduction = @"production"; +NSTimeInterval ARTConnectionStateTtl = 60.0; +NSInteger ARTMaxProductionMessageSize = 65536; +NSInteger ARTMaxSandboxMessageSize = 16384; -static NSString *const ARTDefault_restHost = @"rest.ably.io"; -static NSString *const ARTDefault_realtimeHost = @"realtime.ably.io"; - -static NSTimeInterval _connectionStateTtl = 60.0; -static NSInteger _maxProductionMessageSize = 65536; -static NSInteger _maxSandboxMessageSize = 16384; +NSString *const ARTDefaultConnectivityCheckUrl = @"internet-up.ably-realtime.com/is-the-internet-up.txt"; @implementation ARTDefault + (NSString *)apiVersion { - return ARTDefault_apiVersion; + return ARTDefaultAPIVersion; } + (NSString *)libraryVersion { return ARTClientInformation_libraryVersion; } -+ (NSArray*)fallbackHostsWithEnvironment:(NSString *)environment { - NSArray * fallbacks = @[@"a", @"b", @"c", @"d", @"e"]; - NSString *prefix = @""; - NSString *suffix = @""; - if (environment && ![environment isEqualToString:@""] && ![environment isEqualToString:ARTDefaultProduction]) { - prefix = [NSString stringWithFormat:@"%@-", environment]; - suffix = @"-fallback"; - } - - return [fallbacks artMap:^NSString *(NSString * fallback) { - return [NSString stringWithFormat:@"%@%@%@.ably-realtime.com", prefix, fallback, suffix]; - }]; ++ (NSArray *)fallbackHostsWithEnvironment:(NSString *_Nullable)environment { + return [[[ARTDomainSelector alloc] initWithEndpointClientOption:nil + fallbackHostsClientOption:nil + environmentClientOption:environment + restHostClientOption:nil + realtimeHostClientOption:nil + fallbackHostsUseDefault:false] fallbackDomains]; } -+ (NSArray*)fallbackHosts { ++ (NSArray *)fallbackHosts { return [self fallbackHostsWithEnvironment:nil]; } -+ (NSString*)restHost { - return ARTDefault_restHost; ++ (NSString *)restHost { + return [[[ARTDomainSelector alloc] initWithEndpointClientOption:nil + fallbackHostsClientOption:nil + environmentClientOption:nil + restHostClientOption:nil + realtimeHostClientOption:nil + fallbackHostsUseDefault:false] primaryDomain]; } -+ (NSString*)realtimeHost { - return ARTDefault_realtimeHost; ++ (NSString *)realtimeHost { + return [[[ARTDomainSelector alloc] initWithEndpointClientOption:nil + fallbackHostsClientOption:nil + environmentClientOption:nil + restHostClientOption:nil + realtimeHostClientOption:nil + fallbackHostsUseDefault:false] primaryDomain]; } + (int)port { @@ -63,7 +66,7 @@ + (NSTimeInterval)ttl { } + (NSTimeInterval)connectionStateTtl { - return _connectionStateTtl; + return ARTConnectionStateTtl; } + (NSTimeInterval)realtimeRequestTimeout { @@ -72,45 +75,45 @@ + (NSTimeInterval)realtimeRequestTimeout { + (NSInteger)maxMessageSize { #if DEBUG - return _maxSandboxMessageSize; + return ARTMaxSandboxMessageSize; #else - return _maxProductionMessageSize; + return ARTMaxProductionMessageSize; #endif } + (NSInteger)maxSandboxMessageSize { - return _maxSandboxMessageSize; + return ARTMaxSandboxMessageSize; } + (NSInteger)maxProductionMessageSize { - return _maxProductionMessageSize; + return ARTMaxProductionMessageSize; } + (void)setConnectionStateTtl:(NSTimeInterval)value { @synchronized (self) { - _connectionStateTtl = value; + ARTConnectionStateTtl = value; } } + (void)setMaxMessageSize:(NSInteger)value { @synchronized (self) { #if DEBUG - _maxSandboxMessageSize = value; + ARTMaxSandboxMessageSize = value; #else - _maxProductionMessageSize = value; + ARTMaxProductionMessageSize = value; #endif } } + (void)setMaxProductionMessageSize:(NSInteger)value { @synchronized (self) { - _maxProductionMessageSize = value; + ARTMaxProductionMessageSize = value; } } + (void)setMaxSandboxMessageSize:(NSInteger)value { @synchronized (self) { - _maxSandboxMessageSize = value; + ARTMaxSandboxMessageSize = value; } } @@ -122,4 +125,8 @@ + (NSString *)platformAgent { return [ARTClientInformation platformAgentIdentifier]; } ++ (NSString *)connectivityCheckUrl { + return [NSString stringWithFormat:@"https://%@", ARTDefaultConnectivityCheckUrl]; +} + @end diff --git a/Source/ARTDomainSelector.m b/Source/ARTDomainSelector.m new file mode 100644 index 000000000..55099a037 --- /dev/null +++ b/Source/ARTDomainSelector.m @@ -0,0 +1,218 @@ +#import "ARTDomainSelector.h" + +NS_ASSUME_NONNULL_BEGIN + +// Constants for domain construction +static NSString *const ARTDefaultPrimaryTLD = @"ably.net"; +static NSString *const ARTDefaultNonprodPrimaryTLD = @"ably-nonprod.net"; +static NSString *const ARTDefaultFallbacksTLD = @"ably-realtime.com"; +static NSString *const ARTDefaultNonprodFallbacksTLD = @"ably-realtime-nonprod.com"; +static NSString *const ARTDefaultRoutingSubdomain = @"realtime"; +static NSString *const ARTDefaultRoutingPolicy = @"main"; + +/** + The policy that the library will use to determine its REC1 primary domain. + */ +typedef NS_ENUM(NSInteger, ARTPrimaryDomainSelectionPolicy) { + /// REC1a: The `endpoint` client option has not been specified. + ARTPrimaryDomainSelectionPolicyDefault, + + /// REC1b2: The `endpoint` client option is a hostname. + ARTPrimaryDomainSelectionPolicyHostname, + + /// REC1b3: The `endpoint` client option specifies a non-production routing policy. + ARTPrimaryDomainSelectionPolicyNonProductionRoutingPolicy, + + /// REC1b4: The `endpoint` client option is a production routing policy ID. + ARTPrimaryDomainSelectionPolicyProductionRoutingPolicy, + + /// REC1c: Deprecated `environment` option is being used. + ARTPrimaryDomainSelectionPolicyLegacyEnvironment, + + /// REC1d: Deprecated `restHost` or `realtimeHost` option is being used. + ARTPrimaryDomainSelectionPolicyLegacyHost +}; + +@interface ARTDomainSelector () + +@property (nonatomic) ARTPrimaryDomainSelectionPolicy primaryDomainSelectionPolicy; +@property (nonatomic, nullable) NSString *policyId; +@property (nonatomic, nullable) NSString *hostname; +@property (nonatomic, nullable) NSArray *fallbackHostsClientOption; +@property (nonatomic) BOOL fallbackHostsUseDefault; + +@end + +@implementation ARTDomainSelector + +- (instancetype)initWithEndpointClientOption:(nullable NSString *)endpointClientOption + fallbackHostsClientOption:(nullable NSArray *)fallbackHostsClientOption + environmentClientOption:(nullable NSString *)environmentClientOption + restHostClientOption:(nullable NSString *)restHostClientOption + realtimeHostClientOption:(nullable NSString *)realtimeHostClientOption + fallbackHostsUseDefault:(BOOL)fallbackHostsUseDefault { + self = [super init]; + if (self) { + _fallbackHostsClientOption = fallbackHostsClientOption; + _fallbackHostsUseDefault = fallbackHostsUseDefault; + [self parsePrimaryDomainSelectionPolicyFromOptions:endpointClientOption + environment:environmentClientOption + restHost:restHostClientOption + realtimeHost:realtimeHostClientOption]; + } + return self; +} + +- (void)parsePrimaryDomainSelectionPolicyFromOptions:(nullable NSString *)endpointClientOption + environment:(nullable NSString *)environmentClientOption + restHost:(nullable NSString *)restHostClientOption + realtimeHost:(nullable NSString *)realtimeHostClientOption { + // Check for endpoint first (REC1b) + if (endpointClientOption != nil && endpointClientOption.length > 0) { + // REC1b2 - Check if it's a hostname (contains "." or "::" or is "localhost") + if ([endpointClientOption containsString:@"."] || + [endpointClientOption containsString:@"::"] || + [endpointClientOption isEqualToString:@"localhost"]) { + self.primaryDomainSelectionPolicy = ARTPrimaryDomainSelectionPolicyHostname; + self.hostname = endpointClientOption; + return; + } + + // REC1b3 - Check if it has "nonprod:" prefix + NSString *nonprodPrefix = @"nonprod:"; + if ([endpointClientOption hasPrefix:nonprodPrefix]) { + NSString *policyId = [endpointClientOption substringFromIndex:nonprodPrefix.length]; + self.primaryDomainSelectionPolicy = ARTPrimaryDomainSelectionPolicyNonProductionRoutingPolicy; + self.policyId = policyId; + return; + } + + // REC1b4 - Production routing policy + self.primaryDomainSelectionPolicy = ARTPrimaryDomainSelectionPolicyProductionRoutingPolicy; + self.policyId = endpointClientOption; + return; + } + + // Legacy environment handling (REC1c) + if (environmentClientOption != nil && environmentClientOption.length > 0) { + self.primaryDomainSelectionPolicy = ARTPrimaryDomainSelectionPolicyLegacyEnvironment; + self.policyId = environmentClientOption; + return; + } + + // Legacy host override (REC1d) + if (restHostClientOption != nil && restHostClientOption.length > 0) { + self.primaryDomainSelectionPolicy = ARTPrimaryDomainSelectionPolicyLegacyHost; + self.hostname = restHostClientOption; + return; + } + + if (realtimeHostClientOption != nil && realtimeHostClientOption.length > 0) { + self.primaryDomainSelectionPolicy = ARTPrimaryDomainSelectionPolicyLegacyHost; + self.hostname = realtimeHostClientOption; + return; + } + + // REC1a - Default + self.primaryDomainSelectionPolicy = ARTPrimaryDomainSelectionPolicyDefault; +} + +- (NSString *)primaryDomain { + switch (self.primaryDomainSelectionPolicy) { + case ARTPrimaryDomainSelectionPolicyDefault: + // REC1a + return [NSString stringWithFormat:@"%@.%@.%@", ARTDefaultRoutingPolicy, ARTDefaultRoutingSubdomain, ARTDefaultPrimaryTLD]; + + case ARTPrimaryDomainSelectionPolicyHostname: + // REC1b2 + return self.hostname; + + case ARTPrimaryDomainSelectionPolicyNonProductionRoutingPolicy: + // REC1b3 + return [NSString stringWithFormat:@"%@.%@.%@", self.policyId, ARTDefaultRoutingSubdomain, ARTDefaultNonprodPrimaryTLD]; + + case ARTPrimaryDomainSelectionPolicyProductionRoutingPolicy: + // REC1b4 + return [NSString stringWithFormat:@"%@.%@.%@", self.policyId, ARTDefaultRoutingSubdomain, ARTDefaultPrimaryTLD]; + + case ARTPrimaryDomainSelectionPolicyLegacyEnvironment: + // REC1c + return [NSString stringWithFormat:@"%@.%@.%@", self.policyId, ARTDefaultRoutingSubdomain, ARTDefaultPrimaryTLD]; + + case ARTPrimaryDomainSelectionPolicyLegacyHost: + // REC1d + return self.hostname; + } +} + +- (NSArray *)fallbackDomains { + // REC2a2: First check if explicit fallback hosts are provided + if (self.fallbackHostsClientOption) { + return self.fallbackHostsClientOption; + } + + // REC2b: Check deprecated fallbackHostsUseDefault + if (self.fallbackHostsUseDefault) { + return [self defaultFallbackDomains]; + } + + NSArray *aToE = @[@"a", @"b", @"c", @"d", @"e"]; + + switch (self.primaryDomainSelectionPolicy) { + case ARTPrimaryDomainSelectionPolicyDefault: + // REC2c1 + return [self defaultFallbackDomains]; + + case ARTPrimaryDomainSelectionPolicyHostname: + // REC2c2 + return @[]; + + case ARTPrimaryDomainSelectionPolicyNonProductionRoutingPolicy: { + // REC2c3 + NSMutableArray *domains = [NSMutableArray arrayWithCapacity:aToE.count]; + for (NSString *letter in aToE) { + NSString *subdomain = [NSString stringWithFormat:@"%@.%@.fallback", self.policyId, letter]; + [domains addObject:[NSString stringWithFormat:@"%@.%@", subdomain, ARTDefaultNonprodFallbacksTLD]]; + } + return [domains copy]; + } + + case ARTPrimaryDomainSelectionPolicyProductionRoutingPolicy: { + // REC2c4 + NSMutableArray *domains = [NSMutableArray arrayWithCapacity:aToE.count]; + for (NSString *letter in aToE) { + NSString *subdomain = [NSString stringWithFormat:@"%@.%@.fallback", self.policyId, letter]; + [domains addObject:[NSString stringWithFormat:@"%@.%@", subdomain, ARTDefaultFallbacksTLD]]; + } + return [domains copy]; + } + + case ARTPrimaryDomainSelectionPolicyLegacyEnvironment: { + // REC2c5: legacy environment handling + NSMutableArray *domains = [NSMutableArray arrayWithCapacity:aToE.count]; + for (NSString *letter in aToE) { + NSString *subdomain = [NSString stringWithFormat:@"%@.%@.fallback", self.policyId, letter]; + [domains addObject:[NSString stringWithFormat:@"%@.%@", subdomain, ARTDefaultFallbacksTLD]]; + } + return [domains copy]; + } + + case ARTPrimaryDomainSelectionPolicyLegacyHost: + // REC2c6: legacy hosts handling + return @[]; + } +} + +- (NSArray *)defaultFallbackDomains { + // Returns: ["main.a.fallback.ably-realtime.com", "main.b.fallback.ably-realtime.com", ...] + NSArray *aToE = @[@"a", @"b", @"c", @"d", @"e"]; + NSMutableArray *domains = [NSMutableArray arrayWithCapacity:aToE.count]; + for (NSString *letter in aToE) { + [domains addObject:[NSString stringWithFormat:@"%@.%@.fallback.%@", ARTDefaultRoutingPolicy, letter, ARTDefaultFallbacksTLD]]; + } + return [domains copy]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/ARTFallbackHosts.m b/Source/ARTFallbackHosts.m deleted file mode 100644 index 3ba2b344c..000000000 --- a/Source/ARTFallbackHosts.m +++ /dev/null @@ -1,29 +0,0 @@ -#import "ARTFallbackHosts.h" - -#import "ARTDefault+Private.h" -#import "ARTClientOptions+Private.h" - -@implementation ARTFallbackHosts - -+ (nullable NSArray *)hostsFromOptions:(ARTClientOptions *)options { - if (options.fallbackHosts) { - return options.fallbackHosts; - } - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - if (options.fallbackHostsUseDefault) { - return [ARTDefault fallbackHosts]; - } -#pragma clang diagnostic pop - - if (options.hasEnvironmentDifferentThanProduction) { - return [ARTDefault fallbackHostsWithEnvironment:options.environment]; - } - if (options.hasCustomRestHost || options.hasCustomRealtimeHost || options.hasCustomPort || options.hasCustomTlsPort) { - return nil; - } - return [ARTDefault fallbackHosts]; -} - -@end diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index 4296640ff..883a2e722 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -32,7 +32,6 @@ #import "ARTStats.h" #import "ARTRealtimeTransport.h" #import "ARTFallback.h" -#import "ARTFallbackHosts.h" #import "ARTAuthDetails.h" #import "ARTGCD.h" #import "ARTEncoder.h" @@ -55,6 +54,7 @@ #import "ARTRealtimeTransportFactory.h" #import "ARTConnectRetryState.h" #import "ARTWrapperSDKProxyRealtime+Private.h" +#import "ARTDomainSelector.h" @interface ARTConnectionStateChange () @@ -1521,28 +1521,8 @@ - (BOOL)reconnectWithFallback { - (BOOL)shouldRetryWithFallbackForError:(ARTRealtimeTransportError *)error options:(ARTClientOptions *)options { if ((error.type == ARTRealtimeTransportErrorTypeBadResponse && error.badResponseCode >= 500 && error.badResponseCode <= 504) || error.type == ARTRealtimeTransportErrorTypeHostUnreachable || error.type == ARTRealtimeTransportErrorTypeTimeout) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - // RTN17b3 - if (options.fallbackHostsUseDefault) { - return YES; - } -#pragma clang diagnostic pop - - // RTN17b1 - if (!(options.hasCustomRealtimeHost || options.hasCustomPort || options.hasCustomTlsPort)) { - return YES; - } - - // RTN17b2 - if (options.fallbackHosts) { - return YES; - } - - // RSC15g2 - if (options.hasEnvironmentDifferentThanProduction) { - return YES; - } + // RSC15m + return options.domainSelector.fallbackDomains.count > 0; } return NO; } @@ -1693,7 +1673,7 @@ - (void)realtimeTransportFailed:(id)transport withError:(A if (![self isSuspendMode] && [self shouldRetryWithFallbackForError:transportError options:clientOptions]) { ARTLogDebug(self.logger, @"R:%p host is down; can retry with fallback host", self); if (!_fallbacks) { - NSArray *hosts = [ARTFallbackHosts hostsFromOptions:clientOptions]; + NSArray *hosts = clientOptions.domainSelector.fallbackDomains; _fallbacks = [[ARTFallback alloc] initWithFallbackHosts:hosts shuffleArray:clientOptions.testOptions.shuffleArray]; } if (_fallbacks) { diff --git a/Source/ARTRest.m b/Source/ARTRest.m index b17792a3e..6aac84c15 100644 --- a/Source/ARTRest.m +++ b/Source/ARTRest.m @@ -16,9 +16,9 @@ #import "ARTHttp.h" #import "ARTClientOptions+Private.h" #import "ARTDefault.h" +#import "ARTDefault+Private.h" #import "ARTStats.h" #import "ARTFallback+Private.h" -#import "ARTFallbackHosts.h" #import "ARTNSDictionary+ARTDictionaryUtil.h" #import "ARTNSArray+ARTFunctional.h" #import "ARTRestChannel.h" @@ -47,6 +47,7 @@ #import "ARTPushActivationStateMachine+Private.h" #import "ARTPushActivationEvent.h" #endif +#import "ARTDomainSelector.h" @implementation ARTRest { ARTQueuedDealloc *_dealloc; @@ -372,14 +373,12 @@ - (NSString *)agentIdentifierWithWrapperSDKAgents:(nullable NSDictionary_options.httpMaxRetryCount && [self shouldRetryWithFallback:request response:response error:error]) { if (!blockFallbacks) { - NSArray *hosts = [ARTFallbackHosts hostsFromOptions:self->_options]; + NSArray *hosts = self->_options.domainSelector.fallbackDomains; blockFallbacks = [[ARTFallback alloc] initWithFallbackHosts:hosts shuffleArray:self->_options.testOptions.shuffleArray]; } if (blockFallbacks) { @@ -459,7 +458,7 @@ - (NSString *)agentIdentifierWithWrapperSDKAgents:(nullable NSDictionary *)internetIsUp:(void (^)(BOOL isUp)) cb { - NSURL *requestUrl = [NSURL URLWithString:@"https://internet-up.ably-realtime.com/is-the-internet-up.txt"]; + NSURL *requestUrl = [NSURL URLWithString:self.options.connectivityCheckUrl ?: ARTDefault.connectivityCheckUrl]; // REC3a, REC3b NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestUrl]; request.HTTPMethod = @"GET"; diff --git a/Source/ARTWebSocketTransport.m b/Source/ARTWebSocketTransport.m index 41c37e387..342d9767f 100644 --- a/Source/ARTWebSocketTransport.m +++ b/Source/ARTWebSocketTransport.m @@ -20,6 +20,7 @@ #import "ARTConnection+Private.h" #import "ARTInternalLog.h" #import "ARTWebSocketFactory.h" +#import "ARTDomainSelector.h" enum { ARTWsNeverConnected = -1, @@ -75,7 +76,7 @@ The important thing here is that it has a fixed QoS class (of `QOS_CLASS_DEFAULT - (instancetype)initWithRest:(ARTRestInternal *)rest options:(ARTClientOptions *)options resumeKey:(NSString *)resumeKey logger:(ARTInternalLog *)logger webSocketFactory:(id)webSocketFactory { self = [super init]; if (self) { - _host = options.realtimeHost; + _host = options.domainSelector.primaryDomain; _workQueue = rest.queue; _websocketOpenQueue = dispatch_queue_create("io.ably.websocketOpen", dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT, 0)); _websocket = nil; diff --git a/Source/Ably.modulemap b/Source/Ably.modulemap index e4b4bdd64..fe0de871a 100644 --- a/Source/Ably.modulemap +++ b/Source/Ably.modulemap @@ -50,7 +50,6 @@ framework module Ably { header "ARTMessageAnnotations+Private.h" header "ARTRestAnnotations+Private.h" header "ARTFallback+Private.h" - header "ARTFallbackHosts.h" header "ARTLocalDevice+Private.h" header "ARTDeviceDetails+Private.h" header "ARTDevicePushDetails+Private.h" @@ -140,5 +139,6 @@ framework module Ably { header "ARTPublicRealtimeChannelUnderlyingObjects.h" header "ARTErrorInfo+Private.h" header "ARTSummaryTypes+Private.h" + header "ARTDomainSelector.h" } } diff --git a/Source/PrivateHeaders/Ably/ARTClientOptions+Private.h b/Source/PrivateHeaders/Ably/ARTClientOptions+Private.h index a2d00f49d..b2ba756de 100644 --- a/Source/PrivateHeaders/Ably/ARTClientOptions+Private.h +++ b/Source/PrivateHeaders/Ably/ARTClientOptions+Private.h @@ -4,6 +4,8 @@ @import _AblyPluginSupportPrivate; #endif +@class ARTDomainSelector; + NS_ASSUME_NONNULL_BEGIN #ifdef ABLY_SUPPORTS_PLUGINS @@ -14,16 +16,10 @@ NS_ASSUME_NONNULL_BEGIN @interface ARTClientOptions () @property (readonly) BOOL isProductionEnvironment; -@property (readonly) BOOL hasEnvironment; -@property (readonly) BOOL hasEnvironmentDifferentThanProduction; -@property (readonly) BOOL hasCustomRestHost; -@property (readonly) BOOL hasDefaultRestHost; -@property (readonly) BOOL hasCustomRealtimeHost; -@property (readonly) BOOL hasDefaultRealtimeHost; -@property (readonly) BOOL hasCustomPort; -@property (readonly) BOOL hasCustomTlsPort; - -+ (void)setDefaultEnvironment:(nullable NSString *)environment; + +@property (readonly, nonatomic) ARTDomainSelector *domainSelector; + ++ (void)setDefaultEndpoint:(nullable NSString *)endpoint; + (BOOL)getDefaultIdempotentRestPublishingForVersion:(NSString *)version; - (NSURLComponents *)restUrlComponents; - (NSURL*)realtimeUrlForHost:(NSString *)host; // helps obtain url with an alternative host without changing other params diff --git a/Source/PrivateHeaders/Ably/ARTDefault+Private.h b/Source/PrivateHeaders/Ably/ARTDefault+Private.h index 328326353..213795d58 100644 --- a/Source/PrivateHeaders/Ably/ARTDefault+Private.h +++ b/Source/PrivateHeaders/Ably/ARTDefault+Private.h @@ -1,6 +1,6 @@ #import -extern NSString *const ARTDefaultProduction; +extern NSString *const ARTDefaultProductionEnvironment; @interface ARTDefault (Private) @@ -10,4 +10,6 @@ extern NSString *const ARTDefaultProduction; + (NSInteger)maxSandboxMessageSize; + (NSInteger)maxProductionMessageSize; ++ (NSString *)connectivityCheckUrl; + @end diff --git a/Source/PrivateHeaders/Ably/ARTDomainSelector.h b/Source/PrivateHeaders/Ably/ARTDomainSelector.h new file mode 100644 index 000000000..e233c0079 --- /dev/null +++ b/Source/PrivateHeaders/Ably/ARTDomainSelector.h @@ -0,0 +1,42 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + The policy that the library will use to determine its REC1 primary domain and REC2 set of fallback domains. + */ +@interface ARTDomainSelector : NSObject + +/** + Initialize with all client options that affect domain selection. + + @param endpointClientOption The value of the `endpoint` client option. + @param fallbackHostsClientOption The value of the `fallbackHosts` client option. + @param environmentClientOption The value of the deprecated `environment` client option. + @param restHostClientOption The value of the deprecated `restHost` client option. + @param realtimeHostClientOption The value of the deprecated `realtimeHost` client option. + @param fallbackHostsUseDefault The value of the deprecated `fallbackHostsUseDefault` client option. + @return An initialized instance of ARTDomainSelector. + */ +- (instancetype)initWithEndpointClientOption:(nullable NSString *)endpointClientOption + fallbackHostsClientOption:(nullable NSArray *)fallbackHostsClientOption + environmentClientOption:(nullable NSString *)environmentClientOption + restHostClientOption:(nullable NSString *)restHostClientOption + realtimeHostClientOption:(nullable NSString *)realtimeHostClientOption + fallbackHostsUseDefault:(BOOL)fallbackHostsUseDefault; + +- (instancetype)init NS_UNAVAILABLE; + +/** + The REC1 primary domain. + */ +@property (nonatomic, readonly) NSString *primaryDomain; + +/** + The set of fallback domains, as defined by REC2. + */ +@property (nonatomic, readonly) NSArray *fallbackDomains; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/PrivateHeaders/Ably/ARTFallbackHosts.h b/Source/PrivateHeaders/Ably/ARTFallbackHosts.h deleted file mode 100644 index 2e82be718..000000000 --- a/Source/PrivateHeaders/Ably/ARTFallbackHosts.h +++ /dev/null @@ -1,13 +0,0 @@ -#import - -@class ARTClientOptions; - -NS_ASSUME_NONNULL_BEGIN - -@interface ARTFallbackHosts : NSObject - -+ (nullable NSArray *)hostsFromOptions:(ARTClientOptions *)options; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Source/include/Ably/ARTClientOptions.h b/Source/include/Ably/ARTClientOptions.h index 44acfe41a..ae480c231 100644 --- a/Source/include/Ably/ARTClientOptions.h +++ b/Source/include/Ably/ARTClientOptions.h @@ -19,15 +19,23 @@ extern const ARTPluginName ARTPluginNameLiveObjects; */ @interface ARTClientOptions : ARTAuthOptions +/** + * Enables a custom endpoint for connecting to the Ably service (see [Platform Customization](https://ably.com/docs/platform-customization) for more information). + * This may be a routing policy name (such as `main`), a nonprod routing policy name (such as `nonprod:sandbox`) or a FQDN such as `foo.example.com`. + */ +@property (readwrite, nonatomic, nullable) NSString *endpoint; + /** * Enables a non-default Ably host to be specified. For development environments only. The default value is `rest.ably.io`. + * @deprecated This property is deprecated and will be removed in a future version. Use the `endpoint` client option instead. */ -@property (readwrite, nonatomic) NSString *restHost; +@property (readwrite, nonatomic) NSString *restHost DEPRECATED_MSG_ATTRIBUTE("Use the endpoint client option instead."); /** * Enables a non-default Ably host to be specified for realtime connections. For development environments only. The default value is `realtime.ably.io`. + * @deprecated This property is deprecated and will be removed in a future version. Use the `endpoint` client option instead. */ -@property (readwrite, nonatomic) NSString *realtimeHost; +@property (readwrite, nonatomic) NSString *realtimeHost DEPRECATED_MSG_ATTRIBUTE("Use the endpoint client option instead."); /** * Enables a non-default Ably port to be specified. For development environments only. The default value is 80. @@ -41,14 +49,20 @@ extern const ARTPluginName ARTPluginNameLiveObjects; /** * Enables a [custom environment](https://ably.com/docs/platform-customization) to be used with the Ably service. + * @deprecated This property is deprecated and will be removed in a future version. Use the `endpoint` client option instead. */ -@property (readwrite, nonatomic, nullable) NSString *environment; +@property (readwrite, nonatomic, nullable) NSString *environment DEPRECATED_MSG_ATTRIBUTE("Use the endpoint client option instead."); /** * When `false`, the client will use an insecure connection. The default is `true`, meaning a TLS connection will be used to connect to Ably. */ @property (nonatomic) BOOL tls; +/** + * The URL of an endpoint that the SDK can use to determine whether or not the Internet connection is working. It is only necessary to specify a non-default value if the default endpoint is not expected to be reachable in a particular network setting. + */ +@property (readwrite, nonatomic, nullable) NSString *connectivityCheckUrl; + /** * Controls the log output of the library. This is an object to handle each line of log output. */ diff --git a/Source/include/Ably/ARTDefault.h b/Source/include/Ably/ARTDefault.h index 16b807c39..a96c78ea7 100644 --- a/Source/include/Ably/ARTDefault.h +++ b/Source/include/Ably/ARTDefault.h @@ -12,8 +12,9 @@ NS_ASSUME_NONNULL_BEGIN + (NSArray *)fallbackHosts; + (NSArray *)fallbackHostsWithEnvironment:(NSString *_Nullable)environment; -+ (NSString*)restHost; -+ (NSString*)realtimeHost; ++ (NSString *)restHost; ++ (NSString *)realtimeHost; + + (int)port; + (int)tlsPort; diff --git a/Source/include/module.modulemap b/Source/include/module.modulemap index f179f6aa4..8a964d0df 100644 --- a/Source/include/module.modulemap +++ b/Source/include/module.modulemap @@ -50,7 +50,6 @@ module Ably { header "../PrivateHeaders/Ably/ARTMessageAnnotations+Private.h" header "../PrivateHeaders/Ably/ARTRestAnnotations+Private.h" header "../PrivateHeaders/Ably/ARTFallback+Private.h" - header "../PrivateHeaders/Ably/ARTFallbackHosts.h" header "../PrivateHeaders/Ably/ARTLocalDevice+Private.h" header "../PrivateHeaders/Ably/ARTDeviceDetails+Private.h" header "../PrivateHeaders/Ably/ARTDevicePushDetails+Private.h" @@ -140,5 +139,6 @@ module Ably { header "../PrivateHeaders/Ably/ARTPublicRealtimeChannelUnderlyingObjects.h" header "../PrivateHeaders/Ably/ARTErrorInfo+Private.h" header "../PrivateHeaders/Ably/ARTSummaryTypes+Private.h" + header "../PrivateHeaders/Ably/ARTDomainSelector.h" } } diff --git a/Test/AblyTests/Test Utilities/TestUtilities.swift b/Test/AblyTests/Test Utilities/TestUtilities.swift index ae8217c51..6dfcd875b 100644 --- a/Test/AblyTests/Test Utilities/TestUtilities.swift +++ b/Test/AblyTests/Test Utilities/TestUtilities.swift @@ -116,7 +116,7 @@ class AblyTests { if let testApplication { app = testApplication } else { - let request = NSMutableURLRequest(url: URL(string: "https://\(options.restHost):\(options.tlsPort)/apps")!) + let request = NSMutableURLRequest(url: URL(string: "https://\(options.domainSelector.primaryDomain):\(options.tlsPort)/apps")!) request.httpMethod = "POST" request.httpBody = try JSONUtility.encode(appSetupModel.postApps) @@ -143,7 +143,7 @@ class AblyTests { class func clientOptions(for test: Test, debug: Bool = false, key: String? = nil, requestToken: Bool = false) throws -> ARTClientOptions { let options = ARTClientOptions() - options.environment = getEnvironment() + options.endpoint = getTestEndpoint() if debug { options.logLevel = .verbose } @@ -603,8 +603,7 @@ func getJWTToken(for test: Test, invalid: Bool = false, expiresIn: Int = 3600, c URLQueryItem(name: "clientId", value: clientId), URLQueryItem(name: "capability", value: capability), URLQueryItem(name: "jwtType", value: jwtType), - URLQueryItem(name: "encrypted", value: String(encrypted)), - URLQueryItem(name: "environment", value: getEnvironment()) + URLQueryItem(name: "encrypted", value: String(encrypted)) ] let request = NSMutableURLRequest(url: urlComponents!.url!) @@ -624,12 +623,15 @@ public func delay(_ seconds: TimeInterval, closure: @escaping () -> Void) { DispatchQueue.main.asyncAfter(deadline: .now() + seconds, execute: closure) } -public func getEnvironment() -> String { - let b = Bundle(for: AblyTests.self) - guard let env = b.infoDictionary!["ABLY_ENV"] as? String, env.count > 0 else { - return "sandbox" +public func getTestEndpoint() -> String { + let bundle = Bundle(for: AblyTests.self) + if let endpoint = bundle.infoDictionary!["ABLY_ENDPOINT"] as? String, endpoint.count > 0 { + return endpoint } - return env + if let env = bundle.infoDictionary!["ABLY_ENV"] as? String, env.count > 0 { + return env + } + return "nonprod:sandbox" } public func buildMessagesThatExceedMaxMessageSize() -> [ARTMessage] { diff --git a/Test/AblyTests/Tests/ARTDomainSelectorTests.swift b/Test/AblyTests/Tests/ARTDomainSelectorTests.swift new file mode 100644 index 000000000..c468a74a7 --- /dev/null +++ b/Test/AblyTests/Tests/ARTDomainSelectorTests.swift @@ -0,0 +1,441 @@ +import Ably +import Nimble +import XCTest + +class ARTDomainSelectorTests: XCTestCase { + + // MARK: - REC1: Primary Domain Tests + + // REC1a: Default case + func test__001__ARTDomainSelector__primaryDomain__should_return_default_domain_when_endpoint_is_nil() { + let selector = ARTDomainSelector( + endpointClientOption: nil, + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.primaryDomain, "main.realtime.ably.net") + } + + // REC1b2: Hostname cases + func test__002__ARTDomainSelector__primaryDomain__should_return_hostname_when_endpoint_contains_dot() { + let selector = ARTDomainSelector( + endpointClientOption: "test.ably.net", + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.primaryDomain, "test.ably.net") + } + + func test__003__ARTDomainSelector__primaryDomain__should_return_hostname_when_endpoint_contains_double_colon() { + let selector = ARTDomainSelector( + endpointClientOption: "::1", + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.primaryDomain, "::1") + } + + func test__004__ARTDomainSelector__primaryDomain__should_return_localhost_when_endpoint_is_localhost() { + let selector = ARTDomainSelector( + endpointClientOption: "localhost", + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.primaryDomain, "localhost") + } + + func test__005__ARTDomainSelector__primaryDomain__should_handle_IP_address() { + let selector = ARTDomainSelector( + endpointClientOption: "192.168.1.1", + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.primaryDomain, "192.168.1.1") + } + + // REC1b3: Non-production routing policy + func test__006__ARTDomainSelector__primaryDomain__should_return_nonprod_domain_for_nonprod_routing_policy() { + let selector = ARTDomainSelector( + endpointClientOption: "nonprod:sandbox", + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.primaryDomain, "sandbox.realtime.ably-nonprod.net") + } + + // REC1b4: Production routing policy + func test__008__ARTDomainSelector__primaryDomain__should_return_production_domain_for_routing_policy_id() { + let selector = ARTDomainSelector( + endpointClientOption: "test", + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.primaryDomain, "test.realtime.ably.net") + } + + // MARK: - REC2: Fallback Domains Tests + + // REC2a: Custom fallback hosts + func test__010__ARTDomainSelector__fallbackDomains__should_return_custom_fallback_hosts_when_provided() { + let customHosts = ["custom1.example.com", "custom2.example.com"] + let selector = ARTDomainSelector( + endpointClientOption: nil, + fallbackHostsClientOption: customHosts, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.fallbackDomains, customHosts) + } + + func test__011__ARTDomainSelector__fallbackDomains__should_override_defaults_with_custom_hosts() { + let customHosts = ["fallback1.com", "fallback2.com"] + let selector = ARTDomainSelector( + endpointClientOption: "myrouting", + fallbackHostsClientOption: customHosts, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.fallbackDomains, customHosts) + } + + // REC2c1: Default fallback domains + func test__012__ARTDomainSelector__fallbackDomains__should_return_default_fallback_domains() { + let selector = ARTDomainSelector( + endpointClientOption: nil, + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + let expectedDomains = [ + "main.a.fallback.ably-realtime.com", + "main.b.fallback.ably-realtime.com", + "main.c.fallback.ably-realtime.com", + "main.d.fallback.ably-realtime.com", + "main.e.fallback.ably-realtime.com" + ] + + XCTAssertEqual(selector.fallbackDomains, expectedDomains) + } + + // REC2c2: Hostname has no fallbacks + func test__013__ARTDomainSelector__fallbackDomains__should_return_empty_array_for_hostname() { + let selector = ARTDomainSelector( + endpointClientOption: "custom.ably.io", + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.fallbackDomains.count, 0) + XCTAssertEqual(selector.fallbackDomains, []) + } + + func test__014__ARTDomainSelector__fallbackDomains__should_return_empty_array_for_localhost() { + let selector = ARTDomainSelector( + endpointClientOption: "localhost", + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.fallbackDomains, []) + } + + func test__015__ARTDomainSelector__fallbackDomains__should_return_empty_array_for_IP_address() { + let selector = ARTDomainSelector( + endpointClientOption: "192.168.1.1", + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.fallbackDomains, []) + } + + // REC2c3: Non-production routing policy fallbacks + func test__016__ARTDomainSelector__fallbackDomains__should_return_nonprod_fallbacks_for_nonprod_routing_policy() { + let selector = ARTDomainSelector( + endpointClientOption: "nonprod:sandbox", + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + let expectedDomains = [ + "sandbox.a.fallback.ably-realtime-nonprod.com", + "sandbox.b.fallback.ably-realtime-nonprod.com", + "sandbox.c.fallback.ably-realtime-nonprod.com", + "sandbox.d.fallback.ably-realtime-nonprod.com", + "sandbox.e.fallback.ably-realtime-nonprod.com" + ] + + XCTAssertEqual(selector.fallbackDomains, expectedDomains) + } + + // REC2c4: Production routing policy fallbacks + func test__017__ARTDomainSelector__fallbackDomains__should_return_production_fallbacks_for_routing_policy() { + let selector = ARTDomainSelector( + endpointClientOption: "myrouting", + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + let expectedDomains = [ + "myrouting.a.fallback.ably-realtime.com", + "myrouting.b.fallback.ably-realtime.com", + "myrouting.c.fallback.ably-realtime.com", + "myrouting.d.fallback.ably-realtime.com", + "myrouting.e.fallback.ably-realtime.com" + ] + + XCTAssertEqual(selector.fallbackDomains, expectedDomains) + } + + // MARK: - Legacy Options Tests + + // REC1c2: Deprecated environment option + func test__018__ARTDomainSelector__primaryDomain__should_use_environment_option_when_set() { + let selector = ARTDomainSelector( + endpointClientOption: nil, + fallbackHostsClientOption: nil, + environmentClientOption: "sandbox", + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.primaryDomain, "sandbox.realtime.ably.net") + } + + // REC2c5 + func test__019__ARTDomainSelector__fallbackDomains__should_use_environment_for_fallbacks() { + let selector = ARTDomainSelector( + endpointClientOption: nil, + fallbackHostsClientOption: nil, + environmentClientOption: "sandbox", + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + let expectedDomains = [ + "sandbox.a.fallback.ably-realtime.com", + "sandbox.b.fallback.ably-realtime.com", + "sandbox.c.fallback.ably-realtime.com", + "sandbox.d.fallback.ably-realtime.com", + "sandbox.e.fallback.ably-realtime.com" + ] + + XCTAssertEqual(selector.fallbackDomains, expectedDomains) + } + + // REC1d: Deprecated restHost option + func test__020__ARTDomainSelector__primaryDomain__should_use_restHost_when_set() { + let selector = ARTDomainSelector( + endpointClientOption: nil, + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: "custom-rest.ably.io", + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.primaryDomain, "custom-rest.ably.io") + } + + // REC2c6 + func test__021__ARTDomainSelector__fallbackDomains__should_return_empty_for_restHost() { + let selector = ARTDomainSelector( + endpointClientOption: nil, + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: "custom-rest.ably.io", + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.fallbackDomains, []) + } + + // REC1d: Deprecated realtimeHost option + func test__022__ARTDomainSelector__primaryDomain__should_use_realtimeHost_when_set() { + let selector = ARTDomainSelector( + endpointClientOption: nil, + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: "custom-realtime.ably.io", + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.primaryDomain, "custom-realtime.ably.io") + } + + // REC2c6 + func test__023__ARTDomainSelector__fallbackDomains__should_return_empty_for_realtimeHost() { + let selector = ARTDomainSelector( + endpointClientOption: nil, + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: "custom-realtime.ably.io", + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.fallbackDomains, []) + } + + // REC2b: Deprecated fallbackHostsUseDefault option + func test__024__ARTDomainSelector__fallbackDomains__should_use_defaults_when_fallbackHostsUseDefault_is_true() { + let selector = ARTDomainSelector( + endpointClientOption: nil, + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: true + ) + + let expectedDomains = [ + "main.a.fallback.ably-realtime.com", + "main.b.fallback.ably-realtime.com", + "main.c.fallback.ably-realtime.com", + "main.d.fallback.ably-realtime.com", + "main.e.fallback.ably-realtime.com" + ] + + XCTAssertEqual(selector.fallbackDomains, expectedDomains) + } + + // MARK: - Edge Cases + + func test__025__ARTDomainSelector__should_handle_empty_string_as_default() { + let selector = ARTDomainSelector( + endpointClientOption: "", + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.primaryDomain, "main.realtime.ably.net") + } + + func test__026__ARTDomainSelector__should_handle_empty_fallback_array() { + let selector = ARTDomainSelector( + endpointClientOption: nil, + fallbackHostsClientOption: [], + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + // Empty array is provided, so it should use that instead of defaults + XCTAssertEqual(selector.fallbackDomains, []) + } + + func test__027__ARTDomainSelector__should_handle_complex_routing_policy_ids() { + let selector = ARTDomainSelector( + endpointClientOption: "my-complex-routing-123", + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.primaryDomain, "my-complex-routing-123.realtime.ably.net") + } + + func test__028__ARTDomainSelector__should_handle_nonprod_with_empty_id() { + let selector = ARTDomainSelector( + endpointClientOption: "nonprod:", + fallbackHostsClientOption: nil, + environmentClientOption: nil, + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + XCTAssertEqual(selector.primaryDomain, ".realtime.ably-nonprod.net") + } + + func test__029__ARTDomainSelector__should_prioritize_endpoint_over_environment() { + let selector = ARTDomainSelector( + endpointClientOption: "prod-routing", + fallbackHostsClientOption: nil, + environmentClientOption: "sandbox", + restHostClientOption: nil, + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + // Endpoint should take priority + XCTAssertEqual(selector.primaryDomain, "prod-routing.realtime.ably.net") + } + + func test__030__ARTDomainSelector__should_prioritize_environment_over_restHost() { + let selector = ARTDomainSelector( + endpointClientOption: nil, + fallbackHostsClientOption: nil, + environmentClientOption: "sandbox", + restHostClientOption: "custom-rest.ably.io", + realtimeHostClientOption: nil, + fallbackHostsUseDefault: false + ) + + // Environment should take priority over restHost + XCTAssertEqual(selector.primaryDomain, "sandbox.realtime.ably.net") + } +} + diff --git a/Test/AblyTests/Tests/PushAdminTests.swift b/Test/AblyTests/Tests/PushAdminTests.swift index fb73419bc..d2009c6d1 100644 --- a/Test/AblyTests/Tests/PushAdminTests.swift +++ b/Test/AblyTests/Tests/PushAdminTests.swift @@ -233,7 +233,7 @@ class PushAdminTests: XCTestCase { let publishObject = ["transportType": "ablyChannel", "channel": channel.name, "ablyKey": options.key!, - "ablyUrl": "https://\(options.restHost)"] + "ablyUrl": "https://\(options.domainSelector.primaryDomain)"] waitUntil(timeout: testTimeout) { done in channel.attach { error in diff --git a/Test/AblyTests/Tests/RealtimeClientConnectionTests.swift b/Test/AblyTests/Tests/RealtimeClientConnectionTests.swift index 96deffc03..47ea58264 100644 --- a/Test/AblyTests/Tests/RealtimeClientConnectionTests.swift +++ b/Test/AblyTests/Tests/RealtimeClientConnectionTests.swift @@ -43,7 +43,7 @@ private func testUsesAlternativeHostOnResponse(_ caseTest: FakeNetworkResponse, waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(2, done: done) - // default host with the fake response or wss://[a-e].ably-realtime.com: when a timeout occurs + // default host with the fake response or wss://main.[a-e].fallback.ably-realtime.com: when a timeout occurs client.connection.on(.disconnected) { _ in partialDone() } @@ -51,8 +51,8 @@ private func testUsesAlternativeHostOnResponse(_ caseTest: FakeNetworkResponse, } XCTAssertEqual(urlConnections.count, 2) - XCTAssertTrue(NSRegularExpression.match(urlConnections.at(0)?.absoluteString, pattern: "//realtime.ably.io")) - XCTAssertTrue(NSRegularExpression.match(urlConnections.at(1)?.absoluteString, pattern: "//[a-e].ably-realtime.com")) + XCTAssertTrue(NSRegularExpression.match(urlConnections.at(0)?.absoluteString, pattern: "//main.realtime.ably.net")) + XCTAssertTrue(NSRegularExpression.match(urlConnections.at(1)?.absoluteString, pattern: "//main.[a-e].fallback.ably-realtime.com")) } private func testMovesToDisconnectedWithNetworkingError(_ error: Error, for test: Test) throws { @@ -90,7 +90,6 @@ private func testMovesToDisconnectedWithNetworkingError(_ error: Error, for test } } -private var internetConnectionNotAvailableTestsClient: ARTRealtime! private let fixtures: [String: Any] = try! JSONUtility.jsonObject( data: try! Data(contentsOf: URL(fileURLWithPath: pathForTestResource(testResourcesPath + "messages-encoding.json"))) )! @@ -139,7 +138,6 @@ class RealtimeClientConnectionTests: XCTestCase { _ = customTtlInterval _ = customIdleInterval _ = expectedHostOrder - _ = internetConnectionNotAvailableTestsClient _ = fixtures _ = jsonOptions _ = msgpackOptions @@ -190,7 +188,7 @@ class RealtimeClientConnectionTests: XCTestCase { defer { client.dispose(); client.close() } if let transport = client.internal.transport as? TestProxyTransport, let url = transport.lastUrl { - XCTAssertEqual(url.host, "realtime.ably.io") + XCTAssertEqual(url.host, "main.realtime.ably.net") } else { XCTFail("MockTransport isn't working") } @@ -2179,7 +2177,7 @@ class RealtimeClientConnectionTests: XCTestCase { func test__058__Connection__connection_request_fails__connection_attempt_should_fail_if_not_connected_within_the_default_realtime_request_timeout() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) - options.realtimeHost = "10.255.255.1" // non-routable IP address + options.endpoint = "10.255.255.1" // non-routable IP address options.autoConnect = false let realtimeRequestTimeout = 0.5 options.testOptions.realtimeRequestTimeout = realtimeRequestTimeout @@ -3842,6 +3840,7 @@ class RealtimeClientConnectionTests: XCTestCase { // RTN17 // RTN17b1 (host) + @available(*, deprecated, message: "This test is marked as deprecated so as to not trigger a compiler warning for using the -ARTClientOptions.realtimeHost property. Remove this deprecation when removing the property.") func test__086a__Connection__Host_Fallback__failing_connections_with_custom_endpoint_should_result_in_an_error_immediately() { let test = Test() let options = ARTClientOptions(key: "xxxx:xxxx") @@ -3989,6 +3988,7 @@ class RealtimeClientConnectionTests: XCTestCase { } // RTN17b + @available(*, deprecated, message: "This test is marked as deprecated so as to not trigger a compiler warning for using the -ARTClientOptions.realtimeHost property. Remove this deprecation when removing the property.") func test__087__Connection__Host_Fallback__failing_connections_with_custom_endpoint_should_result_in_time_outs() { let test = Test() let options = ARTClientOptions(key: "xxxx:xxxx") @@ -4027,8 +4027,8 @@ class RealtimeClientConnectionTests: XCTestCase { XCTAssertEqual(urlConnections.count, 1) } - - // RTN17b2 + + // RTN17h, REC2a2 func test__089__Connection__Host_Fallback__applies_when_an_array_of_ClientOptions_fallbackHosts_is_provided() { let test = Test() let options = ARTClientOptions(key: "xxxx:xxxx") @@ -4056,7 +4056,7 @@ class RealtimeClientConnectionTests: XCTestCase { waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(allHostsCount, done: done) - // wss://[a-e].ably-realtime.com: when a timeout occurs + // wss://main.[a-e].fallback.ably-realtime.com: when a timeout occurs client.connection.on(.disconnected) { _ in partialDone() } @@ -4064,13 +4064,13 @@ class RealtimeClientConnectionTests: XCTestCase { } XCTAssertTrue(urlConnections.count == allHostsCount) - XCTAssertTrue(NSRegularExpression.match(urlConnections.at(0)?.absoluteString, pattern: "//realtime.ably.io")) + XCTAssertTrue(NSRegularExpression.match(urlConnections.at(0)?.absoluteString, pattern: "//main.realtime.ably.net")) for connection in urlConnections[1..= 2) // amount depends on how soon fallback receives `.failed` above, it's often `.disconnected` instead - XCTAssertTrue(NSRegularExpression.match(urlConnections.at(0)?.absoluteString, pattern: "//[sandbox-]*realtime.ably.io")) - XCTAssertTrue(NSRegularExpression.match(urlConnections.at(1)?.absoluteString, pattern: "//[sandbox-]*[a-e][-fallback]*.ably-realtime.com")) + if let endpoint { + if endpoint.hasPrefix("nonprod:") { + XCTAssertTrue(NSRegularExpression.match(urlConnections.at(0)?.absoluteString, pattern: "//\(endpoint.dropFirst(8)).realtime.ably-nonprod.net")) + XCTAssertTrue(NSRegularExpression.match(urlConnections.at(1)?.absoluteString, pattern: "//\(endpoint.dropFirst(8)).[a-e].fallback.ably-realtime-nonprod.com")) + } else { + XCTAssertTrue(NSRegularExpression.match(urlConnections.at(0)?.absoluteString, pattern: "//\(endpoint).realtime.ably.net")) + XCTAssertTrue(NSRegularExpression.match(urlConnections.at(1)?.absoluteString, pattern: "//\(endpoint).[a-e].fallback.ably-realtime.com")) + } + } else { + XCTAssertTrue(NSRegularExpression.match(urlConnections.at(0)?.absoluteString, pattern: "//main.realtime.ably.net")) + XCTAssertTrue(NSRegularExpression.match(urlConnections.at(1)?.absoluteString, pattern: "//main.[a-e].fallback.ably-realtime.com")) + } } - func test__091__Connection__Host_Fallback__every_connection_is_first_attempted_to_the_primary_host_realtime_ably_io_prod() { + // REC2c1 + func test__091__Connection__Host_Fallback__every_connection_is_first_attempted_to_the_primary_host_main_realtime_ably_net() { let test = Test() - _test__091__Connection__Host_Fallback__every_connection_is_first_attempted_to_the_primary_host_realtime_ably_io(env: nil, test: test) + _test__091__Connection__Host_Fallback__every_connection_is_first_attempted_to_the_primary_host_main_realtime_ably_net(endpoint: nil, test: test) } - func test__091__Connection__Host_Fallback__every_connection_is_first_attempted_to_the_primary_host_realtime_ably_io_sandbox() { + // REC2c3 + func test__091__Connection__Host_Fallback__every_connection_is_first_attempted_to_the_primary_host_sandbox_realtime_ably_nonprod_net() { let test = Test() - _test__091__Connection__Host_Fallback__every_connection_is_first_attempted_to_the_primary_host_realtime_ably_io(env: "sandbox", test: test) + _test__091__Connection__Host_Fallback__every_connection_is_first_attempted_to_the_primary_host_main_realtime_ably_net(endpoint: "nonprod:sandbox", test: test) + } + + // REC2c4 + func test__091__Connection__Host_Fallback__every_connection_is_first_attempted_to_the_primary_host_test_realtime_ably_nonprod_net() { + let test = Test() + _test__091__Connection__Host_Fallback__every_connection_is_first_attempted_to_the_primary_host_main_realtime_ably_net(endpoint: "test", test: test) } - // RTN17c - func _test__092__Connection__Host_Fallback__should_retry_hosts_in_random_order_after_checkin_if_an_internet_connection_is_available(env: String?, test: Test) { + // RTN17j + + func _test__092__Connection__Host_Fallback__should_retry_hosts_in_random_order_after_checkin_if_an_internet_connection_is_available(endpoint: String?, test: Test) { let options = ARTClientOptions(key: "xxxx:xxxx") options.autoConnect = false options.disconnectedRetryTimeout = 1.0 - if let env { - options.environment = env - } + options.endpoint = endpoint options.testOptions.realtimeRequestTimeout = 5.0 options.testOptions.shuffleArray = shuffleArrayInExpectedHostOrder - let transportFactory = TestProxyTransportFactory() - options.testOptions.transportFactory = transportFactory - let client = ARTRealtime(options: options) + let testEnv = AblyTests.newRealtime(options) + options.testOptions.transportFactory = testEnv.transportFactory + let client = testEnv.client defer { client.dispose(); client.close() } client.channels.get(test.uniqueChannelName()) let testHttpExecutor = TestProxyHTTPExecutor(logger: .init(clientOptions: options)) client.internal.rest.httpExecutor = testHttpExecutor - transportFactory.fakeNetworkResponse = .hostUnreachable + testEnv.transportFactory.fakeNetworkResponse = .hostUnreachable let hostPrefixes = Array("abcde") let extractHostname = { (url: URL) in - if options.hasEnvironmentDifferentThanProduction { - NSRegularExpression.extract(url.absoluteString, pattern: "\(options.environment!)-[\(hostPrefixes.first!)-\(hostPrefixes.last!)]-fallback.ably-realtime.com") + if let endpoint { + if endpoint.hasPrefix("nonprod:") { + NSRegularExpression.extract(url.absoluteString, pattern: "\(endpoint.dropFirst(8)).[\(hostPrefixes.first!)-\(hostPrefixes.last!)].fallback.ably-realtime-nonprod.com") + } else { + NSRegularExpression.extract(url.absoluteString, pattern: "\(endpoint).[\(hostPrefixes.first!)-\(hostPrefixes.last!)].fallback.ably-realtime.com") + } } else { - NSRegularExpression.extract(url.absoluteString, pattern: "[\(hostPrefixes.first!)-\(hostPrefixes.last!)].ably-realtime.com") + NSRegularExpression.extract(url.absoluteString, pattern: "main.[\(hostPrefixes.first!)-\(hostPrefixes.last!)].fallback.ably-realtime.com") } } var urls = [URL]() - let expectedFallbackHosts = Array(expectedHostOrder.map { ARTDefault.fallbackHosts(withEnvironment: options.environment)[$0] }) + let expectedFallbackHosts = Array(expectedHostOrder.map { options.domainSelector.fallbackDomains[$0] }) - transportFactory.networkConnectEvent = { transport, url in + testEnv.transportFactory.networkConnectEvent = { transport, url in if client.internal.transport !== transport { return } @@ -4293,7 +4311,7 @@ class RealtimeClientConnectionTests: XCTestCase { waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(expectedFallbackHosts.count + 1, done: done) - // wss://[a-e].ably-realtime.com: when a timeout occurs + // wss://main.[a-e].fallback.ably-realtime.com: when a timeout occurs client.connection.on(.disconnected) { _ in partialDone() } @@ -4303,10 +4321,10 @@ class RealtimeClientConnectionTests: XCTestCase { var resultFallbackHosts = [String]() var gotInternetIsUpCheck = false for url in urls { - if NSRegularExpression.match(url.absoluteString, pattern: "//internet-up.ably-realtime.com/is-the-internet-up.txt") { + if NSRegularExpression.match(url.absoluteString, pattern: "//internet-up.ably-realtime.com/is-the-internet-up.txt") { // REC3a gotInternetIsUpCheck = true } else if let fallbackHost = extractHostname(url) { - if Optional(fallbackHost) == resultFallbackHosts.last { + if fallbackHost == resultFallbackHosts.last { continue } // Host changed; should've had an internet check before. @@ -4321,12 +4339,17 @@ class RealtimeClientConnectionTests: XCTestCase { func test__092__Connection__Host_Fallback__should_retry_hosts_in_random_order_after_checkin_if_an_internet_connection_is_available_prod() { let test = Test() - _test__092__Connection__Host_Fallback__should_retry_hosts_in_random_order_after_checkin_if_an_internet_connection_is_available(env: nil, test: test) + _test__092__Connection__Host_Fallback__should_retry_hosts_in_random_order_after_checkin_if_an_internet_connection_is_available(endpoint: nil, test: test) } func test__092__Connection__Host_Fallback__should_retry_hosts_in_random_order_after_checkin_if_an_internet_connection_is_available_sandbox() { let test = Test() - _test__092__Connection__Host_Fallback__should_retry_hosts_in_random_order_after_checkin_if_an_internet_connection_is_available(env: "sandbox", test: test) + _test__092__Connection__Host_Fallback__should_retry_hosts_in_random_order_after_checkin_if_an_internet_connection_is_available(endpoint: "nonprod:sandbox", test: test) + } + + func test__092__Connection__Host_Fallback__should_retry_hosts_in_random_order_after_checkin_if_an_internet_connection_is_available_test() { + let test = Test() + _test__092__Connection__Host_Fallback__should_retry_hosts_in_random_order_after_checkin_if_an_internet_connection_is_available(endpoint: "test", test: test) } // RTN17c @@ -4364,11 +4387,11 @@ class RealtimeClientConnectionTests: XCTestCase { mockHTTP.setNetworkState(network: .hostInternalError(code: 500), forHost: "internet-up.ably-realtime.com") waitUntil(timeout: testTimeout) { done in - // wss://[a-e].ably-realtime.com: when a timeout occurs + // wss://main.[a-e].fallback.ably-realtime.com: when a timeout occurs client.connection.once(.disconnected) { _ in done() } - // wss://[a-e].ably-realtime.com: when a 401 occurs because of the `xxxx:xxxx` key + // wss://main.[a-e].fallback.ably-realtime.com: when a 401 occurs because of the `xxxx:xxxx` key client.connection.once(.failed) { _ in done() } @@ -4419,7 +4442,7 @@ class RealtimeClientConnectionTests: XCTestCase { waitUntil(timeout: testTimeout) { done in let partialDone = AblyTests.splitDone(expectedFallbackHosts.count + 1, done: done) - // wss://[a-e].ably-realtime.com: when a timeout occurs + // wss://main.[a-e].fallback.ably-realtime.com: when a timeout occurs client.connection.on(.disconnected) { _ in partialDone() } @@ -4445,39 +4468,47 @@ class RealtimeClientConnectionTests: XCTestCase { XCTAssertEqual(resultFallbackHosts, expectedFallbackHosts) } - func test__095__Connection__Host_Fallback__won_t_use_fallback_hosts_feature_if_an_empty_array_is_provided() { - let test = Test() + // RTN17g + func test__095__Connection__Host_Fallback__won_t_use_fallback_hosts_feature_if_an_empty_array_is_provided() throws { let options = ARTClientOptions(key: "xxxx:xxxx") options.autoConnect = false options.fallbackHosts = [] - let transportFactory = TestProxyTransportFactory() - options.testOptions.transportFactory = transportFactory - let client = ARTRealtime(options: options) - let channel = client.channels.get(test.uniqueChannelName()) + options.disconnectedRetryTimeout = 1.0 // so that the test doesn't have to wait a long time to observe a retry + options.testOptions.realtimeRequestTimeout = 1.0 + let testEnv = AblyTests.newRealtime(options) + let client = testEnv.client - let testHttpExecutor = TestProxyHTTPExecutor(logger: .init(clientOptions: options)) - client.internal.rest.httpExecutor = testHttpExecutor + testEnv.transportFactory.fakeNetworkResponse = .hostUnreachable - transportFactory.fakeNetworkResponse = .hostUnreachable + let dataGatherer = DataGatherer(description: "Observe emitted state changes and transport connection attempts") { submit in + var stateChanges: [ARTConnectionStateChange] = [] + var urlConnections = [URL]() - var urlConnections = [URL]() - transportFactory.networkConnectEvent = { transport, url in - if client.internal.transport !== transport { - return + client.connection.on { stateChange in + stateChanges.append(stateChange) + if (stateChanges.count == 3) { + submit((stateChanges: stateChanges, urlConnections: urlConnections)) + } + } + + testEnv.transportFactory.networkConnectEvent = { transport, url in + if client.internal.transport !== transport { + return + } + urlConnections.append(url) } - urlConnections.append(url) } client.connect() defer { client.dispose(); client.close() } - waitUntil(timeout: testTimeout) { done in - channel.publish(nil, data: "message") { _ in - done() - } - } + let data = try dataGatherer.waitForData(timeout: testTimeout) - XCTAssertEqual(urlConnections.count, 1) + // We expect the first connection attempt to fail due to the .fakeNetworkResponse configured above. This error _does_ meet the criteria for trying a fallback host, but the provided list is empty, which should not provoke the use of any fallback host. Hence the connection should transition to DISCONNECTED, and then subsequently retry, transitioning back to CONNECTING. We should see that there were two connection attempts, both to the primary host. + + XCTAssertEqual(data.stateChanges.map(\.current), [.connecting, .disconnected, .connecting]) + XCTAssertEqual(data.urlConnections.count, 2) + XCTAssertTrue(data.urlConnections.allSatisfy { url in NSRegularExpression.match(url.absoluteString, pattern: "//main.realtime.ably.net") }) } // RTN17e @@ -4512,7 +4543,7 @@ class RealtimeClientConnectionTests: XCTestCase { expect(urlConnections).toEventually(haveCount(2), timeout: testTimeout) - XCTAssertTrue(NSRegularExpression.match(urlConnections.at(1)?.absoluteString, pattern: "//[sandbox-]*[a-e][-fallback]*.ably-realtime.com")) + XCTAssertTrue(NSRegularExpression.match(urlConnections.at(1)?.absoluteString, pattern: "//main.[a-e].fallback.ably-realtime.com")) waitUntil(timeout: testTimeout) { done in client.time { _, _ in @@ -4801,32 +4832,25 @@ class RealtimeClientConnectionTests: XCTestCase { // RTN20a - func beforeEach__Connection__Operating_System_events_for_network_internet_connectivity_changes__should_immediately_change_the_state_to_DISCONNECTED_if_the_operating_system_indicates_that_the_underlying_internet_connection_is_no_longer_available(for test: Test) throws { - let options = try AblyTests.commonAppSetup(for: test) - options.autoConnect = false - internetConnectionNotAvailableTestsClient = ARTRealtime(options: options) - internetConnectionNotAvailableTestsClient.internal.setReachabilityClass(TestReachability.self) - } - - func afterEach__Connection__Operating_System_events_for_network_internet_connectivity_changes__should_immediately_change_the_state_to_DISCONNECTED_if_the_operating_system_indicates_that_the_underlying_internet_connection_is_no_longer_available() { - internetConnectionNotAvailableTestsClient.dispose() - internetConnectionNotAvailableTestsClient.close() - } - func test__109__Connection__Operating_System_events_for_network_internet_connectivity_changes__should_immediately_change_the_state_to_DISCONNECTED_if_the_operating_system_indicates_that_the_underlying_internet_connection_is_no_longer_available__when_CONNECTING() throws { let test = Test() - try beforeEach__Connection__Operating_System_events_for_network_internet_connectivity_changes__should_immediately_change_the_state_to_DISCONNECTED_if_the_operating_system_indicates_that_the_underlying_internet_connection_is_no_longer_available(for: test) - + let options = try AblyTests.commonAppSetup(for: test) + options.autoConnect = false + let client = AblyTests.newRealtime(options).client + defer { + client.dispose() + client.close() + } waitUntil(timeout: testTimeout) { done in - internetConnectionNotAvailableTestsClient.connection.on { stateChange in + client.connection.on { stateChange in switch stateChange.current { case .connecting: XCTAssertNil(stateChange.reason) - guard let reachability = internetConnectionNotAvailableTestsClient.internal.reachability as? TestReachability else { + guard let reachability = client.internal.reachability as? TestReachability else { fail("expected test reachability") done(); return } - XCTAssertEqual(reachability.host, internetConnectionNotAvailableTestsClient.internal.options.realtimeHost) + XCTAssertEqual(reachability.host, client.internal.options.domainSelector.primaryDomain) reachability.simulate(false) case .disconnected: guard let reason = stateChange.reason else { @@ -4839,26 +4863,30 @@ class RealtimeClientConnectionTests: XCTestCase { break } } - internetConnectionNotAvailableTestsClient.connect() + client.connect() } - - afterEach__Connection__Operating_System_events_for_network_internet_connectivity_changes__should_immediately_change_the_state_to_DISCONNECTED_if_the_operating_system_indicates_that_the_underlying_internet_connection_is_no_longer_available() } func test__110__Connection__Operating_System_events_for_network_internet_connectivity_changes__should_immediately_change_the_state_to_DISCONNECTED_if_the_operating_system_indicates_that_the_underlying_internet_connection_is_no_longer_available__when_CONNECTED() throws { let test = Test() - try beforeEach__Connection__Operating_System_events_for_network_internet_connectivity_changes__should_immediately_change_the_state_to_DISCONNECTED_if_the_operating_system_indicates_that_the_underlying_internet_connection_is_no_longer_available(for: test) + let options = try AblyTests.commonAppSetup(for: test) + options.autoConnect = false + let client = AblyTests.newRealtime(options).client + defer { + client.dispose() + client.close() + } waitUntil(timeout: testTimeout) { done in - internetConnectionNotAvailableTestsClient.connection.on { stateChange in + client.connection.on { stateChange in switch stateChange.current { case .connected: XCTAssertNil(stateChange.reason) - guard let reachability = internetConnectionNotAvailableTestsClient.internal.reachability as? TestReachability else { + guard let reachability = client.internal.reachability as? TestReachability else { fail("expected test reachability") done(); return } - XCTAssertEqual(reachability.host, internetConnectionNotAvailableTestsClient.internal.options.realtimeHost) + XCTAssertEqual(reachability.host, client.internal.options.domainSelector.primaryDomain) reachability.simulate(false) case .disconnected: guard let reason = stateChange.reason else { @@ -4871,10 +4899,8 @@ class RealtimeClientConnectionTests: XCTestCase { break } } - internetConnectionNotAvailableTestsClient.connect() + client.connect() } - - afterEach__Connection__Operating_System_events_for_network_internet_connectivity_changes__should_immediately_change_the_state_to_DISCONNECTED_if_the_operating_system_indicates_that_the_underlying_internet_connection_is_no_longer_available() } // RTN20b @@ -4906,7 +4932,7 @@ class RealtimeClientConnectionTests: XCTestCase { fail("expected test reachability") done(); return } - XCTAssertEqual(reachability.host, client.internal.options.realtimeHost) + XCTAssertEqual(reachability.host, client.internal.options.domainSelector.primaryDomain) reachability.simulate(true) default: break @@ -4920,10 +4946,10 @@ class RealtimeClientConnectionTests: XCTestCase { func test__106_b__Connection__Operating_System_events_for_network_internet_connectivity_changes__should_restart_the_pending_connection_attempt_if_the_operating_system_indicates_that_the_underlying_internet_connection_is_now_available_when_CONNECTING() throws { let test = Test() let options = try AblyTests.commonAppSetup(for: test) - let realtimeHost = options.realtimeHost - options.realtimeHost = "10.255.255.1" // non-routable IP address + let primaryDomain = options.domainSelector.primaryDomain + options.endpoint = "10.255.255.1" // non-routable IP address options.autoConnect = false - options.testOptions.reconnectionRealtimeHost = realtimeHost + options.testOptions.reconnectionRealtimeHost = primaryDomain let client = ARTRealtime(options: options) client.internal.setReachabilityClass(TestReachability.self) defer { client.dispose(); client.close() } diff --git a/Test/AblyTests/Tests/RealtimeClientTests.swift b/Test/AblyTests/Tests/RealtimeClientTests.swift index 5f96bc13f..8d56fbed3 100644 --- a/Test/AblyTests/Tests/RealtimeClientTests.swift +++ b/Test/AblyTests/Tests/RealtimeClientTests.swift @@ -153,6 +153,7 @@ class RealtimeClientTests: XCTestCase { } // RTC1d + @available(*, deprecated, message: "This test is marked as deprecated so as to not trigger a compiler warning for using the -ARTClientOptions.realtimeHost property. Remove this deprecation when removing the property.") func test__017__RealtimeClient__options__should_modify_the_realtime_endpoint_host_if_realtimeHost_is_assigned() { let options = ARTClientOptions(key: "secret:key") options.realtimeHost = "fake.ably.io" @@ -178,24 +179,6 @@ class RealtimeClientTests: XCTestCase { } } - // RTC1e - func test__018__RealtimeClient__options__should_modify_both_the_REST_and_realtime_endpoint_if_environment_string_is_assigned() throws { - let test = Test() - let options = try AblyTests.commonAppSetup(for: test) - - let oldRestHost = options.restHost - let oldRealtimeHost = options.realtimeHost - - // Change REST and realtime endpoint hosts - options.environment = "test" - - XCTAssertEqual(options.restHost, "test-rest.ably.io") - XCTAssertEqual(options.realtimeHost, "test-realtime.ably.io") - // Extra care - XCTAssertEqual(oldRestHost, "\(getEnvironment())-rest.ably.io") - XCTAssertEqual(oldRealtimeHost, "\(getEnvironment())-realtime.ably.io") - } - // RTC1f func test__019__RealtimeClient__options__url_should_contains_transport_params() throws { let test = Test() diff --git a/Test/AblyTests/Tests/RestClientTests.swift b/Test/AblyTests/Tests/RestClientTests.swift index 076d0eb57..4307aade8 100644 --- a/Test/AblyTests/Tests/RestClientTests.swift +++ b/Test/AblyTests/Tests/RestClientTests.swift @@ -22,8 +22,6 @@ private let shuffleArrayInExpectedHostOrder = { (array: NSMutableArray) in } } -private let _fallbackHosts = ["f.ably-realtime.com", "g.ably-realtime.com", "h.ably-realtime.com", "i.ably-realtime.com", "j.ably-realtime.com"] - private func testUsesAlternativeHost(_ caseTest: FakeNetworkResponse, channelName: String) { let options = ARTClientOptions(key: "xxxx:xxxx") let client = ARTRest(options: options) @@ -44,8 +42,8 @@ private func testUsesAlternativeHost(_ caseTest: FakeNetworkResponse, channelNam if testHTTPExecutor.requests.count != 2 { return } - XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests[0].url!.absoluteString, pattern: "//rest.ably.io")) - XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests[1].url!.absoluteString, pattern: "//[a-e].ably-realtime.com")) + XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests[0].url!.absoluteString, pattern: "//main.realtime.ably.net")) + XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests[1].url!.absoluteString, pattern: "//main.[a-e].fallback.ably-realtime.com")) } private func testStoresSuccessfulFallbackHostAsDefaultHost(_ caseTest: FakeNetworkResponse, channelName: String) { @@ -65,8 +63,8 @@ private func testStoresSuccessfulFallbackHostAsDefaultHost(_ caseTest: FakeNetwo } XCTAssertEqual(testHTTPExecutor.requests.count, 2) - XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests[0].url!.host, pattern: "rest.ably.io")) - XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests[1].url!.host, pattern: "[a-e].ably-realtime.com")) + XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests[0].url!.host, pattern: "main.realtime.ably.net")) + XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests[1].url!.host, pattern: "main.[a-e].fallback.ably-realtime.com")) // #1 Store fallback used to request let usedFallbackURL = testHTTPExecutor.requests[1].url! @@ -111,7 +109,7 @@ private func testRestoresDefaultPrimaryHostAfterTimeoutExpires(_ caseTest: FakeN } XCTAssertEqual(testHTTPExecutor.requests.count, 3) - XCTAssertEqual(testHTTPExecutor.requests[2].url!.host, "rest.ably.io") + XCTAssertEqual(testHTTPExecutor.requests[2].url!.host, "main.realtime.ably.net") } private func testUsesAnotherFallbackHost(_ caseTest: FakeNetworkResponse, channelName: String) { @@ -133,8 +131,8 @@ private func testUsesAnotherFallbackHost(_ caseTest: FakeNetworkResponse, channe } XCTAssertEqual(testHTTPExecutor.requests.count, 3) - XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests[1].url!.host, pattern: "[a-e].ably-realtime.com")) - XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests[2].url!.host, pattern: "[a-e].ably-realtime.com")) + XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests[1].url!.host, pattern: "main.[a-e].fallback.ably-realtime.com")) + XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests[2].url!.host, pattern: "main.[a-e].fallback.ably-realtime.com")) XCTAssertNotEqual(testHTTPExecutor.requests[1].url!.host, testHTTPExecutor.requests[2].url!.host) } @@ -143,7 +141,6 @@ class RestClientTests: XCTestCase { override class var defaultTestSuite: XCTestSuite { _ = testHTTPExecutor _ = expectedHostOrder - _ = _fallbackHosts return super.defaultTestSuite } @@ -177,11 +174,11 @@ class RestClientTests: XCTestCase { let options = try AblyTests.commonAppSetup(for: test) let client = ARTRest(key: options.key!) - client.internal.prioritizedHost = options.restHost + client.internal.prioritizedHost = options.domainSelector.primaryDomain let publishTask = publishTestMessage(client, channelName: test.uniqueChannelName()) - expect(publishTask.error).toEventually(beNil(), timeout: testTimeout) + expect(publishTask.error).toEventually(beNil(), timeout: testTimeout) // TODO: this check is useless bc publishTask.error == nil at the beginning } func test__016__RestClient__initializer__should_throw_when_provided_an_invalid_key() { @@ -201,12 +198,12 @@ class RestClientTests: XCTestCase { func test__018__RestClient__initializer__should_accept_a_token() throws { let test = Test() - ARTClientOptions.setDefaultEnvironment(getEnvironment()) - defer { ARTClientOptions.setDefaultEnvironment(nil) } + ARTClientOptions.setDefaultEndpoint(getTestEndpoint()) + defer { ARTClientOptions.setDefaultEndpoint(nil) } let client = ARTRest(token: try getTestToken(for: test)) let publishTask = publishTestMessage(client, channelName: test.uniqueChannelName()) - expect(publishTask.error).toEventually(beNil(), timeout: testTimeout) + expect(publishTask.error).toEventually(beNil(), timeout: testTimeout) // TODO: this check is useless bc publishTask.error == nil at the beginning } func test__019__RestClient__initializer__should_accept_an_options_object() throws { @@ -242,11 +239,6 @@ class RestClientTests: XCTestCase { // RSC2 func test__022__RestClient__logging__should_output_to_the_system_log_and_the_log_level_should_be_Warn() { - ARTClientOptions.setDefaultEnvironment(getEnvironment()) - defer { - ARTClientOptions.setDefaultEnvironment(nil) - } - let options = ARTClientOptions(key: "xxxx:xxxx") options.logHandler = ARTLog(capturingOutput: true) let client = ARTRest(options: options) @@ -307,6 +299,7 @@ class RestClientTests: XCTestCase { // RSC11 // RSC11a + @available(*, deprecated, message: "This test is marked as deprecated so as to not trigger a compiler warning for using the -ARTClientOptions.restHost property. Remove this deprecation when removing the property.") func test__025__RestClient__endpoint__should_accept_a_custom_host_and_send_requests_to_the_specified_host() { let test = Test() let options = ARTClientOptions(key: "fake:key") @@ -320,35 +313,84 @@ class RestClientTests: XCTestCase { expect(testHTTPExecutor.requests.first?.url?.host).toEventually(equal("fake.ably.io"), timeout: testTimeout) } - func test__026__RestClient__endpoint__should_ignore_an_environment_when_restHost_is_customized() { - let test = Test() - let options = ARTClientOptions(key: "fake:key") + // REC1b1 + @available(*, deprecated, message: "This test is marked as deprecated so as to not trigger a compiler warning for using the -ARTClientOptions.environment property. Remove this deprecation when removing the property.") + func test__026a__RestClient__if_endpoint_option_is_specified_and_any_of_deprecated_options_are_also_specified_then_options_are_invalid() { + let options = ARTClientOptions(key: "xxxx:xxxx") + options.endpoint = "test" + XCTAssertNotNil(tryInObjC { + options.environment = "sandbox" + }) + XCTAssertNotNil(tryInObjC { + options.restHost = "fake.ably.net" + }) + XCTAssertNotNil(tryInObjC { + options.realtimeHost = "fake.ably.net" + }) + } + + // REC1b2 + func test__026b__RestClient__if_endpoint_option_is_domain_name_then_primary_domain_is_endpoint_value() { + let options = ARTClientOptions(key: "xxxx:xxxx") + options.endpoint = "test.ably.net" + XCTAssertEqual(options.domainSelector.primaryDomain, "test.ably.net") + } + + // REC1b3 + func test__026c__RestClient__if_endpoint_option_is_non_production_routing_policy_name() { + let options = ARTClientOptions(key: "xxxx:xxxx") + options.endpoint = "nonprod:sandbox" + XCTAssertEqual(options.domainSelector.primaryDomain, "sandbox.realtime.ably-nonprod.net") + } + + // REC1b4 + func test__026d__RestClient__if_endpoint_option_is_production_routing_policy_name() { + let options = ARTClientOptions(key: "xxxx:xxxx") + options.endpoint = "test" + XCTAssertEqual(options.domainSelector.primaryDomain, "test.realtime.ably.net") + } + + // REC1c1 + @available(*, deprecated, message: "This test is marked as deprecated so as to not trigger a compiler warning for using the -ARTClientOptions.environment property. Remove this deprecation when removing the property.") + func test__027a__RestClient__if_deprecated_environment_option_is_specified_and_other_deprecated_options_are_also_specified_then_options_are_invalid() { + let options = ARTClientOptions(key: "xxxx:xxxx") options.environment = "test" - options.restHost = "fake.ably.io" - let client = ARTRest(options: options) - testHTTPExecutor = TestProxyHTTPExecutor(logger: .init(clientOptions: options)) - client.internal.httpExecutor = testHTTPExecutor - - publishTestMessage(client, channelName: test.uniqueChannelName(), failOnError: false) - - expect(testHTTPExecutor.requests.first?.url?.host).toEventually(equal("fake.ably.io"), timeout: testTimeout) + XCTAssertNotNil(tryInObjC { + options.restHost = "fake.ably.net" + }) + XCTAssertNotNil(tryInObjC { + options.realtimeHost = "fake.ably.net" + }) } - - // RSC11b - func test__027__RestClient__endpoint__should_accept_an_environment_when_restHost_is_left_unchanged() { - let test = Test() - let options = ARTClientOptions(key: "fake:key") - options.environment = "myEnvironment" - let client = ARTRest(options: options) - testHTTPExecutor = TestProxyHTTPExecutor(logger: .init(clientOptions: options)) - client.internal.httpExecutor = testHTTPExecutor - - publishTestMessage(client, channelName: test.uniqueChannelName(), failOnError: false) - - expect(testHTTPExecutor.requests.first?.url?.host).toEventually(equal("myEnvironment-rest.ably.io"), timeout: testTimeout) + + // REC1c2 + @available(*, deprecated, message: "This test is marked as deprecated so as to not trigger a compiler warning for using the -ARTClientOptions.environment property. Remove this deprecation when removing the property.") + func test__027b__RestClient__if_deprecated_environment_option_is_specified_then_the_primary_domain_is_environment_realtime_ably_net() { + let options = ARTClientOptions(key: "xxxx:xxxx") + let oldPrimaryDomain = options.domainSelector.primaryDomain + options.environment = "test" + XCTAssertEqual(options.domainSelector.primaryDomain, "test.realtime.ably.net") + XCTAssertEqual(oldPrimaryDomain, "main.realtime.ably.net") + } + + // REC1d1 + @available(*, deprecated, message: "This test is marked as deprecated so as to not trigger a compiler warning for using the -ARTClientOptions.restHost property. Remove this deprecation when removing the property.") + func test__027c__RestClient__if_restHost_option_is_specified_then_its_a_primary_domain() { + let options = ARTClientOptions(key: "xxxx:xxxx") + options.restHost = "test.realtime.ably.net" + XCTAssertEqual(options.domainSelector.primaryDomain, "test.realtime.ably.net") } - func test__028__RestClient__endpoint__should_default_to_https___rest_ably_io() { + // REC1d2 + @available(*, deprecated, message: "This test is marked as deprecated so as to not trigger a compiler warning for using the -ARTClientOptions.realtimeHost property. Remove this deprecation when removing the property.") + func test__027d__RestClient__if_realtimeHost_option_is_specified_then_its_a_primary_domain() { + let options = ARTClientOptions(key: "xxxx:xxxx") + options.realtimeHost = "test.realtime.ably.net" + XCTAssertEqual(options.domainSelector.primaryDomain, "test.realtime.ably.net") + } + + // REC1a (default) + func test__028__RestClient__endpoint__should_default_to_https___main_realtime_ably_net() { let test = Test() let options = ARTClientOptions(key: "fake:key") let client = ARTRest(options: options) @@ -357,7 +399,7 @@ class RestClientTests: XCTestCase { publishTestMessage(client, channelName: test.uniqueChannelName(), failOnError: false) - expect(testHTTPExecutor.requests.first?.url?.absoluteString).toEventually(beginWith("https://rest.ably.io"), timeout: testTimeout) + expect(testHTTPExecutor.requests.first?.url?.absoluteString).toEventually(beginWith("https://main.realtime.ably.net"), timeout: testTimeout) } func test__029__RestClient__endpoint__should_connect_over_plain_http____when_tls_is_off() throws { @@ -372,11 +414,10 @@ class RestClientTests: XCTestCase { expect(testHTTPExecutor.requests.first?.url?.scheme).toEventually(equal("http"), timeout: testTimeout) } - - // RSC11b - func test__030__RestClient__endpoint__should_not_prepend_the_environment_if_environment_is_configured_as__production_() { + + @available(*, deprecated, message: "This test is marked as deprecated so as to not trigger a compiler warning for using the -ARTClientOptions.restHost and -ARTClientOptions.realtimeHost property. Remove this deprecation when removing the property.") + func test__030__RestClient__endpoint__options_restHost_realtimeHost_are_default_values_if_not_set() { let options = ARTClientOptions(key: "xxxx:xxxx") - options.environment = "production" let client = ARTRest(options: options) XCTAssertEqual(client.internal.options.restHost, ARTDefault.restHost()) XCTAssertEqual(client.internal.options.realtimeHost, ARTDefault.realtimeHost()) @@ -387,7 +428,7 @@ class RestClientTests: XCTestCase { func test__031__RestClient__should_use_the_the_connection_and_request_timeouts_specified__timeout_for_any_single_HTTP_request_and_response() { let test = Test() let options = ARTClientOptions(key: "xxxx:xxxx") - options.restHost = "10.255.255.1" // non-routable IP address + options.endpoint = "10.255.255.1" // non-routable IP address XCTAssertEqual(options.httpRequestTimeout, 10.0) // Seconds options.httpRequestTimeout = 1.0 let client = ARTRest(options: options) @@ -420,7 +461,7 @@ class RestClientTests: XCTestCase { var totalRetry: UInt = 0 testHTTPExecutor.setListenerAfterRequest { request in - if NSRegularExpression.match(request.url!.absoluteString, pattern: "//[a-e].ably-realtime.com") { + if NSRegularExpression.match(request.url!.absoluteString, pattern: "//main.[a-e].fallback.ably-realtime.com") { totalRetry += 1 } } @@ -466,14 +507,14 @@ class RestClientTests: XCTestCase { XCTAssertTrue(authOptions == options) } - - // RSC12 - func test__003__RestClient__REST_endpoint_host_should_be_configurable_in_the_Client_constructor_with_the_option_restHost() throws { + + // REC1a (overridden) + func test__003__RestClient__endpoint_should_be_configurable_in_the_client_constructor_with_the_option_endpoint() throws { let test = Test() let options = ARTClientOptions(key: "xxxx:xxxx") - XCTAssertEqual(options.restHost, "rest.ably.io") - options.restHost = "rest.ably.test" - XCTAssertEqual(options.restHost, "rest.ably.test") + XCTAssertEqual(options.domainSelector.primaryDomain, "main.realtime.ably.net") + options.endpoint = "test.realtime.ably.test" + XCTAssertEqual(options.domainSelector.primaryDomain, "test.realtime.ably.test") let client = ARTRest(options: options) testHTTPExecutor = TestProxyHTTPExecutor(logger: .init(clientOptions: options)) client.internal.httpExecutor = testHTTPExecutor @@ -486,7 +527,7 @@ class RestClientTests: XCTestCase { let url = try XCTUnwrap(testHTTPExecutor.requests.first?.url, "No request url found") - expect(url.absoluteString).to(contain("//rest.ably.test")) + expect(url.absoluteString).to(contain("//test.realtime.ably.test")) } // RSC16 @@ -641,9 +682,9 @@ class RestClientTests: XCTestCase { guard let components = options.key?.components(separatedBy: ":"), let keyName = components.first, let keySecret = components.last else { fail("Invalid API key: \(options.key ?? "nil")"); return } - ARTClientOptions.setDefaultEnvironment(getEnvironment()) + ARTClientOptions.setDefaultEndpoint(getTestEndpoint()) defer { - ARTClientOptions.setDefaultEnvironment(nil) + ARTClientOptions.setDefaultEndpoint(nil) } let rest = ARTRest(key: "\(keyName):\(keySecret)") waitUntil(timeout: testTimeout) { done in @@ -821,31 +862,6 @@ class RestClientTests: XCTestCase { } } - // RSC15 - - // TO3k7 - - @available(*, deprecated, message: "This test is marked as deprecated so as to not trigger a compiler warning for using the -ARTClientOptions.fallbackHostsUseDefault property. Remove this deprecation when removing the property.") - func test__051__RestClient__Host_Fallback__fallbackHostsUseDefault_option__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) - XCTAssertTrue(client.internal.options.fallbackHostsUseDefault) - // Not production - XCTAssertNotNil(client.internal.options.environment) - XCTAssertNotEqual(client.internal.options.environment, "production") - - let hosts = ARTFallbackHosts.hosts(from: client.internal.options) - let fallback = ARTFallback(fallbackHosts: hosts, shuffleArray: ARTFallback_shuffleArray) - XCTAssertEqual(fallback.hosts.count, ARTDefault.fallbackHosts().count) - - ARTDefault.fallbackHosts().forEach { - expect(fallback.hosts).to(contain($0)) - } - } - @available(*, deprecated, message: "This test is marked as deprecated so as to not trigger a compiler warning for using the -ARTClientOptions.fallbackHostsUseDefault property. Remove this deprecation when removing the property.") func test__052__RestClient__Host_Fallback__fallbackHostsUseDefault_option__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") @@ -856,14 +872,23 @@ class RestClientTests: XCTestCase { let client = ARTRest(options: options) XCTAssertTrue(client.internal.options.fallbackHostsUseDefault) // Custom - XCTAssertNotEqual(client.internal.options.restHost, ARTDefault.restHost()) - XCTAssertNotEqual(client.internal.options.realtimeHost, ARTDefault.realtimeHost()) + XCTAssertNotEqual(client.internal.options.restHost, "main.realtime.ably.net") + XCTAssertNotEqual(client.internal.options.realtimeHost, "main.realtime.ably.net") - let hosts = ARTFallbackHosts.hosts(from: client.internal.options) + let hosts = client.internal.options.domainSelector.fallbackDomains let fallback = ARTFallback(fallbackHosts: hosts, shuffleArray: ARTFallback_shuffleArray) - XCTAssertEqual(fallback.hosts.count, ARTDefault.fallbackHosts().count) - - ARTDefault.fallbackHosts().forEach { + + // When fallbackHostsUseDefault is true, should use default fallback domains + let expectedFallbackDomains = [ + "main.a.fallback.ably-realtime.com", + "main.b.fallback.ably-realtime.com", + "main.c.fallback.ably-realtime.com", + "main.d.fallback.ably-realtime.com", + "main.e.fallback.ably-realtime.com" + ] + XCTAssertEqual(fallback.hosts.count, expectedFallbackDomains.count) + + expectedFallbackDomains.forEach { expect(fallback.hosts).to(contain($0)) } } @@ -874,6 +899,7 @@ class RestClientTests: XCTestCase { XCTAssertFalse(options.fallbackHostsUseDefault) } + // REC2a1 @available(*, deprecated, message: "This test is marked as deprecated so as to not trigger a compiler warning for using the -ARTClientOptions.fallbackHostsUseDefault property. Remove this deprecation when removing the property.") func test__054__RestClient__Host_Fallback__fallbackHostsUseDefault_option__should_never_accept_to_configure__fallbackHost__and_set__fallbackHostsUseDefault__to__true_() { let options = ARTClientOptions(key: "xxxx:xxxx") @@ -903,10 +929,8 @@ class RestClientTests: XCTestCase { XCTAssertEqual(exception4!.name.rawValue, ARTFallbackIncompatibleOptionsException) } - // RSC15b - - // RSC15b1 - func test__055__RestClient__Host_Fallback__Fallback_behavior__should_be_applied_when_restHost__port_and_tlsPort_has_not_been_set_to_an_explicit_value() { + // RSC15m, REC2c1 + func test__055__RestClient__if_primary_domain_is_determined_to_default_then_set_of_fallback_domains_is_default_a_e_ably_realtime_com() { let test = Test() let options = ARTClientOptions(key: "xxxx:xxxx") let client = ARTRest(options: options) @@ -927,21 +951,22 @@ class RestClientTests: XCTestCase { let requests = testHTTPExecutor.requests XCTAssertEqual(requests.count, 3) let capturedURLs = requests.map { $0.url!.absoluteString } - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(0), pattern: "//rest.ably.io")) - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(1), pattern: "//[a-e].ably-realtime.com")) - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(2), pattern: "//[a-e].ably-realtime.com")) + XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(0), pattern: "//main.realtime.ably.net")) + XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(1), pattern: "//main.[a-e].fallback.ably-realtime.com")) + XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(2), pattern: "//main.[a-e].fallback.ably-realtime.com")) } - // RSC15b1 - func test__056__RestClient__Host_Fallback__Fallback_behavior__should_NOT_be_applied_when_ClientOptions_restHost_has_been_set() { + // RSC15m, REC2c2 + func test__056__RestClient__if_primary_domain_is_determined_to_be_an_explicit_domain_then_set_of_fallback_domain_is_empty() { let test = Test() let options = ARTClientOptions(key: "xxxx:xxxx") - options.restHost = "fake.ably.io" + options.endpoint = "fake.ably.io" let client = ARTRest(options: options) let internalLog = InternalLog(clientOptions: options) let mockHTTP = MockHTTP(logger: internalLog) testHTTPExecutor = TestProxyHTTPExecutor(http: mockHTTP, logger: internalLog) client.internal.httpExecutor = testHTTPExecutor + mockHTTP.setNetworkState(network: .hostUnreachable) let channel = client.channels.get(test.uniqueChannelName()) @@ -958,60 +983,7 @@ class RestClientTests: XCTestCase { XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(0), pattern: "//fake.ably.io")) } - // RSC15b1 - func test__057__RestClient__Host_Fallback__Fallback_behavior__should_NOT_be_applied_when_ClientOptions_port_has_been_set() { - let test = Test() - let options = ARTClientOptions(token: "xxxx") - options.tls = false - options.port = 999 - let client = ARTRest(options: options) - let internalLog = InternalLog(clientOptions: options) - let mockHTTP = MockHTTP(logger: internalLog) - testHTTPExecutor = TestProxyHTTPExecutor(http: mockHTTP, logger: internalLog) - client.internal.httpExecutor = testHTTPExecutor - mockHTTP.setNetworkState(network: .hostUnreachable) - let channel = client.channels.get(test.uniqueChannelName()) - - waitUntil(timeout: testTimeout) { done in - channel.publish(nil, data: "") { error in - expect(error?.message).to(contain("hostname could not be found")) - done() - } - } - - let requests = testHTTPExecutor.requests - XCTAssertEqual(requests.count, 1) - let capturedURLs = requests.map { $0.url!.absoluteString } - expect(capturedURLs.at(0)).to(beginWith("http://rest.ably.io:999")) - } - - // RSC15b1 - func test__058__RestClient__Host_Fallback__Fallback_behavior__should_NOT_be_applied_when_ClientOptions_tlsPort_has_been_set() { - let test = Test() - let options = ARTClientOptions(key: "xxxx:xxxx") - options.tlsPort = 999 - let client = ARTRest(options: options) - let internalLog = InternalLog(clientOptions: options) - let mockHTTP = MockHTTP(logger: internalLog) - testHTTPExecutor = TestProxyHTTPExecutor(http: mockHTTP, logger: internalLog) - client.internal.httpExecutor = testHTTPExecutor - mockHTTP.setNetworkState(network: .hostUnreachable) - let channel = client.channels.get(test.uniqueChannelName()) - - waitUntil(timeout: testTimeout) { done in - channel.publish(nil, data: "") { error in - expect(error?.message).to(contain("hostname could not be found")) - done() - } - } - - let requests = testHTTPExecutor.requests - XCTAssertEqual(requests.count, 1) - let capturedURLs = requests.map { $0.url!.absoluteString } - expect(capturedURLs.at(0)).to(beginWith("https://rest.ably.io:999")) - } - - // RSC15b2 + // RSC15n, REC2a2 func test__059__RestClient__Host_Fallback__Fallback_behavior__should_be_applied_when_ClientOptions_fallbackHosts_is_provided() { let test = Test() let options = ARTClientOptions(key: "xxxx:xxxx") @@ -1033,12 +1005,12 @@ class RestClientTests: XCTestCase { XCTAssertEqual(testHTTPExecutor.requests.count, 3) let capturedURLs = testHTTPExecutor.requests.map { $0.url!.absoluteString } - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(0), pattern: "//rest.ably.io")) + XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(0), pattern: "//main.realtime.ably.net")) XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(1), pattern: "//[a-b].cocoa.ably")) XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(2), pattern: "//[a-b].cocoa.ably")) } - // RSC15b3, RSC15g4 + // REC2b, TO3k7 @available(*, deprecated, message: "This test is marked as deprecated so as to not trigger a compiler warning for using the -ARTClientOptions.fallbackHostsUseDefault property. Remove this deprecation when removing the property.") func test__060__RestClient__Host_Fallback__Fallback_behavior__should_be_applied_when_ClientOptions_fallbackHosts_is_not_provided_and_deprecated_fallbackHostsUseDefault_is_on() { let test = Test() @@ -1061,35 +1033,14 @@ class RestClientTests: XCTestCase { XCTAssertEqual(testHTTPExecutor.requests.count, 3) let capturedURLs = testHTTPExecutor.requests.map { $0.url!.absoluteString } - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(0), pattern: "//rest.ably.io")) - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(1), pattern: "//[a-e].ably-realtime.com")) - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(2), pattern: "//[a-e].ably-realtime.com")) + XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(0), pattern: "//main.realtime.ably.net")) + XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(1), pattern: "//main.[a-e].fallback.ably-realtime.com")) + XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(2), pattern: "//main.[a-e].fallback.ably-realtime.com")) } - // RSC15k - func test__045__RestClient__Host_Fallback__failing_HTTP_requests_with_custom_endpoint_should_result_in_an_error_immediately() { - let test = Test() - let options = ARTClientOptions(key: "xxxx:xxxx") - options.restHost = "fake.ably.io" - let client = ARTRest(options: options) - let internalLog = InternalLog(clientOptions: options) - let mockHTTP = MockHTTP(logger: internalLog) - testHTTPExecutor = TestProxyHTTPExecutor(http: mockHTTP, logger: internalLog) - client.internal.httpExecutor = testHTTPExecutor - mockHTTP.setNetworkState(network: .hostUnreachable) - let channel = client.channels.get(test.uniqueChannelName()) - waitUntil(timeout: testTimeout) { done in - channel.publish(nil, data: "message") { error in - expect(error?.message).to(contain("hostname could not be found")) - done() - } - } - XCTAssertEqual(testHTTPExecutor.requests.count, 1) - } + // RSC15n - // RSC15g - - // RSC15g1 + // REC2a2 func test__061__RestClient__Host_Fallback__fallback_hosts_list_and_priorities__should_use_ClientOptions_fallbackHosts_when_list_is_provided() { let test = Test() let options = ARTClientOptions(key: "xxxx:xxxx") @@ -1111,20 +1062,21 @@ class RestClientTests: XCTestCase { XCTAssertEqual(testHTTPExecutor.requests.count, 2) let capturedURLs = testHTTPExecutor.requests.compactMap { $0.url?.absoluteString } - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(0), pattern: "//rest.ably.io")) + XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(0), pattern: "//main.realtime.ably.net")) XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(1), pattern: "//f.ably-realtime.com")) } - // RSC15g2 - func test__062__RestClient__Host_Fallback__fallback_hosts_list_and_priorities__should_use_environment_fallback_hosts_when_ClientOptions_environment_is_set_to_a_value_other_than__production__and_ClientOptions_fallbackHosts_is_not_set() { + // REC2c4 + func test__062__RestClient__Host_Fallback__fallback_hosts_list_and_priorities__should_use_endpoint_fallback_hosts_when_ClientOptions_endpoint_is_set_to_a_routing_policy__and_ClientOptions_fallbackHosts_is_not_set() { let test = Test() let options = ARTClientOptions(key: "xxxx:xxxx") - options.environment = "test" + options.endpoint = "test" let client = ARTRest(options: options) let internalLog = InternalLog(clientOptions: options) let mockHTTP = MockHTTP(logger: internalLog) testHTTPExecutor = TestProxyHTTPExecutor(http: mockHTTP, logger: internalLog) client.internal.httpExecutor = testHTTPExecutor + mockHTTP.setNetworkState(network: .hostUnreachable, resetAfter: 2) let channel = client.channels.get(test.uniqueChannelName()) @@ -1137,65 +1089,9 @@ class RestClientTests: XCTestCase { XCTAssertEqual(testHTTPExecutor.requests.count, 3) let capturedURLs = testHTTPExecutor.requests.compactMap { $0.url?.absoluteString } - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(0), pattern: "//test-rest.ably.io")) - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(1), pattern: "//test-[a-e]-fallback.ably-realtime.com")) - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(1), pattern: "//test-[a-e]-fallback.ably-realtime.com")) - } - - // RSC15g2 - func test__063__RestClient__Host_Fallback__fallback_hosts_list_and_priorities__should_NOT_use_environment_fallback_hosts_when_ClientOptions_environment_is_set_to__production_() { - let test = Test() - let options = ARTClientOptions(key: "xxxx:xxxx") - options.environment = "production" - let client = ARTRest(options: options) - let internalLog = InternalLog(clientOptions: options) - let mockHTTP = MockHTTP(logger: internalLog) - testHTTPExecutor = TestProxyHTTPExecutor(http: mockHTTP, logger: internalLog) - client.internal.httpExecutor = testHTTPExecutor - mockHTTP.setNetworkState(network: .hostUnreachable) - let channel = client.channels.get(test.uniqueChannelName()) - - waitUntil(timeout: testTimeout) { done in - channel.publish(nil, data: "") { error in - expect(error?.message).to(contain("hostname could not be found")) - done() - } - } - - XCTAssertEqual(testHTTPExecutor.requests.count, 4) - let capturedURLs = testHTTPExecutor.requests.compactMap { $0.url?.absoluteString } - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(0), pattern: "//rest.ably.io")) - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(1), pattern: "//[a-e].ably-realtime.com")) - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(2), pattern: "//[a-e].ably-realtime.com")) - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(3), pattern: "//[a-e].ably-realtime.com")) - } - - // RSC15g3 - func test__064__RestClient__Host_Fallback__fallback_hosts_list_and_priorities__should_use_default_fallback_hosts_when_both_ClientOptions_fallbackHosts_and_ClientOptions_environment_are_not_set() { - let test = Test() - let options = ARTClientOptions(key: "xxxx:xxxx") - options.environment = "" - let client = ARTRest(options: options) - let internalLog = InternalLog(clientOptions: options) - let mockHTTP = MockHTTP(logger: internalLog) - testHTTPExecutor = TestProxyHTTPExecutor(http: mockHTTP, logger: internalLog) - client.internal.httpExecutor = testHTTPExecutor - mockHTTP.setNetworkState(network: .hostUnreachable) - let channel = client.channels.get(test.uniqueChannelName()) - - waitUntil(timeout: testTimeout) { done in - channel.publish(nil, data: "") { error in - expect(error?.message).to(contain("hostname could not be found")) - done() - } - } - - XCTAssertEqual(testHTTPExecutor.requests.count, 4) - let capturedURLs = testHTTPExecutor.requests.compactMap { $0.url?.absoluteString } - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(0), pattern: "//rest.ably.io")) - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(1), pattern: "//[a-e].ably-realtime.com")) - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(2), pattern: "//[a-e].ably-realtime.com")) - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(3), pattern: "//[a-e].ably-realtime.com")) + XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(0), pattern: "//test.realtime.ably.net")) + XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(1), pattern: "//test.[a-e].fallback.ably-realtime.com")) + XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(2), pattern: "//test.[a-e].fallback.ably-realtime.com")) } // RSC15g4 @@ -1222,14 +1118,14 @@ class RestClientTests: XCTestCase { XCTAssertEqual(testHTTPExecutor.requests.count, 2) let capturedURLs = testHTTPExecutor.requests.map { $0.url!.absoluteString } - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(1), pattern: "//[a-e].ably-realtime.com")) + XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(1), pattern: "//main.[a-e].fallback.ably-realtime.com")) } - // RSC15g1 + // RSC15m, TO3k6 func test__047__RestClient__Host_Fallback__won_t_apply_fallback_hosts_if_ClientOptions_fallbackHosts_array_is_empty() { let test = Test() let options = ARTClientOptions(key: "xxxx:xxxx") - options.fallbackHosts = [] // to test TO3k6 + options.fallbackHosts = [] let client = ARTRest(options: options) let internalLog = InternalLog(clientOptions: options) let mockHTTP = MockHTTP(logger: internalLog) @@ -1246,39 +1142,11 @@ class RestClientTests: XCTestCase { XCTAssertEqual(testHTTPExecutor.requests.count, 1) let capturedURLs = testHTTPExecutor.requests.map { $0.url!.absoluteString } - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(0), pattern: "//rest.ably.io")) + XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(0), pattern: "//main.realtime.ably.net")) } - // RSC15g3 - func test__048__RestClient__Host_Fallback__won_t_apply_custom_fallback_hosts_if_ClientOptions_fallbackHosts_and_ClientOptions_environment_are_not_set__use_defaults_instead() { - let test = Test() - let options = ARTClientOptions(key: "xxxx:xxxx") - options.fallbackHosts = nil - let client = ARTRest(options: options) - let internalLog = InternalLog(clientOptions: options) - let mockHTTP = MockHTTP(logger: internalLog) - testHTTPExecutor = TestProxyHTTPExecutor(http: mockHTTP, logger: internalLog) - client.internal.httpExecutor = testHTTPExecutor - mockHTTP.setNetworkState(network: .hostUnreachable, resetAfter: 1) - let channel = client.channels.get(test.uniqueChannelName()) - - waitUntil(timeout: testTimeout) { done in - channel.publish(nil, data: "nil") { _ in - done() - } - } - - XCTAssertEqual(testHTTPExecutor.requests.count, 2) - if testHTTPExecutor.requests.count < 2 { - return - } - - let capturedURLs = testHTTPExecutor.requests.map { $0.url!.absoluteString } - XCTAssertTrue(NSRegularExpression.match(capturedURLs.at(1), pattern: "//[a-e].ably-realtime.com")) - } - - // RSC15e - func test__049__RestClient__Host_Fallback__every_new_HTTP_request_is_first_attempted_to_the_default_primary_host_rest_ably_io() { + // RSC25 + func test__049__RestClient__Host_Fallback__every_new_HTTP_request_is_first_attempted_to_the_default_primary_domain() { let test = Test() let options = ARTClientOptions(key: "xxxx:xxxx") options.httpMaxRetryCount = 1 @@ -1288,7 +1156,9 @@ class RestClientTests: XCTestCase { let mockHTTP = MockHTTP(logger: internalLog) testHTTPExecutor = TestProxyHTTPExecutor(http: mockHTTP, logger: internalLog) client.internal.httpExecutor = testHTTPExecutor + mockHTTP.setNetworkState(network: .hostUnreachable, resetAfter: 1) + let channel = client.channels.get(test.uniqueChannelName()) waitUntil(timeout: testTimeout) { done in @@ -1307,31 +1177,92 @@ class RestClientTests: XCTestCase { } XCTAssertEqual(testHTTPExecutor.requests.count, 3) - XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests.at(0)?.url?.absoluteString, pattern: "//\(ARTDefault.restHost())")) - XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests.at(1)?.url?.absoluteString, pattern: "//[a-e].ably-realtime.com")) - XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests.at(2)?.url?.absoluteString, pattern: "//\(ARTDefault.restHost())")) + XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests.at(0)?.url?.absoluteString, pattern: "//main.realtime.ably.net")) + XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests.at(1)?.url?.absoluteString, pattern: "//main.[a-e].fallback.ably-realtime.com")) + XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests.at(2)?.url?.absoluteString, pattern: "//main.realtime.ably.net")) } - // RSC15a + // REC2c1 + func test__065a__RestClient__Host_Fallback__set_of_fallback_domains_is_defined_implicitly() { + let options = ARTClientOptions(key: "fake:key") + let fallbackHosts = options.domainSelector.fallbackDomains + fallbackHosts.forEach { host in + expect(host).to(match("main.[a-e].fallback.ably-realtime.com")) + } + XCTAssertEqual(fallbackHosts.count, 5) + } + + // REC2c2 + func test__065b__RestClient__Host_Fallback__set_of_fallback_domains_is_defined_implicitly() { + let options = ARTClientOptions(key: "fake:key") + options.endpoint = "test.ably-realtime.com" + let fallbackHosts = options.domainSelector.fallbackDomains + XCTAssertEqual(fallbackHosts.count, 0) + } + // REC2c3 + func test__065c__RestClient__Host_Fallback__set_of_fallback_domains_is_defined_implicitly() { + let options = ARTClientOptions(key: "fake:key") + options.endpoint = "nonprod:sandbox" + let fallbackHosts = options.domainSelector.fallbackDomains + fallbackHosts.forEach { host in + expect(host).to(match("sandbox.[a-e].fallback.ably-realtime-nonprod.com")) + } + XCTAssertEqual(fallbackHosts.count, 5) + } + + // REC2c4 + func test__065d__RestClient__Host_Fallback__set_of_fallback_domains_is_defined_implicitly() { + let options = ARTClientOptions(key: "fake:key") + options.endpoint = "sandbox" + let fallbackHosts = options.domainSelector.fallbackDomains + fallbackHosts.forEach { host in + expect(host).to(match("sandbox.[a-e].fallback.ably-realtime.com")) + } + XCTAssertEqual(fallbackHosts.count, 5) + } + + // REC2c5 + @available(*, deprecated, message: "This test is marked as deprecated so as to not trigger a compiler warning for using the -ARTClientOptions.environment property. Remove this deprecation when removing the property.") + func test__065e__RestClient__Host_Fallback__set_of_fallback_domains_is_defined_implicitly() { + let options = ARTClientOptions(key: "fake:key") + options.environment = "sandbox" + let fallbackHosts = options.domainSelector.fallbackDomains + fallbackHosts.forEach { host in + expect(host).to(match("sandbox.[a-e].fallback.ably-realtime.com")) + } + XCTAssertEqual(fallbackHosts.count, 5) + } + + // REC2c6 (restHost) + @available(*, deprecated, message: "This test is marked as deprecated so as to not trigger a compiler warning for using the -ARTClientOptions.restHost property. Remove this deprecation when removing the property.") + func test__065f__RestClient__Host_Fallback__set_of_fallback_domains_is_defined_implicitly() { + let options = ARTClientOptions(key: "fake:key") + options.restHost = "test.ably-realtime.com" + let fallbackHosts = options.domainSelector.fallbackDomains + XCTAssertEqual(fallbackHosts.count, 0) + } + + // REC2c6 (realtimeHost) + @available(*, deprecated, message: "This test is marked as deprecated so as to not trigger a compiler warning for using the -ARTClientOptions.realtimeHost property. Remove this deprecation when removing the property.") + func test__065g__RestClient__Host_Fallback__set_of_fallback_domains_is_defined_implicitly() { + let options = ARTClientOptions(key: "fake:key") + options.realtimeHost = "test.ably-realtime.com" + let fallbackHosts = options.domainSelector.fallbackDomains + XCTAssertEqual(fallbackHosts.count, 0) + } + + // RSC15a + // RSC15h - func test__065__RestClient__Host_Fallback__retry_hosts_in_random_order__default_fallback_hosts_should_match__a_e__ably_realtime_com() { + func test__065__RestClient__Host_Fallback__retry_hosts_in_random_order__default_fallback_hosts_should_match__main_a_e_fallback__ably_realtime_com() { let defaultFallbackHosts = ARTDefault.fallbackHosts() defaultFallbackHosts.forEach { host in - expect(host).to(match("[a-e].ably-realtime.com")) + expect(host).to(match("main.[a-e].fallback.ably-realtime.com")) } XCTAssertEqual(defaultFallbackHosts.count, 5) } - - // RSC15i - func test__066__RestClient__Host_Fallback__retry_hosts_in_random_order__environment_fallback_hosts_have_the_format__environment___a_e__fallback_ably_realtime_com() { - let environmentFallbackHosts = ARTDefault.fallbackHosts(withEnvironment: "sandbox") - environmentFallbackHosts.forEach { host in - expect(host).to(match("sandbox-[a-e]-fallback.ably-realtime.com")) - } - XCTAssertEqual(environmentFallbackHosts.count, 5) - } - + func test__067__RestClient__Host_Fallback__retry_hosts_in_random_order__until_httpMaxRetryCount_has_been_reached() { let test = Test() let options = ARTClientOptions(key: "xxxx:xxxx") @@ -1354,10 +1285,10 @@ class RestClientTests: XCTestCase { XCTAssertEqual(testHTTPExecutor.requests.count, Int(1 + options.httpMaxRetryCount)) let extractHostname = { (request: URLRequest) in - NSRegularExpression.extract(request.url!.absoluteString, pattern: "[a-e].ably-realtime.com") + NSRegularExpression.extract(request.url!.absoluteString, pattern: "main.[a-e].fallback.ably-realtime.com") } let resultFallbackHosts = testHTTPExecutor.requests.compactMap(extractHostname) - let expectedFallbackHosts = Array(expectedHostOrder.map { ARTDefault.fallbackHosts()[$0] }[0 ..< Int(options.httpMaxRetryCount)]) + let expectedFallbackHosts = Array(expectedHostOrder.map { options.domainSelector.fallbackDomains[$0] }[0 ..< Int(options.httpMaxRetryCount)]) XCTAssertEqual(resultFallbackHosts, expectedFallbackHosts) } @@ -1398,107 +1329,17 @@ class RestClientTests: XCTestCase { XCTAssertEqual(resultFallbackHosts, expectedFallbackHosts) } - func test__069__RestClient__Host_Fallback__retry_hosts_in_random_order__until_all_fallback_hosts_have_been_tried() { - let test = Test() - let options = ARTClientOptions(key: "xxxx:xxxx") - options.httpMaxRetryCount = 10 - options.testOptions.shuffleArray = shuffleArrayInExpectedHostOrder - let client = ARTRest(options: options) - let internalLog = InternalLog(clientOptions: options) - let mockHTTP = MockHTTP(logger: internalLog) - testHTTPExecutor = TestProxyHTTPExecutor(http: mockHTTP, logger: internalLog) - client.internal.httpExecutor = testHTTPExecutor - mockHTTP.setNetworkState(network: .hostUnreachable) - let channel = client.channels.get(test.uniqueChannelName()) - - waitUntil(timeout: testTimeout) { done in - channel.publish(nil, data: "nil") { _ in - done() - } - } - - XCTAssertEqual(testHTTPExecutor.requests.count, ARTDefault.fallbackHosts().count + 1) - - let extractHostname = { (request: URLRequest) in - NSRegularExpression.extract(request.url!.absoluteString, pattern: "[a-e].ably-realtime.com") - } - let resultFallbackHosts = testHTTPExecutor.requests.compactMap(extractHostname) - let expectedFallbackHosts = expectedHostOrder.map { ARTDefault.fallbackHosts()[$0] } - - XCTAssertEqual(resultFallbackHosts, expectedFallbackHosts) - } - - func test__070__RestClient__Host_Fallback__retry_hosts_in_random_order__until_httpMaxRetryCount_has_been_reached__if_custom_fallback_hosts_are_provided_in_ClientOptions_fallbackHosts__then_they_will_be_used_instead() { - let test = Test() - let options = ARTClientOptions(key: "xxxx:xxxx") - options.httpMaxRetryCount = 4 - options.fallbackHosts = _fallbackHosts - options.testOptions.shuffleArray = shuffleArrayInExpectedHostOrder - - let client = ARTRest(options: options) - let internalLog = InternalLog(clientOptions: options) - let mockHTTP = MockHTTP(logger: internalLog) - testHTTPExecutor = TestProxyHTTPExecutor(http: mockHTTP, logger: internalLog) - client.internal.httpExecutor = testHTTPExecutor - mockHTTP.setNetworkState(network: .hostUnreachable) - let channel = client.channels.get(test.uniqueChannelName()) - - waitUntil(timeout: testTimeout) { done in - channel.publish(nil, data: "nil") { _ in - done() - } - } - - XCTAssertEqual(testHTTPExecutor.requests.count, Int(1 + options.httpMaxRetryCount)) - XCTAssertTrue((testHTTPExecutor.requests.count) < (_fallbackHosts.count + 1)) - - let extractHostname = { (request: URLRequest) in - NSRegularExpression.extract(request.url!.absoluteString, pattern: "[f-j].ably-realtime.com") - } - let resultFallbackHosts = testHTTPExecutor.requests.compactMap(extractHostname) - let expectedFallbackHosts = Array(expectedHostOrder.map { _fallbackHosts[$0] }[0 ..< Int(options.httpMaxRetryCount)]) - - XCTAssertEqual(resultFallbackHosts, expectedFallbackHosts) - } - - func test__071__RestClient__Host_Fallback__retry_hosts_in_random_order__until_all_fallback_hosts_have_been_tried__if_custom_fallback_hosts_are_provided_in_ClientOptions_fallbackHosts__then_they_will_be_used_instead() { - let test = Test() - let options = ARTClientOptions(key: "xxxx:xxxx") - options.httpMaxRetryCount = 10 - options.fallbackHosts = _fallbackHosts - options.testOptions.shuffleArray = shuffleArrayInExpectedHostOrder - - let client = ARTRest(options: options) - let internalLog = InternalLog(clientOptions: options) - let mockHTTP = MockHTTP(logger: internalLog) - testHTTPExecutor = TestProxyHTTPExecutor(http: mockHTTP, logger: internalLog) - client.internal.httpExecutor = testHTTPExecutor - mockHTTP.setNetworkState(network: .hostUnreachable) - let channel = client.channels.get(test.uniqueChannelName()) - - waitUntil(timeout: testTimeout) { done in - channel.publish(nil, data: "nil") { _ in - done() - } - } - - XCTAssertEqual(testHTTPExecutor.requests.count, ARTDefault.fallbackHosts().count + 1) - - let extractHostname = { (request: URLRequest) in - NSRegularExpression.extract(request.url!.absoluteString, pattern: "[f-j].ably-realtime.com") - } - - let resultFallbackHosts = testHTTPExecutor.requests.compactMap(extractHostname) - let expectedFallbackHosts = expectedHostOrder.map { _fallbackHosts[$0] } - - XCTAssertEqual(resultFallbackHosts, expectedFallbackHosts) - } - + // RSC15j func test__072__RestClient__Host_Fallback__retry_hosts_in_random_order__all_fallback_requests_headers_should_contain__Host__header_with_fallback_host_address() { let test = Test() let options = ARTClientOptions(key: "xxxx:xxxx") options.httpMaxRetryCount = 10 - options.fallbackHosts = _fallbackHosts + let customFallbackHosts = ["j.ably-realtime.com", + "i.ably-realtime.com", + "h.ably-realtime.com", + "g.ably-realtime.com", + "f.ably-realtime.com"] + options.fallbackHosts = customFallbackHosts let client = ARTRest(options: options) let internalLog = InternalLog(clientOptions: options) @@ -1514,7 +1355,7 @@ class RestClientTests: XCTestCase { } } - XCTAssertEqual(testHTTPExecutor.requests.count, ARTDefault.fallbackHosts().count + 1) + XCTAssertEqual(testHTTPExecutor.requests.count, options.domainSelector.fallbackDomains.count + 1) let fallbackRequests = testHTTPExecutor.requests.filter { NSRegularExpression.match($0.url!.absoluteString, pattern: "[f-j].ably-realtime.com") @@ -1527,31 +1368,7 @@ class RestClientTests: XCTestCase { XCTAssertEqual(fallbackRequests.count, fallbackRequestsWithHostHeader.count) } - func test__073__RestClient__Host_Fallback__retry_hosts_in_random_order__if_an_empty_array_of_fallback_hosts_is_provided__then_fallback_host_functionality_is_disabled() { - let test = Test() - let options = ARTClientOptions(key: "xxxx:xxxx") - options.httpMaxRetryCount = 5 - options.fallbackHosts = [] - - let client = ARTRest(options: options) - let internalLog = InternalLog(clientOptions: options) - let mockHTTP = MockHTTP(logger: internalLog) - testHTTPExecutor = TestProxyHTTPExecutor(http: mockHTTP, logger: internalLog) - client.internal.httpExecutor = testHTTPExecutor - mockHTTP.setNetworkState(network: .hostUnreachable) - let channel = client.channels.get(test.uniqueChannelName()) - - waitUntil(timeout: testTimeout) { done in - channel.publish(nil, data: "nil") { _ in - done() - } - } - - XCTAssertEqual(testHTTPExecutor.requests.count, 1) - XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests[0].url!.absoluteString, pattern: "//rest.ably.io")) - } - - // RSC15d + // RSC15l func test__074__RestClient__Host_Fallback__should_use_an_alternative_host_when___hostUnreachable() { let test = Test() @@ -1568,7 +1385,6 @@ class RestClientTests: XCTestCase { testUsesAlternativeHost(.hostInternalError(code: 501), channelName: test.uniqueChannelName()) } - // RSC15d func test__050__RestClient__Host_Fallback__should_not_use_an_alternative_host_when_the_client_receives_an_bad_request() { let test = Test() let options = ARTClientOptions(key: "xxxx:xxxx") @@ -1587,7 +1403,7 @@ class RestClientTests: XCTestCase { } XCTAssertEqual(testHTTPExecutor.requests.count, 1) - XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests[0].url!.absoluteString, pattern: "//rest.ably.io")) + XCTAssertTrue(NSRegularExpression.match(testHTTPExecutor.requests[0].url!.absoluteString, pattern: "//main.realtime.ably.net")) } // RSC15f @@ -1840,7 +1656,7 @@ class RestClientTests: XCTestCase { let acceptHeaderValue = try XCTUnwrap(request.allHTTPHeaderFields?["Accept"], "Accept HTTP Header is missing") XCTAssertEqual(request.httpMethod!.uppercased(), "PATCH") - XCTAssertEqual(url.absoluteString, "https://rest.ably.io:443/feature?foo=1") + XCTAssertEqual(url.absoluteString, "https://main.realtime.ably.net:443/feature?foo=1") XCTAssertEqual(acceptHeaderValue, "application/x-msgpack,application/json") } @@ -2129,7 +1945,7 @@ class RestClientTests: XCTestCase { var fallbackRequests: [URLRequest] = [] testHTTPExecutor.setListenerAfterRequest { request in - if NSRegularExpression.match(request.url!.absoluteString, pattern: "//[a-e].ably-realtime.com") { + if NSRegularExpression.match(request.url!.absoluteString, pattern: "//main.[a-e].fallback.ably-realtime.com") { fallbackRequests += [request] } } From 68b842f5a07d9348b5b3b7b51a9825537fa831f4 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Sat, 29 Nov 2025 15:30:17 +0000 Subject: [PATCH 3/4] Remove detection of production environment which is not needed. --- Source/ARTClientOptions.m | 7 ------- Source/ARTConnection.m | 2 +- Source/ARTDefault.m | 21 ------------------- .../Ably/ARTClientOptions+Private.h | 2 -- .../PrivateHeaders/Ably/ARTDefault+Private.h | 5 ----- 5 files changed, 1 insertion(+), 36 deletions(-) diff --git a/Source/ARTClientOptions.m b/Source/ARTClientOptions.m index c4f1239c2..bac8af9bd 100644 --- a/Source/ARTClientOptions.m +++ b/Source/ARTClientOptions.m @@ -316,13 +316,6 @@ + (BOOL)getDefaultIdempotentRestPublishingForVersion:(NSString *)version { } } -- (BOOL)isProductionEnvironment { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - return [[self.environment lowercaseString] isEqualToString:[ARTDefaultProductionEnvironment lowercaseString]]; -#pragma clang diagnostic pop -} - - (BOOL)hasEnvironment { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" diff --git a/Source/ARTConnection.m b/Source/ARTConnection.m index a8ef175ed..b49d174b6 100644 --- a/Source/ARTConnection.m +++ b/Source/ARTConnection.m @@ -199,7 +199,7 @@ - (NSString *)key_nosync { - (NSInteger)maxMessageSize { if (_maxMessageSize) return _maxMessageSize; - return _realtime.options.isProductionEnvironment ? [ARTDefault maxProductionMessageSize] : [ARTDefault maxSandboxMessageSize]; + return [ARTDefault maxMessageSize]; } - (ARTRealtimeConnectionState)state_nosync { diff --git a/Source/ARTDefault.m b/Source/ARTDefault.m index 5f3f169df..a33fae5c6 100644 --- a/Source/ARTDefault.m +++ b/Source/ARTDefault.m @@ -4,7 +4,6 @@ #import "ARTDomainSelector.h" NSString *const ARTDefaultAPIVersion = @"4"; // CSV2 -NSString *const ARTDefaultProductionEnvironment = @"production"; NSTimeInterval ARTConnectionStateTtl = 60.0; NSInteger ARTMaxProductionMessageSize = 65536; @@ -81,14 +80,6 @@ + (NSInteger)maxMessageSize { #endif } -+ (NSInteger)maxSandboxMessageSize { - return ARTMaxSandboxMessageSize; -} - -+ (NSInteger)maxProductionMessageSize { - return ARTMaxProductionMessageSize; -} - + (void)setConnectionStateTtl:(NSTimeInterval)value { @synchronized (self) { ARTConnectionStateTtl = value; @@ -105,18 +96,6 @@ + (void)setMaxMessageSize:(NSInteger)value { } } -+ (void)setMaxProductionMessageSize:(NSInteger)value { - @synchronized (self) { - ARTMaxProductionMessageSize = value; - } -} - -+ (void)setMaxSandboxMessageSize:(NSInteger)value { - @synchronized (self) { - ARTMaxSandboxMessageSize = value; - } -} - + (NSString *)libraryAgent { return [ARTClientInformation libraryAgentIdentifier]; } diff --git a/Source/PrivateHeaders/Ably/ARTClientOptions+Private.h b/Source/PrivateHeaders/Ably/ARTClientOptions+Private.h index b2ba756de..adae5cbf7 100644 --- a/Source/PrivateHeaders/Ably/ARTClientOptions+Private.h +++ b/Source/PrivateHeaders/Ably/ARTClientOptions+Private.h @@ -15,8 +15,6 @@ NS_ASSUME_NONNULL_BEGIN @interface ARTClientOptions () -@property (readonly) BOOL isProductionEnvironment; - @property (readonly, nonatomic) ARTDomainSelector *domainSelector; + (void)setDefaultEndpoint:(nullable NSString *)endpoint; diff --git a/Source/PrivateHeaders/Ably/ARTDefault+Private.h b/Source/PrivateHeaders/Ably/ARTDefault+Private.h index 213795d58..997e3ceb5 100644 --- a/Source/PrivateHeaders/Ably/ARTDefault+Private.h +++ b/Source/PrivateHeaders/Ably/ARTDefault+Private.h @@ -1,15 +1,10 @@ #import -extern NSString *const ARTDefaultProductionEnvironment; - @interface ARTDefault (Private) + (void)setConnectionStateTtl:(NSTimeInterval)value; + (void)setMaxMessageSize:(NSInteger)value; -+ (NSInteger)maxSandboxMessageSize; -+ (NSInteger)maxProductionMessageSize; - + (NSString *)connectivityCheckUrl; @end From f9450ee74ec5578fa8e5228745839caf5ce218b7 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Sat, 29 Nov 2025 16:36:51 +0000 Subject: [PATCH 4/4] Add some docstrings. --- Source/ARTDomainSelector.m | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Source/ARTDomainSelector.m b/Source/ARTDomainSelector.m index 55099a037..153c8153c 100644 --- a/Source/ARTDomainSelector.m +++ b/Source/ARTDomainSelector.m @@ -14,29 +14,33 @@ The policy that the library will use to determine its REC1 primary domain. */ typedef NS_ENUM(NSInteger, ARTPrimaryDomainSelectionPolicy) { - /// REC1a: The `endpoint` client option has not been specified. + /// REC1a: The `endpoint` client option has not been specified (used default main.realtime.ably.net, policyId = nil, hostname = nil). ARTPrimaryDomainSelectionPolicyDefault, - /// REC1b2: The `endpoint` client option is a hostname. + /// REC1b2: The `endpoint` client option is a hostname (policyId = nil, example hostname = "example.ably.co.uk"). ARTPrimaryDomainSelectionPolicyHostname, - /// REC1b3: The `endpoint` client option specifies a non-production routing policy. + /// REC1b3: The `endpoint` client option specifies a non-production routing policy (example policyId = "sandbox", hostname = nil). ARTPrimaryDomainSelectionPolicyNonProductionRoutingPolicy, - /// REC1b4: The `endpoint` client option is a production routing policy ID. + /// REC1b4: The `endpoint` client option is a production routing policy ID (example policyId = "main", hostname = nil). ARTPrimaryDomainSelectionPolicyProductionRoutingPolicy, - /// REC1c: Deprecated `environment` option is being used. + /// REC1c: Deprecated `environment` option is being used (example policyId = "main", hostname = nil). ARTPrimaryDomainSelectionPolicyLegacyEnvironment, - /// REC1d: Deprecated `restHost` or `realtimeHost` option is being used. + /// REC1d: Deprecated `restHost` or `realtimeHost` option is being used (policyId = nil, example hostname = "example.ably.co.uk"). ARTPrimaryDomainSelectionPolicyLegacyHost }; @interface ARTDomainSelector () @property (nonatomic) ARTPrimaryDomainSelectionPolicy primaryDomainSelectionPolicy; + +/// Prefix used for construction of the primary and fallback domains names (for `ARTPrimaryDomainSelectionPolicyNonProductionRoutingPolicy`, `ARTPrimaryDomainSelectionPolicyProductionRoutingPolicy` and `ARTPrimaryDomainSelectionPolicyLegacyEnvironment` values of ``primaryDomainSelectionPolicy``). @property (nonatomic, nullable) NSString *policyId; + +/// Sores the value of the primary domain if assigned directly (for `ARTPrimaryDomainSelectionPolicyHostname` and `ARTPrimaryDomainSelectionPolicyLegacyHost` values of ``primaryDomainSelectionPolicy``). @property (nonatomic, nullable) NSString *hostname; @property (nonatomic, nullable) NSArray *fallbackHostsClientOption; @property (nonatomic) BOOL fallbackHostsUseDefault;