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;