diff --git a/ably/ably_test.go b/ably/ably_test.go index 7a538424..310fcedb 100644 --- a/ably/ably_test.go +++ b/ably/ably_test.go @@ -356,7 +356,7 @@ func NewRecorder(httpClient *http.Client) *HostRecorder { func (hr *HostRecorder) Options(host string, opts ...ably.ClientOption) []ably.ClientOption { return append(opts, - ably.WithRealtimeHost(host), + ably.WithEndpoint(host), ably.WithAutoConnect(false), ably.WithDial(hr.dialWS), ably.WithHTTPClient(hr.httpClient), diff --git a/ably/auth_integration_test.go b/ably/auth_integration_test.go index 4788f6b7..290aae20 100644 --- a/ably/auth_integration_test.go +++ b/ably/auth_integration_test.go @@ -389,7 +389,7 @@ func TestAuth_JWT_Token_RSA8c(t *testing.T) { rec, optn := ablytest.NewHttpRecorder() rest, err := ably.NewREST( ably.WithToken(jwt), - ably.WithEnvironment(app.Environment), + ably.WithEndpoint(app.Endpoint), optn[0], ) assert.NoError(t, err, "rest()=%v", err) @@ -414,7 +414,7 @@ func TestAuth_JWT_Token_RSA8c(t *testing.T) { rest, err := ably.NewREST( ably.WithAuthURL(ablytest.CREATE_JWT_URL), ably.WithAuthParams(app.GetJwtAuthParams(30*time.Second, false)), - ably.WithEnvironment(app.Environment), + ably.WithEndpoint(app.Endpoint), optn[0], ) assert.NoError(t, err, "rest()=%v", err) @@ -457,7 +457,7 @@ func TestAuth_JWT_Token_RSA8c(t *testing.T) { rec, optn := ablytest.NewHttpRecorder() rest, err := ably.NewREST( - ably.WithEnvironment(app.Environment), + ably.WithEndpoint(app.Endpoint), authCallback, optn[0], ) @@ -485,7 +485,7 @@ func TestAuth_JWT_Token_RSA8c(t *testing.T) { rest, err := ably.NewREST( ably.WithAuthURL(ablytest.CREATE_JWT_URL), ably.WithAuthParams(app.GetJwtAuthParams(30*time.Second, true)), - ably.WithEnvironment(app.Environment), + ably.WithEndpoint(app.Endpoint), optn[0], ) assert.NoError(t, err, "rest()=%v", err) diff --git a/ably/error_test.go b/ably/error_test.go index c5eb860d..fe7ea9e0 100644 --- a/ably/error_test.go +++ b/ably/error_test.go @@ -54,7 +54,7 @@ func TestIssue127ErrorResponse(t *testing.T) { ably.WithKey("xxxxxxx.yyyyyyy:zzzzzzz"), ably.WithTLS(false), ably.WithUseTokenAuth(true), - ably.WithRESTHost(endpointURL.Hostname()), + ably.WithEndpoint(endpointURL.Hostname()), } port, _ := strconv.ParseInt(endpointURL.Port(), 10, 0) opts = append(opts, ably.WithPort(int(port))) @@ -134,7 +134,7 @@ func TestIssue_154(t *testing.T) { ably.WithKey("xxxxxxx.yyyyyyy:zzzzzzz"), ably.WithTLS(false), ably.WithUseTokenAuth(true), - ably.WithRESTHost(endpointURL.Hostname()), + ably.WithEndpoint(endpointURL.Hostname()), } port, _ := strconv.ParseInt(endpointURL.Port(), 10, 0) opts = append(opts, ably.WithPort(int(port))) diff --git a/ably/export_test.go b/ably/export_test.go index c23640d7..2367fe05 100644 --- a/ably/export_test.go +++ b/ably/export_test.go @@ -12,16 +12,12 @@ func NewClientOptions(os ...ClientOption) *clientOptions { return applyOptionsWithDefaults(os...) } -func GetEnvFallbackHosts(env string) []string { - return getEnvFallbackHosts(env) +func GetEndpointFallbackHosts(endpoint string) []string { + return getEndpointFallbackHosts(endpoint) } -func (opts *clientOptions) GetRestHost() string { - return opts.getRestHost() -} - -func (opts *clientOptions) GetRealtimeHost() string { - return opts.getRealtimeHost() +func (opts *clientOptions) GetEndpoint() string { + return opts.getHostname() } func (opts *clientOptions) ActivePort() (int, bool) { @@ -192,6 +188,10 @@ func ApplyOptionsWithDefaults(o ...ClientOption) *clientOptions { return applyOptionsWithDefaults(o...) } +func IsEndpointFQDN(endpoint string) bool { + return isEndpointFQDN(endpoint) +} + type ConnStateChanges = connStateChanges type ChannelStateChanges = channelStateChanges diff --git a/ably/http_paginated_response_integration_test.go b/ably/http_paginated_response_integration_test.go index d47285b2..aac7fa31 100644 --- a/ably/http_paginated_response_integration_test.go +++ b/ably/http_paginated_response_integration_test.go @@ -21,7 +21,7 @@ func TestHTTPPaginatedFallback(t *testing.T) { assert.NoError(t, err) defer app.Close() opts := app.Options(ably.WithUseBinaryProtocol(false), - ably.WithRESTHost("ably.invalid"), + ably.WithEndpoint("ably.invalid"), ably.WithFallbackHosts(nil)) client, err := ably.NewREST(opts...) assert.NoError(t, err) diff --git a/ably/options.go b/ably/options.go index 6c7c8362..d6d221e5 100644 --- a/ably/options.go +++ b/ably/options.go @@ -23,6 +23,9 @@ const ( protocolJSON = "application/json" protocolMsgPack = "application/x-msgpack" + // defaultEndpoint is the default routing policy used to connect to Ably + defaultEndpoint = "main" + // restHost is the primary ably host. restHost = "rest.ably.io" // realtimeHost is the primary ably host. @@ -37,6 +40,7 @@ const ( ) var defaultOptions = clientOptions{ + Endpoint: defaultEndpoint, RESTHost: restHost, FallbackHosts: defaultFallbackHosts(), HTTPMaxRetryCount: 3, @@ -59,23 +63,28 @@ var defaultOptions = clientOptions{ } func defaultFallbackHosts() []string { - return []string{ - "a.ably-realtime.com", - "b.ably-realtime.com", - "c.ably-realtime.com", - "d.ably-realtime.com", - "e.ably-realtime.com", + return endpointFallbacks("main", "ably-realtime.com") +} + +func getEndpointFallbackHosts(endpoint string) []string { + if endpoint == "sandbox" { + return endpointFallbacks("sandbox", "ably-realtime-nonprod.com") } + + if strings.HasPrefix(endpoint, "nonprod:") { + namespace := strings.TrimPrefix(endpoint, "nonprod:") + return endpointFallbacks(namespace, "ably-realtime-nonprod.com") + } + + return endpointFallbacks(endpoint, "ably-realtime.com") } -func getEnvFallbackHosts(env string) []string { - return []string{ - fmt.Sprintf("%s-%s", env, "a-fallback.ably-realtime.com"), - fmt.Sprintf("%s-%s", env, "b-fallback.ably-realtime.com"), - fmt.Sprintf("%s-%s", env, "c-fallback.ably-realtime.com"), - fmt.Sprintf("%s-%s", env, "d-fallback.ably-realtime.com"), - fmt.Sprintf("%s-%s", env, "e-fallback.ably-realtime.com"), +func endpointFallbacks(namespace, root string) []string { + fallbacks := make([]string, 5) + for i, id := range []string{"a", "b", "c", "d", "e"} { + fallbacks[i] = fmt.Sprintf("%s.%s.fallback.%s", namespace, id, root) } + return fallbacks } const ( @@ -244,8 +253,11 @@ type clientOptions struct { // authOptions Embedded an [ably.authOptions] object (TO3j). authOptions - // RESTHost enables a non-default Ably host to be specified. For development environments only. - // The default value is rest.ably.io (RSC12, TO3k2). + // Endpoint specifies either a routing policy name or fully qualified domain name to connect to Ably. + Endpoint string + + // Deprecated: this property is deprecated and will be removed in a future version. + // If the restHost option is specified the primary domain is the value of the restHost option REC1d1). RESTHost string // Deprecated: this property is deprecated and will be removed in a future version. @@ -257,12 +269,14 @@ type clientOptions struct { // please specify them here (RSC15b, RSC15a, TO3k6). FallbackHosts []string - // RealtimeHost enables a non-default Ably host to be specified for realtime connections. - // For development environments only. The default value is realtime.ably.io (RTC1d, TO3k3). + // Deprecated: this property is deprecated and will be removed in a future version. + // If the realtimeHost option is specified the primary domain is the value of the realtimeHost option (REC1d2). RealtimeHost string - // Environment enables a custom environment to be used with the Ably service. - // Optional: prefixes both hostname with the environment string (RSC15b, TO3k1). + // Deprecated: this property is deprecated and will be removed in a future version. + // If the deprecated environment option is specified then it defines a production routing policy name [name] (REC1c): + // If any one of the deprecated options restHost, realtimeHost are also specified then the options as a set are invalid (REC1c1). + // Otherwise, the primary domain is [name].realtime.ably.net (REC1c2). Environment string // Port is used for non-TLS connections and requests @@ -415,6 +429,13 @@ type clientOptions struct { } func (opts *clientOptions) validate() error { + if !empty(opts.Endpoint) && (!empty(opts.Environment) || !empty(opts.RealtimeHost) || !empty(opts.RESTHost)) { + err := errors.New("invalid client option: cannot use endpoint with any of environment, realtimeHost or restHost") + logger := opts.LogHandler + logger.Printf(LogError, "Invalid client options : %v", err.Error()) + return err + } + _, err := opts.getFallbackHosts() if err != nil { logger := opts.LogHandler @@ -424,11 +445,6 @@ func (opts *clientOptions) validate() error { return nil } -func (opts *clientOptions) isProductionEnvironment() bool { - env := opts.Environment - return empty(env) || strings.EqualFold(env, "production") -} - func (opts *clientOptions) activePort() (port int, isDefault bool) { if opts.NoTLS { port = opts.Port @@ -450,29 +466,54 @@ func (opts *clientOptions) activePort() (port int, isDefault bool) { return } -func (opts *clientOptions) getRestHost() string { +// isEndpointFQDN returns true if the given endpoint is a hostname, which may +// be an IPv4 address, IPv6 address or localhost +func isEndpointFQDN(endpoint string) bool { + return strings.Contains(endpoint, ".") || strings.Contains(endpoint, "::") || endpoint == "localhost" +} + +func (opts *clientOptions) endpointValueWithLegacySupport() string { + if !empty(opts.Endpoint) { + return opts.Endpoint + } + + if !empty(opts.Environment) { + if opts.Environment == "production" { + return defaultOptions.Endpoint + } + + return opts.Environment + } + if !empty(opts.RESTHost) { return opts.RESTHost } - if !opts.isProductionEnvironment() { - return opts.Environment + "-" + defaultOptions.RESTHost - } - return defaultOptions.RESTHost -} -func (opts *clientOptions) getRealtimeHost() string { if !empty(opts.RealtimeHost) { return opts.RealtimeHost } - if !empty(opts.RESTHost) { - logger := opts.LogHandler - logger.Printf(LogWarning, "restHost is set to %s but realtimeHost is not set so setting realtimeHost to %s too. If this is not what you want, please set realtimeHost explicitly.", opts.RESTHost, opts.RealtimeHost) - return opts.RESTHost + + return defaultOptions.Endpoint +} + +// REC2 +func (opts *clientOptions) getHostname() string { + endpoint := opts.endpointValueWithLegacySupport() + + if isEndpointFQDN(endpoint) { + return endpoint + } + + if endpoint == "sandbox" { + return "sandbox.realtime.ably-nonprod.net" } - if !opts.isProductionEnvironment() { - return opts.Environment + "-" + defaultOptions.RealtimeHost + + if strings.HasPrefix(endpoint, "nonprod:") { + namespace := strings.TrimPrefix(endpoint, "nonprod:") + return fmt.Sprintf("%s.realtime.ably-nonprod.net", namespace) } - return defaultOptions.RealtimeHost + + return fmt.Sprintf("%s.realtime.ably.net", endpoint) } func empty(s string) bool { @@ -480,7 +521,7 @@ func empty(s string) bool { } func (opts *clientOptions) restURL() (restUrl string) { - baseUrl := opts.getRestHost() + baseUrl := opts.getHostname() _, _, err := net.SplitHostPort(baseUrl) if err != nil { // set port if not set in baseUrl port, _ := opts.activePort() @@ -508,6 +549,7 @@ func (opts *clientOptions) realtimeURL(realtimeHost string) (realtimeUrl string) func (opts *clientOptions) getFallbackHosts() ([]string, error) { logger := opts.LogHandler _, isDefaultPort := opts.activePort() + if opts.FallbackHostsUseDefault { if opts.FallbackHosts != nil { return nil, errors.New("fallbackHosts and fallbackHostsUseDefault cannot both be set") @@ -521,12 +563,15 @@ func (opts *clientOptions) getFallbackHosts() ([]string, error) { logger.Printf(LogWarning, "Deprecated fallbackHostsUseDefault : using default fallbackhosts") return defaultOptions.FallbackHosts, nil } - if opts.FallbackHosts == nil && empty(opts.RESTHost) && empty(opts.RealtimeHost) && isDefaultPort { - if opts.isProductionEnvironment() { - return defaultOptions.FallbackHosts, nil + + if opts.FallbackHosts == nil { + ep := opts.endpointValueWithLegacySupport() + if isEndpointFQDN(ep) { + return opts.FallbackHosts, nil } - return getEnvFallbackHosts(opts.Environment), nil + return getEndpointFallbackHosts(ep), nil } + return opts.FallbackHosts, nil } @@ -1070,9 +1115,23 @@ func WithEchoMessages(echo bool) ClientOption { } } -// WithEnvironment is used for setting Environment using [ably.ClientOption]. -// Environment enables a custom environment to be used with the Ably service. -// Optional: prefixes both hostname with the environment string (RSC15b, TO3k1). +// WithEndpoint sets a custom endpoint for connecting to the Ably service (see +// [Platform Customization] for more information). +// +// [Platform Customization]: https://ably.com/docs/platform-customization +func WithEndpoint(env string) ClientOption { + return func(os *clientOptions) { + os.Endpoint = env + } +} + +// WithEnvironment sets a custom endpoint for connecting to the Ably service +// (see [Platform Customization] for more information). +// +// Deprecated: this option is deprecated and will be removed in a future +// version. +// +// [Platform Customization]: https://ably.com/docs/platform-customization func WithEnvironment(env string) ClientOption { return func(os *clientOptions) { os.Environment = env @@ -1130,6 +1189,9 @@ func WithQueueMessages(queue bool) ClientOption { // WithRESTHost is used for setting RESTHost using [ably.ClientOption]. // RESTHost enables a non-default Ably host to be specified. For development environments only. // The default value is rest.ably.io (RSC12, TO3k2). +// +// Deprecated: this option is deprecated and will be removed in a future +// version. func WithRESTHost(host string) ClientOption { return func(os *clientOptions) { os.RESTHost = host @@ -1149,6 +1211,9 @@ func WithHTTPRequestTimeout(timeout time.Duration) ClientOption { // WithRealtimeHost is used for setting RealtimeHost using [ably.ClientOption]. // RealtimeHost enables a non-default Ably host to be specified for realtime connections. // For development environments only. The default value is realtime.ably.io (RTC1d, TO3k3). +// +// Deprecated: this option is deprecated and will be removed in a future +// version. func WithRealtimeHost(host string) ClientOption { return func(os *clientOptions) { os.RealtimeHost = host @@ -1331,6 +1396,7 @@ func WithInsecureAllowBasicAuthWithoutTLS() ClientOption { func applyOptionsWithDefaults(opts ...ClientOption) *clientOptions { to := defaultOptions // No need to set hosts by default + to.Endpoint = "" to.RESTHost = "" to.RealtimeHost = "" to.FallbackHosts = nil diff --git a/ably/options_test.go b/ably/options_test.go index cc86e794..50c59fe1 100644 --- a/ably/options_test.go +++ b/ably/options_test.go @@ -13,28 +13,54 @@ import ( "github.com/stretchr/testify/assert" ) -func TestDefaultFallbacks_RSC15h(t *testing.T) { +func TestDefaultFallbacks_REC2c(t *testing.T) { expectedFallBackHosts := []string{ - "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", } hosts := ably.DefaultFallbackHosts() assert.Equal(t, expectedFallBackHosts, hosts) } -func TestEnvFallbackHosts_RSC15i(t *testing.T) { - expectedFallBackHosts := []string{ - "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", - } - hosts := ably.GetEnvFallbackHosts("sandbox") - assert.Equal(t, expectedFallBackHosts, hosts) +func TestEndpointFallbacks_REC2c(t *testing.T) { + t.Run("standard endpoint", func(t *testing.T) { + expectedFallBackHosts := []string{ + "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", + } + hosts := ably.GetEndpointFallbackHosts("acme") + assert.Equal(t, expectedFallBackHosts, hosts) + }) + + t.Run("sandbox endpoint", func(t *testing.T) { + expectedFallBackHosts := []string{ + "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", + } + hosts := ably.GetEndpointFallbackHosts("sandbox") + assert.Equal(t, expectedFallBackHosts, hosts) + }) + + t.Run("nonprod endpoint", func(t *testing.T) { + expectedFallBackHosts := []string{ + "acme.a.fallback.ably-realtime-nonprod.com", + "acme.b.fallback.ably-realtime-nonprod.com", + "acme.c.fallback.ably-realtime-nonprod.com", + "acme.d.fallback.ably-realtime-nonprod.com", + "acme.e.fallback.ably-realtime-nonprod.com", + } + hosts := ably.GetEndpointFallbackHosts("nonprod:acme") + assert.Equal(t, expectedFallBackHosts, hosts) + }) } func TestInternetConnectionCheck_RTN17c(t *testing.T) { @@ -42,23 +68,10 @@ func TestInternetConnectionCheck_RTN17c(t *testing.T) { assert.True(t, clientOptions.HasActiveInternetConnection()) } -func TestFallbackHosts_RSC15b(t *testing.T) { - t.Run("RSC15e RSC15g3 with default options", func(t *testing.T) { +func TestHosts_REC1(t *testing.T) { + t.Run("REC1a with default options", func(t *testing.T) { clientOptions := ably.NewClientOptions() - assert.Equal(t, "realtime.ably.io", clientOptions.GetRealtimeHost()) - assert.Equal(t, "rest.ably.io", clientOptions.GetRestHost()) - assert.False(t, clientOptions.NoTLS) - port, isDefaultPort := clientOptions.ActivePort() - assert.Equal(t, 443, port) - assert.True(t, isDefaultPort) - fallbackHosts, _ := clientOptions.GetFallbackHosts() - assert.Equal(t, ably.DefaultFallbackHosts(), fallbackHosts) - }) - - t.Run("RSC15h with production environment", func(t *testing.T) { - clientOptions := ably.NewClientOptions(ably.WithEnvironment("production")) - assert.Equal(t, "realtime.ably.io", clientOptions.GetRealtimeHost()) - assert.Equal(t, "rest.ably.io", clientOptions.GetRestHost()) + assert.Equal(t, "main.realtime.ably.net", clientOptions.GetEndpoint()) assert.False(t, clientOptions.NoTLS) port, isDefaultPort := clientOptions.ActivePort() assert.Equal(t, 443, port) @@ -67,89 +80,163 @@ func TestFallbackHosts_RSC15b(t *testing.T) { assert.Equal(t, ably.DefaultFallbackHosts(), fallbackHosts) }) - t.Run("RSC15g2 RTC1e with custom environment", func(t *testing.T) { - clientOptions := ably.NewClientOptions(ably.WithEnvironment("sandbox")) - assert.Equal(t, "sandbox-realtime.ably.io", clientOptions.GetRealtimeHost()) - assert.Equal(t, "sandbox-rest.ably.io", clientOptions.GetRestHost()) + t.Run("REC1b with endpoint as a custom routing policy name", func(t *testing.T) { + clientOptions := ably.NewClientOptions(ably.WithEndpoint("acme")) + assert.Equal(t, "acme.realtime.ably.net", clientOptions.GetEndpoint()) assert.False(t, clientOptions.NoTLS) port, isDefaultPort := clientOptions.ActivePort() assert.Equal(t, 443, port) assert.True(t, isDefaultPort) fallbackHosts, _ := clientOptions.GetFallbackHosts() - assert.Equal(t, ably.GetEnvFallbackHosts("sandbox"), fallbackHosts) + assert.Equal(t, ably.GetEndpointFallbackHosts("acme"), fallbackHosts) }) - t.Run("RSC15g4 RTC1e with custom environment and fallbackHostUseDefault", func(t *testing.T) { - clientOptions := ably.NewClientOptions(ably.WithEnvironment("sandbox"), ably.WithFallbackHostsUseDefault(true)) - assert.Equal(t, "sandbox-realtime.ably.io", clientOptions.GetRealtimeHost()) - assert.Equal(t, "sandbox-rest.ably.io", clientOptions.GetRestHost()) + t.Run("REC1b3 with endpoint as a nonprod routing policy name", func(t *testing.T) { + clientOptions := ably.NewClientOptions(ably.WithEndpoint("nonprod:acme")) + assert.Equal(t, "acme.realtime.ably-nonprod.net", clientOptions.GetEndpoint()) assert.False(t, clientOptions.NoTLS) port, isDefaultPort := clientOptions.ActivePort() assert.Equal(t, 443, port) assert.True(t, isDefaultPort) fallbackHosts, _ := clientOptions.GetFallbackHosts() - assert.Equal(t, ably.DefaultFallbackHosts(), fallbackHosts) - }) - - t.Run("RSC11b RTN17b RTC1e with custom environment and non default ports", func(t *testing.T) { - clientOptions := ably.NewClientOptions( - ably.WithEnvironment("local"), - ably.WithPort(8080), - ably.WithTLSPort(8081), - ) - assert.Equal(t, "local-realtime.ably.io", clientOptions.GetRealtimeHost()) - assert.Equal(t, "local-rest.ably.io", clientOptions.GetRestHost()) - assert.False(t, clientOptions.NoTLS) - port, isDefaultPort := clientOptions.ActivePort() - assert.Equal(t, 8081, port) - assert.False(t, isDefaultPort) - fallbackHosts, _ := clientOptions.GetFallbackHosts() - assert.Nil(t, fallbackHosts) + assert.Equal(t, ably.GetEndpointFallbackHosts("nonprod:acme"), fallbackHosts) }) - t.Run("RSC11 with custom rest host", func(t *testing.T) { - clientOptions := ably.NewClientOptions(ably.WithRESTHost("test.org")) - assert.Equal(t, "test.org", clientOptions.GetRealtimeHost()) - assert.Equal(t, "test.org", clientOptions.GetRestHost()) + t.Run("REC1b2 with endpoint as a fqdn with no fallbackHosts specified", func(t *testing.T) { + clientOptions := ably.NewClientOptions(ably.WithEndpoint("foo.example.com")) + assert.Equal(t, "foo.example.com", clientOptions.GetEndpoint()) assert.False(t, clientOptions.NoTLS) port, isDefaultPort := clientOptions.ActivePort() assert.Equal(t, 443, port) assert.True(t, isDefaultPort) - fallbackHosts, _ := clientOptions.GetFallbackHosts() + fallbackHosts, err := clientOptions.GetFallbackHosts() + assert.NoError(t, err) assert.Nil(t, fallbackHosts) }) - t.Run("RSC11 with custom rest host and realtime host", func(t *testing.T) { - clientOptions := ably.NewClientOptions(ably.WithRealtimeHost("ws.test.org"), ably.WithRESTHost("test.org")) - assert.Equal(t, "ws.test.org", clientOptions.GetRealtimeHost()) - assert.Equal(t, "test.org", clientOptions.GetRestHost()) + t.Run("REC1b2 REC2a2 with endpoint as a fqdn with fallbackHosts specified", func(t *testing.T) { + clientOptions := ably.NewClientOptions(ably.WithEndpoint("foo.example.com"), ably.WithFallbackHosts([]string{"fallback.foo.example.com"})) + assert.Equal(t, "foo.example.com", clientOptions.GetEndpoint()) assert.False(t, clientOptions.NoTLS) port, isDefaultPort := clientOptions.ActivePort() assert.Equal(t, 443, port) assert.True(t, isDefaultPort) - fallbackHosts, _ := clientOptions.GetFallbackHosts() - assert.Nil(t, fallbackHosts) + fallbackHosts, err := clientOptions.GetFallbackHosts() + assert.NoError(t, err) + assert.Equal(t, []string{"fallback.foo.example.com"}, fallbackHosts) }) - t.Run("RSC15b with custom rest host and realtime host and fallbackHostsUseDefault", func(t *testing.T) { - clientOptions := ably.NewClientOptions( - ably.WithRealtimeHost("ws.test.org"), - ably.WithRESTHost("test.org"), - ably.WithFallbackHostsUseDefault(true)) - assert.Equal(t, "ws.test.org", clientOptions.GetRealtimeHost()) - assert.Equal(t, "test.org", clientOptions.GetRestHost()) - assert.False(t, clientOptions.NoTLS) - port, isDefaultPort := clientOptions.ActivePort() - assert.Equal(t, 443, port) - assert.True(t, isDefaultPort) - fallbackHosts, _ := clientOptions.GetFallbackHosts() - assert.Equal(t, ably.DefaultFallbackHosts(), fallbackHosts) + t.Run("legacy support", func(t *testing.T) { + t.Run("REC1c with production environment", func(t *testing.T) { + clientOptions := ably.NewClientOptions(ably.WithEnvironment("production")) + assert.Equal(t, "main.realtime.ably.net", clientOptions.GetEndpoint()) + assert.False(t, clientOptions.NoTLS) + port, isDefaultPort := clientOptions.ActivePort() + assert.Equal(t, 443, port) + assert.True(t, isDefaultPort) + fallbackHosts, _ := clientOptions.GetFallbackHosts() + assert.Equal(t, ably.DefaultFallbackHosts(), fallbackHosts) + }) + + t.Run("REC1c with sandbox environment", func(t *testing.T) { + clientOptions := ably.NewClientOptions(ably.WithEnvironment("sandbox")) + assert.Equal(t, "sandbox.realtime.ably-nonprod.net", clientOptions.GetEndpoint()) + assert.False(t, clientOptions.NoTLS) + port, isDefaultPort := clientOptions.ActivePort() + assert.Equal(t, 443, port) + assert.True(t, isDefaultPort) + fallbackHosts, _ := clientOptions.GetFallbackHosts() + assert.Equal(t, ably.GetEndpointFallbackHosts("sandbox"), fallbackHosts) + }) + + t.Run("REC1c with custom environment", func(t *testing.T) { + clientOptions := ably.NewClientOptions(ably.WithEnvironment("acme")) + assert.Equal(t, "acme.realtime.ably.net", clientOptions.GetEndpoint()) + assert.False(t, clientOptions.NoTLS) + port, isDefaultPort := clientOptions.ActivePort() + assert.Equal(t, 443, port) + assert.True(t, isDefaultPort) + fallbackHosts, _ := clientOptions.GetFallbackHosts() + assert.Equal(t, ably.GetEndpointFallbackHosts("acme"), fallbackHosts) + }) + + t.Run("REC1c REC2a1 with custom environment and fallbackHostUseDefault", func(t *testing.T) { + clientOptions := ably.NewClientOptions(ably.WithEnvironment("acme"), ably.WithFallbackHostsUseDefault(true)) + assert.Equal(t, "acme.realtime.ably.net", clientOptions.GetEndpoint()) + assert.False(t, clientOptions.NoTLS) + port, isDefaultPort := clientOptions.ActivePort() + assert.Equal(t, 443, port) + assert.True(t, isDefaultPort) + fallbackHosts, _ := clientOptions.GetFallbackHosts() + assert.Equal(t, ably.DefaultFallbackHosts(), fallbackHosts) + }) + + t.Run("REC1c with legacy custom environment and non default ports", func(t *testing.T) { + clientOptions := ably.NewClientOptions( + ably.WithEnvironment("local"), + ably.WithPort(8080), + ably.WithTLSPort(8081), + ) + assert.Equal(t, "local.realtime.ably.net", clientOptions.GetEndpoint()) + assert.False(t, clientOptions.NoTLS) + port, isDefaultPort := clientOptions.ActivePort() + assert.Equal(t, 8081, port) + assert.False(t, isDefaultPort) + fallbackHosts, _ := clientOptions.GetFallbackHosts() + assert.Equal(t, ably.GetEndpointFallbackHosts("local"), fallbackHosts) + }) + + t.Run("REC1d1 with custom restHost", func(t *testing.T) { + clientOptions := ably.NewClientOptions(ably.WithRESTHost("test.org")) + assert.Equal(t, "test.org", clientOptions.GetEndpoint()) + assert.False(t, clientOptions.NoTLS) + port, isDefaultPort := clientOptions.ActivePort() + assert.Equal(t, 443, port) + assert.True(t, isDefaultPort) + fallbackHosts, _ := clientOptions.GetFallbackHosts() + assert.Nil(t, fallbackHosts) + }) + + t.Run("REC1d2 with custom realtimeHost", func(t *testing.T) { + clientOptions := ably.NewClientOptions(ably.WithRealtimeHost("ws.test.org")) + assert.Equal(t, "ws.test.org", clientOptions.GetEndpoint()) + assert.False(t, clientOptions.NoTLS) + port, isDefaultPort := clientOptions.ActivePort() + assert.Equal(t, 443, port) + assert.True(t, isDefaultPort) + fallbackHosts, _ := clientOptions.GetFallbackHosts() + assert.Nil(t, fallbackHosts) + }) + + t.Run("REC1d with custom restHost and realtimeHost", func(t *testing.T) { + clientOptions := ably.NewClientOptions(ably.WithRealtimeHost("ws.test.org"), ably.WithRESTHost("test.org")) + assert.Equal(t, "test.org", clientOptions.GetEndpoint()) + assert.False(t, clientOptions.NoTLS) + port, isDefaultPort := clientOptions.ActivePort() + assert.Equal(t, 443, port) + assert.True(t, isDefaultPort) + fallbackHosts, _ := clientOptions.GetFallbackHosts() + assert.Nil(t, fallbackHosts) + }) + + t.Run("REC1d REC2b with custom restHost and realtimeHost and fallbackHostsUseDefault", func(t *testing.T) { + clientOptions := ably.NewClientOptions( + ably.WithRealtimeHost("ws.test.org"), + ably.WithRESTHost("test.org"), + ably.WithFallbackHostsUseDefault(true)) + assert.Equal(t, "test.org", clientOptions.GetEndpoint()) + assert.False(t, clientOptions.NoTLS) + port, isDefaultPort := clientOptions.ActivePort() + assert.Equal(t, 443, port) + assert.True(t, isDefaultPort) + fallbackHosts, _ := clientOptions.GetFallbackHosts() + assert.Equal(t, ably.DefaultFallbackHosts(), fallbackHosts) + }) }) - t.Run("RSC15g1 with fallbackHosts", func(t *testing.T) { + t.Run("REC2a with fallbackHosts", func(t *testing.T) { clientOptions := ably.NewClientOptions(ably.WithFallbackHosts([]string{"a.example.com", "b.example.com"})) - assert.Equal(t, "realtime.ably.io", clientOptions.GetRealtimeHost()) - assert.Equal(t, "rest.ably.io", clientOptions.GetRestHost()) + assert.Equal(t, "main.realtime.ably.net", clientOptions.GetEndpoint()) assert.False(t, clientOptions.NoTLS) port, isDefaultPort := clientOptions.ActivePort() assert.Equal(t, 443, port) @@ -158,7 +245,7 @@ func TestFallbackHosts_RSC15b(t *testing.T) { assert.Equal(t, []string{"a.example.com", "b.example.com"}, fallbackHosts) }) - t.Run("RSC15b with fallbackHosts and fallbackHostsUseDefault", func(t *testing.T) { + t.Run("REC2a1 with fallbackHosts and fallbackHostsUseDefault", func(t *testing.T) { clientOptions := ably.NewClientOptions( ably.WithFallbackHosts([]string{"a.example.com", "b.example.com"}), ably.WithFallbackHostsUseDefault(true)) @@ -167,7 +254,7 @@ func TestFallbackHosts_RSC15b(t *testing.T) { "fallbackHosts and fallbackHostsUseDefault cannot both be set") }) - t.Run("RSC15b with fallbackHostsUseDefault And custom port", func(t *testing.T) { + t.Run("REC2a1 with fallbackHostsUseDefault And custom port", func(t *testing.T) { clientOptions := ably.NewClientOptions(ably.WithTLSPort(8081), ably.WithFallbackHostsUseDefault(true)) _, isDefaultPort := clientOptions.ActivePort() assert.False(t, isDefaultPort) @@ -204,6 +291,31 @@ func TestClientOptions(t *testing.T) { assert.Error(t, err, "expected an error") }) + t.Run("must return error on invalid combinations", func(t *testing.T) { + _, err := ably.NewREST([]ably.ClientOption{ably.WithEndpoint("acme"), ably.WithEnvironment("acme"), ably.WithRealtimeHost("foo.example.com"), ably.WithRESTHost("foo.example.com")}...) + assert.Error(t, err, + "expected an error") + + _, err = ably.NewREST([]ably.ClientOption{ably.WithEndpoint("acme"), ably.WithEnvironment("acme")}...) + assert.Error(t, err, + "expected an error") + + _, err = ably.NewREST([]ably.ClientOption{ably.WithEnvironment("acme"), ably.WithRealtimeHost("foo.example.com")}...) + assert.Error(t, err, + "expected an error") + + _, err = ably.NewREST([]ably.ClientOption{ably.WithEnvironment("acme"), ably.WithRESTHost("foo.example.com")}...) + assert.Error(t, err, + "expected an error") + + _, err = ably.NewREST([]ably.ClientOption{ably.WithEndpoint("acme"), ably.WithRealtimeHost("foo.example.com")}...) + assert.Error(t, err, + "expected an error") + + _, err = ably.NewREST([]ably.ClientOption{ably.WithEndpoint("acme"), ably.WithRESTHost("foo.example.com")}...) + assert.Error(t, err, + "expected an error") + }) } func TestScopeParams(t *testing.T) { @@ -328,3 +440,10 @@ func TestPaginateParams(t *testing.T) { "expected 100 got %s", values.Get("limit")) }) } + +func TestIsEndpointFQDN(t *testing.T) { + assert.Equal(t, false, ably.IsEndpointFQDN("sandbox")) + assert.Equal(t, true, ably.IsEndpointFQDN("sandbox.example.com")) + assert.Equal(t, true, ably.IsEndpointFQDN("127.0.0.1")) + assert.Equal(t, true, ably.IsEndpointFQDN("localhost")) +} diff --git a/ably/realtime_client_integration_test.go b/ably/realtime_client_integration_test.go index 857a022e..2bb04826 100644 --- a/ably/realtime_client_integration_test.go +++ b/ably/realtime_client_integration_test.go @@ -29,109 +29,215 @@ func TestRealtime_RealtimeHost(t *testing.T) { "localhost", "::1", } - for _, host := range hosts { - dial := make(chan string, 1) - client, err := ably.NewRealtime( - ably.WithKey("xxx:xxx"), - ably.WithRealtimeHost(host), - ably.WithAutoConnect(false), - ably.WithDial(func(protocol string, u *url.URL, timeout time.Duration) (ably.Conn, error) { - dial <- u.Host - return MessagePipe(nil, nil)(protocol, u, timeout) - }), - ) - assert.NoError(t, err) - client.Connect() - var recordedHost string - ablytest.Instantly.Recv(t, &recordedHost, dial, t.Fatalf) - h, _, err := net.SplitHostPort(recordedHost) - assert.NoError(t, err) - assert.Equal(t, host, h, "expected %q got %q", host, h) - } + + t.Run("REC1b with endpoint option", func(t *testing.T) { + for _, host := range hosts { + dial := make(chan string, 1) + client, err := ably.NewRealtime( + ably.WithKey("xxx:xxx"), + ably.WithEndpoint(host), + ably.WithAutoConnect(false), + ably.WithDial(func(protocol string, u *url.URL, timeout time.Duration) (ably.Conn, error) { + dial <- u.Host + return MessagePipe(nil, nil)(protocol, u, timeout) + }), + ) + assert.NoError(t, err) + client.Connect() + var recordedHost string + ablytest.Instantly.Recv(t, &recordedHost, dial, t.Fatalf) + h, _, err := net.SplitHostPort(recordedHost) + assert.NoError(t, err) + assert.Equal(t, host, h, "expected %q got %q", host, h) + } + }) + + t.Run("REC1d2 with legacy realtimeHost option", func(t *testing.T) { + for _, host := range hosts { + dial := make(chan string, 1) + client, err := ably.NewRealtime( + ably.WithKey("xxx:xxx"), + ably.WithRealtimeHost(host), + ably.WithAutoConnect(false), + ably.WithDial(func(protocol string, u *url.URL, timeout time.Duration) (ably.Conn, error) { + dial <- u.Host + return MessagePipe(nil, nil)(protocol, u, timeout) + }), + ) + assert.NoError(t, err) + client.Connect() + var recordedHost string + ablytest.Instantly.Recv(t, &recordedHost, dial, t.Fatalf) + h, _, err := net.SplitHostPort(recordedHost) + assert.NoError(t, err) + assert.Equal(t, host, h, "expected %q got %q", host, h) + } + }) } func TestRealtime_RSC7_AblyAgent(t *testing.T) { - t.Run("RSC7d3 : Should set ablyAgent header with correct identifiers", func(t *testing.T) { - var agentHeaderValue string - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - agentHeaderValue = r.Header.Get(ably.AblyAgentHeader) - w.WriteHeader(http.StatusInternalServerError) - })) - defer server.Close() - serverURL, err := url.Parse(server.URL) - assert.NoError(t, err) - - client, err := ably.NewRealtime( - ably.WithEnvironment(ablytest.Environment), - ably.WithTLS(false), - ably.WithToken("fake:token"), - ably.WithUseTokenAuth(true), - ably.WithRealtimeHost(serverURL.Host)) - assert.NoError(t, err) - defer client.Close() + t.Run("using endpoint option", func(t *testing.T) { + t.Run("RSC7d3 : Should set ablyAgent header with correct identifiers", func(t *testing.T) { + var agentHeaderValue string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + agentHeaderValue = r.Header.Get(ably.AblyAgentHeader) + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + serverURL, err := url.Parse(server.URL) + assert.NoError(t, err) - expectedAgentHeaderValue := ably.AblySDKIdentifier + " " + ably.GoRuntimeIdentifier + " " + ably.GoOSIdentifier() - ablytest.Wait(ablytest.ConnWaiter(client, nil, ably.ConnectionEventDisconnected), nil) + client, err := ably.NewRealtime( + ably.WithEndpoint(serverURL.Host), + ably.WithTLS(false), + ably.WithToken("fake:token"), + ably.WithUseTokenAuth(true)) + assert.NoError(t, err) + defer client.Close() - assert.Equal(t, expectedAgentHeaderValue, agentHeaderValue) - }) + expectedAgentHeaderValue := ably.AblySDKIdentifier + " " + ably.GoRuntimeIdentifier + " " + ably.GoOSIdentifier() + ablytest.Wait(ablytest.ConnWaiter(client, nil, ably.ConnectionEventDisconnected), nil) - t.Run("RSC7d6 : Should set ablyAgent header with custom agents", func(t *testing.T) { - var agentHeaderValue string - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - agentHeaderValue = r.Header.Get(ably.AblyAgentHeader) - w.WriteHeader(http.StatusInternalServerError) - })) - defer server.Close() - serverURL, err := url.Parse(server.URL) - assert.NoError(t, err) - - client, err := ably.NewRealtime( - ably.WithEnvironment(ablytest.Environment), - ably.WithTLS(false), - ably.WithToken("fake:token"), - ably.WithUseTokenAuth(true), - ably.WithRealtimeHost(serverURL.Host), - ably.WithAgents(map[string]string{ - "foo": "1.2.3", - }), - ) - assert.NoError(t, err) - defer client.Close() + assert.Equal(t, expectedAgentHeaderValue, agentHeaderValue) + }) + + t.Run("RSC7d6 : Should set ablyAgent header with custom agents", func(t *testing.T) { + var agentHeaderValue string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + agentHeaderValue = r.Header.Get(ably.AblyAgentHeader) + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + serverURL, err := url.Parse(server.URL) + assert.NoError(t, err) + + client, err := ably.NewRealtime( + ably.WithEndpoint(serverURL.Host), + ably.WithTLS(false), + ably.WithToken("fake:token"), + ably.WithUseTokenAuth(true), + ably.WithAgents(map[string]string{ + "foo": "1.2.3", + }), + ) + assert.NoError(t, err) + defer client.Close() + + expectedAgentHeaderValue := ably.AblySDKIdentifier + " " + ably.GoRuntimeIdentifier + " " + ably.GoOSIdentifier() + " foo/1.2.3" + ablytest.Wait(ablytest.ConnWaiter(client, nil, ably.ConnectionEventDisconnected), nil) + + assert.Equal(t, expectedAgentHeaderValue, agentHeaderValue) + }) - expectedAgentHeaderValue := ably.AblySDKIdentifier + " " + ably.GoRuntimeIdentifier + " " + ably.GoOSIdentifier() + " foo/1.2.3" - ablytest.Wait(ablytest.ConnWaiter(client, nil, ably.ConnectionEventDisconnected), nil) + t.Run("RSC7d6 : Should set ablyAgent header with custom agents missing version", func(t *testing.T) { + var agentHeaderValue string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + agentHeaderValue = r.Header.Get(ably.AblyAgentHeader) + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + serverURL, err := url.Parse(server.URL) + assert.NoError(t, err) + + client, err := ably.NewRealtime( + ably.WithEndpoint(serverURL.Host), + ably.WithTLS(false), + ably.WithToken("fake:token"), + ably.WithUseTokenAuth(true), + ably.WithAgents(map[string]string{ + "bar": "", + }), + ) + assert.NoError(t, err) + defer client.Close() + + expectedAgentHeaderValue := ably.AblySDKIdentifier + " " + ably.GoRuntimeIdentifier + " " + ably.GoOSIdentifier() + " bar" + ablytest.Wait(ablytest.ConnWaiter(client, nil, ably.ConnectionEventDisconnected), nil) - assert.Equal(t, expectedAgentHeaderValue, agentHeaderValue) + assert.Equal(t, expectedAgentHeaderValue, agentHeaderValue) + }) }) - t.Run("RSC7d6 : Should set ablyAgent header with custom agents missing version", func(t *testing.T) { - var agentHeaderValue string - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - agentHeaderValue = r.Header.Get(ably.AblyAgentHeader) - w.WriteHeader(http.StatusInternalServerError) - })) - defer server.Close() - serverURL, err := url.Parse(server.URL) - assert.NoError(t, err) - - client, err := ably.NewRealtime( - ably.WithEnvironment(ablytest.Environment), - ably.WithTLS(false), - ably.WithToken("fake:token"), - ably.WithUseTokenAuth(true), - ably.WithRealtimeHost(serverURL.Host), - ably.WithAgents(map[string]string{ - "bar": "", - }), - ) - assert.NoError(t, err) - defer client.Close() + t.Run("using legacy options", func(t *testing.T) { + t.Run("RSC7d3 : Should set ablyAgent header with correct identifiers", func(t *testing.T) { + var agentHeaderValue string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + agentHeaderValue = r.Header.Get(ably.AblyAgentHeader) + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + serverURL, err := url.Parse(server.URL) + assert.NoError(t, err) + + client, err := ably.NewRealtime( + ably.WithTLS(false), + ably.WithToken("fake:token"), + ably.WithUseTokenAuth(true), + ably.WithRealtimeHost(serverURL.Host)) + assert.NoError(t, err) + defer client.Close() + + expectedAgentHeaderValue := ably.AblySDKIdentifier + " " + ably.GoRuntimeIdentifier + " " + ably.GoOSIdentifier() + ablytest.Wait(ablytest.ConnWaiter(client, nil, ably.ConnectionEventDisconnected), nil) - expectedAgentHeaderValue := ably.AblySDKIdentifier + " " + ably.GoRuntimeIdentifier + " " + ably.GoOSIdentifier() + " bar" - ablytest.Wait(ablytest.ConnWaiter(client, nil, ably.ConnectionEventDisconnected), nil) + assert.Equal(t, expectedAgentHeaderValue, agentHeaderValue) + }) + + t.Run("RSC7d6 : Should set ablyAgent header with custom agents", func(t *testing.T) { + var agentHeaderValue string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + agentHeaderValue = r.Header.Get(ably.AblyAgentHeader) + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + serverURL, err := url.Parse(server.URL) + assert.NoError(t, err) - assert.Equal(t, expectedAgentHeaderValue, agentHeaderValue) + client, err := ably.NewRealtime( + ably.WithTLS(false), + ably.WithToken("fake:token"), + ably.WithUseTokenAuth(true), + ably.WithRealtimeHost(serverURL.Host), + ably.WithAgents(map[string]string{ + "foo": "1.2.3", + }), + ) + assert.NoError(t, err) + defer client.Close() + + expectedAgentHeaderValue := ably.AblySDKIdentifier + " " + ably.GoRuntimeIdentifier + " " + ably.GoOSIdentifier() + " foo/1.2.3" + ablytest.Wait(ablytest.ConnWaiter(client, nil, ably.ConnectionEventDisconnected), nil) + + assert.Equal(t, expectedAgentHeaderValue, agentHeaderValue) + }) + + t.Run("RSC7d6 : Should set ablyAgent header with custom agents missing version", func(t *testing.T) { + var agentHeaderValue string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + agentHeaderValue = r.Header.Get(ably.AblyAgentHeader) + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + serverURL, err := url.Parse(server.URL) + assert.NoError(t, err) + + client, err := ably.NewRealtime( + ably.WithTLS(false), + ably.WithToken("fake:token"), + ably.WithUseTokenAuth(true), + ably.WithRealtimeHost(serverURL.Host), + ably.WithAgents(map[string]string{ + "bar": "", + }), + ) + assert.NoError(t, err) + defer client.Close() + + expectedAgentHeaderValue := ably.AblySDKIdentifier + " " + ably.GoRuntimeIdentifier + " " + ably.GoOSIdentifier() + " bar" + ablytest.Wait(ablytest.ConnWaiter(client, nil, ably.ConnectionEventDisconnected), nil) + + assert.Equal(t, expectedAgentHeaderValue, agentHeaderValue) + }) }) } @@ -161,7 +267,7 @@ func TestRealtime_RTN17_HostFallback(t *testing.T) { t.Run("RTN17a: First attempt should be made on default primary host", func(t *testing.T) { visitedHosts := initClientWithConnError(errors.New("host url is wrong")) - assert.Equal(t, "realtime.ably.io", visitedHosts[0]) + assert.Equal(t, "main.realtime.ably.net", visitedHosts[0]) }) t.Run("RTN17b: Fallback behaviour", func(t *testing.T) { @@ -169,7 +275,7 @@ func TestRealtime_RTN17_HostFallback(t *testing.T) { t.Run("apply when default realtime endpoint is not overridden, port/tlsport not set", func(t *testing.T) { visitedHosts := initClientWithConnError(getTimeoutErr()) - expectedPrimaryHost := "realtime.ably.io" + expectedPrimaryHost := "main.realtime.ably.net" expectedFallbackHosts := ably.DefaultFallbackHosts() assert.Equal(t, 6, len(visitedHosts)) @@ -177,7 +283,15 @@ func TestRealtime_RTN17_HostFallback(t *testing.T) { assert.ElementsMatch(t, expectedFallbackHosts, visitedHosts[1:]) }) - t.Run("does not apply when the custom realtime endpoint is used", func(t *testing.T) { + t.Run("does not apply when endpoint with fqdn is used", func(t *testing.T) { + visitedHosts := initClientWithConnError(getTimeoutErr(), ably.WithEndpoint("custom-realtime.ably.io")) + expectedHost := "custom-realtime.ably.io" + + require.Equal(t, 1, len(visitedHosts)) + assert.Equal(t, expectedHost, visitedHosts[0]) + }) + + t.Run("does not apply when legacy custom realtimeHost is used", func(t *testing.T) { visitedHosts := initClientWithConnError(getTimeoutErr(), ably.WithRealtimeHost("custom-realtime.ably.io")) expectedHost := "custom-realtime.ably.io" @@ -188,18 +302,31 @@ func TestRealtime_RTN17_HostFallback(t *testing.T) { t.Run("apply when fallbacks are provided", func(t *testing.T) { fallbacks := []string{"fallback0", "fallback1", "fallback2"} visitedHosts := initClientWithConnError(getTimeoutErr(), ably.WithFallbackHosts(fallbacks)) - expectedPrimaryHost := "realtime.ably.io" + expectedPrimaryHost := "main.realtime.ably.net" assert.Equal(t, 4, len(visitedHosts)) assert.Equal(t, expectedPrimaryHost, visitedHosts[0]) assert.ElementsMatch(t, fallbacks, visitedHosts[1:]) }) - t.Run("apply when fallbackHostUseDefault is true, even if env. or host is set", func(t *testing.T) { + t.Run("apply when fallbackHostUseDefault is true, even if endpoint option is used", func(t *testing.T) { + visitedHosts := initClientWithConnError( + getTimeoutErr(), + ably.WithFallbackHostsUseDefault(true), + ably.WithEndpoint("custom")) + + expectedPrimaryHost := "custom.realtime.ably.net" + expectedFallbackHosts := ably.DefaultFallbackHosts() + + assert.Equal(t, 6, len(visitedHosts)) + assert.Equal(t, expectedPrimaryHost, visitedHosts[0]) + assert.ElementsMatch(t, expectedFallbackHosts, visitedHosts[1:]) + }) + + t.Run("apply when fallbackHostUseDefault is true, even if legacy realtimeHost is set", func(t *testing.T) { visitedHosts := initClientWithConnError( getTimeoutErr(), ably.WithFallbackHostsUseDefault(true), - ably.WithEnvironment("custom"), ably.WithRealtimeHost("custom-ably.realtime.com")) expectedPrimaryHost := "custom-ably.realtime.com" @@ -253,7 +380,7 @@ func TestRealtime_RTN17_HostFallback(t *testing.T) { t.Fatalf("Error connecting host with error %v", err) } realtimeSuccessHost := realtimeMsgRecorder.URLs()[0].Hostname() - fallbackHosts := ably.GetEnvFallbackHosts("sandbox") + fallbackHosts := ably.GetEndpointFallbackHosts("sandbox") if !ablyutil.SliceContains(fallbackHosts, realtimeSuccessHost) { t.Fatalf("realtime host must be one of fallback hosts, received %v", realtimeSuccessHost) } @@ -294,7 +421,7 @@ func TestRealtime_RTN17_Integration_HostFallback_Internal_Server_Error(t *testin } return conn, err }), - ably.WithRealtimeHost(serverURL.Host)) + ably.WithEndpoint(serverURL.Host)) defer safeclose(t, ablytest.FullRealtimeCloser(realtime), app) @@ -341,7 +468,7 @@ func TestRealtime_RTN17_Integration_HostFallback_Timeout(t *testing.T) { } return conn, err }), - ably.WithRealtimeHost(serverURL.Host)) + ably.WithEndpoint(serverURL.Host)) defer safeclose(t, ablytest.FullRealtimeCloser(realtime), app) diff --git a/ably/realtime_conn.go b/ably/realtime_conn.go index 284ee0bf..f1fc2e56 100644 --- a/ably/realtime_conn.go +++ b/ably/realtime_conn.go @@ -387,7 +387,8 @@ func (c *Connection) connectWith(arg connArgs) (result, error) { } var conn conn - primaryHost := c.opts.getRealtimeHost() + primaryHost := c.opts.getHostname() + hosts := []string{primaryHost} fallbackHosts, err := c.opts.getFallbackHosts() if err != nil { diff --git a/ably/realtime_conn_spec_integration_test.go b/ably/realtime_conn_spec_integration_test.go index 8af4b567..143c7344 100644 --- a/ably/realtime_conn_spec_integration_test.go +++ b/ably/realtime_conn_spec_integration_test.go @@ -222,7 +222,7 @@ func Test_RTN4a_ConnectionEventForStateChange(t *testing.T) { t.Run(fmt.Sprintf("on %s", ably.ConnectionStateFailed), func(t *testing.T) { options := []ably.ClientOption{ - ably.WithEnvironment("sandbox"), + ably.WithEndpoint(ablytest.Endpoint), ably.WithAutoConnect(false), ably.WithKey("made:up"), } @@ -1735,7 +1735,7 @@ func TestRealtimeConn_RTN22a_RTN15h2_Integration_ServerInitiatedAuth(t *testing. realtime, err := ably.NewRealtime( ably.WithAutoConnect(false), ably.WithDial(recorder.Dial), - ably.WithEnvironment(ablytest.Environment), + ably.WithEndpoint(ablytest.Endpoint), ably.WithAuthCallback(authCallback)) assert.NoError(t, err) @@ -1798,7 +1798,7 @@ func TestRealtimeConn_RTN22_RTC8_Integration_ServerInitiatedAuth(t *testing.T) { ably.WithAutoConnect(false), ably.WithDial(recorder.Dial), ably.WithUseBinaryProtocol(false), - ably.WithEnvironment(ablytest.Environment), + ably.WithEndpoint(ablytest.Endpoint), ably.WithAuthCallback(authCallback)) assert.NoError(t, err) @@ -3037,7 +3037,7 @@ func TestRealtimeConn_RTC8a_ExplicitAuthorizeWhileConnected(t *testing.T) { realtimeMsgRecorder := NewMessageRecorder() realtime, err := ably.NewRealtime( ably.WithAutoConnect(false), - ably.WithEnvironment(ablytest.Environment), + ably.WithEndpoint(ablytest.Endpoint), ably.WithDial(realtimeMsgRecorder.Dial), ably.WithAuthCallback(authCallback)) diff --git a/ably/rest_channel_integration_test.go b/ably/rest_channel_integration_test.go index 925f18c4..f28e351b 100644 --- a/ably/rest_channel_integration_test.go +++ b/ably/rest_channel_integration_test.go @@ -142,7 +142,7 @@ func TestRESTChannel(t *testing.T) { } func TestIdempotentPublishing(t *testing.T) { - app, err := ablytest.NewSandboxWithEnv(nil, ablytest.Environment) + app, err := ablytest.NewSandboxWithEndpoint(nil, ablytest.Endpoint, ablytest.Environment) assert.NoError(t, err) defer app.Close() options := app.Options(ably.WithIdempotentRESTPublishing(true)) @@ -295,7 +295,7 @@ func TestIdempotentPublishing(t *testing.T) { } func TestIdempotent_retry(t *testing.T) { - app, err := ablytest.NewSandboxWithEnv(nil, ablytest.Environment) + app, err := ablytest.NewSandboxWithEndpoint(nil, ablytest.Endpoint, ablytest.Environment) assert.NoError(t, err) defer app.Close() randomStr, err := ablyutil.BaseID() @@ -312,7 +312,7 @@ func TestIdempotent_retry(t *testing.T) { // failing all others via the test server fallbackHosts := []string{"fallback0", "fallback1", "fallback2"} nopts := []ably.ClientOption{ - ably.WithEnvironment(ablytest.Environment), + ably.WithEndpoint(ablytest.Endpoint), ably.WithTLS(false), ably.WithFallbackHosts(fallbackHosts), ably.WithIdempotentRESTPublishing(true), diff --git a/ably/rest_client_integration_test.go b/ably/rest_client_integration_test.go index 7b4f9eb5..823d0824 100644 --- a/ably/rest_client_integration_test.go +++ b/ably/rest_client_integration_test.go @@ -250,10 +250,9 @@ func TestRest_RSC7_AblyAgent(t *testing.T) { assert.NoError(t, err) opts := []ably.ClientOption{ - ably.WithEnvironment(ablytest.Environment), + ably.WithEndpoint(serverURL.Host), ably.WithTLS(false), ably.WithUseTokenAuth(true), - ably.WithRESTHost(serverURL.Host), } client, err := ably.NewREST(opts...) @@ -275,10 +274,9 @@ func TestRest_RSC7_AblyAgent(t *testing.T) { assert.NoError(t, err) opts := []ably.ClientOption{ - ably.WithEnvironment(ablytest.Environment), + ably.WithEndpoint(serverURL.Host), ably.WithTLS(false), ably.WithUseTokenAuth(true), - ably.WithRESTHost(serverURL.Host), ably.WithAgents(map[string]string{ "foo": "1.2.3", }), @@ -303,10 +301,9 @@ func TestRest_RSC7_AblyAgent(t *testing.T) { assert.NoError(t, err) opts := []ably.ClientOption{ - ably.WithEnvironment(ablytest.Environment), + ably.WithEndpoint(serverURL.Host), ably.WithTLS(false), ably.WithUseTokenAuth(true), - ably.WithRESTHost(serverURL.Host), ably.WithAgents(map[string]string{ "bar": "", }), @@ -346,13 +343,12 @@ func TestRest_RSC15_HostFallback(t *testing.T) { options := []ably.ClientOption{ ably.WithFallbackHosts(ably.DefaultFallbackHosts()), ably.WithTLS(false), - ably.WithEnvironment(""), // remove default sandbox env ably.WithHTTPMaxRetryCount(10), ably.WithUseTokenAuth(true), } retryCount, hosts := runTestServer(t, options) assert.Equal(t, 6, retryCount) // 1 primary and 5 default fallback hosts - assert.Equal(t, "rest.ably.io", hosts[0]) // primary host + assert.Equal(t, "main.realtime.ably.net", hosts[0]) // primary host assertSubset(t, ably.DefaultFallbackHosts(), hosts[1:]) // remaining fallback hosts assertUnique(t, hosts) // ensure all picked fallbacks are unique }) @@ -393,21 +389,20 @@ func TestRest_RSC15_HostFallback(t *testing.T) { options := []ably.ClientOption{ ably.WithFallbackHosts(ably.DefaultFallbackHosts()), ably.WithTLS(false), - ably.WithEnvironment(""), // remove default sandbox env ably.WithHTTPMaxRetryCount(10), ably.WithUseTokenAuth(true), } retryCount, hosts := runTestServerWithRequestTimeout(t, options) assert.Equal(t, 6, retryCount) // 1 primary and 5 default fallback hosts - assert.Equal(t, "rest.ably.io", hosts[0]) // primary host + assert.Equal(t, "main.realtime.ably.net", hosts[0]) // primary host assertSubset(t, ably.DefaultFallbackHosts(), hosts[1:]) // remaining fallback hosts assertUnique(t, hosts) // ensure all picked fallbacks are unique }) t.Run("RSC15l1 must use alternative host on host unresolvable or unreachable", func(t *testing.T) { options := []ably.ClientOption{ + ably.WithEndpoint("foobar.ably.com"), ably.WithFallbackHosts(ably.DefaultFallbackHosts()), - ably.WithRESTHost("foobar.ably.com"), ably.WithFallbackHosts([]string{ "spam.ably.com", "tatto.ably.com", @@ -430,7 +425,7 @@ func TestRest_RSC15_HostFallback(t *testing.T) { options := []ably.ClientOption{ ably.WithTLS(false), - ably.WithRESTHost("example.com"), + ably.WithEndpoint("example.com"), ably.WithUseTokenAuth(true), } retryCount, hosts := runTestServer(t, options) @@ -444,7 +439,7 @@ func TestRest_RSC15_HostFallback(t *testing.T) { options := []ably.ClientOption{ ably.WithTLS(false), - ably.WithRESTHost("example.com"), + ably.WithEndpoint("example.com"), ably.WithFallbackHosts(ably.DefaultFallbackHosts()), ably.WithUseTokenAuth(true), } @@ -460,7 +455,7 @@ func TestRest_RSC15_HostFallback(t *testing.T) { t.Run("must occur when fallbackHosts is set", func(t *testing.T) { options := []ably.ClientOption{ ably.WithTLS(false), - ably.WithRESTHost("example.com"), + ably.WithEndpoint("example.com"), ably.WithFallbackHosts([]string{"a.example.com"}), ably.WithUseTokenAuth(true), } @@ -476,7 +471,7 @@ func TestRest_RSC15_HostFallback(t *testing.T) { t.Run("RSC15e must start with default host", func(t *testing.T) { options := []ably.ClientOption{ - ably.WithEnvironment("production"), + ably.WithEndpoint("main"), ably.WithTLS(false), ably.WithUseTokenAuth(true), } @@ -492,7 +487,7 @@ func TestRest_RSC15_HostFallback(t *testing.T) { options := []ably.ClientOption{ ably.WithTLS(false), - ably.WithRESTHost("example.com"), + ably.WithEndpoint("example.com"), ably.WithFallbackHosts([]string{}), ably.WithUseTokenAuth(true), } @@ -519,7 +514,7 @@ func TestRest_rememberHostFallback(t *testing.T) { defer server.Close() nopts = []ably.ClientOption{ - ably.WithEnvironment(ablytest.Environment), + ably.WithEndpoint(ablytest.Endpoint), ably.WithTLS(false), ably.WithFallbackHosts([]string{"fallback0", "fallback1", "fallback2"}), ably.WithUseTokenAuth(true), diff --git a/ablytest/ablytest.go b/ablytest/ablytest.go index fc7b8363..7a69f844 100644 --- a/ablytest/ablytest.go +++ b/ablytest/ablytest.go @@ -18,6 +18,7 @@ var Timeout = 30 * time.Second var NoBinaryProtocol bool var DefaultLogLevel = ably.LogNone var Environment = "sandbox" +var Endpoint = "nonprod:sandbox" func nonil(err ...error) error { for _, err := range err { diff --git a/ablytest/sandbox.go b/ablytest/sandbox.go index 11220d03..04613573 100644 --- a/ablytest/sandbox.go +++ b/ablytest/sandbox.go @@ -13,6 +13,7 @@ import ( "net/url" "os" "path" + "regexp" "syscall" "time" @@ -99,7 +100,12 @@ var PresenceFixtures = func() []Presence { } type Sandbox struct { - Config *Config + Config *Config + + // Endpoint is the hostname to connect to + Endpoint string + + // Environment is used in auth parameters Environment string client *http.Client @@ -132,13 +138,14 @@ func MustSandbox(config *Config) *Sandbox { } func NewSandbox(config *Config) (*Sandbox, error) { - return NewSandboxWithEnv(config, Environment) + return NewSandboxWithEndpoint(config, Endpoint, Environment) } -func NewSandboxWithEnv(config *Config, env string) (*Sandbox, error) { +func NewSandboxWithEndpoint(config *Config, endpoint, environment string) (*Sandbox, error) { app := &Sandbox{ Config: config, - Environment: env, + Endpoint: endpoint, + Environment: environment, client: NewHTTPClient(), } if app.Config == nil { @@ -233,7 +240,7 @@ func (app *Sandbox) Options(opts ...ably.ClientOption) []ably.ClientOption { appHTTPClient := NewHTTPClient() appOpts := []ably.ClientOption{ ably.WithKey(app.Key()), - ably.WithEnvironment(app.Environment), + ably.WithEndpoint(app.Endpoint), ably.WithUseBinaryProtocol(!NoBinaryProtocol), ably.WithHTTPClient(appHTTPClient), ably.WithLogLevel(DefaultLogLevel), @@ -252,8 +259,14 @@ func (app *Sandbox) Options(opts ...ably.ClientOption) []ably.ClientOption { return appOpts } +var nonprodRegexp = regexp.MustCompile(`^nonprod:(.*)$`) + func (app *Sandbox) URL(paths ...string) string { - return "https://" + app.Environment + "-rest.ably.io/" + path.Join(paths...) + if match := nonprodRegexp.FindStringSubmatch(app.Endpoint); match != nil { + return fmt.Sprintf("https://%s.realtime.ably-nonprod.net/%s", match[1], path.Join(paths...)) + } + + return fmt.Sprintf("https://%s.realtime.ably.net/%s", app.Endpoint, path.Join(paths...)) } // Source code for the same => https://github.com/ably/echoserver/blob/main/app.js