Filter creation requires the gmail.settings.sharing scope. Without it, users get a 403 insufficientPermissions error when trying to create filters via `gog gmail settings filters create`. Fixes #68
483 lines
14 KiB
Go
483 lines
14 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/steipete/gogcli/internal/googleauth"
|
|
"github.com/steipete/gogcli/internal/secrets"
|
|
)
|
|
|
|
func TestAuthAddCmd_JSON(t *testing.T) {
|
|
origAuth := authorizeGoogle
|
|
origOpen := openSecretsStore
|
|
origKeychain := ensureKeychainAccess
|
|
origFetch := fetchAuthorizedEmail
|
|
t.Cleanup(func() {
|
|
authorizeGoogle = origAuth
|
|
openSecretsStore = origOpen
|
|
ensureKeychainAccess = origKeychain
|
|
fetchAuthorizedEmail = origFetch
|
|
})
|
|
|
|
ensureKeychainAccess = func() error { return nil }
|
|
|
|
store := newMemSecretsStore()
|
|
openSecretsStore = func() (secrets.Store, error) { return store, nil }
|
|
|
|
var gotOpts googleauth.AuthorizeOptions
|
|
authorizeGoogle = func(ctx context.Context, opts googleauth.AuthorizeOptions) (string, error) {
|
|
gotOpts = opts
|
|
return "rt", nil
|
|
}
|
|
fetchAuthorizedEmail = func(context.Context, string, []string, time.Duration) (string, error) {
|
|
return "user@example.com", nil
|
|
}
|
|
|
|
out := captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{
|
|
"--json",
|
|
"auth",
|
|
"add",
|
|
"user@example.com",
|
|
"--services",
|
|
"gmail,drive,gmail",
|
|
"--manual",
|
|
"--force-consent",
|
|
}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
|
|
if !gotOpts.Manual || !gotOpts.ForceConsent {
|
|
t.Fatalf("expected options set, got %+v", gotOpts)
|
|
}
|
|
if len(gotOpts.Services) != 2 {
|
|
t.Fatalf("expected deduped services, got %v", gotOpts.Services)
|
|
}
|
|
|
|
var parsed struct {
|
|
Stored bool `json:"stored"`
|
|
Email string `json:"email"`
|
|
Services []string `json:"services"`
|
|
}
|
|
if err := json.Unmarshal([]byte(out), &parsed); err != nil {
|
|
t.Fatalf("json parse: %v\nout=%q", err, out)
|
|
}
|
|
if !parsed.Stored || parsed.Email != "user@example.com" || len(parsed.Services) != 2 {
|
|
t.Fatalf("unexpected response: %#v", parsed)
|
|
}
|
|
tok, err := store.GetToken("user@example.com")
|
|
if err != nil {
|
|
t.Fatalf("GetToken: %v", err)
|
|
}
|
|
if tok.RefreshToken != "rt" || !strings.Contains(strings.Join(tok.Services, ","), "gmail") {
|
|
t.Fatalf("unexpected token: %#v", tok)
|
|
}
|
|
}
|
|
|
|
func TestAuthAddCmd_KeychainError(t *testing.T) {
|
|
origAuth := authorizeGoogle
|
|
origOpen := openSecretsStore
|
|
origKeychain := ensureKeychainAccess
|
|
origFetch := fetchAuthorizedEmail
|
|
t.Cleanup(func() {
|
|
authorizeGoogle = origAuth
|
|
openSecretsStore = origOpen
|
|
ensureKeychainAccess = origKeychain
|
|
fetchAuthorizedEmail = origFetch
|
|
})
|
|
|
|
// Simulate keychain locked error
|
|
ensureKeychainAccess = func() error {
|
|
return errors.New("keychain is locked")
|
|
}
|
|
|
|
authCalled := false
|
|
authorizeGoogle = func(_ context.Context, _ googleauth.AuthorizeOptions) (string, error) {
|
|
authCalled = true
|
|
return "rt", nil
|
|
}
|
|
fetchAuthorizedEmail = func(context.Context, string, []string, time.Duration) (string, error) {
|
|
t.Fatal("fetchAuthorizedEmail should not be called when keychain check fails")
|
|
return "", nil
|
|
}
|
|
|
|
store := newMemSecretsStore()
|
|
openSecretsStore = func() (secrets.Store, error) { return store, nil }
|
|
|
|
cmd := &AuthAddCmd{Email: "test@example.com", ServicesCSV: "gmail"}
|
|
err := cmd.Run(context.Background())
|
|
|
|
if err == nil {
|
|
t.Fatal("expected error when keychain is locked")
|
|
}
|
|
if !strings.Contains(err.Error(), "keychain") {
|
|
t.Errorf("expected error to mention keychain, got: %v", err)
|
|
}
|
|
if authCalled {
|
|
t.Error("authorizeGoogle should not be called when keychain check fails")
|
|
}
|
|
}
|
|
|
|
func TestAuthAddCmd_DefaultServices_UserPreset(t *testing.T) {
|
|
origAuth := authorizeGoogle
|
|
origOpen := openSecretsStore
|
|
origKeychain := ensureKeychainAccess
|
|
origFetch := fetchAuthorizedEmail
|
|
t.Cleanup(func() {
|
|
authorizeGoogle = origAuth
|
|
openSecretsStore = origOpen
|
|
ensureKeychainAccess = origKeychain
|
|
fetchAuthorizedEmail = origFetch
|
|
})
|
|
|
|
ensureKeychainAccess = func() error { return nil }
|
|
|
|
store := newMemSecretsStore()
|
|
openSecretsStore = func() (secrets.Store, error) { return store, nil }
|
|
|
|
var gotOpts googleauth.AuthorizeOptions
|
|
authorizeGoogle = func(ctx context.Context, opts googleauth.AuthorizeOptions) (string, error) {
|
|
gotOpts = opts
|
|
return "rt", nil
|
|
}
|
|
fetchAuthorizedEmail = func(context.Context, string, []string, time.Duration) (string, error) {
|
|
return "user@example.com", nil
|
|
}
|
|
|
|
_ = captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{"--json", "auth", "add", "user@example.com"}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
|
|
want := googleauth.UserServices()
|
|
if len(gotOpts.Services) != len(want) {
|
|
t.Fatalf("unexpected services: %v", gotOpts.Services)
|
|
}
|
|
for _, s := range gotOpts.Services {
|
|
if s == googleauth.ServiceKeep {
|
|
t.Fatalf("unexpected keep in services: %v", gotOpts.Services)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAuthAddCmd_KeepRejected(t *testing.T) {
|
|
origAuth := authorizeGoogle
|
|
t.Cleanup(func() { authorizeGoogle = origAuth })
|
|
|
|
authorizeCalled := false
|
|
authorizeGoogle = func(context.Context, googleauth.AuthorizeOptions) (string, error) {
|
|
authorizeCalled = true
|
|
return "", nil
|
|
}
|
|
|
|
err := Execute([]string{"auth", "add", "user@example.com", "--services", "keep"})
|
|
if err == nil {
|
|
t.Fatalf("expected error")
|
|
}
|
|
var ee *ExitError
|
|
if !errors.As(err, &ee) || ee.Code != 2 {
|
|
t.Fatalf("expected exit code 2, got %T %#v", err, err)
|
|
}
|
|
if !strings.Contains(err.Error(), "Keep auth") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if authorizeCalled {
|
|
t.Fatalf("authorizeGoogle should not be called")
|
|
}
|
|
}
|
|
|
|
func TestAuthAddCmd_EmailMismatch(t *testing.T) {
|
|
origAuth := authorizeGoogle
|
|
origOpen := openSecretsStore
|
|
origKeychain := ensureKeychainAccess
|
|
origFetch := fetchAuthorizedEmail
|
|
t.Cleanup(func() {
|
|
authorizeGoogle = origAuth
|
|
openSecretsStore = origOpen
|
|
ensureKeychainAccess = origKeychain
|
|
fetchAuthorizedEmail = origFetch
|
|
})
|
|
|
|
ensureKeychainAccess = func() error { return nil }
|
|
openSecretsStore = func() (secrets.Store, error) { return newMemSecretsStore(), nil }
|
|
authorizeGoogle = func(context.Context, googleauth.AuthorizeOptions) (string, error) {
|
|
return "rt", nil
|
|
}
|
|
fetchAuthorizedEmail = func(context.Context, string, []string, time.Duration) (string, error) {
|
|
return "actual@example.com", nil
|
|
}
|
|
|
|
err := Execute([]string{"auth", "add", "expected@example.com"})
|
|
if err == nil {
|
|
t.Fatalf("expected mismatch error")
|
|
}
|
|
if !strings.Contains(err.Error(), "authorized as actual@example.com") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthAddCmd_ReadonlyScopes(t *testing.T) {
|
|
origAuth := authorizeGoogle
|
|
origOpen := openSecretsStore
|
|
origKeychain := ensureKeychainAccess
|
|
origFetch := fetchAuthorizedEmail
|
|
t.Cleanup(func() {
|
|
authorizeGoogle = origAuth
|
|
openSecretsStore = origOpen
|
|
ensureKeychainAccess = origKeychain
|
|
fetchAuthorizedEmail = origFetch
|
|
})
|
|
|
|
ensureKeychainAccess = func() error { return nil }
|
|
|
|
store := newMemSecretsStore()
|
|
openSecretsStore = func() (secrets.Store, error) { return store, nil }
|
|
|
|
var gotOpts googleauth.AuthorizeOptions
|
|
authorizeGoogle = func(ctx context.Context, opts googleauth.AuthorizeOptions) (string, error) {
|
|
gotOpts = opts
|
|
gotOpts.Services = append([]googleauth.Service(nil), opts.Services...)
|
|
gotOpts.Scopes = append([]string(nil), opts.Scopes...)
|
|
return "rt", nil
|
|
}
|
|
fetchAuthorizedEmail = func(context.Context, string, []string, time.Duration) (string, error) {
|
|
return "user@example.com", nil
|
|
}
|
|
|
|
_ = captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{
|
|
"--json",
|
|
"auth",
|
|
"add",
|
|
"user@example.com",
|
|
"--services",
|
|
"gmail,drive,calendar",
|
|
"--readonly",
|
|
}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
|
|
if !containsStringInSlice(gotOpts.Scopes, "https://www.googleapis.com/auth/gmail.readonly") {
|
|
t.Fatalf("missing gmail.readonly in %v", gotOpts.Scopes)
|
|
}
|
|
if !containsStringInSlice(gotOpts.Scopes, "https://www.googleapis.com/auth/drive.readonly") {
|
|
t.Fatalf("missing drive.readonly in %v", gotOpts.Scopes)
|
|
}
|
|
if !containsStringInSlice(gotOpts.Scopes, "https://www.googleapis.com/auth/calendar.readonly") {
|
|
t.Fatalf("missing calendar.readonly in %v", gotOpts.Scopes)
|
|
}
|
|
if containsStringInSlice(gotOpts.Scopes, "https://mail.google.com/") {
|
|
t.Fatalf("unexpected https://mail.google.com/ in %v", gotOpts.Scopes)
|
|
}
|
|
if containsStringInSlice(gotOpts.Scopes, "https://www.googleapis.com/auth/gmail.settings.basic") {
|
|
t.Fatalf("unexpected gmail.settings.basic in %v", gotOpts.Scopes)
|
|
}
|
|
if containsStringInSlice(gotOpts.Scopes, "https://www.googleapis.com/auth/gmail.settings.sharing") {
|
|
t.Fatalf("unexpected gmail.settings.sharing in %v", gotOpts.Scopes)
|
|
}
|
|
if containsStringInSlice(gotOpts.Scopes, "https://www.googleapis.com/auth/drive") {
|
|
t.Fatalf("unexpected drive in %v", gotOpts.Scopes)
|
|
}
|
|
if containsStringInSlice(gotOpts.Scopes, "https://www.googleapis.com/auth/calendar") {
|
|
t.Fatalf("unexpected calendar in %v", gotOpts.Scopes)
|
|
}
|
|
}
|
|
|
|
func TestAuthAddCmd_DriveScopeFile(t *testing.T) {
|
|
origAuth := authorizeGoogle
|
|
origOpen := openSecretsStore
|
|
origKeychain := ensureKeychainAccess
|
|
origFetch := fetchAuthorizedEmail
|
|
t.Cleanup(func() {
|
|
authorizeGoogle = origAuth
|
|
openSecretsStore = origOpen
|
|
ensureKeychainAccess = origKeychain
|
|
fetchAuthorizedEmail = origFetch
|
|
})
|
|
|
|
ensureKeychainAccess = func() error { return nil }
|
|
|
|
store := newMemSecretsStore()
|
|
openSecretsStore = func() (secrets.Store, error) { return store, nil }
|
|
|
|
var gotOpts googleauth.AuthorizeOptions
|
|
authorizeGoogle = func(ctx context.Context, opts googleauth.AuthorizeOptions) (string, error) {
|
|
gotOpts = opts
|
|
gotOpts.Services = append([]googleauth.Service(nil), opts.Services...)
|
|
gotOpts.Scopes = append([]string(nil), opts.Scopes...)
|
|
return "rt", nil
|
|
}
|
|
fetchAuthorizedEmail = func(context.Context, string, []string, time.Duration) (string, error) {
|
|
return "user@example.com", nil
|
|
}
|
|
|
|
_ = captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{
|
|
"--json",
|
|
"auth",
|
|
"add",
|
|
"user@example.com",
|
|
"--services",
|
|
"drive",
|
|
"--drive-scope",
|
|
"file",
|
|
}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
|
|
if !containsStringInSlice(gotOpts.Scopes, "https://www.googleapis.com/auth/drive.file") {
|
|
t.Fatalf("missing drive.file in %v", gotOpts.Scopes)
|
|
}
|
|
if containsStringInSlice(gotOpts.Scopes, "https://www.googleapis.com/auth/drive") {
|
|
t.Fatalf("unexpected drive in %v", gotOpts.Scopes)
|
|
}
|
|
}
|
|
|
|
func TestAuthAddCmd_ReadonlyWithDriveScopeFileRejected(t *testing.T) {
|
|
err := Execute([]string{"auth", "add", "user@example.com", "--services", "drive", "--readonly", "--drive-scope", "file"})
|
|
if err == nil {
|
|
t.Fatalf("expected error")
|
|
}
|
|
var ee *ExitError
|
|
if !errors.As(err, &ee) || ee.Code != 2 {
|
|
t.Fatalf("expected exit code 2, got %T %#v", err, err)
|
|
}
|
|
if !strings.Contains(err.Error(), "--drive-scope=file") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAuthAddCmd_SheetsReadonlyIncludesDriveReadonly(t *testing.T) {
|
|
origAuth := authorizeGoogle
|
|
origOpen := openSecretsStore
|
|
origKeychain := ensureKeychainAccess
|
|
origFetch := fetchAuthorizedEmail
|
|
t.Cleanup(func() {
|
|
authorizeGoogle = origAuth
|
|
openSecretsStore = origOpen
|
|
ensureKeychainAccess = origKeychain
|
|
fetchAuthorizedEmail = origFetch
|
|
})
|
|
|
|
ensureKeychainAccess = func() error { return nil }
|
|
|
|
store := newMemSecretsStore()
|
|
openSecretsStore = func() (secrets.Store, error) { return store, nil }
|
|
|
|
var gotOpts googleauth.AuthorizeOptions
|
|
authorizeGoogle = func(ctx context.Context, opts googleauth.AuthorizeOptions) (string, error) {
|
|
gotOpts = opts
|
|
gotOpts.Services = append([]googleauth.Service(nil), opts.Services...)
|
|
gotOpts.Scopes = append([]string(nil), opts.Scopes...)
|
|
return "rt", nil
|
|
}
|
|
fetchAuthorizedEmail = func(context.Context, string, []string, time.Duration) (string, error) {
|
|
return "user@example.com", nil
|
|
}
|
|
|
|
_ = captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{
|
|
"--json",
|
|
"auth",
|
|
"add",
|
|
"user@example.com",
|
|
"--services",
|
|
"sheets",
|
|
"--readonly",
|
|
}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
|
|
if !containsStringInSlice(gotOpts.Scopes, "https://www.googleapis.com/auth/spreadsheets.readonly") {
|
|
t.Fatalf("missing spreadsheets.readonly in %v", gotOpts.Scopes)
|
|
}
|
|
if !containsStringInSlice(gotOpts.Scopes, "https://www.googleapis.com/auth/drive.readonly") {
|
|
t.Fatalf("missing drive.readonly in %v", gotOpts.Scopes)
|
|
}
|
|
}
|
|
|
|
func TestAuthAddCmd_SheetsDriveScopeFile(t *testing.T) {
|
|
origAuth := authorizeGoogle
|
|
origOpen := openSecretsStore
|
|
origKeychain := ensureKeychainAccess
|
|
origFetch := fetchAuthorizedEmail
|
|
t.Cleanup(func() {
|
|
authorizeGoogle = origAuth
|
|
openSecretsStore = origOpen
|
|
ensureKeychainAccess = origKeychain
|
|
fetchAuthorizedEmail = origFetch
|
|
})
|
|
|
|
ensureKeychainAccess = func() error { return nil }
|
|
|
|
store := newMemSecretsStore()
|
|
openSecretsStore = func() (secrets.Store, error) { return store, nil }
|
|
|
|
var gotOpts googleauth.AuthorizeOptions
|
|
authorizeGoogle = func(ctx context.Context, opts googleauth.AuthorizeOptions) (string, error) {
|
|
gotOpts = opts
|
|
gotOpts.Services = append([]googleauth.Service(nil), opts.Services...)
|
|
gotOpts.Scopes = append([]string(nil), opts.Scopes...)
|
|
return "rt", nil
|
|
}
|
|
fetchAuthorizedEmail = func(context.Context, string, []string, time.Duration) (string, error) {
|
|
return "user@example.com", nil
|
|
}
|
|
|
|
_ = captureStdout(t, func() {
|
|
_ = captureStderr(t, func() {
|
|
if err := Execute([]string{
|
|
"--json",
|
|
"auth",
|
|
"add",
|
|
"user@example.com",
|
|
"--services",
|
|
"sheets",
|
|
"--drive-scope",
|
|
"file",
|
|
}); err != nil {
|
|
t.Fatalf("Execute: %v", err)
|
|
}
|
|
})
|
|
})
|
|
|
|
if !containsStringInSlice(gotOpts.Scopes, "https://www.googleapis.com/auth/drive.file") {
|
|
t.Fatalf("missing drive.file in %v", gotOpts.Scopes)
|
|
}
|
|
if !containsStringInSlice(gotOpts.Scopes, "https://www.googleapis.com/auth/spreadsheets") {
|
|
t.Fatalf("missing spreadsheets in %v", gotOpts.Scopes)
|
|
}
|
|
if containsStringInSlice(gotOpts.Scopes, "https://www.googleapis.com/auth/drive") {
|
|
t.Fatalf("unexpected drive in %v", gotOpts.Scopes)
|
|
}
|
|
}
|
|
|
|
func containsStringInSlice(items []string, want string) bool {
|
|
for _, it := range items {
|
|
if it == want {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|