refactor(grant): share revoke routing; test granteeName + grant rows · Entire
refactor(grant): share revoke routing; test granteeName + grant rows
374ad1d·
toothbrush·2w ago·2 files·+106 added/-29 removed
Extract the ULID-vs-handle revoke routing and message formatting shared by revokeProjectGrantee/revokeRepoGrantee into revokeGrantee. Add tests for granteeName's name/ULID fallback and the project/repo grant row builders.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Sessions
2a9100f0081fView transcript
Changes
2
cmd/entire/cli
Mgrant.go+41/-29
Mgrant_test.go+65
300 unmodified lines
}
// revokeProjectGrantee revokes a grantee (provider:handle or account ULID) from
// a resolved project. A ULID grantee takes the typed-id route directly; a
// handle is resolved to its provider account first and takes the by-provider
// route. projectRef is the user's original (pre-resolution) project ref, used
// only for the success message.
func revokeProjectGrantee(ctx context.Context, cmd *cobra.Command, c *coreapi.Client, projID, projectRef, grantee string) error {
if looksLikeULID(grantee) {
return revokeGrant(cmd, "Revoked", fmt.Sprintf("account %s from project %s", grantee, projectRef), func() error {
return revokeGrantee(ctx, cmd, c, "project", projectRef, grantee,
func() error {
return c.RevokeProjectAccess(ctx, coreapi.RevokeProjectAccessParams{
ProjectId: projID,
GranteeType: "account",
GranteeId: grantee,
})
},
func(provider, providerUserID string) error {
return c.RevokeProjectAccessByProvider(ctx, coreapi.RevokeProjectAccessByProviderParams{
ProjectId: projID,
Provider: provider,
ProviderUserId: providerUserID,
})
})
}
}
// revokeGrantee performs the shared grantee-revocation routing for projects and
// repos: a ULID grantee takes the typed-id route (revokeByID); a provider:handle
// is resolved to its provider account first and takes the by-provider route
// (revokeByProvider). target ("project"/"repo") and ref name the grant in the
// success message.
func revokeGrantee(
ctx context.Context,
cmd *cobra.Command,
c *coreapi.Client,
target, ref, grantee string,
revokeByID func() error,
revokeByProvider func(provider, providerUserID string) error,
) error {
if looksLikeULID(grantee) {
return revokeGrant(cmd, "Revoked", fmt.Sprintf("account %s from %s %s", grantee, target, ref), revokeByID)
}
provider, providerUserID, err := resolveGranteeProvider(ctx, c, grantee)
if err != nil {
return err
}
return revokeGrant(cmd, "Revoked", fmt.Sprintf("%s from project %s", grantee, projectRef), func() error {
return c.RevokeProjectAccessByProvider(ctx, coreapi.RevokeProjectAccessByProviderParams{
ProjectId: projID,
Provider: provider,
ProviderUserId: providerUserID,
})
})
}
// revokeRepoGrantee mirrors revokeProjectGrantee for repos: a ULID grantee
// takes the typed-id revoke route, a provider:handle is resolved first and takes
// the by-provider route. repoRef is the user's original repo ref, for messaging.
func revokeRepoGrantee(ctx context.Context, cmd *cobra.Command, c *coreapi.Client, repoID, repoRef, grantee string) error {
if looksLikeULID(grantee) {
return revokeGrant(cmd, "Revoked", fmt.Sprintf("account %s from repo %s", grantee, repoRef), func() error {
return revokeGrantee(ctx, cmd, c, "repo", repoRef, grantee,
func() error {
return c.RevokeRepoAccess(ctx, coreapi.RevokeRepoAccessParams{
RepoId: repoID,
GranteeType: "account",
GranteeId: grantee,
})
},
func(provider, providerUserID string) error {
return c.RevokeRepoAccessByProvider(ctx, coreapi.RevokeRepoAccessByProviderParams{
RepoId: repoID,
Provider: provider,
ProviderUserId: providerUserID,
})
})
}
}
}
func TestGranteeName(t *testing.T) {
t.Parallel()
const ulid = "01HZX0000000000000000000AB"
tests := []struct {
name string
in coreapi.OptString
id string
want string
}{
{name: "friendly name wins", in: coreapi.NewOptString("github:alice"), id: ulid, want: "github:alice"},
{name: "unset falls back to ULID", in: coreapi.OptString{}, id: ulid, want: ulid},
{name: "empty string falls back to ULID", in: coreapi.NewOptString(""), id: ulid, want: ulid},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := granteeName(tt.in, tt.id); got != tt.want {
t.Errorf("granteeName(%v, %q) = %q, want %q", tt.in, tt.id, got, tt.want)
}
})
}
}
func TestGrantRows(t *testing.T) {
t.Parallel()
const ulid = "01HZX0000000000000000000AB"
// grantColumns and the row builders must stay in lockstep — same width,
// same column order — or the table header and cells misalign.
if got, want := len(grantColumns), 5; got != want {
t.Fatalf("grantColumns has %d columns, want %d", got, want)
}
t.Run("project resolved name", func(t *testing.T) {
t.Parallel()
row := projectGrantRow(coreapi.ProjectGrant{
GranteeId: ulid,
GranteeName: coreapi.NewOptString("github:alice"),
GranteeType: "account",
Role: "writer",
Source: "direct",
})
want := []string{"account", "github:alice", ulid, "writer", "direct"}
if !slices.Equal(row, want) {
t.Errorf("projectGrantRow = %v, want %v", row, want)
}
})
t.Run("repo unresolved name falls back to ULID", func(t *testing.T) {
t.Parallel()
row := repoGrantRow(coreapi.RepoGrant{
GranteeId: ulid,
GranteeName: coreapi.OptString{},
GranteeType: "team",
Role: "reader",
Source: "inherited",
})
want := []string{"team", ulid, ulid, "reader", "inherited"}
if !slices.Equal(row, want) {
t.Errorf("repoGrantRow = %v, want %v", row, want)
}
})
}
func TestParseOrgRole(t *testing.T) {
t.Parallel()