-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add global profile selection and rename #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
75da336
626c32e
1c31e08
6063e54
12d4c83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -69,6 +69,43 @@ grw drive trash --query "name contains 'old'" --dry-run | |
|
|
||
| One desktop OAuth client can be used by both tools, but each tool asks for consent and stores its token under its own identity. Google Workspace administrators should start with [`WORKSPACE_ADMINS.md`](WORKSPACE_ADMINS.md). | ||
|
|
||
| ## Profiles | ||
|
|
||
| Each tool has its own profile namespace. Use a bare profile name with the global | ||
| `--profile` shorthand, or pass the full credential reference with `--ref`: | ||
|
|
||
| ```bash | ||
| gro --profile work mail list | ||
| grw --profile work calendar today | ||
| gro --ref google-readonly/work mail list | ||
| ``` | ||
|
|
||
| The selector precedence is explicit flag (`--profile` or `--ref`), credential | ||
| reference environment variable, saved `credential_ref`, then the built-in | ||
| `default` profile. For environment selection, use | ||
| `GOOGLE_READONLY_CREDENTIAL_REF` with `gro` or | ||
| `GOOGLE_READWRITE_CREDENTIAL_REF` with `grw`, for example: | ||
|
|
||
| ```bash | ||
| GOOGLE_READONLY_CREDENTIAL_REF=google-readonly/work gro mail list | ||
| ``` | ||
|
|
||
| `--profile` and `--ref` cannot be used together. To add an account without | ||
| changing the active profile, run `gro --profile work init` (or the equivalent | ||
| `grw` command). Inspect and manage profiles with: | ||
|
|
||
| ```bash | ||
| gro profiles list | ||
| gro profiles rename old-name new-name | ||
| ``` | ||
|
|
||
| Renaming moves the stored credentials without re-authentication, updates the | ||
| saved active profile when necessary, and refuses a destination that already | ||
| has credentials. If saving the active-profile update fails, the copied | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This states that the copied destination is removed after an active-profile save failure, but the implementation only attempts that rollback; if deleting the copy also fails, it reports that the destination may remain. Qualify this as an attempted removal and mention that a rollback failure is reported, so users do not assume a retry cannot encounter a destination collision. Reply inline to this comment. |
||
| destination is removed when rollback succeeds while the source remains, so the | ||
| command can be retried. If rollback also fails, the command reports that the | ||
| destination may remain. | ||
|
|
||
| ## Documentation | ||
|
|
||
| - [Development](docs/development.md) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| package grw | ||
|
|
||
| import ( | ||
| "path/filepath" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/spf13/cobra" | ||
|
|
||
| initcmd "github.com/open-cli-collective/google-cli/internal/cmd/init" | ||
| "github.com/open-cli-collective/google-cli/internal/cmd/setcred" | ||
| "github.com/open-cli-collective/google-cli/internal/credtest" | ||
| "github.com/open-cli-collective/google-cli/internal/keychain" | ||
| "github.com/open-cli-collective/google-cli/internal/rootutil" | ||
| ) | ||
|
|
||
| func selectorTestRoot() *cobra.Command { | ||
| var verbose, noColor bool | ||
| root := &cobra.Command{ | ||
| Use: "grw", | ||
| PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { | ||
| return rootutil.ApplyGlobalFlags(cmd, verbose, noColor) | ||
| }, | ||
| } | ||
| rootutil.AddGlobalFlags(root, &verbose, &noColor) | ||
| root.AddCommand(initcmd.NewCommand()) | ||
| root.AddCommand(setcred.NewCmd()) | ||
| return root | ||
| } | ||
|
|
||
| func TestProfileFlagInheritedByInitInBothFlagOrders(t *testing.T) { | ||
| for _, tc := range []struct { | ||
| name string | ||
| args func(string) []string | ||
| }{ | ||
| {name: "before command", args: func(path string) []string { | ||
| return []string{"--profile", "work", "init", "--credentials-file", path} | ||
| }}, | ||
| {name: "after command", args: func(path string) []string { | ||
| return []string{"init", "--profile", "work", "--credentials-file", path} | ||
| }}, | ||
| } { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| credtest.Setup(t) | ||
| t.Setenv(keychain.CredentialRefEnvVar(), "") | ||
| root := selectorTestRoot() | ||
| root.SetArgs(tc.args(filepath.Join(t.TempDir(), "missing.json"))) | ||
| if err := root.Execute(); err == nil { | ||
| t.Fatal("init should fail for the intentionally missing client file") | ||
| } | ||
| if got, set := keychain.GetCredentialRefOverride(); !set || got != "google-readwrite/work" { | ||
| t.Fatalf("selector after init path = (%q, %v), want google-readwrite/work", got, set) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestProfileFlagInheritedBySetCredentialTargetsNamedProfile(t *testing.T) { | ||
| for _, tc := range []struct { | ||
| name string | ||
| args []string | ||
| }{ | ||
| {name: "before command", args: []string{"--profile", "work", "set-credential", "--key", "oauth_token", "--stdin"}}, | ||
| {name: "after command", args: []string{"set-credential", "--profile", "work", "--key", "oauth_token", "--stdin"}}, | ||
| } { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| credtest.Setup(t) | ||
| t.Setenv(keychain.CredentialRefEnvVar(), "") | ||
| root := selectorTestRoot() | ||
| root.SetIn(strings.NewReader(`{"access_token":"profile-token","refresh_token":"refresh"}`)) | ||
| root.SetArgs(tc.args) | ||
| if err := root.Execute(); err != nil { | ||
| t.Fatalf("set-credential: %v", err) | ||
| } | ||
| st, err := keychain.OpenRef("google-readwrite/work") | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| tok, err := st.Token() | ||
| _ = st.Close() | ||
| if err != nil || tok.AccessToken != "profile-token" { | ||
| t.Fatalf("named profile token = %+v, err=%v", tok, err) | ||
| } | ||
| assertNoTokenAtRef(t, "google-readwrite/default") | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestProfileAndSetCredentialRefAreMutuallyExclusive(t *testing.T) { | ||
| credtest.Setup(t) | ||
| root := selectorTestRoot() | ||
| root.SetIn(strings.NewReader(`{"access_token":"profile-token"}`)) | ||
| root.SetArgs([]string{"--profile", "work", "set-credential", "--ref", "google-readwrite/other", "--key", "oauth_token", "--stdin"}) | ||
| if err := root.Execute(); err == nil || !strings.Contains(err.Error(), "mutually exclusive") { | ||
| t.Fatalf("profile/local --ref conflict = %v, want mutual-exclusion error", err) | ||
| } | ||
| } | ||
|
|
||
| func assertNoTokenAtRef(t *testing.T, ref string) { | ||
| t.Helper() | ||
| st, err := keychain.OpenRef(ref) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| defer func() { _ = st.Close() }() | ||
| if has, err := st.HasToken(); err != nil || has { | ||
| t.Fatalf("%s token presence = (%v, %v), want (false, nil)", ref, has, err) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package grw | ||
|
|
||
| import ( | ||
| "os" | ||
| "testing" | ||
|
|
||
| "github.com/open-cli-collective/google-cli/internal/config" | ||
| ) | ||
|
|
||
| func TestMain(m *testing.M) { | ||
| config.Register(Identity()) | ||
| os.Exit(m.Run()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The precedence description names a credential-reference environment override but never tells users which variable to set, leaving that documented selector unusable without source inspection. Document the concrete per-binary names, e.g.
GOOGLE_READONLY_CREDENTIAL_REFforgroandGOOGLE_READWRITE_CREDENTIAL_REFforgrw, alongside a short example.Reply inline to this comment.