From e0c53e7a518abcad13b44d3f812e52d20075dcdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Sibiril?= <81782+aureliensibiril@users.noreply.github.com> Date: Thu, 28 May 2026 22:22:46 +0200 Subject: [PATCH] Name Heroku personal account without an API call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /teams/@personal 404s, which would loop the source-name worker the same way a stale Sentry slug did. Short-circuit the personal-account slug to a static name before any HTTP call. Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com> --- pkg/accessreview/drivers/name_resolver.go | 6 ++++ .../drivers/name_resolver_test.go | 36 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/pkg/accessreview/drivers/name_resolver.go b/pkg/accessreview/drivers/name_resolver.go index a92f33548..974dc3349 100644 --- a/pkg/accessreview/drivers/name_resolver.go +++ b/pkg/accessreview/drivers/name_resolver.go @@ -726,6 +726,12 @@ func (r *herokuNameResolver) ResolveInstanceName(ctx context.Context) (string, e return "", nil } + // A personal account has no Team to name; short-circuit before hitting + // GET /teams/@personal, which 404s and would loop the source-name worker. + if r.teamID == herokuPersonalAccountSlug { + return herokuPersonalAccountDisplayName, nil + } + endpoint, err := url.JoinPath("https://api.heroku.com", "teams", url.PathEscape(r.teamID)) if err != nil { return "", fmt.Errorf("cannot build heroku team URL: %w", err) diff --git a/pkg/accessreview/drivers/name_resolver_test.go b/pkg/accessreview/drivers/name_resolver_test.go index 02bd68c2d..85bbff4e0 100644 --- a/pkg/accessreview/drivers/name_resolver_test.go +++ b/pkg/accessreview/drivers/name_resolver_test.go @@ -248,6 +248,42 @@ func TestTailscaleNameResolver(t *testing.T) { } } +func TestHerokuNameResolver(t *testing.T) { + t.Parallel() + + t.Run("personal-account slug returns a name without an HTTP call", func(t *testing.T) { + t.Parallel() + + client := &http.Client{Transport: roundTripperFunc(func(*http.Request) (*http.Response, error) { + t.Fatalf("resolver should not make an HTTP call for a personal account") + return nil, nil + })} + + got, err := NewHerokuNameResolver(client, herokuPersonalAccountSlug).ResolveInstanceName(context.Background()) + require.NoError(t, err) + assert.Equal(t, "Personal account", got) + }) + + t.Run("team slug resolves the team name", func(t *testing.T) { + t.Parallel() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "/teams/acme", r.URL.Path) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"name":"Acme Inc"}`)) + })) + defer srv.Close() + + client := &http.Client{Transport: &hostRewriter{target: srv.URL}} + + got, err := NewHerokuNameResolver(client, "acme").ResolveInstanceName(context.Background()) + require.NoError(t, err) + assert.Equal(t, "Acme Inc", got) + }) +} + // roundTripperFunc adapts a function into an http.RoundTripper, useful for // asserting that a resolver short-circuits before making any HTTP call. type roundTripperFunc func(*http.Request) (*http.Response, error)