From 5c23271416b4b3aaa4223a67eea9dda1ef65404a Mon Sep 17 00:00:00 2001 From: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Date: Fri, 18 Sep 2026 08:35:07 -0700 Subject: [PATCH 1/4] fix(test/rekt): retry address validation until predicate passes Motivation: test/rekt.TestSinkBindingV1Deployment_BrokerAsSinkTLS's "Broker has HTTPS address" requirement flakes occasionally in CI (about 1 in 5 runs per knative-prow-updater-robot's tracking). The same "Broker has HTTPS address" requirement is also used by the PingSource, ApiServerSource, and Broker BrokerAsSinkTLS rekt features. Approach: addressable.ValidateAddress polled Address() until it returned any non-nil address, then called the validate predicate (e.g. AssertHTTPSAddress, which checks addr.URL.Scheme == "https") exactly once with no retry. A Broker's status.address can be reported before it satisfies a given predicate, e.g. reported as http:// before the reconciler flips it to https:// once the Broker's TLS state becomes ready. That one-shot check would fail immediately instead of waiting for the address to settle, causing the observed flake. ValidateAddress now polls (same wait.PollUntilContextTimeout idiom already used in this file and elsewhere in the codebase, e.g. broker.WaitForCondition) until the address is both present and passes validate(), retrying on a validation failure instead of erroring immediately. On timeout it reports the last validation error if one occurred, which is more actionable than a generic timeout error. Address() itself, and its ~8 other callers, are unchanged. Validation: Added test/rekt/resources/addressable/addressable_test.go, which seeds a fake dynamic client with an object whose status.address.url starts as http://, flips it to https:// after 60ms from a background goroutine (simulating the real timing race), and asserts ValidateAddress retries until it observes the https address rather than failing on the initial http one. - go test ./test/rekt/resources/addressable/... -run TestValidateAddress -v: PASS. - Confirmed the new test FAILS against the pre-fix code (verified via git stash on only addressable.go, rerunning the test: fails with "address is not HTTPS"), and PASSES with the fix - a failing-then-passing reproduction of the race, not a live cluster run. - go test -race -count=20 ./test/rekt/resources/addressable/...: PASS, no data race. - go build ./... and go vet ./test/rekt/...: clean. - go test ./test/rekt/...: all non-e2e-tagged packages pass. - golangci-lint run ./test/rekt/resources/addressable/...: 0 issues. - gofmt -l on changed files: clean. This change only affects test code (test/rekt), not production code paths. Report: https://github.com/knative/eventing/issues/9378 Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Assisted-by: claude-sonnet-5 (via Claude Code) --- .../rekt/resources/addressable/addressable.go | 32 ++++++- .../resources/addressable/addressable_test.go | 94 +++++++++++++++++++ 2 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 test/rekt/resources/addressable/addressable_test.go diff --git a/test/rekt/resources/addressable/addressable.go b/test/rekt/resources/addressable/addressable.go index 0c278d4aee7..4726db12820 100644 --- a/test/rekt/resources/addressable/addressable.go +++ b/test/rekt/resources/addressable/addressable.go @@ -58,12 +58,34 @@ func Address(ctx context.Context, gvr schema.GroupVersionResource, name string, func ValidateAddress(gvr schema.GroupVersionResource, name string, validate ValidateAddressFn, timings ...time.Duration) feature.StepFn { return func(ctx context.Context, t feature.T) { - addr, err := Address(ctx, gvr, name, timings...) + interval, timeout := k8s.PollTimings(ctx, timings) + var validateErr error + err := wait.PollUntilContextTimeout(ctx, interval, timeout, true, func(ctx context.Context) (bool, error) { + addr, err := k8s.Address(ctx, gvr, name) + if err != nil { + if apierrors.IsNotFound(err) { + // keep polling + return false, nil + } + // seems fatal. + return false, err + } + if addr == nil { + // keep polling + return false, nil + } + if validateErr = validate(addr); validateErr != nil { + // address exists but doesn't satisfy validate yet, keep polling + return false, nil + } + // success! + return true, nil + }) if err != nil { - t.Error(err) - return - } - if err := validate(addr); err != nil { + if validateErr != nil { + t.Error(validateErr) + return + } t.Error(err) return } diff --git a/test/rekt/resources/addressable/addressable_test.go b/test/rekt/resources/addressable/addressable_test.go new file mode 100644 index 00000000000..a215b5aaaf8 --- /dev/null +++ b/test/rekt/resources/addressable/addressable_test.go @@ -0,0 +1,94 @@ +/* +Copyright 2026 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package addressable + +import ( + "context" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + dynamicfake "k8s.io/client-go/dynamic/fake" + "knative.dev/pkg/injection/clients/dynamicclient" + "knative.dev/reconciler-test/pkg/environment" +) + +// fakeEnvironment satisfies environment.Environment by only implementing +// Namespace(), which is all k8s.Address needs from it. +type fakeEnvironment struct { + environment.Environment + ns string +} + +func (f *fakeEnvironment) Namespace() string { return f.ns } + +// TestValidateAddress_RetriesUntilValid ensures ValidateAddress keeps polling +// until the fetched address actually satisfies the validate function, rather +// than validating once as soon as any address appears. A Broker (or any +// addressable) can report an address before it satisfies a given predicate, +// e.g. reporting an http:// address before its TLS address is ready. +func TestValidateAddress_RetriesUntilValid(t *testing.T) { + const ns = "test-ns" + const name = "my-broker" + gvr := schema.GroupVersionResource{Group: "eventing.knative.dev", Version: "v1", Resource: "brokers"} + + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "eventing.knative.dev/v1", + "kind": "Broker", + "metadata": map[string]interface{}{ + "name": name, + "namespace": ns, + }, + "status": map[string]interface{}{ + "address": map[string]interface{}{ + "url": "http://my-broker.test-ns.svc.cluster.local", + }, + }, + }, + } + + scheme := runtime.NewScheme() + dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, + map[schema.GroupVersionResource]string{gvr: "BrokerList"}, obj) + + ctx := context.WithValue(context.Background(), dynamicclient.Key{}, dc) + ctx = environment.ContextWith(ctx, &fakeEnvironment{ns: ns}) + + // Simulate the address flipping to https shortly after the requirement + // starts polling, the way a Broker's address does once its TLS + // certificate becomes ready after first being reported over http. + go func() { + time.Sleep(60 * time.Millisecond) + u, err := dc.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return + } + _ = unstructured.SetNestedField(u.Object, "https://my-broker.test-ns.svc.cluster.local", "status", "address", "url") + _, _ = dc.Resource(gvr).Namespace(ns).Update(ctx, u, metav1.UpdateOptions{}) + }() + + step := ValidateAddress(gvr, name, AssertHTTPSAddress, 20*time.Millisecond, 2*time.Second) + step(ctx, t) + + if t.Failed() { + t.Fatal("ValidateAddress should have retried until the address became https, not failed on the initial http address") + } +} From 806a1ab5c1e664778a54541cc09fd4a667680158 Mon Sep 17 00:00:00 2001 From: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Date: Mon, 21 Sep 2026 05:06:43 -0700 Subject: [PATCH 2/4] test/rekt: dedupe addressable poll loop and retry transient errors Extract the polling logic shared by Address and ValidateAddress into a single pollAddress helper, and retry timeout/throttling errors from the apiserver in addition to NotFound, per review feedback on #9407. Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> --- .../rekt/resources/addressable/addressable.go | 87 ++++++++++--------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/test/rekt/resources/addressable/addressable.go b/test/rekt/resources/addressable/addressable.go index 4726db12820..c29b0d151e7 100644 --- a/test/rekt/resources/addressable/addressable.go +++ b/test/rekt/resources/addressable/addressable.go @@ -33,63 +33,66 @@ type ValidateAddressFn func(addressable *duckv1.Addressable) error // Address returns a broker's address. func Address(ctx context.Context, gvr schema.GroupVersionResource, name string, timings ...time.Duration) (*duckv1.Addressable, error) { + addr, _, err := pollAddress(ctx, gvr, name, nil, timings...) + return addr, err +} + +func ValidateAddress(gvr schema.GroupVersionResource, name string, validate ValidateAddressFn, timings ...time.Duration) feature.StepFn { + return func(ctx context.Context, t feature.T) { + _, validateErr, err := pollAddress(ctx, gvr, name, validate, timings...) + if err != nil { + if validateErr != nil { + t.Error(validateErr) + return + } + t.Error(err) + return + } + } +} + +// pollAddress polls for an addressable's Address until it is found and, if +// validate is non-nil, satisfies validate, or the poll times out. Errors +// that look transient (e.g. the resource not existing yet, a request +// timeout, or the API server throttling requests) are retried instead of +// failing the poll immediately. +func pollAddress(ctx context.Context, gvr schema.GroupVersionResource, name string, validate ValidateAddressFn, timings ...time.Duration) (addr *duckv1.Addressable, validateErr error, err error) { interval, timeout := k8s.PollTimings(ctx, timings) - var addr *duckv1.Addressable - err := wait.PollUntilContextTimeout(ctx, interval, timeout, true, func(ctx context.Context) (bool, error) { + err = wait.PollUntilContextTimeout(ctx, interval, timeout, true, func(ctx context.Context) (bool, error) { var err error addr, err = k8s.Address(ctx, gvr, name) - if err == nil && addr == nil { - // keep polling - return false, nil - } if err != nil { - if apierrors.IsNotFound(err) { + if isRetryableError(err) { // keep polling return false, nil } // seems fatal. return false, err } - // success! - return true, nil - }) - return addr, err -} - -func ValidateAddress(gvr schema.GroupVersionResource, name string, validate ValidateAddressFn, timings ...time.Duration) feature.StepFn { - return func(ctx context.Context, t feature.T) { - interval, timeout := k8s.PollTimings(ctx, timings) - var validateErr error - err := wait.PollUntilContextTimeout(ctx, interval, timeout, true, func(ctx context.Context) (bool, error) { - addr, err := k8s.Address(ctx, gvr, name) - if err != nil { - if apierrors.IsNotFound(err) { - // keep polling - return false, nil - } - // seems fatal. - return false, err - } - if addr == nil { - // keep polling - return false, nil - } + if addr == nil { + // keep polling + return false, nil + } + if validate != nil { if validateErr = validate(addr); validateErr != nil { // address exists but doesn't satisfy validate yet, keep polling return false, nil } - // success! - return true, nil - }) - if err != nil { - if validateErr != nil { - t.Error(validateErr) - return - } - t.Error(err) - return } - } + // success! + return true, nil + }) + return addr, validateErr, err +} + +// isRetryableError reports whether err is transient enough to be worth +// retrying while polling for an address: the resource not existing yet, a +// request timeout, or the API server throttling requests. +func isRetryableError(err error) bool { + return apierrors.IsNotFound(err) || + apierrors.IsTimeout(err) || + apierrors.IsServerTimeout(err) || + apierrors.IsTooManyRequests(err) } func AssertHTTPSAddress(addr *duckv1.Addressable) error { From 92037678e94a5df0180ed6ecfd285479c4d3ecee Mon Sep 17 00:00:00 2001 From: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:19:50 -0700 Subject: [PATCH 3/4] test/rekt: cover pollAddress transient-error retries Add tests for the shared pollAddress loop through Address(): it retries NotFound, Timeout, ServerTimeout and TooManyRequests errors until the address is returned, and stops on a non-transient error (Forbidden) instead of waiting for the timeout. Move the fake Broker setup into a helper shared by the new tests and the existing ValidateAddress test. Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Assisted-by: claude-opus-5-5 (via Claude Code) --- .../resources/addressable/addressable_test.go | 99 +++++++++++++++---- 1 file changed, 79 insertions(+), 20 deletions(-) diff --git a/test/rekt/resources/addressable/addressable_test.go b/test/rekt/resources/addressable/addressable_test.go index a215b5aaaf8..6ead4c9b633 100644 --- a/test/rekt/resources/addressable/addressable_test.go +++ b/test/rekt/resources/addressable/addressable_test.go @@ -21,15 +21,24 @@ import ( "testing" "time" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" dynamicfake "k8s.io/client-go/dynamic/fake" + clientgotesting "k8s.io/client-go/testing" "knative.dev/pkg/injection/clients/dynamicclient" "knative.dev/reconciler-test/pkg/environment" ) +const ( + testNamespace = "test-ns" + testName = "my-broker" +) + +var testGVR = schema.GroupVersionResource{Group: "eventing.knative.dev", Version: "v1", Resource: "brokers"} + // fakeEnvironment satisfies environment.Environment by only implementing // Namespace(), which is all k8s.Address needs from it. type fakeEnvironment struct { @@ -39,56 +48,106 @@ type fakeEnvironment struct { func (f *fakeEnvironment) Namespace() string { return f.ns } -// TestValidateAddress_RetriesUntilValid ensures ValidateAddress keeps polling -// until the fetched address actually satisfies the validate function, rather -// than validating once as soon as any address appears. A Broker (or any -// addressable) can report an address before it satisfies a given predicate, -// e.g. reporting an http:// address before its TLS address is ready. -func TestValidateAddress_RetriesUntilValid(t *testing.T) { - const ns = "test-ns" - const name = "my-broker" - gvr := schema.GroupVersionResource{Group: "eventing.knative.dev", Version: "v1", Resource: "brokers"} - +// newFakeBroker returns a context wired to a fake dynamic client that holds a +// single Broker whose status.address.url is url. +func newFakeBroker(url string) (context.Context, *dynamicfake.FakeDynamicClient) { obj := &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "eventing.knative.dev/v1", "kind": "Broker", "metadata": map[string]interface{}{ - "name": name, - "namespace": ns, + "name": testName, + "namespace": testNamespace, }, "status": map[string]interface{}{ "address": map[string]interface{}{ - "url": "http://my-broker.test-ns.svc.cluster.local", + "url": url, }, }, }, } - scheme := runtime.NewScheme() - dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(scheme, - map[schema.GroupVersionResource]string{gvr: "BrokerList"}, obj) + dc := dynamicfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), + map[schema.GroupVersionResource]string{testGVR: "BrokerList"}, obj) ctx := context.WithValue(context.Background(), dynamicclient.Key{}, dc) - ctx = environment.ContextWith(ctx, &fakeEnvironment{ns: ns}) + ctx = environment.ContextWith(ctx, &fakeEnvironment{ns: testNamespace}) + return ctx, dc +} + +// failFirstGets makes the first n gets of the Broker fail with err. +func failFirstGets(dc *dynamicfake.FakeDynamicClient, n int, err error) { + calls := 0 + dc.PrependReactor("get", testGVR.Resource, func(clientgotesting.Action) (bool, runtime.Object, error) { + calls++ + if calls <= n { + return true, nil, err + } + return false, nil, nil + }) +} + +// TestValidateAddress_RetriesUntilValid ensures ValidateAddress keeps polling +// until the fetched address actually satisfies the validate function, rather +// than validating once as soon as any address appears. A Broker (or any +// addressable) can report an address before it satisfies a given predicate, +// e.g. reporting an http:// address before its TLS address is ready. +func TestValidateAddress_RetriesUntilValid(t *testing.T) { + ctx, dc := newFakeBroker("http://my-broker.test-ns.svc.cluster.local") // Simulate the address flipping to https shortly after the requirement // starts polling, the way a Broker's address does once its TLS // certificate becomes ready after first being reported over http. go func() { time.Sleep(60 * time.Millisecond) - u, err := dc.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{}) + u, err := dc.Resource(testGVR).Namespace(testNamespace).Get(ctx, testName, metav1.GetOptions{}) if err != nil { return } _ = unstructured.SetNestedField(u.Object, "https://my-broker.test-ns.svc.cluster.local", "status", "address", "url") - _, _ = dc.Resource(gvr).Namespace(ns).Update(ctx, u, metav1.UpdateOptions{}) + _, _ = dc.Resource(testGVR).Namespace(testNamespace).Update(ctx, u, metav1.UpdateOptions{}) }() - step := ValidateAddress(gvr, name, AssertHTTPSAddress, 20*time.Millisecond, 2*time.Second) + step := ValidateAddress(testGVR, testName, AssertHTTPSAddress, 20*time.Millisecond, 2*time.Second) step(ctx, t) if t.Failed() { t.Fatal("ValidateAddress should have retried until the address became https, not failed on the initial http address") } } + +func TestAddress_RetriesTransientErrors(t *testing.T) { + tests := map[string]error{ + "not found": apierrors.NewNotFound(testGVR.GroupResource(), testName), + "timeout": apierrors.NewTimeoutError("request timed out", 1), + "server timeout": apierrors.NewServerTimeout(testGVR.GroupResource(), "get", 1), + "too many requests": apierrors.NewTooManyRequests("throttled", 1), + } + for name, transientErr := range tests { + t.Run(name, func(t *testing.T) { + ctx, dc := newFakeBroker("https://my-broker.test-ns.svc.cluster.local") + failFirstGets(dc, 2, transientErr) + + addr, err := Address(ctx, testGVR, testName, 10*time.Millisecond, 2*time.Second) + if err != nil { + t.Fatalf("Address() should have retried past %q, got error: %v", name, err) + } + if addr == nil || addr.URL == nil || addr.URL.String() != "https://my-broker.test-ns.svc.cluster.local" { + t.Fatalf("Address() = %v, want the Broker's address", addr) + } + }) + } +} + +func TestAddress_FailsFastOnNonTransientError(t *testing.T) { + ctx, dc := newFakeBroker("https://my-broker.test-ns.svc.cluster.local") + failFirstGets(dc, 1, apierrors.NewForbidden(testGVR.GroupResource(), testName, nil)) + + start := time.Now() + if _, err := Address(ctx, testGVR, testName, 10*time.Millisecond, 5*time.Second); !apierrors.IsForbidden(err) { + t.Fatalf("Address() error = %v, want a Forbidden error", err) + } + if elapsed := time.Since(start); elapsed > time.Second { + t.Fatalf("Address() took %v, want it to stop polling on a non-transient error", elapsed) + } +} From 24d1e2d3bff22630ac97d664e8a740786159a489 Mon Sep 17 00:00:00 2001 From: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Date: Wed, 30 Sep 2026 23:22:28 -0700 Subject: [PATCH 4/4] test/rekt: report the poll error, not a stale validation error ValidateAddress reported the last validation error whenever pollAddress returned one, even if polling had then stopped on a non-transient error (e.g. Forbidden), which hid the real failure. Only prefer the validation error when the poll was interrupted by its timeout, and add tests for both cases. Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com> Assisted-by: claude-opus-5-5 (via Claude Code) --- .../rekt/resources/addressable/addressable.go | 4 +- .../resources/addressable/addressable_test.go | 62 ++++++++++++++++++- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/test/rekt/resources/addressable/addressable.go b/test/rekt/resources/addressable/addressable.go index c29b0d151e7..f2ddd693f5b 100644 --- a/test/rekt/resources/addressable/addressable.go +++ b/test/rekt/resources/addressable/addressable.go @@ -41,7 +41,9 @@ func ValidateAddress(gvr schema.GroupVersionResource, name string, validate Vali return func(ctx context.Context, t feature.T) { _, validateErr, err := pollAddress(ctx, gvr, name, validate, timings...) if err != nil { - if validateErr != nil { + if validateErr != nil && wait.Interrupted(err) { + // Timed out while the address still failed validation: the + // validation error says more than the generic timeout. t.Error(validateErr) return } diff --git a/test/rekt/resources/addressable/addressable_test.go b/test/rekt/resources/addressable/addressable_test.go index 6ead4c9b633..12a60106f3f 100644 --- a/test/rekt/resources/addressable/addressable_test.go +++ b/test/rekt/resources/addressable/addressable_test.go @@ -18,6 +18,8 @@ package addressable import ( "context" + "fmt" + "strings" "testing" "time" @@ -75,18 +77,41 @@ func newFakeBroker(url string) (context.Context, *dynamicfake.FakeDynamicClient) return ctx, dc } -// failFirstGets makes the first n gets of the Broker fail with err. -func failFirstGets(dc *dynamicfake.FakeDynamicClient, n int, err error) { +// failGets makes the Broker get number call (starting at 1) fail with +// errFor(call); a nil error lets the get through to the fake object. +func failGets(dc *dynamicfake.FakeDynamicClient, errFor func(call int) error) { calls := 0 dc.PrependReactor("get", testGVR.Resource, func(clientgotesting.Action) (bool, runtime.Object, error) { calls++ - if calls <= n { + if err := errFor(calls); err != nil { return true, nil, err } return false, nil, nil }) } +// failFirstGets makes the first n gets of the Broker fail with err. +func failFirstGets(dc *dynamicfake.FakeDynamicClient, n int, err error) { + failGets(dc, func(call int) error { + if call <= n { + return err + } + return nil + }) +} + +// recordingT records the errors reported by a step instead of failing the test. +type recordingT struct { + *testing.T + errs []string +} + +func (r *recordingT) Error(args ...interface{}) { r.errs = append(r.errs, fmt.Sprint(args...)) } + +func (r *recordingT) Errorf(format string, args ...interface{}) { + r.errs = append(r.errs, fmt.Sprintf(format, args...)) +} + // TestValidateAddress_RetriesUntilValid ensures ValidateAddress keeps polling // until the fetched address actually satisfies the validate function, rather // than validating once as soon as any address appears. A Broker (or any @@ -151,3 +176,34 @@ func TestAddress_FailsFastOnNonTransientError(t *testing.T) { t.Fatalf("Address() took %v, want it to stop polling on a non-transient error", elapsed) } } + +func TestValidateAddress_ReportsValidationErrorOnTimeout(t *testing.T) { + ctx, _ := newFakeBroker("http://my-broker.test-ns.svc.cluster.local") + + rt := &recordingT{T: t} + ValidateAddress(testGVR, testName, AssertHTTPSAddress, 10*time.Millisecond, 100*time.Millisecond)(ctx, rt) + + if len(rt.errs) != 1 || !strings.Contains(rt.errs[0], "not HTTPS") { + t.Fatalf("ValidateAddress reported %q, want the last validation error", rt.errs) + } +} + +func TestValidateAddress_ReportsFatalErrorAfterValidationFailure(t *testing.T) { + ctx, dc := newFakeBroker("http://my-broker.test-ns.svc.cluster.local") + // The first get returns the http address, which fails validation; the + // next one fails with a non-transient error that must not be hidden by + // that earlier validation error. + failGets(dc, func(call int) error { + if call > 1 { + return apierrors.NewForbidden(testGVR.GroupResource(), testName, nil) + } + return nil + }) + + rt := &recordingT{T: t} + ValidateAddress(testGVR, testName, AssertHTTPSAddress, 10*time.Millisecond, 5*time.Second)(ctx, rt) + + if len(rt.errs) != 1 || !strings.Contains(rt.errs[0], "forbidden") { + t.Fatalf("ValidateAddress reported %q, want the Forbidden error", rt.errs) + } +}