diff --git a/coderd/oauth2provider/metadata_test.go b/coderd/oauth2provider/metadata_test.go index 067cb6e74f..c51bb80984 100644 --- a/coderd/oauth2provider/metadata_test.go +++ b/coderd/oauth2provider/metadata_test.go @@ -2,8 +2,6 @@ package oauth2provider_test import ( "context" - "encoding/json" - "net/http" "net/url" "testing" @@ -23,20 +21,11 @@ func TestOAuth2AuthorizationServerMetadata(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - // Use a plain HTTP client since this endpoint doesn't require authentication + // Use a plain HTTP client since this endpoint doesn't require authentication. + // Add a short readiness wait to avoid rare races with server startup. endpoint := serverURL.ResolveReference(&url.URL{Path: "/.well-known/oauth-authorization-server"}).String() - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) - require.NoError(t, err) - - resp, err := http.DefaultClient.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - - require.Equal(t, http.StatusOK, resp.StatusCode) - var metadata codersdk.OAuth2AuthorizationServerMetadata - err = json.NewDecoder(resp.Body).Decode(&metadata) - require.NoError(t, err) + testutil.RequireEventuallyResponseOK(ctx, t, endpoint, &metadata) // Verify the metadata require.NotEmpty(t, metadata.Issuer) @@ -57,20 +46,11 @@ func TestOAuth2ProtectedResourceMetadata(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - // Use a plain HTTP client since this endpoint doesn't require authentication + // Use a plain HTTP client since this endpoint doesn't require authentication. + // Add a short readiness wait to avoid rare races with server startup. endpoint := serverURL.ResolveReference(&url.URL{Path: "/.well-known/oauth-protected-resource"}).String() - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) - require.NoError(t, err) - - resp, err := http.DefaultClient.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - - require.Equal(t, http.StatusOK, resp.StatusCode) - var metadata codersdk.OAuth2ProtectedResourceMetadata - err = json.NewDecoder(resp.Body).Decode(&metadata) - require.NoError(t, err) + testutil.RequireEventuallyResponseOK(ctx, t, endpoint, &metadata) // Verify the metadata require.NotEmpty(t, metadata.Resource) diff --git a/testutil/http.go b/testutil/http.go new file mode 100644 index 0000000000..653c8f642c --- /dev/null +++ b/testutil/http.go @@ -0,0 +1,39 @@ +package testutil + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +// RequireEventuallyResponseOK makes HTTP GET requests to the given endpoint until it returns +// 200 OK with a valid JSON response that can be decoded into target, or until the context +// times out. This is useful for waiting for HTTP servers to become ready during tests, +// especially for metadata endpoints that may not be immediately available. +func RequireEventuallyResponseOK(ctx context.Context, t testing.TB, endpoint string, target interface{}) { + t.Helper() + + ok := Eventually(ctx, t, func(ctx context.Context) (done bool) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return false + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return false + } + if err := json.NewDecoder(resp.Body).Decode(target); err != nil { + return false + } + return true + }, IntervalFast) + + require.True(t, ok, "endpoint %s not ready in time", endpoint) +}