From 49affcece37d42f3e1fa6ef9852986bcd592154b Mon Sep 17 00:00:00 2001 From: David Newgas Date: Tue, 7 Jul 2026 14:15:43 -0700 Subject: [PATCH 01/16] Add client helper methods and error to httputil Motiation: I am adding a client generator to gofoji/foji. To maintain consistency with the foji handler generator, this client should handle HTTP details and provide a simple go interface. This necessitates an error type for non-2xx HTTP responses, which must be in a library foji imports. Change: Add UnexpectedResponseError. This error allows wrappers of http.Client to signal non-successful HTTP responses as an error. It preserves the initial request URL (resp.Req.URL only shows the last URL in a series of redirects) for clear error messages. It preserves the body both for error messages and also to allow inspection for error handling. --- httputil/client.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 httputil/client.go diff --git a/httputil/client.go b/httputil/client.go new file mode 100644 index 0000000..7715e4c --- /dev/null +++ b/httputil/client.go @@ -0,0 +1,16 @@ +package httputil + +import ( + "fmt" + "net/http" +) + +type UnexpectedResponseError struct { + Resp *http.Response + URL string + Body []byte +} + +func (e UnexpectedResponseError) Error() string { + return fmt.Sprintf("url: %q, status: %d, body: %q: unexpected response status code", e.URL, e.Resp.StatusCode, e.Body) +} From 09401b01435a187b535330ed8dd213cb82990ccb Mon Sep 17 00:00:00 2001 From: David Newgas Date: Fri, 10 Jul 2026 10:01:17 -0700 Subject: [PATCH 02/16] Add auth methods for future foji client generator Motivation: I am creating a foji client generation template. To retain the same basic structure as the handler generation, users will supply a auth class and helpers to convert the auth class to the required token/username/password etc. To match the handler generation, this will use common types and helper funcs in iken. Change: 1. Add ClientAuthenticateFunc, the basic type for a function that adds authorization to an outbound request. Unlikle AuthenticateFunc, this also allows the function to update the http.Client. This allows using x/oauth2, which requires you to use a client they generate. This client does additional oauth2 refreshs when required, so cannot be replicated as a mere http.Request change. 2. Add *ClientAuth functions for Bearer auth, header auth, query auth, basic auth, cookie auth and "http client wrapping" auth. These do the actual work of setting the request/client for a specific request. 3. Add Client*AuthenticatorFunc for tokens, basic auth, cookie auth and http.Client wrapping. These will be what the application is expected to supply to the foji generated client, and represent how to go from the user object to the credentials the *ClientAuth functions need. --- httputil/auth.go | 103 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/httputil/auth.go b/httputil/auth.go index fef4aa5..70a9d97 100644 --- a/httputil/auth.go +++ b/httputil/auth.go @@ -52,6 +52,26 @@ type TokenAuthenticatorFunc[T any] func(ctx context.Context, token string) (T, e // BasicAuthenticatorFunc is the signature of a function used to authenticate a request given use user/pass. type BasicAuthenticatorFunc[T any] func(ctx context.Context, user, pass string) (T, error) +// ClientAuthenticateFunc is the signature of a function used to add authentication to an outbound HTTP request. +// It also has an opportunity to wrap the httpClient to be used for the request. +type ClientAuthenticateFunc[T any] func(r *http.Request, innerClient *http.Client, user T) (*http.Client, error) + +// ClientTokenAuthenticatorFunc is the signature of a function used to determine the token that should be added +// to an outbound HTTP request as authentication. +type ClientTokenAuthenticatorFunc[T any] func(ctx context.Context, user T) (string, error) + +// ClientBasicAuthenticatorFunc is the signature of a function used to determine the username and password +// that should be used to add Bsaic authentication to an outbound HTTP request. +type ClientBasicAuthenticatorFunc[T any] func(ctx context.Context, user T) (string, string, error) + +// ClientCookieAuthenticatorFunc is the signature of a function used to determine the cookie +// that should be added to an outbound HTTP request. +type ClientCookieAuthenticatorFunc[T any] func(ctx context.Context, user T) (*http.Cookie, error) + +// ClientWrappingAuthenticatorFunc is the signature of a function used to wrap an http client so that it will +// add authentication. This is intended for integration with x/oauth2. +type ClientWrappingAuthenticatorFunc[T any] func(ctx context.Context, innerClient *http.Client, user T) (*http.Client, error) + // AuthorizeFunc is the signature of a function used to authorize a request. If unable // to authorize the user it returns an error. type AuthorizeFunc[T any] func(ctx context.Context, user T, scopes []string) error @@ -77,6 +97,19 @@ func HeaderAuth[T any](key string, fn TokenAuthenticatorFunc[T]) AuthenticateFun } } +func HeaderClientAuth[T any](key string, fn ClientTokenAuthenticatorFunc[T]) ClientAuthenticateFunc[T] { + return func(r *http.Request, inner *http.Client, user T) (*http.Client, error) { + token, err := fn(r.Context(), user) + if err != nil { + return inner, err + } + + r.Header.Add(key, token) + + return inner, nil + } +} + const bearerAuthPrefix = "Bearer " func BearerAuth[T any](key string, tokenAuth TokenAuthenticatorFunc[T]) AuthenticateFunc[T] { @@ -92,6 +125,19 @@ func BearerAuth[T any](key string, tokenAuth TokenAuthenticatorFunc[T]) Authenti } } +func BearerClientAuth[T any](key string, fn ClientTokenAuthenticatorFunc[T]) ClientAuthenticateFunc[T] { + return func(r *http.Request, inner *http.Client, user T) (*http.Client, error) { + token, err := fn(r.Context(), user) + if err != nil { + return inner, err + } + + r.Header.Add(key, bearerAuthPrefix+token) + + return inner, nil + } +} + func QueryAuth[T any](key string, fn TokenAuthenticatorFunc[T]) AuthenticateFunc[T] { return func(r *http.Request) (T, error) { var empty T @@ -105,6 +151,22 @@ func QueryAuth[T any](key string, fn TokenAuthenticatorFunc[T]) AuthenticateFunc } } +func QueryClientAuth[T any](key string, fn ClientTokenAuthenticatorFunc[T]) ClientAuthenticateFunc[T] { + return func(r *http.Request, inner *http.Client, user T) (*http.Client, error) { + token, err := fn(r.Context(), user) + if err != nil { + return inner, err + } + + q := r.URL.Query() + q.Add(key, token) + + r.URL.RawQuery = q.Encode() + + return inner, nil + } +} + func BasicAuth[T any](authFn BasicAuthenticatorFunc[T]) AuthenticateFunc[T] { return func(r *http.Request) (T, error) { var empty T @@ -133,6 +195,21 @@ func BasicAuth[T any](authFn BasicAuthenticatorFunc[T]) AuthenticateFunc[T] { } } +func BasicClientAuth[T any](fn ClientBasicAuthenticatorFunc[T]) ClientAuthenticateFunc[T] { + return func(r *http.Request, inner *http.Client, user T) (*http.Client, error) { + username, password, err := fn(r.Context(), user) + if err != nil { + return inner, err + } + + r.SetBasicAuth(username, password) + + r.BasicAuth() + + return inner, nil + } +} + func CookieAuth[T any](key string, fn TokenAuthenticatorFunc[T]) AuthenticateFunc[T] { return func(r *http.Request) (T, error) { var empty T @@ -146,6 +223,32 @@ func CookieAuth[T any](key string, fn TokenAuthenticatorFunc[T]) AuthenticateFun } } +func CookieClientAuth[T any](fn ClientCookieAuthenticatorFunc[T]) ClientAuthenticateFunc[T] { + return func(r *http.Request, inner *http.Client, user T) (*http.Client, error) { + cookie, err := fn(r.Context(), user) + if err != nil { + return inner, err + } + + if cookie != nil { + r.AddCookie(cookie) + } + + return inner, nil + } +} + +func WrapClientAuth[T any](fn ClientWrappingAuthenticatorFunc[T]) ClientAuthenticateFunc[T] { + return func(r *http.Request, inner *http.Client, user T) (*http.Client, error) { + client, err := fn(r.Context(), inner, user) + if err != nil { + return inner, err + } + + return client, nil + } +} + func NewAuthCheck[T any](authenticate AuthenticateFunc[T], authorize AuthorizeFunc[T], scopes ...string) AuthCheck[T] { return AuthCheck[T]{ authenticate: authenticate, From a2f17d43e8fa5dc7818e8c142c7d6151276a88bb Mon Sep 17 00:00:00 2001 From: David Newgas Date: Fri, 10 Jul 2026 10:35:49 -0700 Subject: [PATCH 03/16] Document how client authenticators should signal failure --- httputil/auth.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/httputil/auth.go b/httputil/auth.go index 70a9d97..68179d0 100644 --- a/httputil/auth.go +++ b/httputil/auth.go @@ -29,6 +29,8 @@ const ( // ErrMissingAuthorizer is caused by internal configuration errors when evaluating authorization. ErrMissingAuthorizer = AuthError("missing authenticator") + ErrCouldntAuthenticate = AuthError("could not authenticate request") + // BasicAuthPrefix as defined by https://datatracker.ietf.org/doc/html/rfc7617 BasicAuthPrefix = "Basic " @@ -53,23 +55,28 @@ type TokenAuthenticatorFunc[T any] func(ctx context.Context, token string) (T, e type BasicAuthenticatorFunc[T any] func(ctx context.Context, user, pass string) (T, error) // ClientAuthenticateFunc is the signature of a function used to add authentication to an outbound HTTP request. -// It also has an opportunity to wrap the httpClient to be used for the request. +// It also has an opportunity to wrap the httpClient to be used for the request. Should return [ErrCouldntAuthenticate] +// if the method was not able to provide authorization for this user. type ClientAuthenticateFunc[T any] func(r *http.Request, innerClient *http.Client, user T) (*http.Client, error) // ClientTokenAuthenticatorFunc is the signature of a function used to determine the token that should be added -// to an outbound HTTP request as authentication. +// to an outbound HTTP request as authentication. Should return [ErrCouldntAuthenticate] if the method was not able to +// provide authorization for this user. type ClientTokenAuthenticatorFunc[T any] func(ctx context.Context, user T) (string, error) // ClientBasicAuthenticatorFunc is the signature of a function used to determine the username and password -// that should be used to add Bsaic authentication to an outbound HTTP request. +// that should be used to add Bsaic authentication to an outbound HTTP request. Should return [ErrCouldntAuthenticate] +// if the method was not able to provide authorization for this user. type ClientBasicAuthenticatorFunc[T any] func(ctx context.Context, user T) (string, string, error) -// ClientCookieAuthenticatorFunc is the signature of a function used to determine the cookie -// that should be added to an outbound HTTP request. +// ClientCookieAuthenticatorFunc is the signature of a function used to determine the cookie that should be added +// to an outbound HTTP request. Should return [ErrCouldntAuthenticate] if the method was not able to provide +// authorization for this user. type ClientCookieAuthenticatorFunc[T any] func(ctx context.Context, user T) (*http.Cookie, error) // ClientWrappingAuthenticatorFunc is the signature of a function used to wrap an http client so that it will -// add authentication. This is intended for integration with x/oauth2. +// add authentication. This is intended for integration with x/oauth2. Should return [ErrCouldntAuthenticate] if the +// method was not able to provide authorization for this user. type ClientWrappingAuthenticatorFunc[T any] func(ctx context.Context, innerClient *http.Client, user T) (*http.Client, error) // AuthorizeFunc is the signature of a function used to authorize a request. If unable From 0bfb42545e9c2a63fc9cce11cf9a487d49ecf3d5 Mon Sep 17 00:00:00 2001 From: David Newgas Date: Fri, 10 Jul 2026 12:52:04 -0700 Subject: [PATCH 04/16] Add client security group helper types Mirroring SecurityGroup and SecurityGroups, this commit adds ClientSecurityGroup and ClientSecurityGroups to provide the logic for a foji client to combine authenticators to represent the security requirements for an openapi operation. --- httputil/auth.go | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/httputil/auth.go b/httputil/auth.go index 68179d0..2decbf5 100644 --- a/httputil/auth.go +++ b/httputil/auth.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/base64" + "errors" "fmt" "net/http" "strings" @@ -327,3 +328,43 @@ func (s SecurityGroups[T]) Auth(r *http.Request) (T, error) { return user, err } + +// ClientSecurityGroup are valid if all the authenticate functions succeed. +type ClientSecurityGroup[T any] []ClientAuthenticateFunc[T] + +// Auth authenticates a client request with all the authhenticate functions or returns the first failure. +func (s ClientSecurityGroup[T]) Auth(r *http.Request, innerClient *http.Client, u T) (*http.Client, error) { + var err error + outerClient := innerClient + modifiedReq := r.Clone(r.Context()) + + for _, a := range s { + outerClient, err = a(modifiedReq, outerClient, u) + if err != nil { + return innerClient, err + } + } + + *r = *modifiedReq + + return outerClient, nil +} + +// ClientSecurityGroups are valid if ANY group is valid. +type ClientSecurityGroups[T any] []ClientSecurityGroup[T] + +// Auth authenticates a client request with the first group that successfully authenticates, or returns +// [ErrCouldntAuthenticate]. +func (s ClientSecurityGroups[T]) Auth(r *http.Request, innerClient *http.Client, u T) (*http.Client, error) { + var err error + for _, a := range s { + innerClient, err = a.Auth(r, innerClient, u) + if errors.Is(err, ErrCouldntAuthenticate) { + continue + } + // return on success or true failure + return innerClient, err + } + + return innerClient, ErrCouldntAuthenticate +} From 345fb0aaf970fa18a4548006c44c52349a55ce23 Mon Sep 17 00:00:00 2001 From: David Newgas Date: Fri, 10 Jul 2026 13:10:59 -0700 Subject: [PATCH 05/16] Add tests --- httputil/auth_test.go | 352 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 352 insertions(+) diff --git a/httputil/auth_test.go b/httputil/auth_test.go index a10be1b..0156543 100644 --- a/httputil/auth_test.go +++ b/httputil/auth_test.go @@ -331,3 +331,355 @@ func TestBasicAuth(t *testing.T) { }) } } + +func strClientAuth(_ context.Context, user string) (string, error) { + switch user { + case "good": + return "token", nil + case "bad": + return "", ErrBad + } + + return "", httputil.ErrCouldntAuthenticate +} + +func failClientAuth(_ context.Context, _ string) (string, error) { + return "", ErrBad +} + +func onlyUser(name string) httputil.ClientTokenAuthenticatorFunc[string] { + return func(_ context.Context, user string) (string, error) { + if user == name { + return "token", nil + } + + return "", httputil.ErrCouldntAuthenticate + } +} + +func basicClientAuth(_ context.Context, user string) (string, string, error) { + switch user { + case "good": + return "u", "p", nil + case "bad": + return "", "", ErrBad + } + + return "", "", httputil.ErrCouldntAuthenticate +} + +func cookieClientAuth(_ context.Context, user string) (*http.Cookie, error) { + switch user { + case "good": + return &http.Cookie{Name: "session", Value: "token"}, nil + case "nil": + return nil, nil + case "bad": + return nil, ErrBad + } + + return nil, httputil.ErrCouldntAuthenticate +} + +func TestHeaderClientAuth(t *testing.T) { + type testCase struct { + name string + key string + user string + want string + err error + } + tests := []testCase{ + {"Good", "X-Auth", "good", "token", nil}, + {"Bad", "X-Auth", "bad", "", ErrBad}, + {"Couldnt", "X-Auth", "other", "", httputil.ErrCouldntAuthenticate}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest("FOO", "/asdf", nil) + inner := &http.Client{} + + got, err := httputil.HeaderClientAuth(tt.key, strClientAuth)(r, inner, tt.user) + if tt.err != nil { + assert.ErrorIs(t, err, tt.err) + } else { + require.NoError(t, err) + } + + assert.Same(t, inner, got) + assert.Equal(t, tt.want, r.Header.Get(tt.key)) + }) + } +} + +func TestBearerClientAuth(t *testing.T) { + type testCase struct { + name string + key string + user string + want string + err error + } + tests := []testCase{ + {"Good", "Authorization", "good", "Bearer token", nil}, + {"Bad", "Authorization", "bad", "", ErrBad}, + {"Couldnt", "Authorization", "other", "", httputil.ErrCouldntAuthenticate}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest("FOO", "/asdf", nil) + inner := &http.Client{} + + got, err := httputil.BearerClientAuth(tt.key, strClientAuth)(r, inner, tt.user) + if tt.err != nil { + assert.ErrorIs(t, err, tt.err) + } else { + require.NoError(t, err) + } + + assert.Same(t, inner, got) + assert.Equal(t, tt.want, r.Header.Get(tt.key)) + }) + } +} + +func TestQueryClientAuth(t *testing.T) { + type testCase struct { + name string + key string + user string + want string + err error + } + tests := []testCase{ + {"Good", "auth", "good", "token", nil}, + {"Bad", "auth", "bad", "", ErrBad}, + {"Couldnt", "auth", "other", "", httputil.ErrCouldntAuthenticate}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest("FOO", "/asdf", nil) + inner := &http.Client{} + + got, err := httputil.QueryClientAuth(tt.key, strClientAuth)(r, inner, tt.user) + if tt.err != nil { + assert.ErrorIs(t, err, tt.err) + } else { + require.NoError(t, err) + } + + assert.Same(t, inner, got) + assert.Equal(t, tt.want, r.URL.Query().Get(tt.key)) + }) + } +} + +func TestBasicClientAuth(t *testing.T) { + type testCase struct { + name string + user string + wantUser string + wantPass string + err error + } + tests := []testCase{ + {"Good", "good", "u", "p", nil}, + {"Bad", "bad", "", "", ErrBad}, + {"Couldnt", "other", "", "", httputil.ErrCouldntAuthenticate}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest("FOO", "/asdf", nil) + inner := &http.Client{} + + got, err := httputil.BasicClientAuth(basicClientAuth)(r, inner, tt.user) + if tt.err != nil { + assert.ErrorIs(t, err, tt.err) + } else { + require.NoError(t, err) + } + + assert.Same(t, inner, got) + + u, p, ok := r.BasicAuth() + if tt.err == nil { + assert.True(t, ok) + assert.Equal(t, tt.wantUser, u) + assert.Equal(t, tt.wantPass, p) + } else { + assert.False(t, ok) + } + }) + } +} + +func TestCookieClientAuth(t *testing.T) { + type testCase struct { + name string + user string + want string + wantErr error + } + tests := []testCase{ + {"Good", "good", "token", nil}, + {"Nil", "nil", "", nil}, + {"Bad", "bad", "", ErrBad}, + {"Couldnt", "other", "", httputil.ErrCouldntAuthenticate}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest("FOO", "/asdf", nil) + inner := &http.Client{} + + got, err := httputil.CookieClientAuth(cookieClientAuth)(r, inner, tt.user) + if tt.wantErr != nil { + assert.ErrorIs(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + + assert.Same(t, inner, got) + + cookie, cookieErr := r.Cookie("session") + if tt.want != "" { + require.NoError(t, cookieErr) + assert.Equal(t, tt.want, cookie.Value) + } else { + assert.ErrorIs(t, cookieErr, http.ErrNoCookie) + } + }) + } +} + +func TestWrapClientAuth(t *testing.T) { + inner := &http.Client{} + wrapped := &http.Client{} + + fn := func(_ context.Context, in *http.Client, user string) (*http.Client, error) { + switch user { + case "good": + return wrapped, nil + case "bad": + return nil, ErrBad + } + + return in, httputil.ErrCouldntAuthenticate + } + + type testCase struct { + name string + user string + want *http.Client + err error + } + tests := []testCase{ + {"good", "good", wrapped, nil}, + {"bad", "bad", inner, ErrBad}, + {"couldnt", "other", inner, httputil.ErrCouldntAuthenticate}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest("FOO", "/asdf", nil) + + got, err := httputil.WrapClientAuth(fn)(r, inner, tt.user) + if tt.err != nil { + assert.ErrorIs(t, err, tt.err) + } else { + require.NoError(t, err) + } + + assert.Same(t, tt.want, got) + }) + } +} + +func TestClientSecurityGroup_Auth(t *testing.T) { + inner := &http.Client{} + + type testCase struct { + name string + g httputil.ClientSecurityGroup[string] + user string + wantA string + wantB string + err error + } + tests := []testCase{ + { + "all succeed", + httputil.ClientSecurityGroup[string]{ + httputil.HeaderClientAuth("X-A", strClientAuth), + httputil.HeaderClientAuth("X-B", strClientAuth), + }, + "good", "token", "token", nil, + }, + { + "failure returns inner and leaves request unmodified", + httputil.ClientSecurityGroup[string]{ + httputil.HeaderClientAuth("X-A", strClientAuth), + httputil.HeaderClientAuth("X-B", failClientAuth), + }, + "good", "", "", ErrBad, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest("FOO", "/asdf", nil) + + got, err := tt.g.Auth(r, inner, tt.user) + if tt.err != nil { + assert.ErrorIs(t, err, tt.err) + } else { + require.NoError(t, err) + } + + assert.Same(t, inner, got) + assert.Equal(t, tt.wantA, r.Header.Get("X-A")) + assert.Equal(t, tt.wantB, r.Header.Get("X-B")) + }) + } +} + +func TestClientSecurityGroups_Auth(t *testing.T) { + inner := &http.Client{} + + groups := httputil.ClientSecurityGroups[string]{ + {httputil.HeaderClientAuth("X-A", onlyUser("a"))}, + {httputil.HeaderClientAuth("X-B", onlyUser("b"))}, + } + failing := httputil.ClientSecurityGroups[string]{ + {httputil.HeaderClientAuth("X-A", failClientAuth)}, + {httputil.HeaderClientAuth("X-B", onlyUser("b"))}, + } + + type testCase struct { + name string + s httputil.ClientSecurityGroups[string] + user string + wantA string + wantB string + err error + } + tests := []testCase{ + {"first group succeeds", groups, "a", "token", "", nil}, + {"falls through to a later group", groups, "b", "", "token", nil}, + {"none can authenticate", groups, "c", "", "", httputil.ErrCouldntAuthenticate}, + {"true failure stops iteration", failing, "b", "", "", ErrBad}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := httptest.NewRequest("FOO", "/asdf", nil) + + got, err := tt.s.Auth(r, inner, tt.user) + if tt.err != nil { + assert.ErrorIs(t, err, tt.err) + } else { + require.NoError(t, err) + } + + assert.Same(t, inner, got) + assert.Equal(t, tt.wantA, r.Header.Get("X-A")) + assert.Equal(t, tt.wantB, r.Header.Get("X-B")) + }) + } +} From b05f545e6d8711b9cee3f5a9723084440a9b8ba5 Mon Sep 17 00:00:00 2001 From: David Newgas Date: Fri, 10 Jul 2026 13:14:15 -0700 Subject: [PATCH 06/16] fmt & lint changes --- httputil/auth.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/httputil/auth.go b/httputil/auth.go index 2decbf5..906bd12 100644 --- a/httputil/auth.go +++ b/httputil/auth.go @@ -78,7 +78,8 @@ type ClientCookieAuthenticatorFunc[T any] func(ctx context.Context, user T) (*ht // ClientWrappingAuthenticatorFunc is the signature of a function used to wrap an http client so that it will // add authentication. This is intended for integration with x/oauth2. Should return [ErrCouldntAuthenticate] if the // method was not able to provide authorization for this user. -type ClientWrappingAuthenticatorFunc[T any] func(ctx context.Context, innerClient *http.Client, user T) (*http.Client, error) +type ClientWrappingAuthenticatorFunc[T any] func( + ctx context.Context, innerClient *http.Client, user T) (*http.Client, error) // AuthorizeFunc is the signature of a function used to authorize a request. If unable // to authorize the user it returns an error. From 66fb4f4374990e3eac9c51cd8f05cd2bbc385c7b Mon Sep 17 00:00:00 2001 From: David Newgas Date: Fri, 10 Jul 2026 13:14:59 -0700 Subject: [PATCH 07/16] fmt & lint existing code --- currency/fixed_format.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/currency/fixed_format.go b/currency/fixed_format.go index 386405c..e89eeb5 100644 --- a/currency/fixed_format.go +++ b/currency/fixed_format.go @@ -78,7 +78,7 @@ func (cf FixedFormatter) formatFractionalPart(fractionalPart int64) string { offset := Power10(cf.precision) / cf.factor for offset > 1 { - fractionalStr += "0" //nolint:perfsprint + fractionalStr += "0" //nolint:perfsprint,modernize offset /= base } From 52ba018ddde56733f95337ef8f260d0a5f438abd Mon Sep 17 00:00:00 2001 From: David Newgas Date: Fri, 10 Jul 2026 13:21:54 -0700 Subject: [PATCH 08/16] Add test for UnexpectedResponseError --- httputil/client_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 httputil/client_test.go diff --git a/httputil/client_test.go b/httputil/client_test.go new file mode 100644 index 0000000..94cee26 --- /dev/null +++ b/httputil/client_test.go @@ -0,0 +1,22 @@ +package httputil_test + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/bir/iken/httputil" +) + +func TestUnexpectedResponseError_Error(t *testing.T) { + err := httputil.UnexpectedResponseError{ + Resp: &http.Response{StatusCode: http.StatusNotFound}, + URL: "http://example.com/foo", + Body: []byte("not found"), + } + + assert.Equal(t, + `url: "http://example.com/foo", status: 404, body: "not found": unexpected response status code`, + err.Error()) +} From f66422bea4a2ec1896b6b566043e4a1605ac3414 Mon Sep 17 00:00:00 2001 From: David Newgas Date: Fri, 10 Jul 2026 16:07:36 -0700 Subject: [PATCH 09/16] Remove redundant http.Request.BasicAuth call --- httputil/auth.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/httputil/auth.go b/httputil/auth.go index 906bd12..0fbf99c 100644 --- a/httputil/auth.go +++ b/httputil/auth.go @@ -213,8 +213,6 @@ func BasicClientAuth[T any](fn ClientBasicAuthenticatorFunc[T]) ClientAuthentica r.SetBasicAuth(username, password) - r.BasicAuth() - return inner, nil } } From e1ec5f0c57a80250abac3a3b51ac623ad380cd8c Mon Sep 17 00:00:00 2001 From: David Newgas Date: Fri, 10 Jul 2026 16:12:48 -0700 Subject: [PATCH 10/16] Handle nil Resp in UnexpectedResponseError --- httputil/client.go | 8 +++++++- httputil/client_test.go | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/httputil/client.go b/httputil/client.go index 7715e4c..235366a 100644 --- a/httputil/client.go +++ b/httputil/client.go @@ -3,6 +3,7 @@ package httputil import ( "fmt" "net/http" + "strconv" ) type UnexpectedResponseError struct { @@ -12,5 +13,10 @@ type UnexpectedResponseError struct { } func (e UnexpectedResponseError) Error() string { - return fmt.Sprintf("url: %q, status: %d, body: %q: unexpected response status code", e.URL, e.Resp.StatusCode, e.Body) + status := "unknown" + if e.Resp != nil { + status = strconv.Itoa(e.Resp.StatusCode) + } + + return fmt.Sprintf("url: %q, status: %s, body: %q: unexpected response status code", e.URL, status, e.Body) } diff --git a/httputil/client_test.go b/httputil/client_test.go index 94cee26..37dbca5 100644 --- a/httputil/client_test.go +++ b/httputil/client_test.go @@ -20,3 +20,14 @@ func TestUnexpectedResponseError_Error(t *testing.T) { `url: "http://example.com/foo", status: 404, body: "not found": unexpected response status code`, err.Error()) } + +func TestUnexpectedResponseError_Error_NilResp(t *testing.T) { + err := httputil.UnexpectedResponseError{ + URL: "http://example.com/foo", + Body: []byte("not found"), + } + + assert.Equal(t, + `url: "http://example.com/foo", status: unknown, body: "not found": unexpected response status code`, + err.Error()) +} From 810e9e0924f1ee447092d3903b4c60062a9ac1b6 Mon Sep 17 00:00:00 2001 From: David Newgas Date: Fri, 10 Jul 2026 16:17:07 -0700 Subject: [PATCH 11/16] Ensure ClientSecurityGroups never mixes http.Client modifications We only want the http.Client used in a request to be modified by authorizers that are actually getting used in the request. ClientSecurityGroup does avoid returning a modified http.Client if it fails, but for defensive programming https://github.com/bir/iken/pull/57#discussion_r3562042547 requests that ClientSecurityGroups doesn't rely on this behaviour. We therefore make two changes: 1. ClientSecurityGroups stores the http.Client returned by a ClientSecurityGroup separately from the inbound client, so it is discarded when the next ClientSecurityGroup is tried. 2. If ClientSecurityGroups returns because of an error, it returns the unmodified original http.Client. --- httputil/auth.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/httputil/auth.go b/httputil/auth.go index 0fbf99c..78c5b1b 100644 --- a/httputil/auth.go +++ b/httputil/auth.go @@ -355,14 +355,17 @@ type ClientSecurityGroups[T any] []ClientSecurityGroup[T] // Auth authenticates a client request with the first group that successfully authenticates, or returns // [ErrCouldntAuthenticate]. func (s ClientSecurityGroups[T]) Auth(r *http.Request, innerClient *http.Client, u T) (*http.Client, error) { - var err error for _, a := range s { - innerClient, err = a.Auth(r, innerClient, u) + outerClient, err := a.Auth(r, innerClient, u) if errors.Is(err, ErrCouldntAuthenticate) { continue } - // return on success or true failure - return innerClient, err + + if err != nil { + return innerClient, err + } + + return outerClient, nil } return innerClient, ErrCouldntAuthenticate From 769ecda29bf6965a5c6c7d23384eaad87a8f365c Mon Sep 17 00:00:00 2001 From: David Newgas Date: Fri, 10 Jul 2026 16:23:46 -0700 Subject: [PATCH 12/16] Add missing comment --- httputil/auth.go | 1 + 1 file changed, 1 insertion(+) diff --git a/httputil/auth.go b/httputil/auth.go index 78c5b1b..dd19af2 100644 --- a/httputil/auth.go +++ b/httputil/auth.go @@ -30,6 +30,7 @@ const ( // ErrMissingAuthorizer is caused by internal configuration errors when evaluating authorization. ErrMissingAuthorizer = AuthError("missing authenticator") + // ErrCannotAuthenticate indicates an authenticator could not provide credentials for the user. ClientSecurityGroups treats it as "try the next group" rather than a hard failure.```` ErrCouldntAuthenticate = AuthError("could not authenticate request") // BasicAuthPrefix as defined by https://datatracker.ietf.org/doc/html/rfc7617 From 32ed4a492dfd8e6cf7001bc882a3033d1ff3a5d0 Mon Sep 17 00:00:00 2001 From: David Newgas Date: Fri, 10 Jul 2026 16:24:12 -0700 Subject: [PATCH 13/16] Fix typo --- httputil/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httputil/auth.go b/httputil/auth.go index dd19af2..43d642d 100644 --- a/httputil/auth.go +++ b/httputil/auth.go @@ -67,7 +67,7 @@ type ClientAuthenticateFunc[T any] func(r *http.Request, innerClient *http.Clien type ClientTokenAuthenticatorFunc[T any] func(ctx context.Context, user T) (string, error) // ClientBasicAuthenticatorFunc is the signature of a function used to determine the username and password -// that should be used to add Bsaic authentication to an outbound HTTP request. Should return [ErrCouldntAuthenticate] +// that should be used to add Basic authentication to an outbound HTTP request. Should return [ErrCouldntAuthenticate] // if the method was not able to provide authorization for this user. type ClientBasicAuthenticatorFunc[T any] func(ctx context.Context, user T) (string, string, error) From f6a86c54a8881692a237ec16be667b4d87951919 Mon Sep 17 00:00:00 2001 From: David Newgas Date: Fri, 10 Jul 2026 16:51:38 -0700 Subject: [PATCH 14/16] Rename to ErrCannotAuthenticate --- httputil/auth.go | 21 +++++++++++---------- httputil/auth_test.go | 24 ++++++++++++------------ 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/httputil/auth.go b/httputil/auth.go index 43d642d..2f84f64 100644 --- a/httputil/auth.go +++ b/httputil/auth.go @@ -30,8 +30,9 @@ const ( // ErrMissingAuthorizer is caused by internal configuration errors when evaluating authorization. ErrMissingAuthorizer = AuthError("missing authenticator") - // ErrCannotAuthenticate indicates an authenticator could not provide credentials for the user. ClientSecurityGroups treats it as "try the next group" rather than a hard failure.```` - ErrCouldntAuthenticate = AuthError("could not authenticate request") + // ErrCannotAuthenticate indicates an authenticator could not provide credentials for the user. + // ClientSecurityGroups treats it as "try the next group" rather than a hard failure. + ErrCannotAuthenticate = AuthError("could not authenticate request") // BasicAuthPrefix as defined by https://datatracker.ietf.org/doc/html/rfc7617 BasicAuthPrefix = "Basic " @@ -57,27 +58,27 @@ type TokenAuthenticatorFunc[T any] func(ctx context.Context, token string) (T, e type BasicAuthenticatorFunc[T any] func(ctx context.Context, user, pass string) (T, error) // ClientAuthenticateFunc is the signature of a function used to add authentication to an outbound HTTP request. -// It also has an opportunity to wrap the httpClient to be used for the request. Should return [ErrCouldntAuthenticate] +// It also has an opportunity to wrap the httpClient to be used for the request. Should return [ErrCannotAuthenticate] // if the method was not able to provide authorization for this user. type ClientAuthenticateFunc[T any] func(r *http.Request, innerClient *http.Client, user T) (*http.Client, error) // ClientTokenAuthenticatorFunc is the signature of a function used to determine the token that should be added -// to an outbound HTTP request as authentication. Should return [ErrCouldntAuthenticate] if the method was not able to +// to an outbound HTTP request as authentication. Should return [ErrCannotAuthenticate] if the method was not able to // provide authorization for this user. type ClientTokenAuthenticatorFunc[T any] func(ctx context.Context, user T) (string, error) // ClientBasicAuthenticatorFunc is the signature of a function used to determine the username and password -// that should be used to add Basic authentication to an outbound HTTP request. Should return [ErrCouldntAuthenticate] +// that should be used to add Basic authentication to an outbound HTTP request. Should return [ErrCannotAuthenticate] // if the method was not able to provide authorization for this user. type ClientBasicAuthenticatorFunc[T any] func(ctx context.Context, user T) (string, string, error) // ClientCookieAuthenticatorFunc is the signature of a function used to determine the cookie that should be added -// to an outbound HTTP request. Should return [ErrCouldntAuthenticate] if the method was not able to provide +// to an outbound HTTP request. Should return [ErrCannotAuthenticate] if the method was not able to provide // authorization for this user. type ClientCookieAuthenticatorFunc[T any] func(ctx context.Context, user T) (*http.Cookie, error) // ClientWrappingAuthenticatorFunc is the signature of a function used to wrap an http client so that it will -// add authentication. This is intended for integration with x/oauth2. Should return [ErrCouldntAuthenticate] if the +// add authentication. This is intended for integration with x/oauth2. Should return [ErrCannotAuthenticate] if the // method was not able to provide authorization for this user. type ClientWrappingAuthenticatorFunc[T any] func( ctx context.Context, innerClient *http.Client, user T) (*http.Client, error) @@ -354,11 +355,11 @@ func (s ClientSecurityGroup[T]) Auth(r *http.Request, innerClient *http.Client, type ClientSecurityGroups[T any] []ClientSecurityGroup[T] // Auth authenticates a client request with the first group that successfully authenticates, or returns -// [ErrCouldntAuthenticate]. +// [ErrCannotAuthenticate]. func (s ClientSecurityGroups[T]) Auth(r *http.Request, innerClient *http.Client, u T) (*http.Client, error) { for _, a := range s { outerClient, err := a.Auth(r, innerClient, u) - if errors.Is(err, ErrCouldntAuthenticate) { + if errors.Is(err, ErrCannotAuthenticate) { continue } @@ -369,5 +370,5 @@ func (s ClientSecurityGroups[T]) Auth(r *http.Request, innerClient *http.Client, return outerClient, nil } - return innerClient, ErrCouldntAuthenticate + return innerClient, ErrCannotAuthenticate } diff --git a/httputil/auth_test.go b/httputil/auth_test.go index 0156543..da48f9b 100644 --- a/httputil/auth_test.go +++ b/httputil/auth_test.go @@ -340,7 +340,7 @@ func strClientAuth(_ context.Context, user string) (string, error) { return "", ErrBad } - return "", httputil.ErrCouldntAuthenticate + return "", httputil.ErrCannotAuthenticate } func failClientAuth(_ context.Context, _ string) (string, error) { @@ -353,7 +353,7 @@ func onlyUser(name string) httputil.ClientTokenAuthenticatorFunc[string] { return "token", nil } - return "", httputil.ErrCouldntAuthenticate + return "", httputil.ErrCannotAuthenticate } } @@ -365,7 +365,7 @@ func basicClientAuth(_ context.Context, user string) (string, string, error) { return "", "", ErrBad } - return "", "", httputil.ErrCouldntAuthenticate + return "", "", httputil.ErrCannotAuthenticate } func cookieClientAuth(_ context.Context, user string) (*http.Cookie, error) { @@ -378,7 +378,7 @@ func cookieClientAuth(_ context.Context, user string) (*http.Cookie, error) { return nil, ErrBad } - return nil, httputil.ErrCouldntAuthenticate + return nil, httputil.ErrCannotAuthenticate } func TestHeaderClientAuth(t *testing.T) { @@ -392,7 +392,7 @@ func TestHeaderClientAuth(t *testing.T) { tests := []testCase{ {"Good", "X-Auth", "good", "token", nil}, {"Bad", "X-Auth", "bad", "", ErrBad}, - {"Couldnt", "X-Auth", "other", "", httputil.ErrCouldntAuthenticate}, + {"Couldnt", "X-Auth", "other", "", httputil.ErrCannotAuthenticate}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -423,7 +423,7 @@ func TestBearerClientAuth(t *testing.T) { tests := []testCase{ {"Good", "Authorization", "good", "Bearer token", nil}, {"Bad", "Authorization", "bad", "", ErrBad}, - {"Couldnt", "Authorization", "other", "", httputil.ErrCouldntAuthenticate}, + {"Couldnt", "Authorization", "other", "", httputil.ErrCannotAuthenticate}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -454,7 +454,7 @@ func TestQueryClientAuth(t *testing.T) { tests := []testCase{ {"Good", "auth", "good", "token", nil}, {"Bad", "auth", "bad", "", ErrBad}, - {"Couldnt", "auth", "other", "", httputil.ErrCouldntAuthenticate}, + {"Couldnt", "auth", "other", "", httputil.ErrCannotAuthenticate}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -485,7 +485,7 @@ func TestBasicClientAuth(t *testing.T) { tests := []testCase{ {"Good", "good", "u", "p", nil}, {"Bad", "bad", "", "", ErrBad}, - {"Couldnt", "other", "", "", httputil.ErrCouldntAuthenticate}, + {"Couldnt", "other", "", "", httputil.ErrCannotAuthenticate}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -524,7 +524,7 @@ func TestCookieClientAuth(t *testing.T) { {"Good", "good", "token", nil}, {"Nil", "nil", "", nil}, {"Bad", "bad", "", ErrBad}, - {"Couldnt", "other", "", httputil.ErrCouldntAuthenticate}, + {"Couldnt", "other", "", httputil.ErrCannotAuthenticate}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -563,7 +563,7 @@ func TestWrapClientAuth(t *testing.T) { return nil, ErrBad } - return in, httputil.ErrCouldntAuthenticate + return in, httputil.ErrCannotAuthenticate } type testCase struct { @@ -575,7 +575,7 @@ func TestWrapClientAuth(t *testing.T) { tests := []testCase{ {"good", "good", wrapped, nil}, {"bad", "bad", inner, ErrBad}, - {"couldnt", "other", inner, httputil.ErrCouldntAuthenticate}, + {"couldnt", "other", inner, httputil.ErrCannotAuthenticate}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -663,7 +663,7 @@ func TestClientSecurityGroups_Auth(t *testing.T) { tests := []testCase{ {"first group succeeds", groups, "a", "token", "", nil}, {"falls through to a later group", groups, "b", "", "token", nil}, - {"none can authenticate", groups, "c", "", "", httputil.ErrCouldntAuthenticate}, + {"none can authenticate", groups, "c", "", "", httputil.ErrCannotAuthenticate}, {"true failure stops iteration", failing, "b", "", "", ErrBad}, } for _, tt := range tests { From 68a758c0071e2a7bf1c55c8146de60d6abca35fb Mon Sep 17 00:00:00 2001 From: David Newgas Date: Mon, 13 Jul 2026 09:56:00 -0700 Subject: [PATCH 15/16] Header/bearer auth should set, not add a header --- httputil/auth.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httputil/auth.go b/httputil/auth.go index 2f84f64..36a42c4 100644 --- a/httputil/auth.go +++ b/httputil/auth.go @@ -115,7 +115,7 @@ func HeaderClientAuth[T any](key string, fn ClientTokenAuthenticatorFunc[T]) Cli return inner, err } - r.Header.Add(key, token) + r.Header.Set(key, token) return inner, nil } @@ -143,7 +143,7 @@ func BearerClientAuth[T any](key string, fn ClientTokenAuthenticatorFunc[T]) Cli return inner, err } - r.Header.Add(key, bearerAuthPrefix+token) + r.Header.Set(key, bearerAuthPrefix+token) return inner, nil } From 7a62181e9f2716b8c4718f938eff835c9172fa6d Mon Sep 17 00:00:00 2001 From: David Newgas Date: Tue, 14 Jul 2026 11:59:12 -0700 Subject: [PATCH 16/16] Don't return original httpClient when returning error --- httputil/auth.go | 18 +++++++++--------- httputil/auth_test.go | 19 ++++++++----------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/httputil/auth.go b/httputil/auth.go index 36a42c4..170eaf3 100644 --- a/httputil/auth.go +++ b/httputil/auth.go @@ -112,7 +112,7 @@ func HeaderClientAuth[T any](key string, fn ClientTokenAuthenticatorFunc[T]) Cli return func(r *http.Request, inner *http.Client, user T) (*http.Client, error) { token, err := fn(r.Context(), user) if err != nil { - return inner, err + return nil, err } r.Header.Set(key, token) @@ -140,7 +140,7 @@ func BearerClientAuth[T any](key string, fn ClientTokenAuthenticatorFunc[T]) Cli return func(r *http.Request, inner *http.Client, user T) (*http.Client, error) { token, err := fn(r.Context(), user) if err != nil { - return inner, err + return nil, err } r.Header.Set(key, bearerAuthPrefix+token) @@ -166,7 +166,7 @@ func QueryClientAuth[T any](key string, fn ClientTokenAuthenticatorFunc[T]) Clie return func(r *http.Request, inner *http.Client, user T) (*http.Client, error) { token, err := fn(r.Context(), user) if err != nil { - return inner, err + return nil, err } q := r.URL.Query() @@ -210,7 +210,7 @@ func BasicClientAuth[T any](fn ClientBasicAuthenticatorFunc[T]) ClientAuthentica return func(r *http.Request, inner *http.Client, user T) (*http.Client, error) { username, password, err := fn(r.Context(), user) if err != nil { - return inner, err + return nil, err } r.SetBasicAuth(username, password) @@ -236,7 +236,7 @@ func CookieClientAuth[T any](fn ClientCookieAuthenticatorFunc[T]) ClientAuthenti return func(r *http.Request, inner *http.Client, user T) (*http.Client, error) { cookie, err := fn(r.Context(), user) if err != nil { - return inner, err + return nil, err } if cookie != nil { @@ -251,7 +251,7 @@ func WrapClientAuth[T any](fn ClientWrappingAuthenticatorFunc[T]) ClientAuthenti return func(r *http.Request, inner *http.Client, user T) (*http.Client, error) { client, err := fn(r.Context(), inner, user) if err != nil { - return inner, err + return nil, err } return client, nil @@ -342,7 +342,7 @@ func (s ClientSecurityGroup[T]) Auth(r *http.Request, innerClient *http.Client, for _, a := range s { outerClient, err = a(modifiedReq, outerClient, u) if err != nil { - return innerClient, err + return nil, err } } @@ -364,11 +364,11 @@ func (s ClientSecurityGroups[T]) Auth(r *http.Request, innerClient *http.Client, } if err != nil { - return innerClient, err + return nil, err } return outerClient, nil } - return innerClient, ErrCannotAuthenticate + return nil, ErrCannotAuthenticate } diff --git a/httputil/auth_test.go b/httputil/auth_test.go index da48f9b..9c48c63 100644 --- a/httputil/auth_test.go +++ b/httputil/auth_test.go @@ -404,9 +404,9 @@ func TestHeaderClientAuth(t *testing.T) { assert.ErrorIs(t, err, tt.err) } else { require.NoError(t, err) + assert.Same(t, inner, got) } - assert.Same(t, inner, got) assert.Equal(t, tt.want, r.Header.Get(tt.key)) }) } @@ -435,9 +435,9 @@ func TestBearerClientAuth(t *testing.T) { assert.ErrorIs(t, err, tt.err) } else { require.NoError(t, err) + assert.Same(t, inner, got) } - assert.Same(t, inner, got) assert.Equal(t, tt.want, r.Header.Get(tt.key)) }) } @@ -466,9 +466,9 @@ func TestQueryClientAuth(t *testing.T) { assert.ErrorIs(t, err, tt.err) } else { require.NoError(t, err) + assert.Same(t, inner, got) } - assert.Same(t, inner, got) assert.Equal(t, tt.want, r.URL.Query().Get(tt.key)) }) } @@ -497,10 +497,9 @@ func TestBasicClientAuth(t *testing.T) { assert.ErrorIs(t, err, tt.err) } else { require.NoError(t, err) + assert.Same(t, inner, got) } - assert.Same(t, inner, got) - u, p, ok := r.BasicAuth() if tt.err == nil { assert.True(t, ok) @@ -536,10 +535,9 @@ func TestCookieClientAuth(t *testing.T) { assert.ErrorIs(t, err, tt.wantErr) } else { require.NoError(t, err) + assert.Same(t, inner, got) } - assert.Same(t, inner, got) - cookie, cookieErr := r.Cookie("session") if tt.want != "" { require.NoError(t, cookieErr) @@ -586,9 +584,8 @@ func TestWrapClientAuth(t *testing.T) { assert.ErrorIs(t, err, tt.err) } else { require.NoError(t, err) + assert.Same(t, tt.want, got) } - - assert.Same(t, tt.want, got) }) } } @@ -631,9 +628,9 @@ func TestClientSecurityGroup_Auth(t *testing.T) { assert.ErrorIs(t, err, tt.err) } else { require.NoError(t, err) + assert.Same(t, inner, got) } - assert.Same(t, inner, got) assert.Equal(t, tt.wantA, r.Header.Get("X-A")) assert.Equal(t, tt.wantB, r.Header.Get("X-B")) }) @@ -675,9 +672,9 @@ func TestClientSecurityGroups_Auth(t *testing.T) { assert.ErrorIs(t, err, tt.err) } else { require.NoError(t, err) + assert.Same(t, inner, got) } - assert.Same(t, inner, got) assert.Equal(t, tt.wantA, r.Header.Get("X-A")) assert.Equal(t, tt.wantB, r.Header.Get("X-B")) })