From 857ae5fb868d3fa4e628800e9a9f1e288e96796b Mon Sep 17 00:00:00 2001 From: Lewis Marshall Date: Sun, 19 Jul 2026 17:28:02 +0100 Subject: [PATCH 1/7] ably: drop redundant WithTLS(true) in TestAuth_IgnoreTimestamp_QueryTime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each case in this test appended WithTLS(true) to the sandbox options. The assertions only concern query-time/timestamp handling in Authorize, so the TLS override is incidental — but because per-test options are merged after the sandbox defaults, it overrode the transport the harness selected. Against a plaintext local server that forced HTTPS on the default TLS port instead of the app's plain port, so the token request stalled and the test timed out. Dropping it lets the test inherit the harness's transport (TLS against the cloud sandbox, as before; plain against a local ably-sandbox), and it passes in both. Co-Authored-By: Claude Opus 4.8 (1M context) --- ably/auth_integration_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ably/auth_integration_test.go b/ably/auth_integration_test.go index 29af315a..c38a9507 100644 --- a/ably/auth_integration_test.go +++ b/ably/auth_integration_test.go @@ -780,7 +780,6 @@ func TestAuth_IgnoreTimestamp_QueryTime(t *testing.T) { }{ "Should not save query time and timestamp when WithQueryTime is false and token params has no timestamp": { opt: []ably.ClientOption{ - ably.WithTLS(true), ably.WithUseTokenAuth(true), ably.WithQueryTime(false), }, @@ -794,7 +793,6 @@ func TestAuth_IgnoreTimestamp_QueryTime(t *testing.T) { }, "Should not save query time and timestamp when WithQueryTime is true and token params has no timestamp": { opt: []ably.ClientOption{ - ably.WithTLS(true), ably.WithUseTokenAuth(true), ably.WithQueryTime(true), }, @@ -808,7 +806,6 @@ func TestAuth_IgnoreTimestamp_QueryTime(t *testing.T) { }, "Should not save query time and timestamp when WithQueryTime is true and token params has a timestamp": { opt: []ably.ClientOption{ - ably.WithTLS(true), ably.WithUseTokenAuth(true), ably.WithQueryTime(true), }, @@ -822,7 +819,6 @@ func TestAuth_IgnoreTimestamp_QueryTime(t *testing.T) { }, "Should not save query time and timestamp when WithQueryTime is false and token params has a timestamp": { opt: []ably.ClientOption{ - ably.WithTLS(true), ably.WithUseTokenAuth(true), ably.WithQueryTime(false), }, From a79b93574ad7de5bb7d0fdc99355eb564148a3c8 Mon Sep 17 00:00:00 2001 From: Lewis Marshall Date: Sun, 19 Jul 2026 17:57:57 +0100 Subject: [PATCH 2/7] ably: stop TestRealtimeConn_ReconnectFromSuspendedState hanging on a wedged dial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test's mock dial blocked on an empty dialErr channel whenever the connection attempted a reconnect the test hadn't pre-fed an outcome for. A dial blocked there wedges the connection in CONNECTING, where the DISCONNECTED->SUSPENDED timer does not run, so the connection never became SUSPENDED and the test hung until the outer timeout — despite exercising a fully mocked transport that never touches the server. Make the mock sticky: reuse the most recently injected dial outcome when the channel is empty instead of blocking, mirroring a real dial (which always returns within its timeout). The connection now retries freely, reaches SUSPENDED on the shortened TTL, and the test passes in ~1s. Co-Authored-By: Claude Opus 4.8 (1M context) --- ably/realtime_conn_integration_test.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/ably/realtime_conn_integration_test.go b/ably/realtime_conn_integration_test.go index 02cc4489..bbbc78b2 100644 --- a/ably/realtime_conn_integration_test.go +++ b/ably/realtime_conn_integration_test.go @@ -7,6 +7,7 @@ import ( "context" "errors" "net/url" + "sync" "testing" "time" @@ -267,8 +268,24 @@ func TestRealtimeConn_ReconnectFromSuspendedState(t *testing.T) { dialErr := make(chan error, 1) msgReceiveErr := make(chan error, 1) + // lastDialErr sticks the most recently injected dial outcome. A dial made + // when the test hasn't queued a new one reuses it instead of blocking on an + // empty channel: a real dial always returns within its timeout, and a dial + // wedged here would strand the connection in CONNECTING, where the + // DISCONNECTED->SUSPENDED timer does not run — so it could never reach + // SUSPENDED and the test would hang on the harness timeout rather than + // exercising suspended-state reconnection. The mutex keeps it race-free even + // if a reconnect attempt overlaps a test-driven Connect. + var dialMu sync.Mutex + var lastDialErr error dial := DialFunc(func(p string, url *url.URL, timeout time.Duration) (ably.Conn, error) { - err := <-dialErr + dialMu.Lock() + select { + case lastDialErr = <-dialErr: + default: + } + err := lastDialErr + dialMu.Unlock() if err != nil { return nil, err } From 975aebe463dc5dcbfd7c9f4792cb6605425fbd65 Mon Sep 17 00:00:00 2001 From: Lewis Marshall Date: Sun, 19 Jul 2026 19:17:27 +0100 Subject: [PATCH 3/7] ably: build server-initiated-auth test clients from app.Options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestRealtimeConn_RTN22a_RTN15h2_/RTN22_RTC8_Integration_ServerInitiatedAuth and the RTC8a4 JWT-reauth subtest each constructed their Realtime client directly with a bare WithEndpoint(ablytest.Endpoint) and no port/TLS override, discarding the Sandbox returned by NewREST. That bypasses the harness's transport, so against a plaintext local server the client dials the default TLS port, never connects, and the test's unbounded Connect wait hangs until the outer timeout — a harness artifact, not a server result (cf. ably-server TASK-78). Build these clients from app.Options(...) like the rest of the suite so they use the provisioned app's endpoint/port/TLS. Against the cloud sandbox this is the same transport as before; against a local ably-sandbox the three tests now connect and exercise the real behaviour — RTN22/RTN22a and RTC8a4 pass, and the one genuine gap they surface (RTC8a's channel detach on capability downgrade) is now a real failure rather than an artefact. Co-Authored-By: Claude Opus 4.8 (1M context) --- ably/realtime_conn_spec_integration_test.go | 34 ++++++++++++++------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/ably/realtime_conn_spec_integration_test.go b/ably/realtime_conn_spec_integration_test.go index 54115eaa..97f34580 100644 --- a/ably/realtime_conn_spec_integration_test.go +++ b/ably/realtime_conn_spec_integration_test.go @@ -1718,7 +1718,7 @@ func TestRealtimeConn_RTN15h3_Success(t *testing.T) { func TestRealtimeConn_RTN22a_RTN15h2_Integration_ServerInitiatedAuth(t *testing.T) { t.Parallel() - _, restClient := ablytest.NewREST() + app, restClient := ablytest.NewREST() recorder := NewMessageRecorder() authCallbackTokens := []string{} @@ -1730,11 +1730,15 @@ func TestRealtimeConn_RTN22a_RTN15h2_Integration_ServerInitiatedAuth(t *testing. return token, err } - realtime, err := ably.NewRealtime( + // Build from app.Options so the client uses the provisioned app's transport + // (endpoint/port/TLS). A bare WithEndpoint dials the default TLS port, which a + // plaintext local server doesn't serve — the connect would then never succeed + // and the test would hang, a harness artifact rather than a server result. + realtime, err := ably.NewRealtime(app.Options( ably.WithAutoConnect(false), ably.WithDial(recorder.Dial), - ably.WithEndpoint(ablytest.Endpoint), - ably.WithAuthCallback(authCallback)) + ably.WithAuthCallback(authCallback), + )...) assert.NoError(t, err) defer realtime.Close() @@ -1777,7 +1781,7 @@ func TestRealtimeConn_RTN22a_RTN15h2_Integration_ServerInitiatedAuth(t *testing. } func TestRealtimeConn_RTN22_RTC8_Integration_ServerInitiatedAuth(t *testing.T) { - _, restClient := ablytest.NewREST() + app, restClient := ablytest.NewREST() recorder := NewMessageRecorder() authCallbackTokens := []string{} @@ -1791,12 +1795,16 @@ func TestRealtimeConn_RTN22_RTC8_Integration_ServerInitiatedAuth(t *testing.T) { return token, err } - realtime, err := ably.NewRealtime( + // Build from app.Options so the client uses the provisioned app's transport + // (endpoint/port/TLS). A bare WithEndpoint dials the default TLS port, which a + // plaintext local server doesn't serve — the connect would then never succeed + // and the test would hang, a harness artifact rather than a server result. + realtime, err := ably.NewRealtime(app.Options( ably.WithAutoConnect(false), ably.WithDial(recorder.Dial), ably.WithUseBinaryProtocol(false), - ably.WithEndpoint(ablytest.Endpoint), - ably.WithAuthCallback(authCallback)) + ably.WithAuthCallback(authCallback), + )...) assert.NoError(t, err) defer realtime.Close() @@ -3031,11 +3039,15 @@ func TestRealtimeConn_RTC8a_ExplicitAuthorizeWhileConnected(t *testing.T) { } realtimeMsgRecorder := NewMessageRecorder() - realtime, err := ably.NewRealtime( + // Build from app.Options so the client uses the provisioned app's + // transport (endpoint/port/TLS) rather than dialing the default TLS port, + // which a plaintext local server doesn't serve — see the note on the + // RTN22 tests above. + realtime, err := ably.NewRealtime(app.Options( ably.WithAutoConnect(false), - ably.WithEndpoint(ablytest.Endpoint), ably.WithDial(realtimeMsgRecorder.Dial), - ably.WithAuthCallback(authCallback)) + ably.WithAuthCallback(authCallback), + )...) assert.NoError(t, err) defer realtime.Close() From 2a6833c199ebc14a58e24679aabe572eb0d65006 Mon Sep 17 00:00:00 2001 From: Lewis Marshall Date: Sun, 19 Jul 2026 22:15:21 +0100 Subject: [PATCH 4/7] ably: thread the provisioned app's transport through JWT and retry tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestAuth_JWT_Token_RSA8c and TestIdempotent_retry built their secondary request paths from a bare WithEndpoint(app.Endpoint)/WithEndpoint(ablytest.Endpoint) with no port/TLS — the JWT-auth REST clients directly, and the idempotent-retry proxy's target URL via ApplyOptionsWithDefaults(nopts...). Since a per-test local app's endpoint/port/TLS live only in app.Options(), those requests went to the endpoint name's default host rather than the provisioned child, which 404s the app id ("No application found with id ..."). With the cloud sandbox the endpoint name resolved to the right host so it happened to work. Build these from app.Options(...) so the JWT clients and the proxy's default URL use the provisioned app's transport. TestIdempotent_retry now passes; the JWT test's four connect paths pass, and its remaining failure is a genuine server divergence (the local server returns 40101 rather than 40144 for an invalid JWT), not a harness artifact. Co-Authored-By: Claude Opus 4.8 (1M context) --- ably/auth_integration_test.go | 20 ++++++++------------ ably/rest_channel_integration_test.go | 8 ++++++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ably/auth_integration_test.go b/ably/auth_integration_test.go index c38a9507..4973c735 100644 --- a/ably/auth_integration_test.go +++ b/ably/auth_integration_test.go @@ -381,11 +381,10 @@ func TestAuth_JWT_Token_RSA8c(t *testing.T) { assert.True(t, strings.HasPrefix(jwt, "ey")) rec, optn := ablytest.NewHttpRecorder() - rest, err := ably.NewREST( + rest, err := ably.NewREST(app.Options( ably.WithToken(jwt), - ably.WithEndpoint(app.Endpoint), optn[0], - ) + )...) assert.NoError(t, err, "rest()=%v", err) _, err = rest.Stats().Pages(context.Background()) @@ -404,12 +403,11 @@ func TestAuth_JWT_Token_RSA8c(t *testing.T) { app := ablytest.MustSandbox() rec, optn := ablytest.NewHttpRecorder() - rest, err := ably.NewREST( + rest, err := ably.NewREST(app.Options( ably.WithAuthURL(ablytest.CREATE_JWT_URL), ably.WithAuthParams(app.GetJwtAuthParams(30*time.Second, false)), - ably.WithEndpoint(app.Endpoint), optn[0], - ) + )...) assert.NoError(t, err, "rest()=%v", err) _, err = rest.Stats().Pages(context.Background()) @@ -448,11 +446,10 @@ func TestAuth_JWT_Token_RSA8c(t *testing.T) { }) rec, optn := ablytest.NewHttpRecorder() - rest, err := ably.NewREST( - ably.WithEndpoint(app.Endpoint), + rest, err := ably.NewREST(app.Options( authCallback, optn[0], - ) + )...) assert.NoError(t, err) _, err = rest.Stats().Pages(context.Background()) @@ -473,12 +470,11 @@ func TestAuth_JWT_Token_RSA8c(t *testing.T) { app := ablytest.MustSandbox() rec, optn := ablytest.NewHttpRecorder() - rest, err := ably.NewREST( + rest, err := ably.NewREST(app.Options( ably.WithAuthURL(ablytest.CREATE_JWT_URL), ably.WithAuthParams(app.GetJwtAuthParams(30*time.Second, true)), - ably.WithEndpoint(app.Endpoint), optn[0], - ) + )...) assert.NoError(t, err, "rest()=%v", err) _, err = rest.Stats().Pages(context.Background()) diff --git a/ably/rest_channel_integration_test.go b/ably/rest_channel_integration_test.go index 5f60680b..c47f12b2 100644 --- a/ably/rest_channel_integration_test.go +++ b/ably/rest_channel_integration_test.go @@ -309,7 +309,6 @@ func TestIdempotent_retry(t *testing.T) { // failing all others via the test server fallbackHosts := []string{"fallback0", "fallback1", "fallback2"} nopts := []ably.ClientOption{ - ably.WithEndpoint(ablytest.Endpoint), ably.WithTLS(false), ably.WithFallbackHosts(fallbackHosts), ably.WithIdempotentRESTPublishing(true), @@ -317,7 +316,12 @@ func TestIdempotent_retry(t *testing.T) { } serverURL, _ := url.Parse(server.URL) - defaultURL, _ := url.Parse(ably.ApplyOptionsWithDefaults(nopts...).RestURL()) + // Resolve the real destination from app.Options, not nopts alone, so the + // proxy forwards token requests and successful retries to the provisioned + // app's endpoint/port. Against a per-test local child that address is not + // derivable from the endpoint name, so a bare nopts URL would send those + // requests to the wrong server (which 404s the app id). + defaultURL, _ := url.Parse(ably.ApplyOptionsWithDefaults(app.Options(nopts...)...).RestURL()) proxy := func(r *http.Request) (*url.URL, error) { if !strings.HasPrefix(r.URL.Path, "/channels/") { // this is to handle token requests From 6092ab4110d540c64a0f27e7d6526186ab912a12 Mon Sep 17 00:00:00 2001 From: Lewis Marshall Date: Sun, 19 Jul 2026 22:18:53 +0100 Subject: [PATCH 5/7] ably: resolve the fallback proxy target from app.Options in rememberHostFallback TestRest_rememberHostFallback (RSC15f) had the same defaultURL-from-nopts bug as TestIdempotent_retry: the client used app.Options() but the proxy's forward target was resolved from nopts alone, so against a per-test local child the "success host" request went to the wrong server and 404'd the app id. Resolve it from app.Options(nopts...) too. The test now passes against the local server. Co-Authored-By: Claude Opus 4.8 (1M context) --- ably/rest_client_integration_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ably/rest_client_integration_test.go b/ably/rest_client_integration_test.go index 7a7acda5..2f0ac0b7 100644 --- a/ably/rest_client_integration_test.go +++ b/ably/rest_client_integration_test.go @@ -511,16 +511,18 @@ func TestRest_rememberHostFallback(t *testing.T) { defer server.Close() nopts = []ably.ClientOption{ - ably.WithEndpoint(ablytest.Endpoint), ably.WithTLS(false), ably.WithFallbackHosts([]string{"fallback0", "fallback1", "fallback2"}), ably.WithUseTokenAuth(true), } // set up the proxy to forward all requests except a specific fallback to the server, - // whilst that fallback goes to the regular endpoint + // whilst that fallback goes to the regular endpoint. Resolve that endpoint + // from app.Options (not nopts alone) so it is the provisioned app's + // host/port — for a per-test local child that isn't derivable from the + // endpoint name, and a bare nopts URL would 404 the app id. serverURL, _ := url.Parse(server.URL) - defaultURL, _ := url.Parse(ably.ApplyOptionsWithDefaults(nopts...).RestURL()) + defaultURL, _ := url.Parse(ably.ApplyOptionsWithDefaults(app.Options(nopts...)...).RestURL()) proxy := func(r *http.Request) (*url.URL, error) { if r.URL.Hostname() == "fallback2" { From c620b681cdb97d5e1c428698a02861bd71f611ec Mon Sep 17 00:00:00 2001 From: Lewis Marshall Date: Mon, 20 Jul 2026 00:22:18 +0100 Subject: [PATCH 6/7] ably: use require.NoError where the result is used right after the check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Across the integration suite, many tests did `x, err := call(); assert.NoError (t, err)` and then immediately dereferenced or indexed x. assert.NoError only records the failure and lets the test continue, so when call() returned (nil, err) — as the client correctly does against an unimplemented or failing server endpoint — execution fell through to a nil dereference or out-of-range index, surfacing as a crash (or a panic-in-teardown hang) rather than a clean FAIL. Convert those sites to require.NoError so the test stops at the failed check. No passing test changes behaviour — require only differs from assert when err is non-nil, where continuing was never valid anyway. Co-Authored-By: Claude Opus 4.8 (1M context) --- ably/auth_integration_test.go | 25 +++++----- ...ttp_paginated_response_integration_test.go | 15 +++--- ably/proto_message_integration_test.go | 9 ++-- ably/realtime_channel_integration_test.go | 5 +- .../realtime_channel_spec_integration_test.go | 47 ++++++++++--------- ably/realtime_client_integration_test.go | 14 +++--- ably/realtime_conn_integration_test.go | 3 +- ably/realtime_conn_spec_integration_test.go | 13 ++--- ably/rest_channel_integration_test.go | 11 +++-- ably/rest_channel_spec_integration_test.go | 11 +++-- ably/rest_client_integration_test.go | 40 ++++++++-------- 11 files changed, 101 insertions(+), 92 deletions(-) diff --git a/ably/auth_integration_test.go b/ably/auth_integration_test.go index 4973c735..7dd4e1c4 100644 --- a/ably/auth_integration_test.go +++ b/ably/auth_integration_test.go @@ -21,6 +21,7 @@ import ( "github.com/ably/ably-go/internal/ablytest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func single() *ably.PaginateParams { @@ -134,7 +135,7 @@ func TestAuth_TokenAuth(t *testing.T) { "want url.Scheme=http; got %s", requestUrl.Scheme) rec.Reset() tok, err := client.Auth.Authorize(context.Background(), nil) - assert.NoError(t, err, + require.NoError(t, err, "Authorize()=%v", err) // Call to Authorize should always refresh the token. assert.Equal(t, 1, rec.Len(), @@ -162,7 +163,7 @@ func TestAuth_TokenAuth_Renew(t *testing.T) { TTL: time.Second.Milliseconds(), } tok, err := client.Auth.Authorize(context.Background(), params) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 1, rec.Len(), "want rec.Len()=1; got %d", rec.Len()) ttl := tok.ExpireTime().Sub(tok.IssueTime()) @@ -197,7 +198,7 @@ func TestAuth_TokenAuth_Renew(t *testing.T) { opts = app.Options(opts...) opts = append(opts, ably.WithKey(""), ably.WithTokenDetails(tok)) client, err = ably.NewREST(opts...) - assert.NoError(t, err, + require.NoError(t, err, "NewREST()=%v", err) _, err = client.Stats().Pages(context.Background()) assert.Error(t, err) @@ -250,7 +251,7 @@ func TestAuth_RequestToken(t *testing.T) { ably.AuthWithCallback(server.Callback(callback)), } tokCallback, err := client.Auth.RequestToken(context.Background(), nil, authOpts...) - assert.NoError(t, err, + require.NoError(t, err, "RequestToken()=%v (callback=%s)", err, callback) // Ensure no requests to Ably servers were made. assert.Equal(t, 0, rec.Len(), @@ -268,7 +269,7 @@ func TestAuth_RequestToken(t *testing.T) { ably.AuthWithCallback(server.Callback("request")), } tokCallback, err := client.Auth.RequestToken(context.Background(), nil, authOpts...) - assert.NoError(t, err, + require.NoError(t, err, "RequestToken()=%v", err) assert.Equal(t, 1, rec.Len(), "want rec.Len()=1; got %d", rec.Len()) @@ -301,7 +302,7 @@ func TestAuth_RequestToken(t *testing.T) { } tokURL, err := client.Auth.RequestToken(context.Background(), params, authOpts...) - assert.NoError(t, err, + require.NoError(t, err, "RequestToken()=%v (method=%s)", err, method) assert.NotEqual(t, tokURL.Token, token2.Token, "want tokURL.Token != token2.Token: %s (method=%s)", tokURL.Token, method) @@ -333,7 +334,7 @@ func TestAuth_RequestToken(t *testing.T) { ably.WithToken(tokURL.Token), ) c, err := ably.NewREST(optsURL...) - assert.NoError(t, err, + require.NoError(t, err, "NewREST()=%v", err) _, err = c.Stats().Pages(context.Background()) assert.NoError(t, err, @@ -498,14 +499,14 @@ func TestAuth_ReuseClientID(t *testing.T) { ClientID: "reuse-me", } tok, err := client.Auth.Authorize(context.Background(), params) - assert.NoError(t, err, + require.NoError(t, err, "Authorize()=%v", err) assert.Equal(t, params.ClientID, tok.ClientID, "want ClientID=%q; got %q", params.ClientID, tok.ClientID) assert.Equal(t, params.ClientID, client.Auth.ClientID(), "want ClientID=%q; got %q", params.ClientID, client.Auth.ClientID()) tok2, err := client.Auth.Authorize(context.Background(), nil) - assert.NoError(t, err, + require.NoError(t, err, "Authorize()=%v", err) assert.Equal(t, params.ClientID, tok2.ClientID, "want ClientID=%q; got %q", params.ClientID, tok2.ClientID) @@ -528,7 +529,7 @@ func TestAuth_RequestToken_PublishClientID(t *testing.T) { for i, cas := range cases { rclient, err := ably.NewREST(app.Options()...) - assert.NoError(t, err) + require.NoError(t, err) params := &ably.TokenParams{ ClientID: cas.authAs, } @@ -714,13 +715,13 @@ func TestAuth_CreateTokenRequest(t *testing.T) { }) t.Run("RSA9c must generate a unique 16+ character nonce", func(t *testing.T) { req, err := client.Auth.CreateTokenRequest(params, opts...) - assert.NoError(t, err) + require.NoError(t, err) assert.GreaterOrEqual(t, len(req.Nonce), 16, "want len(nonce)>=16; got %d", len(req.Nonce)) }) t.Run("RSA9g generate a signed request", func(t *testing.T) { req, err := client.Auth.CreateTokenRequest(nil) - assert.NoError(t, err, + require.NoError(t, err, "CreateTokenRequest()=%v", err) assert.NotEqual(t, "", req.MAC, "want mac to be not empty") diff --git a/ably/http_paginated_response_integration_test.go b/ably/http_paginated_response_integration_test.go index f41b64c3..4bbfdd0a 100644 --- a/ably/http_paginated_response_integration_test.go +++ b/ably/http_paginated_response_integration_test.go @@ -14,16 +14,17 @@ import ( "github.com/ably/ably-go/internal/ablytest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestHTTPPaginatedFallback(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) opts := app.Options(ably.WithUseBinaryProtocol(false), ably.WithEndpoint("ably.invalid"), ably.WithFallbackHosts(nil)) client, err := ably.NewREST(opts...) - assert.NoError(t, err) + require.NoError(t, err) t.Run("request_time", func(t *testing.T) { _, err := client.Request("get", "/time").Pages(context.Background()) assert.Error(t, err) @@ -32,12 +33,12 @@ func TestHTTPPaginatedFallback(t *testing.T) { func TestHTTPPaginatedResponse(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) client, err := ably.NewREST(app.Options()...) - assert.NoError(t, err) + require.NoError(t, err) t.Run("request_time", func(t *testing.T) { res, err := client.Request("get", "/time").Pages(context.Background()) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, http.StatusOK, res.StatusCode(), "expected %d got %d", http.StatusOK, res.StatusCode()) assert.True(t, res.Success(), "expected success to be true") @@ -51,7 +52,7 @@ func TestHTTPPaginatedResponse(t *testing.T) { t.Run("request_404", func(t *testing.T) { res, err := client.Request("get", "/keys/ablyjs.test/requestToken").Pages(context.Background()) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, http.StatusNotFound, res.StatusCode(), "expected %d got %d", http.StatusNotFound, res.StatusCode()) assert.Equal(t, ably.ErrNotFound, res.ErrorCode(), @@ -73,7 +74,7 @@ func TestHTTPPaginatedResponse(t *testing.T) { t.Run("post", func(t *testing.T) { for _, message := range msgs { res, err := client.Request("POST", channelPath, ably.RequestWithBody(message)).Pages(context.Background()) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, http.StatusCreated, res.StatusCode(), "expected %d got %d", http.StatusCreated, res.StatusCode()) assert.True(t, res.Success(), diff --git a/ably/proto_message_integration_test.go b/ably/proto_message_integration_test.go index d2b28122..ec0ead23 100644 --- a/ably/proto_message_integration_test.go +++ b/ably/proto_message_integration_test.go @@ -11,6 +11,7 @@ import ( "github.com/ably/ably-go/internal/ablytest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMessage_CryptoDataFixtures_RSL6a1_RSL5b_RSL5c(t *testing.T) { @@ -47,14 +48,14 @@ func TestMessage_CryptoDataFixtures_RSL6a1_RSL5b_RSL5c(t *testing.T) { assert.NoError(t, err) encoded, err = ably.MessageWithDecodedData(encoded, cipher) - assert.NoError(t, err) + require.NoError(t, err) var encrypted ably.Message err = json.Unmarshal(item.Encoded, &encrypted) assert.NoError(t, err) encrypted, err = ably.MessageWithDecodedData(encrypted, cipher) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, encoded.Name, encrypted.Name, "expected %s got %s", encoded.Name, encrypted.Name) assert.Equal(t, encoded.Data, encrypted.Data, @@ -98,14 +99,14 @@ func TestMessage_CryptoDataFixtures_RSL6a1_RSL5b_RSL5c_TM3(t *testing.T) { assert.NoError(t, err) encoded, err = ably.MessageWithDecodedData(encoded, cipher) - assert.NoError(t, err) + require.NoError(t, err) var encrypted ably.Message err = json.Unmarshal(item.Encoded, &encrypted) assert.NoError(t, err) encrypted, err = ably.MessageWithDecodedData(encrypted, cipher) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, encoded.Name, encrypted.Name) assert.Equal(t, encoded.Data, encrypted.Data) } diff --git a/ably/realtime_channel_integration_test.go b/ably/realtime_channel_integration_test.go index c5a411e2..9e96b8e8 100644 --- a/ably/realtime_channel_integration_test.go +++ b/ably/realtime_channel_integration_test.go @@ -17,6 +17,7 @@ import ( "github.com/ably/ably-go/internal/ablytest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func expectMsg(ch <-chan *ably.Message, name string, data interface{}, t time.Duration, received bool) error { @@ -111,7 +112,7 @@ func TestRealtimeChannel_Subscribe(t *testing.T) { func TestRealtimeChannel_SubscriptionFilters(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) options := app.Options() restClient, err := ably.NewREST(options...) assert.NoError(t, err) @@ -173,7 +174,7 @@ func TestRealtimeChannel_SubscriptionFilters(t *testing.T) { unsub, err := rtDerivedChannel.SubscribeAll(ctx, func(msg *ably.Message) { filteredMessages <- msg }) - assert.NoError(t, err) + require.NoError(t, err) defer unsub() unfilteredMessages := make(chan *ably.Message, 10) diff --git a/ably/realtime_channel_spec_integration_test.go b/ably/realtime_channel_spec_integration_test.go index 8e60bac6..d0d129ee 100644 --- a/ably/realtime_channel_spec_integration_test.go +++ b/ably/realtime_channel_spec_integration_test.go @@ -17,6 +17,7 @@ import ( "github.com/ably/ably-go/internal/ablytest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRealtimeChannel_RTL2_ChannelEventForStateChange(t *testing.T) { @@ -214,7 +215,7 @@ func TestRealtimeChannel_RTL4_Attach(t *testing.T) { t.Run("RTL4b: If connection state is INITIALIZED, CLOSING, CLOSED returns error", func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() @@ -288,7 +289,7 @@ func TestRealtimeChannel_RTL4_Attach(t *testing.T) { t.Run("RTL4b: If connection state is FAILED, returns error", func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() @@ -329,7 +330,7 @@ func TestRealtimeChannel_RTL4_Attach(t *testing.T) { t.Run("RTL4b: If connection state is SUSPENDED, returns error", func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() @@ -429,7 +430,7 @@ func TestRealtimeChannel_RTL4_Attach(t *testing.T) { t.Run("RTL4d : should return error on FAILED while attaching channel", func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() c, closer := TransitionConn(t, recorder.Dial, app.Options()...) @@ -478,7 +479,7 @@ func TestRealtimeChannel_RTL4_Attach(t *testing.T) { t.Run("RTL4d : should return error on DETACHED while attaching channel", func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() c, closer := TransitionConn(t, recorder.Dial, app.Options()...) @@ -528,7 +529,7 @@ func TestRealtimeChannel_RTL4_Attach(t *testing.T) { t.Run("RTL4d : should return error on SUSPENDED while attaching channel", func(t *testing.T) { t.Skip("Channel SUSPENDED not implemented yet") app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() c, closer := TransitionConn(t, recorder.Dial, app.Options()...) @@ -578,7 +579,7 @@ func TestRealtimeChannel_RTL4_Attach(t *testing.T) { t.Run("RTL4e: Transition to failed if no attach permission", func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) rest, _ := ably.NewREST(app.Options()...) var params ably.TokenParams @@ -645,7 +646,7 @@ func TestRealtimeChannel_RTL4_Attach(t *testing.T) { t.Run("RTL4g: If channel in FAILED state, set err to null and proceed with attach", func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() c, closer := TransitionConn(t, recorder.Dial, app.Options()...) @@ -715,7 +716,7 @@ func TestRealtimeChannel_RTL4_Attach(t *testing.T) { t.Run("RTL4h: If channel is ATTACHING, listen to the attach event and don't send attach event", func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() c, closer := TransitionConn(t, recorder.Dial, app.Options()...) @@ -779,7 +780,7 @@ func TestRealtimeChannel_RTL4_Attach(t *testing.T) { t.Run("RTL4h: If channel is DETACHING, do attach after completion of request", func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() c, closer := TransitionConn(t, recorder.Dial, app.Options()...) @@ -862,7 +863,7 @@ func TestRealtimeChannel_RTL4_Attach(t *testing.T) { t.Run("RTL4i : If connection state is CONNECTING, do ATTACH after CONNECTED", func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() c, closer := TransitionConn(t, recorder.Dial, app.Options()...) @@ -917,7 +918,7 @@ func TestRealtimeChannel_RTL4_Attach(t *testing.T) { t.Run("RTL4i : If connection state is DISCONNECTED, do ATTACH after CONNECTED", func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() c, closer := TransitionConn(t, recorder.Dial, app.Options()...) @@ -1317,7 +1318,7 @@ func TestRealtimeChannel_RTL5_Detach(t *testing.T) { t.Run("RTL5a: If channel is INITIALIZED or DETACHED, do nothing", func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() c, closer := TransitionConn(t, recorder.Dial, app.Options()...) @@ -1392,7 +1393,7 @@ func TestRealtimeChannel_RTL5_Detach(t *testing.T) { t.Run("RTL5b: If channel state is FAILED, return error", func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() c, closer := TransitionConn(t, recorder.Dial, app.Options()...) @@ -1509,7 +1510,7 @@ func TestRealtimeChannel_RTL5_Detach(t *testing.T) { t.Run("RTL5e: return error if channel detach fails", func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() c, closer := TransitionConn(t, recorder.Dial, app.Options()...) @@ -1613,7 +1614,7 @@ func TestRealtimeChannel_RTL5_Detach(t *testing.T) { t.Run("RTL5g: If connection state CLOSING or FAILED, should return error", func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() c, closer := TransitionConn(t, recorder.Dial, app.Options()...) @@ -1677,7 +1678,7 @@ func TestRealtimeChannel_RTL5_Detach(t *testing.T) { t.Run("RTL5h : If Connection state CONNECTING, queue the DETACH message and send on CONNECTED", func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() c, closer := TransitionConn(t, recorder.Dial, app.Options()...) @@ -1747,7 +1748,7 @@ func TestRealtimeChannel_RTL5_Detach(t *testing.T) { t.Run("RTL5h, RTN19b: If Connection state DISCONNECTED, queue the DETACH message and send on CONNECTED", func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() c, closer := TransitionConn(t, recorder.Dial, app.Options()...) @@ -1816,7 +1817,7 @@ func TestRealtimeChannel_RTL5_Detach(t *testing.T) { t.Run("RTL5i: If channel in DETACHING or ATTACHING state, do detach after completion of operation", func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() c, closer := TransitionConn(t, recorder.Dial, app.Options()...) @@ -1966,7 +1967,7 @@ func TestRealtimeChannel_RTL6c1_PublishNow(t *testing.T) { t.Run(fmt.Sprintf("when %s", state), func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) c, closer := TransitionConn(t, nil, app.Options()...) defer safeclose(t, closer) @@ -1985,7 +1986,7 @@ func TestRealtimeChannel_RTL6c1_PublishNow(t *testing.T) { // published without interferring with the first client's state. subClient, err := ably.NewRealtime(app.Options()...) - assert.NoError(t, err) + require.NoError(t, err) defer safeclose(t, ablytest.FullRealtimeCloser(subClient)) err = ablytest.Wait(ablytest.ConnWaiter(subClient, subClient.Connect, ably.ConnectionEventConnected), nil) assert.NoError(t, err) @@ -2083,7 +2084,7 @@ func TestRealtimeChannel_RTL6c2_PublishEnqueue(t *testing.T) { t.Run(fmt.Sprintf("when connection is %v, channel is %v", connTarget, chanTarget), func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() @@ -2181,7 +2182,7 @@ func TestRealtimeChannel_RTL6c4_PublishFail(t *testing.T) { t.Run(fmt.Sprintf("when connection is %v, channel is %v", connTarget, chanTarget), func(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) recorder := NewMessageRecorder() diff --git a/ably/realtime_client_integration_test.go b/ably/realtime_client_integration_test.go index c224bf1e..5fff5801 100644 --- a/ably/realtime_client_integration_test.go +++ b/ably/realtime_client_integration_test.go @@ -85,7 +85,7 @@ func TestRealtime_RSC7_AblyAgent(t *testing.T) { })) defer server.Close() serverURL, err := url.Parse(server.URL) - assert.NoError(t, err) + require.NoError(t, err) client, err := ably.NewRealtime( ably.WithEndpoint(serverURL.Host), @@ -109,7 +109,7 @@ func TestRealtime_RSC7_AblyAgent(t *testing.T) { })) defer server.Close() serverURL, err := url.Parse(server.URL) - assert.NoError(t, err) + require.NoError(t, err) client, err := ably.NewRealtime( ably.WithEndpoint(serverURL.Host), @@ -137,7 +137,7 @@ func TestRealtime_RSC7_AblyAgent(t *testing.T) { })) defer server.Close() serverURL, err := url.Parse(server.URL) - assert.NoError(t, err) + require.NoError(t, err) client, err := ably.NewRealtime( ably.WithEndpoint(serverURL.Host), @@ -167,7 +167,7 @@ func TestRealtime_RSC7_AblyAgent(t *testing.T) { })) defer server.Close() serverURL, err := url.Parse(server.URL) - assert.NoError(t, err) + require.NoError(t, err) client, err := ably.NewRealtime( ably.WithTLS(false), @@ -191,7 +191,7 @@ func TestRealtime_RSC7_AblyAgent(t *testing.T) { })) defer server.Close() serverURL, err := url.Parse(server.URL) - assert.NoError(t, err) + require.NoError(t, err) client, err := ably.NewRealtime( ably.WithTLS(false), @@ -219,7 +219,7 @@ func TestRealtime_RSC7_AblyAgent(t *testing.T) { })) defer server.Close() serverURL, err := url.Parse(server.URL) - assert.NoError(t, err) + require.NoError(t, err) client, err := ably.NewRealtime( ably.WithTLS(false), @@ -493,7 +493,7 @@ func TestRealtime_multiple(t *testing.T) { var all ablytest.ResultGroup var wg sync.WaitGroup app, err := ablytest.NewSandbox() - assert.NoError(t, err, + require.NoError(t, err, "NewSandbox()=%v", err) wg.Add(N) idch := make(chan string, N) diff --git a/ably/realtime_conn_integration_test.go b/ably/realtime_conn_integration_test.go index bbbc78b2..384f166d 100644 --- a/ably/realtime_conn_integration_test.go +++ b/ably/realtime_conn_integration_test.go @@ -15,6 +15,7 @@ import ( "github.com/ably/ably-go/internal/ablytest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var connTransitions = []ably.ConnectionState{ @@ -92,7 +93,7 @@ func TestRealtimeConn_AuthError(t *testing.T) { ably.WithAutoConnect(false), } client, err := ably.NewRealtime(opts...) - assert.NoError(t, err, + require.NoError(t, err, "NewRealtime()=%v", err) err = ablytest.Wait(ablytest.ConnWaiter(client, client.Connect, ably.ConnectionEventConnected), nil) diff --git a/ably/realtime_conn_spec_integration_test.go b/ably/realtime_conn_spec_integration_test.go index 97f34580..91b360a7 100644 --- a/ably/realtime_conn_spec_integration_test.go +++ b/ably/realtime_conn_spec_integration_test.go @@ -18,6 +18,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/ably/ably-go/ably" "github.com/ably/ably-go/internal/ablytest" @@ -227,7 +228,7 @@ func Test_RTN4a_ConnectionEventForStateChange(t *testing.T) { } realtime, err := ably.NewRealtime(options...) - assert.NoError(t, err, + require.NoError(t, err, "unexpected err: %s", err) changes := make(chan ably.ConnectionStateChange) @@ -750,7 +751,7 @@ func TestRealtimeConn_RTN15a_ReconnectOnEOF(t *testing.T) { // succeeds, we should then receive it without reattaching. rest, err := ably.NewREST(app.Options()...) - assert.NoError(t, err) + require.NoError(t, err) err = rest.Channels.Get(ablytest.UniqueChannelName(t, "channel")).Publish(context.Background(), "name", "data") assert.NoError(t, err) @@ -884,7 +885,7 @@ func TestRealtimeConn_RTN15b(t *testing.T) { // succeeds, we should then receive it without reattaching. rest, err := ably.NewREST(app.Options()...) - assert.NoError(t, err) + require.NoError(t, err) goOn := <-gotDial err = rest.Channels.Get(ablytest.UniqueChannelName(t, "channel")).Publish(context.Background(), "name", "data") assert.NoError(t, err) @@ -985,7 +986,7 @@ func TestRealtimeConn_RTN15c6(t *testing.T) { "expected transition to %v, got %v", ably.ConnectionStateDisconnected, connState.Current) rest, err := ably.NewREST(app.Options()...) - assert.NoError(t, err) + require.NoError(t, err) err = rest.Channels.Get(ablytest.UniqueChannelName(t, "channel")).Publish(context.Background(), "name", "data") assert.NoError(t, err) @@ -1097,7 +1098,7 @@ func TestRealtimeConn_RTN15c7_attached(t *testing.T) { "expected transition to %v, got %v", ably.ConnectionStateDisconnected, connState.Current) rest, err := ably.NewREST(app.Options()...) - assert.NoError(t, err) + require.NoError(t, err) err = rest.Channels.Get(ablytest.UniqueChannelName(t, "channel")).Publish(context.Background(), "name", "data") assert.NoError(t, err) @@ -1165,7 +1166,7 @@ func TestRealtimeConn_RTN15d_MessageRecovery(t *testing.T) { // still be attached and the messages will arrive. rest, err := ably.NewREST(app.Options()...) - assert.NoError(t, err) + require.NoError(t, err) for i := 0; i < 3; i++ { err := rest.Channels.Get(ablytest.UniqueChannelName(t, "test")).Publish(context.Background(), "test", fmt.Sprintf("msg %d", i)) assert.NoError(t, err, diff --git a/ably/rest_channel_integration_test.go b/ably/rest_channel_integration_test.go index c47f12b2..964d94a1 100644 --- a/ably/rest_channel_integration_test.go +++ b/ably/rest_channel_integration_test.go @@ -22,14 +22,15 @@ import ( "github.com/ably/ably-go/internal/ablytest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRESTChannel(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) options := app.Options() client, err := ably.NewREST(options...) - assert.NoError(t, err) + require.NoError(t, err) t.Run("Publish", func(t *testing.T) { channel := client.Channels.Get("test_publish_channel") @@ -142,10 +143,10 @@ func TestRESTChannel(t *testing.T) { func TestIdempotentPublishing(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) options := app.Options(ably.WithIdempotentRESTPublishing(true)) client, err := ably.NewREST(options...) - assert.NoError(t, err) + require.NoError(t, err) randomStr, err := ablyutil.BaseID() assert.NoError(t, err) t.Run("when ID is not included (#RSL1k2)", func(t *testing.T) { @@ -347,7 +348,7 @@ func TestIdempotent_retry(t *testing.T) { })) client, err := ably.NewREST(app.Options(nopts...)...) - assert.NoError(t, err) + require.NoError(t, err) t.Run("two REST publish retries result in only one message being published'", func(t *testing.T) { channel := client.Channels.Get("idempotent_test_fallback") diff --git a/ably/rest_channel_spec_integration_test.go b/ably/rest_channel_spec_integration_test.go index 96203a13..46eab736 100644 --- a/ably/rest_channel_spec_integration_test.go +++ b/ably/rest_channel_spec_integration_test.go @@ -13,16 +13,17 @@ import ( "github.com/ably/ably-go/internal/ablytest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRSL1f1(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) opts := app.Options() // RSL1f opts = append(opts, ably.WithUseTokenAuth(false)) client, err := ably.NewREST(opts...) - assert.NoError(t, err) + require.NoError(t, err) channel := client.Channels.Get("RSL1f") var msgs []*ably.Message size := 10 @@ -47,13 +48,13 @@ func TestRSL1f1(t *testing.T) { func TestRSL1g(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) opts := append(app.Options(), ably.WithUseTokenAuth(true), ) opts = append(opts, ably.WithClientID("some_client_id")) client, err := ably.NewREST(opts...) - assert.NoError(t, err) + require.NoError(t, err) t.Run("RSL1g1b", func(t *testing.T) { channel := client.Channels.Get("RSL1g1b") err := channel.PublishMultiple(context.Background(), []*ably.Message{ @@ -167,7 +168,7 @@ func TestGetChannelLifecycleStatus_RSL8(t *testing.T) { err := channel.Publish(ctx, "event", "data") assert.NoError(t, err) status, err := channel.Status(ctx) - assert.NoError(t, err) + require.NoError(t, err) assert.NotNil(t, status.ChannelId) assert.True(t, status.Status.IsActive) assert.Equal(t, "lifecycle:test", status.ChannelId) diff --git a/ably/rest_client_integration_test.go b/ably/rest_client_integration_test.go index 2f0ac0b7..62d048fd 100644 --- a/ably/rest_client_integration_test.go +++ b/ably/rest_client_integration_test.go @@ -27,6 +27,7 @@ import ( "github.com/ably/ably-go/internal/ablytest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func newHTTPClientMock(srv *httptest.Server) *http.Client { @@ -63,7 +64,7 @@ func TestRestClient(t *testing.T) { } client, err := ably.NewREST(app.Options(options...)...) - assert.NoError(t, err) + require.NoError(t, err) err = client.Channels.Get(ablytest.UniqueChannelName(t, "test")).Publish(context.Background(), "ping", "pong") assert.NoError(t, err) var anyJson []map[string]interface{} @@ -90,7 +91,7 @@ func TestRestClient(t *testing.T) { } client, err := ably.NewREST(app.Options(options...)...) - assert.NoError(t, err) + require.NoError(t, err) err = client.Channels.Get(ablytest.UniqueChannelName(t, "test")).Publish(context.Background(), "ping", "pong") assert.NoError(t, err) var anyMsgPack []map[string]interface{} @@ -107,9 +108,9 @@ func TestRestClient(t *testing.T) { t.Run("Time", func(t *testing.T) { client, err := ably.NewREST(app.Options()...) - assert.NoError(t, err) + require.NoError(t, err) ti, err := client.Time(context.Background()) - assert.NoError(t, err) + require.NoError(t, err) before := time.Now().Add(2 * time.Second).Unix() after := time.Now().Add(-2 * time.Second).Unix() n := ti.Unix() @@ -153,7 +154,7 @@ func TestRestClient(t *testing.T) { stats[2].IntervalID = intervalFormatFor(lastInterval.Add(-1*time.Minute), ably.StatGranularityMinute) res, err := client.Post(context.Background(), "/stats", &stats, nil) - assert.NoError(t, err) + require.NoError(t, err) res.Body.Close() statsCh := make(chan []*ably.Stats, 1) @@ -246,7 +247,7 @@ func TestRest_RSC7_AblyAgent(t *testing.T) { })) defer server.Close() serverURL, err := url.Parse(server.URL) - assert.NoError(t, err) + require.NoError(t, err) opts := []ably.ClientOption{ ably.WithEndpoint(serverURL.Host), @@ -255,7 +256,7 @@ func TestRest_RSC7_AblyAgent(t *testing.T) { } client, err := ably.NewREST(opts...) - assert.NoError(t, err) + require.NoError(t, err) expectedAgentHeaderValue := ably.AblySDKIdentifier + " " + ably.GoRuntimeIdentifier + " " + ably.GoOSIdentifier() client.Time(context.Background()) @@ -270,7 +271,7 @@ func TestRest_RSC7_AblyAgent(t *testing.T) { })) defer server.Close() serverURL, err := url.Parse(server.URL) - assert.NoError(t, err) + require.NoError(t, err) opts := []ably.ClientOption{ ably.WithEndpoint(serverURL.Host), @@ -282,7 +283,7 @@ func TestRest_RSC7_AblyAgent(t *testing.T) { } client, err := ably.NewREST(opts...) - assert.NoError(t, err) + require.NoError(t, err) expectedAgentHeaderValue := ably.AblySDKIdentifier + " " + ably.GoRuntimeIdentifier + " " + ably.GoOSIdentifier() + " foo/1.2.3" client.Time(context.Background()) @@ -297,7 +298,7 @@ func TestRest_RSC7_AblyAgent(t *testing.T) { })) defer server.Close() serverURL, err := url.Parse(server.URL) - assert.NoError(t, err) + require.NoError(t, err) opts := []ably.ClientOption{ ably.WithEndpoint(serverURL.Host), @@ -309,7 +310,7 @@ func TestRest_RSC7_AblyAgent(t *testing.T) { } client, err := ably.NewREST(opts...) - assert.NoError(t, err) + require.NoError(t, err) expectedAgentHeaderValue := ably.AblySDKIdentifier + " " + ably.GoRuntimeIdentifier + " " + ably.GoOSIdentifier() + " bar" client.Time(context.Background()) @@ -331,7 +332,7 @@ func TestRest_RSC15_HostFallback(t *testing.T) { })) defer server.Close() client, err := ably.NewREST(app.Options(append(options, ably.WithHTTPClient(newHTTPClientMock(server)))...)...) - assert.NoError(t, err) + require.NoError(t, err) err = client.Channels.Get(ablytest.UniqueChannelName(t, "test")).Publish(context.Background(), "ping", "pong") assert.Error(t, err, "expected an error") return retryCount, hosts @@ -375,7 +376,7 @@ func TestRest_RSC15_HostFallback(t *testing.T) { }, } client, err := ably.NewREST(app.Options(append(options, ably.WithHTTPClient(httpClientMock))...)...) - assert.NoError(t, err) + require.NoError(t, err) err = client.Channels.Get(ablytest.UniqueChannelName(t, "test")).Publish(context.Background(), "ping", "pong") <-allHostsTried assert.Contains(t, err.Error(), "context deadline exceeded (Client.Timeout exceeded while awaiting headers)") @@ -409,7 +410,7 @@ func TestRest_RSC15_HostFallback(t *testing.T) { ably.WithUseTokenAuth(true), } client, err := ably.NewREST(app.Options(options...)...) - assert.NoError(t, err) + require.NoError(t, err) tm, err := client.Time(context.Background()) assert.Nil(t, err) assert.NotNil(t, tm) @@ -541,7 +542,7 @@ func TestRest_rememberHostFallback(t *testing.T) { })) client, err := ably.NewREST(app.Options(nopts...)...) - assert.NoError(t, err) + require.NoError(t, err) channel := client.Channels.Get("remember_fallback_host") err = channel.Publish(context.Background(), "ping", "pong") assert.NoError(t, err) @@ -563,9 +564,9 @@ func TestRest_rememberHostFallback(t *testing.T) { func TestRESTChannels_RSN1(t *testing.T) { app, err := ablytest.NewSandbox() - assert.NoError(t, err) + require.NoError(t, err) client, err := ably.NewREST(app.Options()...) - assert.NoError(t, err) + require.NoError(t, err) assert.NotNil(t, client.Channels, "expected Channels to be initialized") sample := []struct { @@ -617,7 +618,7 @@ func TestFixConnLeak_ISSUE89(t *testing.T) { opts := app.Options(ably.WithHTTPClient(httpClient)) client, err := ably.NewREST(opts...) - assert.NoError(t, err) + require.NoError(t, err) channel := client.Channels.Get("issue89") for i := 0; i < 10; i++ { err := channel.Publish(context.Background(), fmt.Sprintf("msg_%d", i), fmt.Sprint(i)) @@ -760,8 +761,7 @@ func TestStats_Unit_RSC6b4(t *testing.T) { got = append(got, pages.Items()...) } assert.NoError(t, pages.Err()) - assert.Equal(t, 1, len(got), - "expected: 1; got: %v", got) + require.Equal(t, 1, len(got), "expected: 1; got: %v", got) stats := got[0] assert.Equal(t, "month", stats.Unit, From ecb6c876a109751103a9338387a5d197fd6d443b Mon Sep 17 00:00:00 2001 From: Lewis Marshall Date: Sun, 19 Jul 2026 23:52:49 +0100 Subject: [PATCH 7/7] Support running the integration suite against ABLY_LOCAL_SANDBOX_URL When ABLY_LOCAL_SANDBOX_URL is set, internal/ablytest provisions each test's app through a local sandbox's POST /apps (the same code path as the cloud sandbox) instead of the cloud, and routes clients to the isolated ably-server child it returns (its own endpoint/port/TLS). Every test gets a fresh app, so tests don't contend over shared server state and can run concurrently. The all-powerful test key is selected by parsing each key's capability for the "[*]*" wildcard rather than string-matching it, so provisioning is insensitive to the local sandbox's JSON formatting. The cloud path is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/ablytest/ablytest.go | 11 +++++++ internal/ablytest/sandbox.go | 61 ++++++++++++++++++++++++++++++----- 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/internal/ablytest/ablytest.go b/internal/ablytest/ablytest.go index d88c4d76..b1373247 100644 --- a/internal/ablytest/ablytest.go +++ b/internal/ablytest/ablytest.go @@ -69,6 +69,14 @@ func UniqueChannelName(t *testing.T, base string) string { return ns + name + "-" + channelNameSanitizer.Replace(t.Name()) } +// LocalSandboxURL, when set (via ABLY_LOCAL_SANDBOX_URL, e.g. "http://localhost:9010"), +// points app provisioning at a local sandbox instead of the cloud sandbox. +// Each NewSandbox then POSTs the appspec to /apps, which boots an +// isolated ably-server child and returns that child's keys, endpoint and port; +// clients are configured to talk to that child (see provisionSandbox / Options). +// This is the hook used by the ably-server compatibility harness. +var LocalSandboxURL string + func nonil(err ...error) error { for _, err := range err { if err != nil { @@ -93,6 +101,9 @@ func init() { if s := os.Getenv("ABLY_ENDPOINT"); s != "" { Endpoint = s } + if s := os.Getenv("ABLY_LOCAL_SANDBOX_URL"); s != "" { + LocalSandboxURL = strings.TrimRight(s, "/") + } } func MergeOptions(opts ...[]ably.ClientOption) []ably.ClientOption { diff --git a/internal/ablytest/sandbox.go b/internal/ablytest/sandbox.go index 46da86ec..6c19d870 100644 --- a/internal/ablytest/sandbox.go +++ b/internal/ablytest/sandbox.go @@ -38,6 +38,14 @@ type Key struct { type Config struct { AppID string `json:"appId,omitempty"` Keys []Key `json:"keys"` + + // LocalEndpoint/LocalPort/LocalTLS are populated only when provisioning + // against a local sandbox (see LocalSandboxURL), which runs each app on its + // own child server and returns how to reach it. They are absent (zero) for + // the cloud sandbox, where routing is derived from Sandbox.Endpoint instead. + LocalEndpoint string `json:"endpoint,omitempty"` + LocalPort int `json:"port,omitempty"` + LocalTLS bool `json:"tls,omitempty"` } // Presence describes a presence fixture member provisioned on the @@ -131,6 +139,11 @@ type Sandbox struct { Config *Config Endpoint string client *http.Client + + // local is set when this app was provisioned against a local sandbox + // (see LocalSandboxURL) rather than the cloud sandbox; it selects the local + // routing in Options and the unauthenticated teardown in delete. + local bool } func NewRealtime(opts ...ably.ClientOption) (*Sandbox, *ably.Realtime) { @@ -198,7 +211,9 @@ func provisionSandbox(endpoint string) (*Sandbox, error) { Config: &Config{}, Endpoint: endpoint, client: NewHTTPClient(), + local: LocalSandboxURL != "", } + p := []byte(loadAppSetup().PostApps) const RetryCount = 4 @@ -271,21 +286,18 @@ func (app *Sandbox) NewRealtime(opts ...ably.ClientOption) *ably.Realtime { return client } -// wildcardCapability is the all-resources, all-operations capability. The shared -// appspec provisions several keys with differing capabilities; the tests expect -// a single all-powerful key, so KeyParts selects the one carrying this -// capability (in particular [*]* is required for qualified/derived channels, -// which the default capability does not grant). -const wildcardCapability = `{"[*]*":["*"]}` - func (app *Sandbox) KeyParts() (name, secret string) { key := app.wildcardKey() return app.Config.AppID + "." + key.ID, key.Value } +// wildcardKey returns the all-powerful key. The shared appspec provisions +// several keys with differing capabilities; the tests expect a single key that +// grants every operation on every resource, including the qualified/derived +// namespace ("[*]*") that the plain "*" resource does not cover. func (app *Sandbox) wildcardKey() Key { for _, k := range app.Config.Keys { - if k.Capability == wildcardCapability { + if grantsWildcard(k.Capability) { return k } } @@ -294,6 +306,22 @@ func (app *Sandbox) wildcardKey() Key { return app.Config.Keys[0] } +// grantsWildcard reports whether a capability grants all operations ("*") on +// the all-resources qualifier ("[*]*"). It parses the capability rather than +// string-matching so it is insensitive to JSON whitespace and key ordering. +func grantsWildcard(capability string) bool { + var caps map[string][]string + if json.Unmarshal([]byte(capability), &caps) != nil { + return false + } + for _, op := range caps["[*]*"] { + if op == "*" { + return true + } + } + return false +} + func (app *Sandbox) Key() string { name, secret := app.KeyParts() return name + ":" + secret @@ -312,6 +340,20 @@ func (app *Sandbox) Options(opts ...ably.ClientOption) []ably.ClientOption { ably.WithLogLevel(DefaultLogLevel), } + // local sandbox: route to the app's child server (its own host/port, + // plain ws/http), overriding the cloud endpoint set above. Basic auth is + // allowed without TLS since the child terminates plaintext. + if app.local { + appOpts = append(appOpts, + ably.WithEndpoint(app.Config.LocalEndpoint), + ably.WithTLS(app.Config.LocalTLS), + ably.WithPort(app.Config.LocalPort), + ) + if !app.Config.LocalTLS { + appOpts = append(appOpts, ably.WithInsecureAllowBasicAuthWithoutTLS()) + } + } + // If opts want to record round trips inject the recording transport // via TransportHijacker interface. if httpClient := ClientOptionsInspector.HTTPClient(opts); httpClient != nil { @@ -326,6 +368,9 @@ func (app *Sandbox) Options(opts ...ably.ClientOption) []ably.ClientOption { } func (app *Sandbox) URL(paths ...string) string { + if app.local { + return LocalSandboxURL + "/" + path.Join(paths...) + } if strings.HasPrefix(app.Endpoint, "nonprod:") { namespace := strings.TrimPrefix(app.Endpoint, "nonprod:") return fmt.Sprintf("https://%s.realtime.ably-nonprod.net/%s", namespace, path.Join(paths...))