# refactor(grant): share revoke routing; test granteeName + grant rows

`4bf8bdf`·

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.
// a resolved project. 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,
                    })
                })
        }
    }
    provider, providerUserID, err := resolveGranteeProvider(ctx, c, grantee)
    if err != nil {
        return err
    }
    return revokeGrant(cmd, "Revoked", fmt.Sprintf("%s from repo %s", grantee, repoRef), func() error {
        return c.RevokeRepoAccessByProvider(ctx, coreapi.RevokeRepoAccessByProviderParams{
            RepoId:         repoID,
            Provider:       provider,
            ProviderUserId: providerUserID,
        })
    })
}

// revokeGrant runs a grant-removal API call idempotently. A 404 means the
```

Mcmd/entire/cli/grant.go+41/-29

```

package cli

import (
	"slices"
	"testing"

"github.com/entireio/cli/internal/coreapi"

}

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) {
			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) {
			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()
	tests := []struct {
