Files
dingtalk-workspace-cli/internal/app/auth_command_full_coverage_test.go
T

842 lines
36 KiB
Go

package app
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"testing"
"time"
authpkg "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/keychain"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/pat"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
"github.com/charmbracelet/huh"
"github.com/spf13/cobra"
)
type authCoverageCaller struct {
result *edition.ToolResult
err error
}
func (c *authCoverageCaller) CallTool(context.Context, string, string, map[string]any) (*edition.ToolResult, error) {
return c.result, c.err
}
func (c *authCoverageCaller) CallToolWithToken(ctx context.Context, _ string, productID, toolName string, args map[string]any) (*edition.ToolResult, error) {
return c.CallTool(ctx, productID, toolName, args)
}
func (*authCoverageCaller) Format() string { return "json" }
func (*authCoverageCaller) DryRun() bool { return false }
func (*authCoverageCaller) Fields() string { return "" }
func (*authCoverageCaller) JQ() string { return "" }
func authCoverageRoot(child *cobra.Command, format string, yes bool) (*cobra.Command, *bytes.Buffer, *bytes.Buffer) {
root := &cobra.Command{Use: "dws"}
root.SetContext(context.Background())
child.SetContext(context.Background())
root.PersistentFlags().String("format", format, "")
root.PersistentFlags().Bool("yes", yes, "")
root.PersistentFlags().String("profile", "", "")
root.AddCommand(child)
out := &bytes.Buffer{}
errOut := &bytes.Buffer{}
root.SetOut(out)
root.SetErr(errOut)
return root, out, errOut
}
func authCoverageRunLogin(t *testing.T, caller edition.ToolCaller, format string, yes bool, flags map[string]string) (string, string, error) {
t.Helper()
cmd := newAuthLoginCommand(caller)
root, out, errOut := authCoverageRoot(cmd, format, yes)
for name, value := range flags {
flagSet := cmd.Flags()
if root.PersistentFlags().Lookup(name) != nil {
flagSet = root.PersistentFlags()
}
if err := flagSet.Set(name, value); err != nil {
t.Fatalf("set %s: %v", name, err)
}
}
err := cmd.RunE(cmd, nil)
return out.String(), errOut.String(), err
}
func TestCrossPlatformCoverageAuthCoverageFormsParentAndTargets(t *testing.T) {
oldEdition := edition.Get()
oldClientID := authpkg.ClientID()
oldClientSecret := authpkg.ClientSecret()
oldRunForm := authRunForm
oldPrompt := authLoginManualCredentialsPrompt
oldSaveConfig := authSaveAppConfig
oldResolve := authResolveProfile
t.Cleanup(func() {
edition.Override(oldEdition)
authpkg.SetClientID(oldClientID)
authpkg.SetClientSecret(oldClientSecret)
authRunForm = oldRunForm
authLoginManualCredentialsPrompt = oldPrompt
authSaveAppConfig = oldSaveConfig
authResolveProfile = oldResolve
})
edition.Override(&edition.Hooks{})
parent := buildAuthCommand(nil)
if parent.CommandPath() == "" || parent.RunE(parent, nil) != nil {
t.Fatal("auth parent should render help")
}
edition.Override(&edition.Hooks{HideAuthLogin: true})
if got := buildAuthCommand(nil).Commands(); len(got) != 7 {
t.Fatalf("hidden-login subcommands = %d, want 7", len(got))
}
authRunForm = func(*huh.Form) error { return nil }
if choice, err := selectAuthLoginGuideAction(); err != nil || choice != authLoginGuideDirectCLI {
t.Fatalf("guide choice = %q, %v", choice, err)
}
if id, secret, err := promptAuthLoginManualCredentials(); err != nil || id != "" || secret != "" {
t.Fatalf("manual prompt = %q/%q, %v", id, secret, err)
}
if mode, err := selectLoginRecommendScopeMode(); err != nil || mode != pat.LoginRecommendScopeRecommended {
t.Fatalf("scope mode = %q, %v", mode, err)
}
products := []pat.LoginRecommendProduct{{ProductCode: "doc"}, {ProductCode: ""}}
if selected, err := selectLoginRecommendProducts(products); err != nil || len(selected) != 1 || selected[0] != "doc" {
t.Fatalf("selected products = %#v, %v", selected, err)
}
if selected, err := selectLoginRecommendProducts(nil); err != nil || selected != nil {
t.Fatalf("empty selected products = %#v, %v", selected, err)
}
if selected, err := selectLoginRecommendProducts([]pat.LoginRecommendProduct{{}}); err != nil || selected != nil {
t.Fatalf("blank selected products = %#v, %v", selected, err)
}
many := make([]pat.LoginRecommendProduct, 16)
for i := range many {
many[i].ProductCode = fmt.Sprintf("p%d", i)
}
if selected, err := selectLoginRecommendProducts(many); err != nil || len(selected) != 16 {
t.Fatalf("many selected products = %d, %v", len(selected), err)
}
if authLoginProductsNonEmpty(nil) == nil || authLoginProductsNonEmpty([]string{"doc"}) != nil {
t.Fatal("product validator mismatch")
}
authRunForm = func(*huh.Form) error { return errors.New("cancel") }
if _, err := selectAuthLoginGuideAction(); err == nil {
t.Fatal("guide cancellation should fail")
}
if _, _, err := promptAuthLoginManualCredentials(); err == nil {
t.Fatal("credential cancellation should fail")
}
if _, err := selectLoginRecommendScopeMode(); err == nil {
t.Fatal("scope cancellation should fail")
}
if _, err := selectLoginRecommendProducts(products); err == nil {
t.Fatal("product cancellation should fail")
}
if authLoginNonEmpty("field")(" ") == nil || authLoginNonEmpty("field")("value") != nil {
t.Fatal("non-empty validator mismatch")
}
cmd := &cobra.Command{}
cmd.SetErr(io.Discard)
if err := applyAuthLoginGuideAction(cmd, t.TempDir(), authLoginGuideDirectCLI); err != nil {
t.Fatal(err)
}
if err := applyAuthLoginGuideAction(cmd, t.TempDir(), authLoginGuideConfigureAgentApp); err != nil {
t.Fatal(err)
}
if err := applyAuthLoginGuideAction(cmd, t.TempDir(), "unknown"); err == nil {
t.Fatal("unknown guide action should fail")
}
authLoginManualCredentialsPrompt = func() (string, string, error) { return "", "", errors.New("cancel") }
if err := applyAuthLoginGuideAction(cmd, t.TempDir(), authLoginGuideManualCredentials); err == nil {
t.Fatal("manual prompt error should propagate")
}
authLoginManualCredentialsPrompt = func() (string, string, error) { return "id", "secret", nil }
authSaveAppConfig = func(string, *authpkg.AppConfig) error { return errors.New("save") }
if err := applyAuthLoginGuideAction(cmd, t.TempDir(), authLoginGuideManualCredentials); err == nil {
t.Fatal("app-config save error should propagate")
}
authSaveAppConfig = func(string, *authpkg.AppConfig) error { return nil }
if err := applyAuthLoginGuideAction(cmd, t.TempDir(), authLoginGuideManualCredentials); err != nil {
t.Fatal(err)
}
authResolveProfile = func(string, string) (*authpkg.Profile, error) {
return &authpkg.Profile{CorpID: " ding-profile "}, nil
}
if got, err := resolveAuthLoginTargetCorpID("cfg", "name"); err != nil || got != "ding-profile" {
t.Fatalf("resolved target = %q, %v", got, err)
}
authResolveProfile = func(string, string) (*authpkg.Profile, error) { return nil, errors.New("missing") }
for selector, want := range map[string]struct {
value string
err bool
}{"": {"", false}, "ding-direct": {"ding-direct", false}, "other": {"", true}} {
got, err := resolveAuthLoginTargetCorpID("cfg", selector)
if got != want.value || (err != nil) != want.err {
t.Fatalf("target %q = %q, %v", selector, got, err)
}
}
badToken := &cobra.Command{}
badToken.Flags().Bool("token", false, "")
if _, err := resolveAuthLoginConfig(badToken); err == nil {
t.Fatal("invalid token flag should fail")
}
badDevice := &cobra.Command{}
badDevice.Flags().String("token", "", "")
badDevice.Flags().String("device", "", "")
if _, err := resolveAuthLoginConfig(badDevice); err == nil {
t.Fatal("invalid device flag should fail")
}
badForce := &cobra.Command{}
badForce.Flags().String("token", "", "")
badForce.Flags().Bool("device", false, "")
badForce.Flags().String("force", "", "")
if _, err := resolveAuthLoginConfig(badForce); err == nil {
t.Fatal("invalid force flag should fail")
}
badRecommend := &cobra.Command{}
badRecommend.Flags().String("token", "", "")
badRecommend.Flags().Bool("device", false, "")
badRecommend.Flags().Bool("force", false, "")
badRecommend.Flags().String("recommend", "", "")
if _, err := resolveAuthLoginConfig(badRecommend); err == nil {
t.Fatal("invalid recommend flag should fail")
}
}
func TestCrossPlatformCoverageAuthCoverageLoginFlows(t *testing.T) {
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
oldSave := authSaveTokenData
oldDevice := authDeviceLogin
oldOAuth := authOAuthLogin
oldRunRecommend := authRunLoginRecommend
oldRunWait := authRunDirectPATWait
oldPlan := authPlanLoginRecommend
oldScope := loginRecommendScopeModeSelector
oldProducts := loginRecommendProductSelector
oldInteractive := authLoginInteractiveTerminal
oldResolve := authResolveProfile
t.Cleanup(func() {
authSaveTokenData = oldSave
authDeviceLogin = oldDevice
authOAuthLogin = oldOAuth
authRunLoginRecommend = oldRunRecommend
authRunDirectPATWait = oldRunWait
authPlanLoginRecommend = oldPlan
loginRecommendScopeModeSelector = oldScope
loginRecommendProductSelector = oldProducts
authLoginInteractiveTerminal = oldInteractive
authResolveProfile = oldResolve
})
authInteractiveFalse := func() bool { return false }
authLoginInteractiveTerminal = authInteractiveFalse
authResolveProfile = func(string, string) (*authpkg.Profile, error) { return nil, errors.New("missing") }
if _, _, err := authCoverageRunLogin(t, nil, "table", true, map[string]string{"profile": "bad"}); err == nil {
t.Fatal("invalid login profile should fail")
}
authSaveTokenData = func(string, *authpkg.TokenData) error { return errors.New("save") }
if _, _, err := authCoverageRunLogin(t, nil, "table", true, map[string]string{"token": "token"}); err == nil {
t.Fatal("token save should fail")
}
authSaveTokenData = func(string, *authpkg.TokenData) error { return nil }
if out, _, err := authCoverageRunLogin(t, nil, "table", true, map[string]string{"token": " token "}); err != nil || !strings.Contains(out, "登录成功") {
t.Fatalf("token login = %q, %v", out, err)
}
if out, _, err := authCoverageRunLogin(t, nil, "json", true, map[string]string{"token": "token"}); err != nil || !strings.Contains(out, `"token_valid": true`) {
t.Fatalf("json token login = %q, %v", out, err)
}
authDeviceLogin = func(*authpkg.DeviceFlowProvider, context.Context) (*authpkg.TokenData, error) {
return nil, errors.New("device")
}
if _, _, err := authCoverageRunLogin(t, nil, "table", true, map[string]string{"device": "true"}); err == nil {
t.Fatal("device error should propagate")
}
authDeviceLogin = func(*authpkg.DeviceFlowProvider, context.Context) (*authpkg.TokenData, error) {
return &authpkg.TokenData{AccessToken: "a", ExpiresAt: time.Now().Add(time.Hour)}, nil
}
if _, _, err := authCoverageRunLogin(t, nil, "table", true, map[string]string{"device": "true", "no-browser": "true"}); err != nil {
t.Fatal(err)
}
authOAuthLogin = func(*authpkg.OAuthProvider, context.Context, bool) (*authpkg.TokenData, error) {
return nil, errors.New("oauth")
}
if _, _, err := authCoverageRunLogin(t, nil, "table", true, nil); err == nil {
t.Fatal("oauth error should propagate")
}
authOAuthLogin = func(*authpkg.OAuthProvider, context.Context, bool) (*authpkg.TokenData, error) {
return &authpkg.TokenData{
AccessToken: "a", ExpiresAt: time.Now().Add(time.Hour), RefreshToken: "r", RefreshExpAt: time.Now().Add(48 * time.Hour),
CorpName: "Corp", CorpID: "ding1", UserName: "User", UserID: "u",
}, nil
}
caller := &authCoverageCaller{result: &edition.ToolResult{Content: []edition.ContentBlock{{Text: `{}`}}}}
if out, _, err := authCoverageRunLogin(t, caller, "table", true, map[string]string{"no-browser": "true"}); err != nil || !strings.Contains(out, "Corp") {
t.Fatalf("oauth success = %q, %v", out, err)
}
authRunLoginRecommend = func(context.Context, edition.ToolCaller, io.Writer, pat.LoginRecommendOptions) error {
return errors.New("recommend")
}
if _, _, err := authCoverageRunLogin(t, nil, "table", true, map[string]string{"token": "x", "recommend": "true"}); err == nil {
t.Fatal("recommend error should propagate")
}
if _, _, err := authCoverageRunLogin(t, nil, "json", true, map[string]string{"token": "x", "recommend": "true"}); err == nil {
t.Fatal("JSON recommend error should propagate")
}
authRunLoginRecommend = func(context.Context, edition.ToolCaller, io.Writer, pat.LoginRecommendOptions) error {
return &apperrors.PATError{RawJSON: `{"code":"PAT_SCOPE_AUTH_REQUIRED"}`}
}
waited := false
authRunDirectPATWait = func(context.Context, *GlobalFlags, *apperrors.PATError, io.Writer) error {
waited = true
return nil
}
if _, _, err := authCoverageRunLogin(t, nil, "json", true, map[string]string{"token": "x", "recommend": "true"}); err != nil || !waited {
t.Fatalf("PAT wait = %v, waited=%v", err, waited)
}
authLoginInteractiveTerminal = func() bool { return true }
authPlanLoginRecommend = func(context.Context, edition.ToolCaller) (*pat.LoginRecommendPlan, error) {
return nil, errors.New("plan")
}
if _, _, err := authCoverageRunLogin(t, nil, "table", false, map[string]string{"token": "x"}); err == nil {
t.Fatal("plan error should propagate")
}
authPlanLoginRecommend = func(context.Context, edition.ToolCaller) (*pat.LoginRecommendPlan, error) {
return &pat.LoginRecommendPlan{AllGranted: true}, nil
}
if _, stderr, err := authCoverageRunLogin(t, nil, "table", false, map[string]string{"token": "x"}); err != nil || !strings.Contains(stderr, "全部授权") {
t.Fatalf("all-granted plan = %q, %v", stderr, err)
}
authPlanLoginRecommend = func(context.Context, edition.ToolCaller) (*pat.LoginRecommendPlan, error) {
return &pat.LoginRecommendPlan{Scopes: []string{"scope"}, Products: []pat.LoginRecommendProduct{{ProductCode: "doc"}}}, nil
}
loginRecommendScopeModeSelector = func() (pat.LoginRecommendScopeMode, error) { return "", errors.New("scope") }
if _, _, err := authCoverageRunLogin(t, nil, "table", false, map[string]string{"token": "x"}); err == nil {
t.Fatal("scope selector error should propagate")
}
loginRecommendScopeModeSelector = func() (pat.LoginRecommendScopeMode, error) { return pat.LoginRecommendScopeAll, nil }
loginRecommendProductSelector = func([]pat.LoginRecommendProduct) ([]string, error) { return []string{"doc"}, nil }
selected := false
authRunLoginRecommend = func(_ context.Context, _ edition.ToolCaller, _ io.Writer, opts pat.LoginRecommendOptions) error {
if opts.ProductSelector != nil {
_, err := opts.ProductSelector(opts.InitialPlan.Products)
selected = err == nil
}
return nil
}
if _, _, err := authCoverageRunLogin(t, nil, "table", false, map[string]string{"token": "x"}); err != nil || !selected {
t.Fatalf("interactive recommendation = %v, selected=%v", err, selected)
}
}
func TestCrossPlatformCoverageAuthCoverageContactEnrichment(t *testing.T) {
oldSave := authSaveTokenData
t.Cleanup(func() { authSaveTokenData = oldSave })
ctx := context.Background()
if err := enrichAuthLoginProfileFromContact(ctx, "cfg", nil, nil); err != nil {
t.Fatal(err)
}
if err := enrichAuthLoginProfileFromContact(ctx, "cfg", &authCoverageCaller{}, &authpkg.TokenData{}); err != nil {
t.Fatal(err)
}
complete := &authpkg.TokenData{CorpID: "ding", CorpName: "Corp", UserID: "u", UserName: "User"}
if err := enrichAuthLoginProfileFromContact(ctx, "cfg", &authCoverageCaller{}, complete); err != nil {
t.Fatal(err)
}
if err := enrichAuthLoginProfileFromContact(ctx, "cfg", &authCoverageCaller{err: errors.New("call")}, &authpkg.TokenData{CorpID: "ding"}); err == nil {
t.Fatal("caller error should propagate")
}
if err := enrichAuthLoginProfileFromContact(
ctx,
"cfg",
&authCoverageCaller{err: errors.New("call")},
&authpkg.TokenData{CorpID: "ding", UserID: "known", AccessToken: "token"},
); err != nil {
t.Fatalf("optional contact metadata failure with known userId = %v", err)
}
for _, text := range []string{"", "not-json", `{"result":[]}`, `{"result":[{"orgEmployeeModel":{}}]}`} {
caller := &authCoverageCaller{result: &edition.ToolResult{Content: []edition.ContentBlock{{Text: text}}}}
if err := enrichAuthLoginProfileFromContact(ctx, "cfg", caller, &authpkg.TokenData{CorpID: "ding", AccessToken: "token"}); err != nil {
t.Fatalf("invalid contact %q: %v", text, err)
}
}
mismatch := &authCoverageCaller{result: &edition.ToolResult{Content: []edition.ContentBlock{{Text: `{"result":[{"orgEmployeeModel":{"corpId":"other"}}]}`}}}}
if err := enrichAuthLoginProfileFromContact(ctx, "cfg", mismatch, &authpkg.TokenData{CorpID: "ding", AccessToken: "token"}); err == nil {
t.Fatal("corp mismatch should fail")
}
same := &authCoverageCaller{result: &edition.ToolResult{Content: []edition.ContentBlock{{Text: `{"result":[{"orgEmployeeModel":{"corpId":"ding","orgName":"Corp","userid":"u","name":"User"}}]}`}}}}
if err := enrichAuthLoginProfileFromContact(ctx, "cfg", same, complete); err != nil {
t.Fatal(err)
}
unchangedPartial := &authCoverageCaller{result: &edition.ToolResult{Content: []edition.ContentBlock{{Text: `{"result":[{"orgEmployeeModel":{"corpId":"ding","orgName":"Corp","userid":"u"}}]}`}}}}
if err := enrichAuthLoginProfileFromContact(ctx, "cfg", unchangedPartial, &authpkg.TokenData{CorpID: "ding", CorpName: "Corp", UserID: "u"}); err != nil {
t.Fatal(err)
}
authSaveTokenData = func(string, *authpkg.TokenData) error { return errors.New("save") }
data := &authpkg.TokenData{CorpID: "ding", AccessToken: "token"}
if err := enrichAuthLoginProfileFromContact(ctx, "cfg", same, data); err != nil || data.CorpName != "Corp" || data.UserID != "u" {
t.Fatalf("enriched = %#v, %v", data, err)
}
if _, ok := contactProfileIdentityFromToolResult(nil); ok {
t.Fatal("nil result should not parse")
}
if got := firstNonEmptyString(" ", " value ", "later"); got != "value" {
t.Fatalf("first non-empty = %q", got)
}
}
func TestCrossPlatformCoverageAuthCoverageDefaultSeamClosures(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
configDir := t.TempDir()
device := authpkg.NewDeviceFlowProvider(configDir, nil)
_, _ = authDeviceLogin(device, ctx)
oauth := authpkg.NewOAuthProvider(configDir, nil)
oauth.NoBrowser = true
_, _ = authOAuthStatus(oauth)
_, _ = authOAuthAccessToken(oauth, ctx)
_, _ = authOAuthLogin(oauth, ctx, true)
_, _ = authOAuthExchange(oauth, ctx, "code", "uid")
_ = fmt.Sprintf("%v", os.ErrNotExist)
}
func TestCrossPlatformCoverageAuthCoverageStatusAndLogout(t *testing.T) {
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
oldEdition := edition.Get()
oldStatus := authOAuthStatus
oldAccess := authOAuthAccessToken
oldDelete := authDeleteTokenData
oldMark := authMarkProfileStatus
oldResolve := authResolveProfile
oldResolveDeletion := authResolveProfileDeletion
oldRevoke := authRevokeToken
oldRevokeForData := authRevokeTokenForData
oldLoadTokenForProfile := authLoadTokenForProfile
oldDeleteProfile := authDeleteProfileToken
oldMigrate := authEnsureProfilesMigration
oldLoadProfiles := authLoadProfiles
oldDeleteAll := authDeleteAllTokenData
t.Cleanup(func() {
edition.Override(oldEdition)
authOAuthStatus = oldStatus
authOAuthAccessToken = oldAccess
authDeleteTokenData = oldDelete
authMarkProfileStatus = oldMark
authResolveProfile = oldResolve
authResolveProfileDeletion = oldResolveDeletion
authRevokeToken = oldRevoke
authRevokeTokenForData = oldRevokeForData
authLoadTokenForProfile = oldLoadTokenForProfile
authDeleteProfileToken = oldDeleteProfile
authEnsureProfilesMigration = oldMigrate
authLoadProfiles = oldLoadProfiles
authDeleteAllTokenData = oldDeleteAll
})
badStatus := newAuthStatusCommand()
bad := &cobra.Command{}
bad.Flags().Bool("profile", false, "")
if err := badStatus.RunE(bad, nil); err == nil {
t.Fatal("invalid profile flag should fail")
}
runStatus := func(format string) (string, error) {
cmd := newAuthStatusCommand()
_, out, _ := authCoverageRoot(cmd, format, false)
err := cmd.RunE(cmd, nil)
return out.String(), err
}
authOAuthStatus = func(*authpkg.OAuthProvider) (*authpkg.TokenData, error) { return nil, nil }
edition.Override(&edition.Hooks{})
if out, err := runStatus("table"); err != nil || !strings.Contains(out, "auth login") {
t.Fatalf("plain unauthenticated status = %q, %v", out, err)
}
authOAuthStatus = func(*authpkg.OAuthProvider) (*authpkg.TokenData, error) {
return nil, keychain.NewUnavailableError("read", errors.New("status"))
}
edition.Override(&edition.Hooks{})
if out, err := runStatus("table"); err != nil || !strings.Contains(out, "未登录") {
t.Fatalf("status error = %q, %v", out, err)
}
if out, err := runStatus("json"); err != nil || !strings.Contains(out, `"authenticated": false`) {
t.Fatalf("json status error = %q, %v", out, err)
}
now := time.Now()
valid := &authpkg.TokenData{AccessToken: "a", ExpiresAt: now.Add(time.Hour), CorpID: "ding", CorpName: "Corp"}
authOAuthStatus = func(*authpkg.OAuthProvider) (*authpkg.TokenData, error) { return valid, nil }
if out, err := runStatus("table"); err != nil || !strings.Contains(out, "已登录") || !strings.Contains(out, "缺失或已过期") {
t.Fatalf("valid access status = %q, %v", out, err)
}
valid.RefreshToken = "r"
valid.RefreshExpAt = now.Add(time.Hour)
if out, err := runStatus("table"); err != nil || !strings.Contains(out, "Refresh Token:") {
t.Fatalf("valid refresh status = %q, %v", out, err)
}
expired := &authpkg.TokenData{AccessToken: "a", ExpiresAt: now.Add(-time.Hour), RefreshToken: "r", RefreshExpAt: now.Add(time.Hour), CorpID: "ding"}
updated := &authpkg.TokenData{AccessToken: "new", ExpiresAt: now.Add(time.Hour), RefreshToken: "r", RefreshExpAt: now.Add(time.Hour), CorpID: "ding"}
calls := 0
authOAuthStatus = func(*authpkg.OAuthProvider) (*authpkg.TokenData, error) {
calls++
if calls == 1 {
return expired, nil
}
return updated, nil
}
authOAuthAccessToken = func(*authpkg.OAuthProvider, context.Context) (string, error) { return "new", nil }
if out, err := runStatus("table"); err != nil || !strings.Contains(out, "自动刷新") {
t.Fatalf("refreshed status = %q, %v", out, err)
}
calls = 0
authOAuthStatus = func(*authpkg.OAuthProvider) (*authpkg.TokenData, error) {
calls++
if calls == 1 {
return expired, nil
}
return nil, errors.New("second status")
}
if _, err := runStatus("table"); err != nil {
t.Fatal(err)
}
authOAuthStatus = func(*authpkg.OAuthProvider) (*authpkg.TokenData, error) { return expired, nil }
authOAuthAccessToken = func(*authpkg.OAuthProvider, context.Context) (string, error) { return "", errors.New("refresh") }
deleted := false
marked := false
authDeleteTokenData = func(string) error { deleted = true; return errors.New("ignored") }
authMarkProfileStatus = func(string, string, string) error { marked = true; return errors.New("ignored") }
edition.Override(&edition.Hooks{AutoPurgeToken: true})
if _, err := runStatus("table"); err != nil || !deleted {
t.Fatalf("auto-purge = %v, deleted=%v", err, deleted)
}
edition.Override(&edition.Hooks{})
if _, err := runStatus("table"); err != nil || !marked {
t.Fatalf("mark-expired = %v, marked=%v", err, marked)
}
authResolveProfileDeletion = func(string, string) (*authpkg.Profile, bool, error) { return nil, false, errors.New("missing") }
if err := logoutOneProfile(nil, context.Background(), "cfg", "x"); err == nil {
t.Fatal("missing profile should fail")
}
authResolveProfileDeletion = func(string, string) (*authpkg.Profile, bool, error) {
return &authpkg.Profile{CorpID: "ding", UserID: "user"}, true, nil
}
authLoadTokenForProfile = func(string, string) (*authpkg.TokenData, error) {
return &authpkg.TokenData{CorpID: "ding", UserID: "user"}, nil
}
authRevokeTokenForData = func(context.Context, *authpkg.TokenData) error { return errors.New("ignored") }
var deletedSelector string
authDeleteProfileToken = func(_ string, selector string) error {
deletedSelector = selector
return errors.New("delete")
}
if err := logoutOneProfile(nil, context.Background(), "cfg", "x"); err == nil {
t.Fatal("profile delete should fail")
}
if deletedSelector != "ding:user" {
t.Fatalf("exact deletion selector = %q, want stable identity selector", deletedSelector)
}
authDeleteProfileToken = func(_ string, selector string) error {
deletedSelector = selector
return nil
}
if err := logoutOneProfile(nil, context.Background(), "cfg", "x"); err != nil {
t.Fatal(err)
}
if deletedSelector != "ding:user" {
t.Fatalf("exact deletion selector = %q, want stable identity selector", deletedSelector)
}
authResolveProfileDeletion = func(string, string) (*authpkg.Profile, bool, error) {
return &authpkg.Profile{CorpID: "ding", UserID: "user"}, false, nil
}
authLoadProfiles = func(string) (*authpkg.ProfilesConfig, error) {
return &authpkg.ProfilesConfig{Profiles: []authpkg.Profile{{CorpID: "ding", UserID: "user"}}}, nil
}
if err := logoutOneProfile(nil, context.Background(), "cfg", "organization-name"); err != nil {
t.Fatal(err)
}
if deletedSelector != "ding" {
t.Fatalf("organization deletion selector = %q, want stable corpId", deletedSelector)
}
authEnsureProfilesMigration = func(string) error { return errors.New("migrate") }
if err := logoutAllProfiles(nil, context.Background(), "cfg"); err == nil {
t.Fatal("migration should fail")
}
authEnsureProfilesMigration = func(string) error { return nil }
authLoadProfiles = func(string) (*authpkg.ProfilesConfig, error) { return nil, errors.New("load") }
if err := logoutAllProfiles(nil, context.Background(), "cfg"); err == nil {
t.Fatal("load profiles should fail")
}
revokes := 0
authRevokeToken = func(context.Context) error { revokes++; return nil }
authRevokeTokenForData = func(context.Context, *authpkg.TokenData) error { revokes++; return nil }
authLoadTokenForProfile = func(string, string) (*authpkg.TokenData, error) {
return &authpkg.TokenData{AccessToken: "token"}, nil
}
authLoadProfiles = func(string) (*authpkg.ProfilesConfig, error) { return nil, nil }
authDeleteAllTokenData = func(string) error { return nil }
if err := logoutAllProfiles(nil, context.Background(), "cfg"); err != nil || revokes != 1 {
t.Fatalf("empty profiles = %v, revokes=%d", err, revokes)
}
authLoadProfiles = func(string) (*authpkg.ProfilesConfig, error) {
return &authpkg.ProfilesConfig{Profiles: []authpkg.Profile{{CorpID: "a"}, {CorpID: "b"}}}, nil
}
if err := logoutAllProfiles(nil, context.Background(), "cfg"); err != nil || revokes != 3 {
t.Fatalf("profile revokes = %v, revokes=%d", err, revokes)
}
authDeleteAllTokenData = func(string) error { return errors.New("delete all") }
if err := logoutAllProfiles(nil, context.Background(), "cfg"); err == nil {
t.Fatal("delete-all should fail")
}
logoutFailure := newAuthLogoutCommand()
_, _, _ = authCoverageRoot(logoutFailure, "table", false)
if err := logoutFailure.RunE(logoutFailure, nil); err == nil {
t.Fatal("logout-all command failure should propagate")
}
logoutFailure = newAuthLogoutCommand()
_, _, _ = authCoverageRoot(logoutFailure, "table", false)
_ = logoutFailure.Flags().Set("profile", "ding")
authDeleteProfileToken = func(string, string) error { return errors.New("delete") }
if err := logoutFailure.RunE(logoutFailure, nil); err == nil {
t.Fatal("logout-one command failure should propagate")
}
logout := newAuthLogoutCommand()
badLogout := &cobra.Command{}
badLogout.Flags().Bool("profile", false, "")
badLogout.SetContext(context.Background())
if err := logout.RunE(badLogout, nil); err == nil {
t.Fatal("invalid logout profile flag should fail")
}
authDeleteAllTokenData = func(string) error { return nil }
authLoadProfiles = func(string) (*authpkg.ProfilesConfig, error) { return &authpkg.ProfilesConfig{}, nil }
_, out, _ := authCoverageRoot(logout, "table", false)
if err := logout.RunE(logout, nil); err != nil || !strings.Contains(out.String(), "重新登录") {
t.Fatalf("logout = %q, %v", out.String(), err)
}
edition.Override(&edition.Hooks{IsEmbedded: true})
authDeleteProfileToken = func(string, string) error { return nil }
logout = newAuthLogoutCommand()
_, out, _ = authCoverageRoot(logout, "table", false)
if err := logout.Flags().Set("profile", "ding"); err != nil {
t.Fatal(err)
}
if err := logout.RunE(logout, nil); err != nil || strings.Contains(out.String(), "重新登录") {
t.Fatalf("embedded logout = %q, %v", out.String(), err)
}
}
func TestCrossPlatformCoverageAuthCoveragePortableExchangeAndReset(t *testing.T) {
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
oldEdition := edition.Get()
oldSupported := authPortableExportSupported
oldReady := authPortableSourceReady
oldTarget := authPortableTargetPopulated
oldExport := authExportPortableBundle
oldImport := authImportPortableBundle
oldAtomic := authAtomicWrite
oldRead := authReadFile
oldExchange := authOAuthExchange
oldDeleteAll := authDeleteAllTokenData
oldRemove := authRemove
oldDeleteConfig := authDeleteAppConfig
t.Cleanup(func() {
edition.Override(oldEdition)
authPortableExportSupported = oldSupported
authPortableSourceReady = oldReady
authPortableTargetPopulated = oldTarget
authExportPortableBundle = oldExport
authImportPortableBundle = oldImport
authAtomicWrite = oldAtomic
authReadFile = oldRead
authOAuthExchange = oldExchange
authDeleteAllTokenData = oldDeleteAll
authRemove = oldRemove
authDeleteAppConfig = oldDeleteConfig
})
export := newAuthExportCommandWithSupport(func() error { return nil })
badString := &cobra.Command{}
badString.Flags().Bool("output", false, "")
if err := export.RunE(badString, nil); err == nil {
t.Fatal("invalid output flag should fail")
}
badBool := &cobra.Command{}
badBool.Flags().String("output", "x", "")
badBool.Flags().String("base64", "", "")
if err := export.RunE(badBool, nil); err == nil {
t.Fatal("invalid base64 flag should fail")
}
_, _, _ = authCoverageRoot(export, "table", false)
if err := export.RunE(export, nil); err == nil {
t.Fatal("missing export output should fail")
}
authPortableExportSupported = func() bool { return false }
_ = export.Flags().Set("output", "out")
if err := export.RunE(export, nil); err == nil {
t.Fatal("unsupported export should fail")
}
authPortableExportSupported = func() bool { return true }
authPortableSourceReady = func() bool { return false }
if err := export.RunE(export, nil); err == nil {
t.Fatal("unready export should fail")
}
authPortableSourceReady = func() bool { return true }
authExportPortableBundle = func(string, io.Writer) error { return errors.New("export") }
if err := export.RunE(export, nil); err == nil {
t.Fatal("export failure should propagate")
}
authExportPortableBundle = func(_ string, w io.Writer) error { _, _ = io.WriteString(w, "bundle"); return nil }
authAtomicWrite = func(string, []byte, os.FileMode) error { return errors.New("write") }
if err := export.RunE(export, nil); err == nil {
t.Fatal("raw write should fail")
}
authAtomicWrite = func(string, []byte, os.FileMode) error { return nil }
if err := export.RunE(export, nil); err != nil {
t.Fatal(err)
}
export = newAuthExportCommandWithSupport(func() error { return nil })
_, out, _ := authCoverageRoot(export, "table", false)
_ = export.Flags().Set("base64", "true")
export.SetOut(&appFailWriter{err: errors.New("stdout")})
if err := export.RunE(export, nil); err == nil {
t.Fatal("stdout failure should propagate")
}
export.SetOut(out)
_ = export.Flags().Set("output", "encoded")
authAtomicWrite = func(string, []byte, os.FileMode) error { return errors.New("write") }
if err := export.RunE(export, nil); err == nil {
t.Fatal("base64 write should fail")
}
authAtomicWrite = func(string, []byte, os.FileMode) error { return nil }
if err := export.RunE(export, nil); err != nil {
t.Fatal(err)
}
importCmd := newAuthImportCommandWithSupport(func() error { return nil })
badInput := &cobra.Command{}
badInput.Flags().Bool("input", false, "")
if err := importCmd.RunE(badInput, nil); err == nil {
t.Fatal("invalid input flag should fail")
}
badImportBase64 := &cobra.Command{}
badImportBase64.Flags().String("input", "x", "")
badImportBase64.Flags().String("base64", "", "")
if err := importCmd.RunE(badImportBase64, nil); err == nil {
t.Fatal("invalid import base64 flag should fail")
}
badForce := &cobra.Command{}
badForce.Flags().String("input", "x", "")
badForce.Flags().Bool("base64", false, "")
badForce.Flags().String("force", "", "")
if err := importCmd.RunE(badForce, nil); err == nil {
t.Fatal("invalid force flag should fail")
}
_, out, _ = authCoverageRoot(importCmd, "table", false)
if err := importCmd.RunE(importCmd, nil); err == nil {
t.Fatal("missing input should fail")
}
_ = importCmd.Flags().Set("input", "bundle")
authPortableTargetPopulated = func(string) bool { return true }
if err := importCmd.RunE(importCmd, nil); err == nil {
t.Fatal("populated target should require force")
}
authPortableTargetPopulated = func(string) bool { return false }
authReadFile = func(string) ([]byte, error) { return nil, errors.New("read") }
if err := importCmd.RunE(importCmd, nil); err == nil {
t.Fatal("read failure should propagate")
}
authReadFile = func(string) ([]byte, error) { return []byte("%%%"), nil }
_ = importCmd.Flags().Set("base64", "true")
if err := importCmd.RunE(importCmd, nil); err == nil {
t.Fatal("invalid base64 should fail")
}
authReadFile = func(string) ([]byte, error) { return []byte("YnVuZGxl"), nil }
authImportPortableBundle = func(string, io.Reader) (authpkg.PortableImportReport, error) {
return authpkg.PortableImportReport{}, errors.New("import")
}
if err := importCmd.RunE(importCmd, nil); err == nil {
t.Fatal("import failure should propagate")
}
authImportPortableBundle = func(string, io.Reader) (authpkg.PortableImportReport, error) {
return authpkg.PortableImportReport{BundleOS: "other", OSMismatch: true}, nil
}
if err := importCmd.RunE(importCmd, nil); err != nil {
t.Fatal(err)
}
authImportPortableBundle = func(string, io.Reader) (authpkg.PortableImportReport, error) {
return authpkg.PortableImportReport{}, nil
}
if err := importCmd.RunE(importCmd, nil); err != nil {
t.Fatal(err)
}
exchange := newAuthExchangeCommand(nil)
badCode := &cobra.Command{}
badCode.Flags().Bool("code", false, "")
if err := exchange.RunE(badCode, nil); err == nil {
t.Fatal("invalid code flag should fail")
}
badUID := &cobra.Command{}
badUID.Flags().String("code", "code", "")
badUID.Flags().Bool("uid", false, "")
if err := exchange.RunE(badUID, nil); err == nil {
t.Fatal("invalid uid flag should fail")
}
_, out, _ = authCoverageRoot(exchange, "table", false)
if err := exchange.RunE(exchange, nil); err == nil {
t.Fatal("missing code should fail")
}
_ = exchange.Flags().Set("code", "code")
authOAuthExchange = func(*authpkg.OAuthProvider, context.Context, string, string) (*authpkg.TokenData, error) {
return nil, errors.New("exchange")
}
if err := exchange.RunE(exchange, nil); err == nil {
t.Fatal("exchange error should propagate")
}
authOAuthExchange = func(*authpkg.OAuthProvider, context.Context, string, string) (*authpkg.TokenData, error) {
return &authpkg.TokenData{CorpID: "ding", ExpiresAt: time.Now().Add(time.Hour)}, nil
}
_ = exchange.Flags().Set("uid", " user ")
if err := exchange.RunE(exchange, nil); err != nil || !strings.Contains(out.String(), "ding") {
t.Fatalf("exchange = %q, %v", out.String(), err)
}
reset := newAuthResetCommand()
_, out, _ = authCoverageRoot(reset, "table", false)
authDeleteAllTokenData = func(string) error { return errors.New("reset") }
if err := reset.RunE(reset, nil); err == nil {
t.Fatal("reset delete should fail")
}
removed := 0
authDeleteAllTokenData = func(string) error { return nil }
authRemove = func(string) error { removed++; return errors.New("ignored") }
authDeleteAppConfig = func(string) error { removed++; return errors.New("ignored") }
edition.Override(&edition.Hooks{})
if err := reset.RunE(reset, nil); err != nil || removed != 3 || !strings.Contains(out.String(), "重新登录") {
t.Fatalf("reset = %q, %v, removed=%d", out.String(), err, removed)
}
edition.Override(&edition.Hooks{IsEmbedded: true})
reset = newAuthResetCommand()
_, out, _ = authCoverageRoot(reset, "table", false)
if err := reset.RunE(reset, nil); err != nil || strings.Contains(out.String(), "重新登录") {
t.Fatalf("embedded reset = %q, %v", out.String(), err)
}
}