From 7b72fbc9505b27ce527df46697fe145e5eb229b0 Mon Sep 17 00:00:00 2001 From: evgeny Date: Fri, 9 Oct 2026 19:40:37 +0100 Subject: [PATCH] Replace environment and restHost with endpoint Make the `endpoint` client option (REC1, REC2) the only way to choose where the client connects, aligning with the other Ably SDKs' next major. The default primary domain moves from rest.ably.io to main.realtime.ably.net, with fallbacks main.[a-e].fallback.ably-realtime.com; `nonprod:[id]` resolves to the non-production cluster, and a hostname endpoint is used as given with no default fallbacks. - Remove the `environment` and `restHost` options and the legacy host and environment-fallback derivation - Rename ClientOptions::getPrimaryRestHost() to getPrimaryDomain() - Stop suppressing default fallbacks when port or tlsPort is set, following the spec - Run tests against ABLY_ENDPOINT (default nonprod:sandbox) instead of ABLY_ENV; cover routing-policy, nonprod, hostname and custom fallback resolution - Document the migration in UPDATING.md and CHANGELOG.md Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 4 ++ CONTRIBUTING.md | 2 +- UPDATING.md | 42 +++++++++++- src/Defaults.php | 64 +++++++++++++----- src/Host.php | 2 +- src/Models/ClientOptions.php | 58 +++++++---------- src/PubSubHttpClient.php | 2 +- tests/ChannelIdempotentTest.php | 6 +- tests/ChannelMessagesTest.php | 4 +- tests/ClientOptionsTest.php | 111 ++++++++++++++++---------------- tests/DefaultsTest.php | 32 ++++----- tests/HostCacheTest.php | 7 +- tests/HostTest.php | 44 ++++++------- tests/PubSubHttpClientTest.php | 91 +++++++++++++------------- tests/TypesTest.php | 3 +- tests/factories/TestApp.php | 4 +- 16 files changed, 269 insertions(+), 207 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f300625..2cf20b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ the full mapping table. - Clients are constructed through `Ably\PubSub\Server::createHttpClient()`, which declares the server side on the wire. It accepts everything the constructor accepted: an options array, an API key string, a token string, or a `ClientOptions` instance. A client constructed directly declares no side, and is rejected on accounts with monthly-active-user pricing enabled. - Removed `AblyRest::setAblyAgentHeader()` and `AblyRest::setLibraryFlavourString()`, replaced by the per-client `agents` client option. - Removed `ably-loader.php`; Composer's autoloader is the only supported install path. +- Added the `endpoint` client option (REC1, REC2), which is now the only way to choose where the client connects. The default primary domain is `main.realtime.ably.net`, with fallback hosts `main.[a-e].fallback.ably-realtime.com`. +- Removed the `environment` and `restHost` client options in favour of `endpoint`. They are now ignored if passed, so a client still setting `environment` connects to production. +- Setting `port` or `tlsPort` no longer disables the default fallback hosts; a hostname `endpoint` does. +- `ClientOptions::getPrimaryRestHost()` is now `getPrimaryDomain()`. `Defaults::$restHost`, `Defaults::$realtimeHost` and `Defaults::getEnvironmentFallbackHosts()` are replaced by `Defaults::$endpoint`, `Defaults::getPrimaryDomain()` and `Defaults::getEndpointFallbackHosts()`. - Removed the `demo/` Heroku application and its `Procfile`. - The minimum supported PHP version is now 8.1; the SDK is tested on 8.1 through 8.5. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index def08b9..e22e01f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,6 +3,6 @@ 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) -4. Ensure you have added suitable tests and the test suite is passing (run `vendor/bin/phpunit`) +4. Ensure you have added suitable tests and the test suite is passing (run `vendor/bin/phpunit`). Tests run against the `nonprod:sandbox` endpoint; set `ABLY_ENDPOINT` to use another. 4. Push to the branch (`git push origin my-new-feature`) 5. Create a new Pull Request \ No newline at end of file diff --git a/UPDATING.md b/UPDATING.md index f669df4..7778488 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -6,7 +6,7 @@ Version 2.0.0 ships from a new package, `ably/pubsub-server`, under a new namesp Under monthly-active-user pricing the platform has to classify every connection as device-side or server-side. The new package declares that automatically, on every request, in the `Ably-Agent` header; the old constructor cannot declare anything. That is the forcing function for this migration: once monthly-active-user pricing is live, `new Ably\AblyRest(...)` from `ably/ably-php` is rejected on accounts where it is enabled. -The client `Server::createHttpClient()` returns is the same REST client as before, renamed from `AblyRest` to `PubSubHttpClient`. Channels, message publishing, history, presence, authentication, push admin, crypto and every `ClientOptions` key behave exactly as they did in 1.x. For most applications the migration is confined to the `composer require` line, the `use` statements, and the constructor call. +The client `Server::createHttpClient()` returns is the same REST client as before, renamed from `AblyRest` to `PubSubHttpClient`. Channels, message publishing, history, presence, authentication, push admin and crypto behave exactly as they did in 1.x. The one change to `ClientOptions` is how the client chooses where to connect: the `endpoint` option replaces `environment` and `restHost` (see [Endpoint](#endpoint-replaces-environment-and-resthost)). For most applications the migration is confined to the `composer require` line, the `use` statements, the constructor call, and any host options. ### Mapping @@ -20,6 +20,7 @@ The client `Server::createHttpClient()` returns is the same REST client as befor | `require 'ably-loader.php';` | removed — use Composer's autoloader (`vendor/autoload.php`) | | PHP 7.2 – 8.0 | PHP `^8.1` (tested on 8.1 – 8.5) | | `\Ably\AblyRest` type hints | `\Ably\PubSub\PubSubHttpClient` ([091d](https://ably.atlassian.net/wiki/spaces/product/pages/5363957781)) | +| `'environment' => 'sandbox'` / `'restHost' => '…'` | `'endpoint' => …` (see [Endpoint](#endpoint-replaces-environment-and-resthost)) | Every class moves namespace, and `AblyRest` is the only one that also changes name, so the rename is mechanical: replace the prefix `Ably\` with `Ably\PubSub\` throughout — including in type hints, `catch` blocks and fully-qualified string class names — and then `AblyRest` with `PubSubHttpClient`. @@ -41,6 +42,42 @@ $ably->channel('test-channel')->publish('test-event', 'hello world'); `createHttpClient()` accepts everything the 1.x constructor accepted: an options array, an API key string, a token string, or a `ClientOptions` instance. A `ClientOptions` instance you pass in is copied rather than mutated. +### Endpoint replaces environment and restHost + +The `endpoint` client option is now the only way to choose where the client connects. `environment` and `restHost` are removed. Like any other unknown key, they are now ignored, so a client still passing `'environment' => 'sandbox'` connects to production: replace them before upgrading. + +| 1.x | 2.0 | +| --- | --- | +| no host options | no change; traffic moves from `rest.ably.io` to `main.realtime.ably.net` | +| `'environment' => 'sandbox'` | `'endpoint' => 'nonprod:sandbox'` | +| `'environment' => 'acme'` (dedicated cluster) | `'endpoint' => 'acme'` | +| `'restHost' => 'localhost'` | `'endpoint' => 'localhost'` | +| `'restHost' => 'rest.example.com'` | `'endpoint' => 'rest.example.com'` | +| custom `'fallbackHosts' => […]` | unchanged | + +How `endpoint` resolves: + +| `endpoint` | Primary domain | Default fallback hosts | +| --- | --- | --- | +| unset (`main`) | `main.realtime.ably.net` | `main.[a-e].fallback.ably-realtime.com` | +| `[id]`, e.g. `acme` | `[id].realtime.ably.net` | `[id].[a-e].fallback.ably-realtime.com` | +| `nonprod:[id]`, e.g. `nonprod:sandbox` | `[id].realtime.ably-nonprod.net` | `[id].[a-e].fallback.ably-realtime-nonprod.com` | +| a hostname: contains `.` or `::`, or is `localhost` | the value as given | none | + +A non-empty `fallbackHosts` still replaces the default fallback hosts. Setting `port` or `tlsPort` no longer disables the default fallback hosts; only a hostname `endpoint` does. + +```php +// 1.x +$ably = new AblyRest(['key' => getenv('ABLY_API_KEY'), 'environment' => 'sandbox']); + +// 2.0 +$ably = Server::createHttpClient(['key' => getenv('ABLY_API_KEY'), 'endpoint' => 'nonprod:sandbox']); +``` + +`ClientOptions::getPrimaryRestHost()` is renamed `getPrimaryDomain()`, and `Defaults::$restHost`, `Defaults::$realtimeHost` and `Defaults::getEnvironmentFallbackHosts()` are replaced by `Defaults::$endpoint`, `Defaults::getPrimaryDomain()` and `Defaults::getEndpointFallbackHosts()`. + +Firewall allowlists and outbound proxies must allow `*.realtime.ably.net` and `*.fallback.ably-realtime.com` in place of `rest.ably.io` and `*.ably-realtime.com`. + ### Declaring the side Construct through the door. `new Ably\PubSub\PubSubHttpClient(...)` still works — the library and its own tests use it — but it declares no side, and will be rejected on monthly-active-user-enabled accounts just as the 1.x constructor is. The door produces: @@ -67,6 +104,7 @@ The `agents` option is per-client, unlike the process-global static setters it r * `AblyRest::setAblyAgentHeader()` and `AblyRest::$agents` — replaced by the per-client `agents` option. * `AblyRest::setLibraryFlavourString()` — already deprecated in 1.x; replaced by the same option. * `ably-loader.php`, the hand-rolled autoloader — Composer is the only supported install path. +* The `environment` and `restHost` client options — replaced by `endpoint`. They are ignored if passed. * The `demo/` Heroku application and its `Procfile`. * PHP 7.2 – 8.0 support. @@ -75,7 +113,7 @@ The `agents` option is per-client, unlike the process-global static setters it r ### Unchanged * The REST client and its whole surface: `channel()`, `channels`, publishing, message history, presence and presence history, `auth`, token requests and token issuing, `push` admin, `stats`, `time()`, and crypto. -* Every `ClientOptions` key, and the array / key-string / token-string / `ClientOptions` forms of the constructor argument. +* Every `ClientOptions` key other than the host options above, and the array / key-string / token-string / `ClientOptions` forms of the constructor argument. * Message and error semantics, including `AblyException` and its codes. * Requirements: `ext-json`, `ext-curl`, `ext-openssl`, and `rybakit/msgpack` for the msgpack protocol. diff --git a/src/Defaults.php b/src/Defaults.php index 151178f..00b6ce0 100644 --- a/src/Defaults.php +++ b/src/Defaults.php @@ -5,8 +5,7 @@ class Defaults { const API_VERSION = '2'; const LIB_VERSION = '2.0.0'; - static $restHost = "rest.ably.io"; - static $realtimeHost = "realtime.ably.io"; + static $endpoint = 'main'; static $port = 80; static $tlsPort = 443; @@ -14,20 +13,55 @@ class Defaults { static $internetCheckOk = "yes\n"; static $fallbackHosts = [ - 'a.ably-realtime.com', - 'b.ably-realtime.com', - 'c.ably-realtime.com', - 'd.ably-realtime.com', - 'e.ably-realtime.com', + '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', ]; - static function getEnvironmentFallbackHosts($environment) { - return [ - $environment."-a-fallback.ably-realtime.com", - $environment."-b-fallback.ably-realtime.com", - $environment."-c-fallback.ably-realtime.com", - $environment."-d-fallback.ably-realtime.com", - $environment."-e-fallback.ably-realtime.com" - ]; + const NONPROD_PREFIX = 'nonprod:'; + + /** + * An endpoint is taken as a hostname rather than a routing policy id when + * it contains a `.` or `::`, or is `localhost` (REC1b2). + */ + static function isHostname($endpoint) { + return strpos($endpoint, '.') !== false + || strpos($endpoint, '::') !== false + || $endpoint === 'localhost'; + } + + /** + * Resolves the primary domain from an `endpoint` client option (REC1). + */ + static function getPrimaryDomain($endpoint) { + if (self::isHostname($endpoint)) { + return $endpoint; // REC1b2 + } + if (strpos($endpoint, self::NONPROD_PREFIX) === 0) { + return substr($endpoint, strlen(self::NONPROD_PREFIX)).'.realtime.ably-nonprod.net'; // REC1b3 + } + return $endpoint.'.realtime.ably.net'; // REC1b4 + } + + /** + * Resolves the default fallback domains for an `endpoint` client option (REC2c). + */ + static function getEndpointFallbackHosts($endpoint) { + if (self::isHostname($endpoint)) { + return []; // REC2c2 + } + if (strpos($endpoint, self::NONPROD_PREFIX) === 0) { + $id = substr($endpoint, strlen(self::NONPROD_PREFIX)); + return self::fallbackHostsFor($id, 'ably-realtime-nonprod.com'); // REC2c3 + } + return self::fallbackHostsFor($endpoint, 'ably-realtime.com'); // REC2c1, REC2c4 + } + + private static function fallbackHostsFor($id, $domain) { + return array_map(function ($letter) use ($id, $domain) { + return $id.'.'.$letter.'.fallback.'.$domain; + }, ['a', 'b', 'c', 'd', 'e']); } } diff --git a/src/Host.php b/src/Host.php index d62c2c5..302e2e8 100644 --- a/src/Host.php +++ b/src/Host.php @@ -8,7 +8,7 @@ class Host { public function __construct($clientOptions) { - $this->primaryHost = $clientOptions->getPrimaryRestHost(); + $this->primaryHost = $clientOptions->getPrimaryDomain(); $this->fallbackHosts = $clientOptions->getFallbackHosts(); $this->hostCache = new HostCache($clientOptions->fallbackRetryTimeout); } diff --git a/src/Models/ClientOptions.php b/src/Models/ClientOptions.php index 6204061..a285d3a 100644 --- a/src/Models/ClientOptions.php +++ b/src/Models/ClientOptions.php @@ -35,10 +35,14 @@ class ClientOptions extends AuthOptions { public $useBinaryProtocol = true; /** - * @var string alternate server domain - * For development environments only. + * @var string|null where the client connects (REC1), default `main`. + * One of: a routing policy id such as `main` or a dedicated cluster id, + * which resolves to `[id].realtime.ably.net`; `nonprod:[id]`, which + * resolves to `[id].realtime.ably-nonprod.net`; or a hostname (anything + * containing `.` or `::`, or `localhost`), which is used as given and has + * no default fallback hosts. */ - public $restHost; + public $endpoint; /** * @var integer Allows a non-default Ably non-TLS port to be used. @@ -53,19 +57,13 @@ class ClientOptions extends AuthOptions { public $tlsPort; /** - * @var string optional prefix to be prepended to $restHost - * Example: 'sandbox' -> 'sandbox-rest.ably.io' - */ - public $environment; - - /** - * @var string[] fallback hosts, used when connection to default host fails, populated automatically + * @var string[] fallback hosts, used when connection to the primary domain fails; when empty, derived from $endpoint */ public $fallbackHosts = []; /** * @var integer – default 600000 (10 minutes) the period in milliseconds - * before HTTP requests are retried against the default endpoint + * before HTTP requests are retried against the primary domain */ public $fallbackRetryTimeout = 600000; @@ -151,35 +149,27 @@ public static function normalizeConstructorArgument( $options ) { return $options; } - private function isProductionEnvironment() { - return empty($this->environment) || strcasecmp($this->environment, "production") == 0; - } - - private function isDefaultPort() { - return $this->tls ? $this->tlsPort == Defaults::$tlsPort : $this->port == Defaults::$port; - } - private function activePort() { return $this->tls ? $this->tlsPort : $this->port; } - private function isDefaultRestHost() { - return $this->restHost == Defaults::$restHost; - } - - public function getPrimaryRestHost() { - if ($this->isDefaultRestHost()) { - return $this->isProductionEnvironment() ? $this->restHost : $this->environment.'-'.$this->restHost; - } - return $this->restHost; + /** + * The domain both REST requests and, in SDKs that have one, the realtime + * connection use (REC1, RSC25). + */ + public function getPrimaryDomain() { + return Defaults::getPrimaryDomain($this->endpoint); } + /** + * An explicit, non-empty `fallbackHosts` replaces the defaults (REC2a2); + * otherwise the defaults are derived from `endpoint` (REC2c). + */ public function getFallbackHosts() { - $fallbacks = $this->fallbackHosts ?? []; - if (empty($this->fallbackHosts) && $this->isDefaultRestHost() && $this->isDefaultPort()) { - $fallbacks = $this->isProductionEnvironment() ? Defaults::$fallbackHosts : Defaults::getEnvironmentFallbackHosts($this->environment); + if (!empty($this->fallbackHosts)) { + return $this->fallbackHosts; } - return $fallbacks; + return Defaults::getEndpointFallbackHosts($this->endpoint); } public function getHostUrl($host) { @@ -188,8 +178,8 @@ public function getHostUrl($host) { public function __construct( $options = [] ) { parent::__construct( $options ); - if (empty($this->restHost)) { - $this->restHost = Defaults::$restHost; + if (empty($this->endpoint)) { + $this->endpoint = Defaults::$endpoint; } if (empty($this->port)) { $this->port = Defaults::$port; diff --git a/src/PubSubHttpClient.php b/src/PubSubHttpClient.php index 78560bd..b03330c 100644 --- a/src/PubSubHttpClient.php +++ b/src/PubSubHttpClient.php @@ -219,7 +219,7 @@ public function requestInternal( $method, $path, $headers = [], $params = [], $r $hostUrl = $this->options->getHostUrl($host). $path; try { $updatedHeaders = $mergedHeaders; - if ($host != $this->options->getPrimaryRestHost()) { // set hostHeader for fallback host (RSC15j) + if ($host != $this->options->getPrimaryDomain()) { // set hostHeader for fallback host (RSC15j) $updatedHeaders[] = "Host: " . $host; } $response = $this->http->request( $method, $hostUrl, $updatedHeaders, $params ); diff --git a/tests/ChannelIdempotentTest.php b/tests/ChannelIdempotentTest.php index a8e23ee..142e8fc 100644 --- a/tests/ChannelIdempotentTest.php +++ b/tests/ChannelIdempotentTest.php @@ -168,9 +168,9 @@ public function testIdempotentLibraryGeneratedPublish() { 'idempotentRestPublishing' => true, 'httpClass' => 'tests\HttpMockIdempotent', 'fallbackHosts' => [ - self::$ably->options->getPrimaryRestHost(), - self::$ably->options->getPrimaryRestHost(), - self::$ably->options->getPrimaryRestHost(), + self::$ably->options->getPrimaryDomain(), + self::$ably->options->getPrimaryDomain(), + self::$ably->options->getPrimaryDomain(), ], ] ) ); diff --git a/tests/ChannelMessagesTest.php b/tests/ChannelMessagesTest.php index 638ec98..dc3de2c 100644 --- a/tests/ChannelMessagesTest.php +++ b/tests/ChannelMessagesTest.php @@ -468,7 +468,7 @@ public function testEncodingInteroperabilityRawToAbly() { $defaultOpts = new \Ably\PubSub\Models\ClientOptions( self::$defaultOptions ); $http = new \Ably\PubSub\Http( $defaultOpts ); // initialize http class for raw requests with default timeouts - $server = 'https://' . $defaultOpts->getPrimaryRestHost(); + $server = 'https://' . $defaultOpts->getPrimaryDomain(); $messages = []; foreach ($fixture->messages as $i => $testMsgData) { @@ -514,7 +514,7 @@ public function testEncodingInteroperabilityAblyToRaw() { $defaultOpts = new \Ably\PubSub\Models\ClientOptions( self::$defaultOptions ); $http = new \Ably\PubSub\Http( $defaultOpts ); // initialize http class for raw requests with default timeouts - $server = 'https://' . $defaultOpts->getPrimaryRestHost(); + $server = 'https://' . $defaultOpts->getPrimaryDomain(); $messages = []; foreach ($fixture->messages as $i => $testMsgData) { diff --git a/tests/ClientOptionsTest.php b/tests/ClientOptionsTest.php index dbaa813..690cbb1 100644 --- a/tests/ClientOptionsTest.php +++ b/tests/ClientOptionsTest.php @@ -2,18 +2,18 @@ namespace tests; use Ably\PubSub\Defaults; use Ably\PubSub\Models\ClientOptions; -use http\Env; /** * @testdox RSC15b */ class ClientOptionsTest extends \PHPUnit\Framework\TestCase { /** - * @testdox RSC15e RSC15g3 + * @testdox RSC15e REC1a REC2c1 */ public function testWithDefaultOptions() { $clientOptions = new ClientOptions(); - self::assertEquals('rest.ably.io', $clientOptions->getPrimaryRestHost()); + self::assertEquals('main', $clientOptions->endpoint); + self::assertEquals('main.realtime.ably.net', $clientOptions->getPrimaryDomain()); self::assertTrue($clientOptions->tls); self::assertEquals(443, $clientOptions->tlsPort); $fallbackHosts = $clientOptions->getFallbackHosts(); @@ -22,74 +22,77 @@ public function testWithDefaultOptions() { } /** - * @testdox RSC15h + * @testdox REC1b4 REC2c4 */ - public function testWithProductionEnvironment() { - $clientOptions = new ClientOptions(); - $clientOptions->environment = "Production"; - self::assertEquals('rest.ably.io', $clientOptions->getPrimaryRestHost()); - self::assertTrue($clientOptions->tls); - self::assertEquals(443, $clientOptions->tlsPort); - $fallbackHosts = $clientOptions->getFallbackHosts(); - sort($fallbackHosts); - $this->assertEquals(Defaults::$fallbackHosts, $fallbackHosts); + public function testWithRoutingPolicyEndpoint() { + $clientOptions = new ClientOptions(['endpoint' => 'acme']); + self::assertEquals('acme.realtime.ably.net', $clientOptions->getPrimaryDomain()); + self::assertEquals([ + 'acme.a.fallback.ably-realtime.com', + 'acme.b.fallback.ably-realtime.com', + 'acme.c.fallback.ably-realtime.com', + 'acme.d.fallback.ably-realtime.com', + 'acme.e.fallback.ably-realtime.com', + ], $clientOptions->getFallbackHosts()); } /** - * @testdox RSC15g2 RTC1e + * @testdox REC1b3 REC2c3 */ - public function testWithCustomEnvironment() { - $clientOptions = new ClientOptions(); - $clientOptions->environment = "sandbox"; - self::assertEquals('sandbox-rest.ably.io', $clientOptions->getPrimaryRestHost()); - self::assertTrue($clientOptions->tls); - self::assertEquals(443, $clientOptions->tlsPort); - $fallbackHosts = $clientOptions->getFallbackHosts(); - sort($fallbackHosts); - $this->assertEquals(Defaults::getEnvironmentFallbackHosts('sandbox'), $fallbackHosts); + public function testWithNonprodEndpoint() { + $clientOptions = new ClientOptions(['endpoint' => 'nonprod:sandbox']); + self::assertEquals('sandbox.realtime.ably-nonprod.net', $clientOptions->getPrimaryDomain()); + self::assertEquals([ + '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', + ], $clientOptions->getFallbackHosts()); + } + + public static function hostnameEndpointProvider() { + return [ + ['foo.example.com'], + ['localhost'], + ['127.0.0.1'], + ['::1'], + ]; } /** - * @testdox RSC11b RTN17b RTC1e + * @testdox REC1b2 REC2c2 + * @dataProvider hostnameEndpointProvider */ - public function testWithCustomEnvironmentAndNonDefaultPorts() { - $clientOptions = new ClientOptions(); - $clientOptions->environment = "local"; - $clientOptions->port = 8080; - $clientOptions->tlsPort = 8081; - self::assertEquals('local-rest.ably.io', $clientOptions->getPrimaryRestHost()); - self::assertEquals(8080, $clientOptions->port); - self::assertEquals(8081, $clientOptions->tlsPort); - self::assertTrue($clientOptions->tls); - $fallbackHosts = $clientOptions->getFallbackHosts(); - self::assertEmpty($fallbackHosts); + public function testWithHostnameEndpoint($endpoint) { + $clientOptions = new ClientOptions(['endpoint' => $endpoint]); + self::assertEquals($endpoint, $clientOptions->getPrimaryDomain()); + self::assertEmpty($clientOptions->getFallbackHosts()); } /** - * @testdox RSC11 + * Non-default ports do not suppress the default fallback hosts; only a + * hostname endpoint does (REC2c). + * @testdox REC2c1 */ - public function testWithCustomRestHost() { - $clientOptions = new ClientOptions(); - $clientOptions->restHost = "test.org"; - self::assertEquals('test.org', $clientOptions->getPrimaryRestHost()); - self::assertEquals(80, $clientOptions->port); - self::assertEquals(443, $clientOptions->tlsPort); - self::assertTrue($clientOptions->tls); - $fallbackHosts = $clientOptions->getFallbackHosts(); - self::assertEmpty($fallbackHosts); + public function testWithNonDefaultPorts() { + $clientOptions = new ClientOptions(['port' => 8080, 'tlsPort' => 8081]); + self::assertEquals(8080, $clientOptions->port); + self::assertEquals(8081, $clientOptions->tlsPort); + self::assertEquals('main.realtime.ably.net', $clientOptions->getPrimaryDomain()); + self::assertEquals(Defaults::$fallbackHosts, $clientOptions->getFallbackHosts()); } /** - * @testdox RSC15g1 + * @testdox RSC15g1 REC2a2 */ public function testWithFallbacks() { - $clientOptions = new ClientOptions(); - $clientOptions->fallbackHosts = ["a.example.com", "b.example.com"]; - self::assertEquals('rest.ably.io', $clientOptions->getPrimaryRestHost()); - self::assertTrue($clientOptions->tls); - self::assertEquals(443, $clientOptions->tlsPort); - $fallbackHosts = $clientOptions->getFallbackHosts(); - sort($fallbackHosts); - self::assertEquals(["a.example.com", "b.example.com"], $fallbackHosts); + foreach (['main', 'nonprod:sandbox', 'foo.example.com'] as $endpoint) { + $clientOptions = new ClientOptions([ + 'endpoint' => $endpoint, + 'fallbackHosts' => ["a.example.com", "b.example.com"], + ]); + self::assertEquals(["a.example.com", "b.example.com"], $clientOptions->getFallbackHosts()); + } } } diff --git a/tests/DefaultsTest.php b/tests/DefaultsTest.php index 82e4d91..1f975e0 100644 --- a/tests/DefaultsTest.php +++ b/tests/DefaultsTest.php @@ -1,37 +1,29 @@ assertEquals($expectedFallbackHosts, $fallbackHosts); + $this->assertEquals($expectedFallbackHosts, Defaults::$fallbackHosts); + $this->assertEquals($expectedFallbackHosts, Defaults::getEndpointFallbackHosts(Defaults::$endpoint)); } /** - * @testdox RSC15i + * @testdox REC1a */ - public function testEnvironmentFallbackHosts() { - $expectedFallbackHosts = [ - "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" - ]; - $fallbackHosts = Defaults::getEnvironmentFallbackHosts("sandbox"); - $this->assertEquals($expectedFallbackHosts, $fallbackHosts); + public function testDefaultEndpoint() { + $this->assertEquals('main', Defaults::$endpoint); + $this->assertEquals('main.realtime.ably.net', Defaults::getPrimaryDomain(Defaults::$endpoint)); } } diff --git a/tests/HostCacheTest.php b/tests/HostCacheTest.php index 315347e..55eb042 100644 --- a/tests/HostCacheTest.php +++ b/tests/HostCacheTest.php @@ -1,7 +1,6 @@ put(Defaults::$restHost); - self::assertEquals("rest.ably.io", $hostCache->get()); + $hostCache->put("main.realtime.ably.net"); + self::assertEquals("main.realtime.ably.net", $hostCache->get()); } /** @@ -23,7 +22,7 @@ public function testHost() { */ public function testExpiredHost() { $hostCache = new HostCache(999); - $hostCache->put(Defaults::$restHost); + $hostCache->put("main.realtime.ably.net"); sleep(1); self::assertEquals("", $hostCache->get()); } diff --git a/tests/HostTest.php b/tests/HostTest.php index 928d880..0a10b2c 100644 --- a/tests/HostTest.php +++ b/tests/HostTest.php @@ -17,15 +17,15 @@ public function testFallbacksInRandomOrder() { $restHosts = new Host($clientOptions); // All expected hosts supposed to be tried upon $expectedFallbackHosts = [ - "a.ably-realtime.com", - "b.ably-realtime.com", - "c.ably-realtime.com", - "d.ably-realtime.com", - "e.ably-realtime.com", + "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", ]; $prefHost = $restHosts->getPreferredHost(); - self::assertEquals("rest.ably.io", $prefHost); + self::assertEquals("main.realtime.ably.net", $prefHost); $fallbacks = iterator_to_array($restHosts->fallbackHosts($prefHost)); self::assertNotEquals($expectedFallbackHosts, $fallbacks); @@ -42,20 +42,20 @@ public function testFallbacksOtherThanPreferredHost() { $restHosts = new Host($clientOptions); // All expected hosts supposed to be tried upon $expectedFallbackHosts = [ - "rest.ably.io", - "a.ably-realtime.com", - "c.ably-realtime.com", - "d.ably-realtime.com", - "e.ably-realtime.com", + "main.realtime.ably.net", + "main.a.fallback.ably-realtime.com", + "main.c.fallback.ably-realtime.com", + "main.d.fallback.ably-realtime.com", + "main.e.fallback.ably-realtime.com", ]; - $restHosts->setPreferredHost("b.ably-realtime.com"); + $restHosts->setPreferredHost("main.b.fallback.ably-realtime.com"); $prefHost = $restHosts->getPreferredHost(); - self::assertEquals("b.ably-realtime.com", $prefHost); + self::assertEquals("main.b.fallback.ably-realtime.com", $prefHost); $fallbacks = iterator_to_array($restHosts->fallbackHosts($prefHost)); - self::assertEquals("rest.ably.io", $fallbacks[0]); + self::assertEquals("main.realtime.ably.net", $fallbacks[0]); sort($fallbacks); sort($expectedFallbackHosts); @@ -70,16 +70,16 @@ public function testGetAllFallbacksWithNoPreferredHost() { $restHosts = new Host($clientOptions); // All expected hosts supposed to be tried upon $expectedFallbackHosts = [ - "rest.ably.io", - "b.ably-realtime.com", - "a.ably-realtime.com", - "c.ably-realtime.com", - "d.ably-realtime.com", - "e.ably-realtime.com", + "main.realtime.ably.net", + "main.b.fallback.ably-realtime.com", + "main.a.fallback.ably-realtime.com", + "main.c.fallback.ably-realtime.com", + "main.d.fallback.ably-realtime.com", + "main.e.fallback.ably-realtime.com", ]; $fallbacks = iterator_to_array($restHosts->fallbackHosts("")); - self::assertEquals("rest.ably.io", $fallbacks[0]); + self::assertEquals("main.realtime.ably.net", $fallbacks[0]); sort($fallbacks); sort($expectedFallbackHosts); @@ -93,7 +93,7 @@ public function testGetPrimaryHostIfNothingIsCached() { $clientOptions = new ClientOptions(); $restHosts = new Host($clientOptions); $prefHost = $restHosts->getPreferredHost(); - self::assertEquals("rest.ably.io", $prefHost); + self::assertEquals("main.realtime.ably.net", $prefHost); } } diff --git a/tests/PubSubHttpClientTest.php b/tests/PubSubHttpClientTest.php index 0436bca..60c344d 100644 --- a/tests/PubSubHttpClientTest.php +++ b/tests/PubSubHttpClientTest.php @@ -76,12 +76,13 @@ public function testInitLibWithTokenDetailsOption() { } /** - * Init library with a specified host + * Init library with a hostname endpoint + * @testdox REC1b2 */ public function testInitLibWithSpecifiedHost() { $opts = [ 'key' => 'fake.key:veryFake', - 'restHost' => 'some.other.host', + 'endpoint' => 'some.other.host', 'httpClass' => 'tests\HttpMockInitTest', ]; $ably = new PubSubHttpClient( $opts ); @@ -95,21 +96,21 @@ public function testInitLibWithSpecifiedHost() { public function testInitLibWithSpecifiedPort() { $opts = [ 'key' => 'fake.key:veryFake', - 'restHost' => 'some.other.host', + 'endpoint' => 'some.other.host', 'tlsPort' => 999, 'httpClass' => 'tests\HttpMockInitTest', ]; $ably = new PubSubHttpClient( $opts ); $ably->time(); // make a request $this->assertStringContainsString( - 'https://' . $opts['restHost'] . ':' . $opts['tlsPort'], + 'https://' . $opts['endpoint'] . ':' . $opts['tlsPort'], $ably->http->lastUrl, 'Unexpected host/port mismatch' ); $opts = [ 'token' => 'fakeToken', - 'restHost' => 'some.other.host', + 'endpoint' => 'some.other.host', 'port' => 999, 'tls' => false, 'httpClass' => 'tests\HttpMockInitTest', @@ -117,27 +118,28 @@ public function testInitLibWithSpecifiedPort() { $ably = new PubSubHttpClient( $opts ); $ably->time(); // make a request $this->assertStringContainsString( - 'http://' . $opts['restHost'] . ':' . $opts['port'], + 'http://' . $opts['endpoint'] . ':' . $opts['port'], $ably->http->lastUrl, 'Unexpected host/port mismatch' ); } /** - * Init library with specified environment + * Init library with a nonprod routing policy endpoint + * @testdox REC1b3 */ - public function testInitLibWithSpecifiedEnv() { + public function testInitLibWithNonprodEndpoint() { $ably = new PubSubHttpClient( [ 'key' => 'fake.key:veryFake', - 'environment' => 'sandbox', + 'endpoint' => 'nonprod:sandbox', 'httpClass' => 'tests\HttpMockInitTest', ] ); $ably->time(); // make a request - $this->assertEquals( 'https://sandbox-rest.ably.io:443/time', $ably->http->lastUrl, 'Unexpected host mismatch' ); + $this->assertEquals( 'https://sandbox.realtime.ably-nonprod.net:443/time', $ably->http->lastUrl, 'Unexpected host mismatch' ); } /** - * Verify encrypted defaults to true, makes a request to https://rest.ably.io/... + * Verify encrypted defaults to true, makes a request to https://main.realtime.ably.net/... */ public function testTLSDefaultIsTrue() { $opts = [ @@ -146,11 +148,11 @@ public function testTLSDefaultIsTrue() { ]; $ably = new PubSubHttpClient( $opts ); $ably->time(); // make a request - $this->assertMatchesRegularExpression( '/^https:\/\/rest\.ably\.io/', $ably->http->lastUrl, 'Unexpected scheme/url mismatch' ); + $this->assertMatchesRegularExpression( '/^https:\/\/main\.realtime\.ably\.net/', $ably->http->lastUrl, 'Unexpected scheme/url mismatch' ); } /** - * Verify encrypted can be set to false, makes a request to http://rest.ably.io/... + * Verify encrypted can be set to false, makes a request to http://main.realtime.ably.net/... */ public function testTLSCanBeFalse() { $opts = [ @@ -160,11 +162,11 @@ public function testTLSCanBeFalse() { ]; $ably = new PubSubHttpClient( $opts ); $ably->time(); // make a request - $this->assertMatchesRegularExpression( '/^http:\/\/rest\.ably\.io/', $ably->http->lastUrl, 'Unexpected scheme/url mismatch' ); + $this->assertMatchesRegularExpression( '/^http:\/\/main\.realtime\.ably\.net/', $ably->http->lastUrl, 'Unexpected scheme/url mismatch' ); } /** - * Verify that connection is encrypted when set to true explicitly, makes a request to https://rest.ably.io/... + * Verify that connection is encrypted when set to true explicitly, makes a request to https://main.realtime.ably.net/... */ public function testTLSExplicitTrue() { $opts = [ @@ -174,7 +176,7 @@ public function testTLSExplicitTrue() { ]; $ably = new PubSubHttpClient( $opts ); $ably->time(); // make a request - $this->assertMatchesRegularExpression( '/^https:\/\/rest\.ably\.io/', $ably->http->lastUrl, 'Unexpected scheme/url mismatch' ); + $this->assertMatchesRegularExpression( '/^https:\/\/main\.realtime\.ably\.net/', $ably->http->lastUrl, 'Unexpected scheme/url mismatch' ); } @@ -214,9 +216,9 @@ public function testFallbackHosts() { $this->fail('Expected the request to fail'); } catch(AblyRequestException $e) { $this->assertCount(6, $ably->http->visitedHosts); - $this->assertEquals( 'rest.ably.io' , $ably->http->visitedHosts[0],'Expected to try primary restHost first' ); + $this->assertEquals( 'main.realtime.ably.net' , $ably->http->visitedHosts[0],'Expected to try primary domain first' ); - $expectedFallbackHosts = array_merge( [ 'rest.ably.io' ], Defaults::$fallbackHosts ); + $expectedFallbackHosts = array_merge( [ 'main.realtime.ably.net' ], Defaults::$fallbackHosts ); $this->assertNotEquals( $expectedFallbackHosts, $ably->http->visitedHosts,'Expected to have fallback hosts randomized' ); sort($expectedFallbackHosts); @@ -227,15 +229,15 @@ public function testFallbackHosts() { } /** - * When using custom environment, should use custom env. fallback hosts - * @testdox RSC15b1,RSC15g2 + * When using a routing policy endpoint, should use that endpoint's fallback hosts + * @testdox REC1b4, REC2c4 */ - public function testEnvFallbackHosts() { + public function testEndpointFallbackHosts() { $opts = [ 'key' => 'fake.key:veryFake', 'httpClass' => 'tests\HttpMockInitTestTimeout', 'httpMaxRetryCount' => 5, - 'environment' => 'alpha' + 'endpoint' => 'alpha' ]; $ably = new PubSubHttpClient( $opts ); try { @@ -243,9 +245,9 @@ public function testEnvFallbackHosts() { $this->fail('Expected the request to fail'); } catch(AblyRequestException $e) { $this->assertCount(6, $ably->http->visitedHosts); - $this->assertEquals( 'alpha-rest.ably.io' , $ably->http->visitedHosts[0],'Expected to try primary restHost first' ); + $this->assertEquals( 'alpha.realtime.ably.net' , $ably->http->visitedHosts[0],'Expected to try primary domain first' ); - $expectedFallbackHosts = array_merge( [ 'alpha-rest.ably.io' ], Defaults::getEnvironmentFallbackHosts('alpha')); + $expectedFallbackHosts = array_merge( [ 'alpha.realtime.ably.net' ], Defaults::getEndpointFallbackHosts('alpha')); $this->assertNotEquals( $expectedFallbackHosts, $ably->http->visitedHosts,'Expected to have fallback hosts randomized' ); sort($expectedFallbackHosts); @@ -263,7 +265,7 @@ public function testNoFallbackOnEmptyCustomFallbacks() { $opts = [ 'key' => 'fake.key:veryFake', 'httpClass' => 'tests\HttpMockInitTestTimeout', - 'restHost' => 'custom.host.com', + 'endpoint' => 'custom.host.com', 'fallbackHosts' => [], ]; $ably = new PubSubHttpClient( $opts ); @@ -277,7 +279,7 @@ public function testNoFallbackOnEmptyCustomFallbacks() { } /** - * Verify that custom restHost and custom fallbackHosts are working + * Verify that a hostname endpoint and custom fallbackHosts are working * @testdox RSC15b2, RSC15g1 */ public function testCustomHostAndFallbacks() { @@ -287,7 +289,7 @@ public function testCustomHostAndFallbacks() { 'third-fallback.custom.com', ]; $defaultOpts = new ClientOptions([ - 'restHost' => 'rest.custom.com', + 'endpoint' => 'rest.custom.com', 'fallbackHosts' => $customFallbacks, ]); @@ -302,9 +304,9 @@ public function testCustomHostAndFallbacks() { $this->fail('Expected the request to fail'); } catch(AblyRequestException $e) { $this->assertCount(4, $ably->http->visitedHosts); - $this->assertEquals( 'rest.custom.com' , $ably->http->visitedHosts[0],'Expected to try primary restHost first' ); + $this->assertEquals( 'rest.custom.com' , $ably->http->visitedHosts[0],'Expected to try primary domain first' ); - $expectedFallbackHosts = array_merge( [ $defaultOpts->restHost ], $customFallbacks); + $expectedFallbackHosts = array_merge( [ $defaultOpts->getPrimaryDomain() ], $customFallbacks); sort($expectedFallbackHosts); $actualVisitedHosts = $ably->http->visitedHosts; // copied by value; sort($actualVisitedHosts); @@ -332,19 +334,20 @@ public function testNoFallbackOnClientError() { $this->fail('Expected the request to fail'); } catch(AblyRequestException $e) { $this->assertCount(1, $ably->http->visitedHosts); - $this->assertEquals( [ 'rest.ably.io' ], $ably->http->visitedHosts, 'Expected to have tried only the default host' ); + $this->assertEquals( [ 'main.realtime.ably.net' ], $ably->http->visitedHosts, 'Expected to have tried only the default host' ); } } /** - * Verify that default fallback hosts are NOT used when using a custom host - * @testdox RSC15k + * Verify that default fallback hosts are NOT used with a hostname endpoint, + * so a qualifying error fails immediately + * @testdox RSC15k, REC2c2, RSC15m */ public function testNoFallbackOnCustomHost() { $opts = [ 'key' => 'fake.key:veryFake', 'httpClass' => 'tests\HttpMockInitTestTimeout', - 'restHost' => 'custom.host.com', + 'endpoint' => 'custom.host.com', ]; $ably = new PubSubHttpClient( $opts ); try { @@ -385,7 +388,7 @@ public function testFallbackHostHeader() { 'third-fallback.custom.com', ]; $opts = new ClientOptions([ - 'restHost' => 'rest.custom.com', + 'endpoint' => 'rest.custom.com', 'fallbackHosts' => $customFallbacks, 'key' => 'fake.key:veryFake', 'httpClass' => 'tests\HttpMockInitTestTimeout' @@ -412,35 +415,35 @@ public function testCachedFallback() { 'key' => self::$testApp->getAppKeyDefault()->string, 'fallbackRetryTimeout' => $fallbackCacheTimeoutInMs, 'httpClass' => 'tests\HttpMockCachedFallback', - 'restHost' => 'custom.host.com', + 'endpoint' => 'custom.host.com', 'fallbackHosts' => [ 'fallback1', - 'c.ably-realtime.com', // valid fallback host + 'main.c.fallback.ably-realtime.com', // valid fallback host 'fallback2', ], ])); $ably->time(); - $this->assertEquals("c.ably-realtime.com", $ably->host->getPreferredHost()); // check for cached host + $this->assertEquals("main.c.fallback.ably-realtime.com", $ably->host->getPreferredHost()); // check for cached host $this->assertGreaterThanOrEqual(1, $ably->http->fallbackRetries ); $ably->http->resetRetries(); $ably->time(); $ably->time(); - $this->assertEquals("c.ably-realtime.com", $ably->host->getPreferredHost()); // check for cached host + $this->assertEquals("main.c.fallback.ably-realtime.com", $ably->host->getPreferredHost()); // check for cached host $this->assertEquals( 0, $ably->http->fallbackRetries); // zero retries since cached fallback is used sleep( 2); // expire cached host $ably->time(); - $this->assertEquals("c.ably-realtime.com", $ably->host->getPreferredHost()); // check for cached host + $this->assertEquals("main.c.fallback.ably-realtime.com", $ably->host->getPreferredHost()); // check for cached host $this->assertGreaterThanOrEqual( 1, $ably->http->fallbackRetries ); } /** * Verify accuracy of time (to within 2 seconds of actual time) * - * RSC16 PubSubHttpClient#time function sends a get request to rest.ably.io/time + * RSC16 PubSubHttpClient#time function sends a get request to main.realtime.ably.net/time * and returns the server time in milliseconds since epoch */ public function testTimeAndAccuracy() { @@ -475,7 +478,7 @@ public function testActiveInternetConnection() { public function testTimeFailsWithInvalidHost() { $ablyInvalidHost = new PubSubHttpClient( [ 'key' => 'fake.key:veryFake', - 'restHost' => 'this.host.does.not.exist', + 'endpoint' => 'this.host.does.not.exist', ]); $this->expectException(AblyRequestException::class); @@ -496,10 +499,10 @@ public function testHttpTimeout() { 'httpRequestTimeout' => 20, // 20 ms ]); - $ably->http->get('https://rest.ably.io/time'); // should work + $ably->http->get('https://main.realtime.ably.net/time'); // should work $this->expectException(AblyRequestException::class); $this->expectExceptionCode(50003); - $ablyTimeout->http->get('https://rest.ably.io/time'); // guaranteed to take more than 20 ms + $ablyTimeout->http->get('https://main.realtime.ably.net/time'); // guaranteed to take more than 20 ms } } @@ -564,7 +567,7 @@ public function __construct( $clientOptions ) { } public function request( $method, $url, $headers = [], $params = [] ) { - if ( parse_url($url, PHP_URL_HOST) == "c.ably-realtime.com" ) { // cache specific host + if ( parse_url($url, PHP_URL_HOST) == "main.c.fallback.ably-realtime.com" ) { // cache specific host return parent::request($method, $url, $headers, $params); } $this->fallbackRetries++; diff --git a/tests/TypesTest.php b/tests/TypesTest.php index ec3196f..0d28c6a 100644 --- a/tests/TypesTest.php +++ b/tests/TypesTest.php @@ -141,8 +141,7 @@ public function testClientOptionsType() { 'authHeaders', 'authParams', 'queryTime', - 'environment', - 'restHost', + 'endpoint', 'port', 'tlsPort', 'httpOpenTimeout', diff --git a/tests/factories/TestApp.php b/tests/factories/TestApp.php index 67332f4..c6ccdc0 100644 --- a/tests/factories/TestApp.php +++ b/tests/factories/TestApp.php @@ -25,7 +25,7 @@ public function __construct() { $settings = []; - $settings['environment'] = getenv( 'ABLY_ENV' ) ? : 'sandbox'; + $settings['endpoint'] = getenv( 'ABLY_ENDPOINT' ) ? : 'nonprod:sandbox'; $settings['useBinaryProtocol'] = getenv('PROTOCOL') !== 'json'; //$settings['logLevel'] = Log::DEBUG; @@ -33,7 +33,7 @@ public function __construct() { $this->options = $settings; - $this->server = $clientOpts->getHostUrl($clientOpts->getPrimaryRestHost()); + $this->server = $clientOpts->getHostUrl($clientOpts->getPrimaryDomain()); $this->init(); return $this;