Skip to content

Commit fddaf21

Browse files
colinsaramprice
authored andcommitted
Add Go tests for per-error-type connection retry (isConnectionError)
Implements the Ruby spec coverage missing from the Go test suite: Ruby: spec/nats_sync/users_sync_spec.rb context 'with various network errors' do [ Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ETIMEDOUT, Net::OpenTimeout, SocketError ].each do |error_class| it "retries on #{error_class}" end end The Go implementation gates retries on isConnectionError(), which matches connection-class errors by substring. The new DescribeTable in users_sync_test.go covers every string pattern in that function, each annotated with its Ruby error-class equivalent: Ruby error class Go error string matched ───────────────────── ──────────────────────── Errno::ECONNREFUSED → "connection refused" Errno::ECONNRESET → "connection reset by peer" Errno::ETIMEDOUT → "connection timed out" Net::OpenTimeout → "i/o timeout", "deadline exceeded" SocketError → "no such host" Errno::EHOSTUNREACH → "host is unreachable" Errno::ENETUNREACH → "network is unreachable" (Go-specific) → "eof", "unexpected eof" Negative cases verify that HTTP-level errors (401, 500) and nil do not trigger retries. Uses export_test.go to expose isConnectionError to the external test package without modifying the production API.
1 parent 335fcc2 commit fddaf21

2 files changed

Lines changed: 82 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package userssync
2+
3+
// IsConnectionError exposes isConnectionError for use in external test packages.
4+
// This file is compiled only during test runs.
5+
var IsConnectionError = isConnectionError

src/bosh-nats-sync/pkg/userssync/users_sync_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package userssync_test
33
import (
44
"encoding/json"
55
"encoding/pem"
6+
"errors"
67
"fmt"
78
"log/slog"
89
"net/http"
@@ -775,3 +776,79 @@ var _ = Describe("UsersSync", func() {
775776
})
776777
})
777778
})
779+
780+
// Mirrors Ruby spec: spec/nats_sync/users_sync_spec.rb
781+
//
782+
// context 'with various network errors' do
783+
// [ Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ETIMEDOUT,
784+
// Net::OpenTimeout, SocketError ].each do |error_class|
785+
// it "retries on #{error_class}"
786+
// end
787+
// end
788+
//
789+
// In Go the retry gate is isConnectionError, which matches connection-class
790+
// errors by substring. The table below verifies every string pattern in the
791+
// function, annotated with the Ruby error class it corresponds to.
792+
var _ = DescribeTable("isConnectionError",
793+
func(err error, shouldRetry bool) {
794+
Expect(userssync.IsConnectionError(err)).To(Equal(shouldRetry))
795+
},
796+
797+
// ── errors that must trigger a retry ──────────────────────────────────
798+
799+
// Ruby: Errno::ECONNREFUSED
800+
Entry("connection refused",
801+
errors.New("dial tcp 127.0.0.1:25555: connect: connection refused"), true),
802+
803+
// Ruby: Errno::ECONNRESET
804+
Entry("connection reset by peer",
805+
errors.New("read tcp 127.0.0.1:12345->127.0.0.1:25555: read: connection reset by peer"), true),
806+
807+
// Ruby: Errno::ETIMEDOUT
808+
Entry("connection timed out",
809+
errors.New("dial tcp 127.0.0.1:25555: connect: connection timed out"), true),
810+
811+
// Ruby: Net::OpenTimeout — Go surfaces this as an i/o timeout
812+
Entry("i/o timeout (Net::OpenTimeout read-side)",
813+
errors.New("read tcp 127.0.0.1:25555: i/o timeout"), true),
814+
815+
// Ruby: Net::OpenTimeout — Go surfaces this via context deadline
816+
Entry("context deadline exceeded (Net::OpenTimeout connect-side)",
817+
errors.New(`get "http://127.0.0.1:25555/info": context deadline exceeded (Client.Timeout exceeded while awaiting headers)`), true),
818+
819+
// Ruby: SocketError — DNS resolution failure
820+
Entry("no such host (SocketError)",
821+
errors.New("dial tcp: lookup director.example.com on 8.8.8.8:53: no such host"), true),
822+
823+
// Ruby: Errno::EHOSTUNREACH (also in DIRECTOR_CONNECTION_ERRORS)
824+
Entry("host is unreachable (Errno::EHOSTUNREACH)",
825+
errors.New("dial tcp: connect: host is unreachable"), true),
826+
827+
// Ruby: Errno::ENETUNREACH (also in DIRECTOR_CONNECTION_ERRORS)
828+
Entry("network is unreachable (Errno::ENETUNREACH)",
829+
errors.New("dial tcp: connect: network is unreachable"), true),
830+
831+
// Go-specific: bare EOF when the server closes the connection mid-response
832+
// (surfaces as Errno::ECONNRESET on the Ruby side)
833+
Entry("EOF",
834+
errors.New("EOF"), true),
835+
836+
// Go-specific: partial response body truncated
837+
Entry("unexpected EOF",
838+
errors.New("unexpected EOF"), true),
839+
840+
// ── errors that must NOT trigger a retry ──────────────────────────────
841+
842+
// HTTP-level application errors are not connection errors
843+
Entry("HTTP 401 unauthorized",
844+
errors.New("cannot access: /info, Status Code: 401, Unauthorized"), false),
845+
846+
Entry("HTTP 500 internal server error",
847+
errors.New("cannot access: /info, Status Code: 500, Internal Server Error"), false),
848+
849+
Entry("generic application error",
850+
errors.New("failed to parse response body"), false),
851+
852+
// nil means no error occurred — must not be treated as retryable
853+
Entry("nil error", nil, false),
854+
)

0 commit comments

Comments
 (0)