package app import ( "bytes" "context" "errors" "fmt" "io" "os" "path/filepath" "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) { configDir := t.TempDir() t.Setenv("DWS_CONFIG_DIR", configDir) 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) } if _, _, err := authCoverageRunLogin(t, nil, "table", true, map[string]string{"token": "token", "pre-url": "https://example.com"}); err == nil { t.Fatal("invalid pre-release host should fail") } if _, _, err := authCoverageRunLogin(t, nil, "table", true, map[string]string{"token": "token", "mcp-url": "http://remote.example.com"}); err == nil { t.Fatal("remote plaintext MCP URL should fail") } if _, _, err := authCoverageRunLogin(t, nil, "table", true, map[string]string{"token": "token", "pre-url": "https://pre-login.dingtalk.io"}); err != nil { t.Fatalf("pre-release token login = %v", 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(provider *authpkg.DeviceFlowProvider, _ context.Context) (*authpkg.TokenData, error) { if provider.IdentityEnricher == nil { t.Error("device login missing shared identity enricher") } 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) } authDeviceLogin = func(provider *authpkg.DeviceFlowProvider, _ context.Context) (*authpkg.TokenData, error) { if provider.LoginRegion != authpkg.LoginRegionInternational { t.Errorf("device login region = %q, want international", provider.LoginRegion) } return &authpkg.TokenData{AccessToken: "a", ExpiresAt: time.Now().Add(time.Hour)}, nil } if _, _, err := authCoverageRunLogin(t, nil, "table", true, map[string]string{"device": "true", "intl": "true"}); err != nil { t.Fatalf("international device login = %v", 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(provider *authpkg.OAuthProvider, _ context.Context, _ bool) (*authpkg.TokenData, error) { if provider.IdentityEnricher == nil { t.Error("OAuth login missing shared identity enricher") } 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) } authOAuthLogin = func(provider *authpkg.OAuthProvider, _ context.Context, _ bool) (*authpkg.TokenData, error) { if provider.LoginRegion != authpkg.LoginRegionInternational { t.Errorf("OAuth login region = %q, want international", provider.LoginRegion) } return &authpkg.TokenData{AccessToken: "a", ExpiresAt: time.Now().Add(time.Hour)}, nil } if _, _, err := authCoverageRunLogin(t, nil, "table", true, map[string]string{"intl": "true"}); err != nil { t.Fatalf("international OAuth login = %v", err) } blockedConfigDir := t.TempDir() if err := os.Mkdir(filepath.Join(blockedConfigDir, "mcp_url"), 0o700); err != nil { t.Fatal(err) } t.Setenv("DWS_CONFIG_DIR", blockedConfigDir) if _, _, err := authCoverageRunLogin(t, nil, "table", true, map[string]string{"token": "token", "intl": "true"}); err == nil || !strings.Contains(err.Error(), "failed to persist MCP URL") { t.Fatalf("MCP URL persist failure = %v", err) } t.Setenv("DWS_CONFIG_DIR", configDir) 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.Fatalf("contact failure must remain best effort: %v", err) } 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.Fatalf("contact corp mismatch must remain best effort: %v", err) } 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) } known := &authpkg.TokenData{CorpID: "ding", UserID: "exchange-user", AccessToken: "token"} differentContactUser := &authCoverageCaller{result: &edition.ToolResult{Content: []edition.ContentBlock{{Text: `{"result":[{"orgEmployeeModel":{"corpId":"ding","orgName":"Corp","userid":"other-user","name":"Other User"}}]}`}}}} if err := enrichAuthLoginProfileFromContact(ctx, "cfg", differentContactUser, known); err != nil { t.Fatal(err) } if known.UserID != "exchange-user" || known.UserName != "" || known.CorpName != "Corp" { t.Fatalf("token-exchange identity was overwritten: %#v", known) } multiOrg := &authCoverageCaller{result: &edition.ToolResult{Content: []edition.ContentBlock{{Text: `{"result":[{"orgEmployeeModel":{"corpId":"other","userid":"other-user"}},{"orgEmployeeModel":{"corpId":"ding","orgName":"Target Corp","userid":"target-user","name":"Target User"}}]}`}}}} multiOrgData := &authpkg.TokenData{CorpID: "ding", AccessToken: "token"} if err := enrichAuthLoginProfileFromContact(ctx, "cfg", multiOrg, multiOrgData); err != nil || multiOrgData.UserID != "target-user" || multiOrgData.CorpName != "Target Corp" { t.Fatalf("multi-org contact selection = %#v, %v", multiOrgData, err) } if _, ok := contactProfileIdentityFromJSON( []byte(`{"result":[{"orgEmployeeModel":{"corpId":"other-a","userid":"user-a"}},{"orgEmployeeModel":{"corpId":"other-b","userid":"user-b"}}]}`), "ding", ); ok { t.Fatal("multiple nonmatching organizations must not select an arbitrary contact identity") } 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 TestCrossPlatformCoverageContactFailureReusesOnlySameCorpHistoricalDisplayMetadata(t *testing.T) { configDir := t.TempDir() if err := authpkg.SaveProfiles(configDir, &authpkg.ProfilesConfig{ Version: 1, Profiles: []authpkg.Profile{{ CorpID: "ding_ecological_worker", CorpName: "Historical Corp", UserID: "external-user", UserName: "Historical Worker", }}, }); err != nil { t.Fatalf("SaveProfiles() error = %v", err) } for _, tc := range []struct { name string caller edition.ToolCaller wantCorp string }{ { name: "contact business error", caller: &authCoverageCaller{err: apperrors.NewAPI( "business error: success=false", apperrors.WithReason("business_error"), )}, wantCorp: "Fresh Corp", }, { name: "contact has no identity", caller: &authCoverageCaller{result: &edition.ToolResult{Content: []edition.ContentBlock{{Text: `{"success":false}`}}}}, wantCorp: "Fresh Corp", }, { name: "contact identity is missing user id", caller: &authCoverageCaller{result: &edition.ToolResult{Content: []edition.ContentBlock{{ Text: `{"result":[{"orgEmployeeModel":{"corpId":"ding_ecological_worker","orgName":"Contact Corp"}}]}`, }}}}, wantCorp: "Contact Corp", }, { name: "ordinary contact error", caller: &authCoverageCaller{err: errors.New("network failure")}, wantCorp: "Fresh Corp", }, { name: "other contact business error", caller: &authCoverageCaller{err: apperrors.NewAPI( "permission denied", apperrors.WithReason("business_error"), )}, wantCorp: "Fresh Corp", }, { name: "contact caller unavailable", caller: nil, wantCorp: "Fresh Corp", }, { name: "contact returns another organization", caller: &authCoverageCaller{result: &edition.ToolResult{Content: []edition.ContentBlock{{ Text: `{"result":[{"orgEmployeeModel":{"corpId":"ding_other","userid":"other-user"}}]}`, }}}}, wantCorp: "Fresh Corp", }, } { t.Run(tc.name, func(t *testing.T) { data := &authpkg.TokenData{ AccessToken: "new-access", RefreshToken: "new-refresh", CorpID: "ding_ecological_worker", CorpName: "Fresh Corp", } if err := enrichAuthLoginProfileFromContact(context.Background(), configDir, tc.caller, data); err != nil { t.Fatalf("contact failure blocked historical identity recovery: %v", err) } if data.UserID != "" || data.UserName != "Historical Worker" { t.Fatalf("historical metadata supplied UID evidence: %#v", data) } if data.CorpName != tc.wantCorp { t.Fatalf("corp name = %q, want %q", data.CorpName, tc.wantCorp) } if data.AccessToken != "new-access" || data.RefreshToken != "new-refresh" { t.Fatalf("new token material was changed: %#v", data) } }) } } func TestCrossPlatformCoverageContactFailureDoesNotGuessHistoricalIdentity(t *testing.T) { businessErr := apperrors.NewAPI( "business error: success=false", apperrors.WithReason("business_error"), ) for _, tc := range []struct { name string corpID string profiles []authpkg.Profile callErr error }{ { name: "same corp has two identities", corpID: "ding_ecological_worker", profiles: []authpkg.Profile{ {CorpID: "ding_ecological_worker", UserID: "external-user"}, {CorpID: "ding_ecological_worker", UserID: "external-user-b"}, }, callErr: businessErr, }, { name: "same corp has one identity and one blank profile", corpID: "ding_ecological_worker", profiles: []authpkg.Profile{ {CorpID: "ding_ecological_worker", UserID: "external-user"}, {CorpID: "ding_ecological_worker"}, }, callErr: businessErr, }, { name: "identity belongs to another corp", corpID: "ding_ecological_worker", profiles: []authpkg.Profile{ {CorpID: "ding_other", UserID: "external-user"}, }, callErr: businessErr, }, { name: "no historical identity", corpID: "ding_ecological_worker", callErr: businessErr, }, } { t.Run(tc.name, func(t *testing.T) { configDir := t.TempDir() if err := authpkg.SaveProfiles(configDir, &authpkg.ProfilesConfig{Version: 2, Profiles: tc.profiles}); err != nil { t.Fatalf("SaveProfiles() error = %v", err) } data := &authpkg.TokenData{AccessToken: "new-access", CorpID: tc.corpID} err := enrichAuthLoginProfileFromContact( context.Background(), configDir, &authCoverageCaller{err: tc.callErr}, data, ) if err != nil { t.Fatalf("contact failure must not block unresolved legacy login: %v", err) } if data.UserID != "" { t.Fatalf("ambiguous/cross-corp identity was reused: %#v", data) } }) } } func TestCrossPlatformCoverageContactHistoryFallbackEdges(t *testing.T) { for _, data := range []*authpkg.TokenData{ nil, {UserID: "known"}, {}, } { reused, err := enrichAuthLoginProfileFromHistory(t.TempDir(), data) if reused || err != nil { t.Fatalf("ineligible history fallback = %v, %v", reused, err) } } configDir := t.TempDir() if err := authpkg.SaveProfiles(configDir, &authpkg.ProfilesConfig{ Version: 2, Profiles: []authpkg.Profile{{ CorpID: "ding_external", CorpName: "Historical Corp", UserID: "external-user", UserName: "Historical Worker", }}, }); err != nil { t.Fatalf("SaveProfiles() error = %v", err) } data := &authpkg.TokenData{CorpID: "ding_external"} reused, err := enrichAuthLoginProfileFromHistory(configDir, data) if err != nil || !reused { t.Fatalf("history fallback = %v, %v", reused, err) } if data.CorpName != "Historical Corp" || data.UserName != "Historical Worker" || data.UserID != "" { t.Fatalf("history metadata = %#v", data) } corruptDir := t.TempDir() if err := os.Mkdir(authpkg.ProfilesPath(corruptDir), 0o700); err != nil { t.Fatalf("create unreadable profiles path: %v", err) } if reused, err := enrichAuthLoginProfileFromHistory(corruptDir, &authpkg.TokenData{CorpID: "ding_external"}); reused || err == nil { t.Fatalf("corrupt history fallback = %v, %v; want load error", reused, err) } businessErr := apperrors.NewAPI( "business error: success=false", apperrors.WithReason("business_error"), ) for _, tc := range []struct { name string caller *authCoverageCaller }{ { name: "contact business error", caller: &authCoverageCaller{err: businessErr}, }, { name: "contact has no identity", caller: &authCoverageCaller{result: &edition.ToolResult{}}, }, { name: "contact identity is missing user id", caller: &authCoverageCaller{result: &edition.ToolResult{Content: []edition.ContentBlock{{ Text: `{"result":[{"orgEmployeeModel":{"corpId":"ding_external"}}]}`, }}}}, }, } { t.Run(tc.name, func(t *testing.T) { err := enrichAuthLoginProfileFromContact( context.Background(), corruptDir, tc.caller, &authpkg.TokenData{CorpID: "ding_external", AccessToken: "new-access"}, ) if err != nil { t.Fatalf("best-effort contact/history lookup blocked login: %v", err) } }) } } func TestCrossPlatformCoverageAuthLoginConfigPreservesHistoryIdentityHint(t *testing.T) { t.Setenv("DWS_CONFIG_DIR", t.TempDir()) oldResolve := authResolveProfile oldLoad := authLoadProfiles t.Cleanup(func() { authResolveProfile = oldResolve authLoadProfiles = oldLoad }) explicit := &authpkg.Profile{CorpID: "ding_same", UserID: "user_2", Name: "second"} current := &authpkg.Profile{CorpID: "ding_current", UserID: "current_user"} authResolveProfile = func(_ string, selector string) (*authpkg.Profile, error) { switch selector { case "ding_same:user_2": clone := *explicit return &clone, nil case "external-worker": return &authpkg.Profile{Name: "external-worker", CorpID: "ding_external"}, nil case "": clone := *current return &clone, nil default: return nil, errors.New("missing") } } authLoadProfiles = func(string) (*authpkg.ProfilesConfig, error) { return &authpkg.ProfilesConfig{}, nil } cmd := newAuthLoginCommand(nil) root, _, _ := authCoverageRoot(cmd, "table", true) if err := root.PersistentFlags().Set("profile", "ding_same:user_2"); err != nil { t.Fatal(err) } cfg, err := resolveAuthLoginConfig(cmd) if err != nil { t.Fatal(err) } if cfg.TargetCorpID != "ding_same" || cfg.HistoryProfileSelector != "ding_same:user_2" || !cfg.HistoryProfileSelectorExplicit { t.Fatalf("explicit login config = %#v", cfg) } if target, hint, exact, err := resolveAuthLoginTarget("cfg", "external-worker"); err != nil || target != "ding_external" || hint != "ding_external" || !exact { t.Fatalf("blank-userId profile target = %q/%q/%v, %v", target, hint, exact, err) } if err := root.PersistentFlags().Set("profile", ""); err != nil { t.Fatal(err) } cfg, err = resolveAuthLoginConfig(cmd) if err != nil { t.Fatal(err) } if cfg.TargetCorpID != "" || cfg.HistoryProfileSelector != "ding_current:current_user" || cfg.HistoryProfileSelectorExplicit { t.Fatalf("implicit login config constrained authorization target: %#v", cfg) } if _, _, _, err := resolveAuthLoginTarget("cfg", "ding_same:missing"); err == nil { t.Fatal("missing exact profile must not be reinterpreted as a corpId") } if target, hint, explicitHint, err := resolveAuthLoginTarget("cfg", "ding_new"); err != nil || target != "ding_new" || hint != "" || explicitHint { t.Fatalf("new organization target = %q/%q/%v, %v", target, hint, explicitHint, err) } authLoadProfiles = func(string) (*authpkg.ProfilesConfig, error) { return &authpkg.ProfilesConfig{Profiles: []authpkg.Profile{ {CorpID: "ding_ambiguous", UserID: "user_1"}, {CorpID: "ding_ambiguous", UserID: "user_2"}, }}, nil } if _, _, _, err := resolveAuthLoginTarget("cfg", "ding_ambiguous"); err == nil { t.Fatal("ambiguous known organization must require an exact profile") } } func TestCrossPlatformCoverageOAuthAndDeviceKeepFreshUnknownIdentityIsolatedFromExactHistory(t *testing.T) { oldResolve := authResolveProfile oldLoad := authLoadProfiles oldDevice := authDeviceLogin oldOAuth := authOAuthLogin oldInteractive := authLoginInteractiveTerminal t.Cleanup(func() { authResolveProfile = oldResolve authLoadProfiles = oldLoad authDeviceLogin = oldDevice authOAuthLogin = oldOAuth authLoginInteractiveTerminal = oldInteractive }) authResolveProfile = authpkg.ResolveProfile authLoadProfiles = authpkg.LoadProfiles authLoginInteractiveTerminal = func() bool { return false } t.Cleanup(func() { authpkg.SetRuntimeProfile("") }) for _, flow := range []string{"oauth", "device"} { t.Run(flow, func(t *testing.T) { configDir := t.TempDir() keychainDir := t.TempDir() t.Setenv("DWS_CONFIG_DIR", configDir) t.Setenv(keychain.DisableKeychainEnv, "1") t.Setenv(keychain.StorageDirEnv, keychainDir) // StorageDirEnv isolates file-backed keychains, while Windows uses // DPAPI-protected HKCU values. Give every flow its own namespace so // OAuth/device fixtures cannot leak into each other or later tests. t.Setenv(keychain.TestNamespaceEnv, keychainDir) t.Cleanup(func() { if err := keychain.RemoveAuthTokenEntries(keychain.Service); err != nil { t.Errorf("clean auth keychain fixture: %v", err) } }) authpkg.SetRuntimeProfile("") const ( corpID = "ding_same" historicalUID = "user_a" exactSelector = corpID + ":" + historicalUID ) oldToken := &authpkg.TokenData{ AccessToken: "old-user-a-access", RefreshToken: "old-user-a-refresh", ExpiresAt: time.Now().Add(time.Hour), RefreshExpAt: time.Now().Add(24 * time.Hour), CorpID: corpID, CorpName: "Same Corp", UserID: historicalUID, UserName: "Historical User A", } if err := authpkg.SaveTokenData(configDir, oldToken); err != nil { t.Fatalf("persist historical exact identity: %v", err) } caller := &authCoverageCaller{err: errors.New("contact unavailable")} var enriched *authpkg.TokenData freshToken := func() *authpkg.TokenData { return &authpkg.TokenData{ AccessToken: "fresh-user-b-access-" + flow, RefreshToken: "fresh-user-b-refresh-" + flow, ExpiresAt: time.Now().Add(time.Hour), RefreshExpAt: time.Now().Add(24 * time.Hour), CorpID: corpID, } } persistUnknown := func(ctx context.Context, identityEnricher func(context.Context, *authpkg.TokenData) error) (*authpkg.TokenData, error) { if identityEnricher == nil { return nil, errors.New("missing identity enricher") } data := freshToken() if err := identityEnricher(ctx, data); err != nil { return nil, err } enriched = data if data.UserID != "" { return nil, fmt.Errorf("historical profile supplied unproven userId %q", data.UserID) } if err := authpkg.SaveTokenData(configDir, data); err != nil { return nil, err } return data, nil } flags := map[string]string{"profile": exactSelector} switch flow { case "device": flags["device"] = "true" authDeviceLogin = func(provider *authpkg.DeviceFlowProvider, ctx context.Context) (*authpkg.TokenData, error) { return persistUnknown(ctx, provider.IdentityEnricher) } case "oauth": authOAuthLogin = func(provider *authpkg.OAuthProvider, ctx context.Context, _ bool) (*authpkg.TokenData, error) { if provider.TargetCorpID != corpID { return nil, fmt.Errorf("OAuth target corp = %q", provider.TargetCorpID) } return persistUnknown(ctx, provider.IdentityEnricher) } } if _, _, err := authCoverageRunLogin(t, caller, "table", true, flags); err != nil { t.Fatalf("%s login with unresolved fresh identity: %v", flow, err) } if enriched == nil || enriched.UserID != "" || enriched.LegacyOrgScopedProfile != exactSelector || enriched.CorpName != "Same Corp" || enriched.UserName != "Historical User A" { t.Fatalf("%s history hint became identity evidence: %#v", flow, enriched) } historical, err := authpkg.LoadTokenDataForProfile(configDir, exactSelector) if err != nil { t.Fatalf("load historical exact identity: %v", err) } if historical.AccessToken != oldToken.AccessToken || historical.UserID != historicalUID { t.Fatalf("historical exact slot was overwritten: %#v", historical) } profiles, err := authpkg.LoadProfiles(configDir) if err != nil { t.Fatalf("load profiles: %v", err) } var unresolved *authpkg.Profile for i := range profiles.Profiles { profile := &profiles.Profiles[i] if profile.CorpID == corpID && profile.UserID == "" { unresolved = profile break } } if unresolved == nil { t.Fatalf("fresh UID-less token did not create an unresolved profile: %#v", profiles.Profiles) } unresolvedSelector := authpkg.ProfileSelectionSelector(*unresolved, profiles) if unresolvedSelector == "" || unresolvedSelector == exactSelector { t.Fatalf("unresolved selector = %q", unresolvedSelector) } fresh, err := authpkg.LoadTokenDataForProfile(configDir, unresolvedSelector) if err != nil { t.Fatalf("load fresh unresolved identity: %v", err) } if fresh.AccessToken != "fresh-user-b-access-"+flow || fresh.UserID != "" { t.Fatalf("fresh token was not isolated in unresolved org slot: %#v", fresh) } }) } } func TestCrossPlatformCoverageHistoricalIdentityPriorityAndBlankUserID(t *testing.T) { oldLoad := authLoadProfiles t.Cleanup(func() { authLoadProfiles = oldLoad }) authLoadProfiles = func(string) (*authpkg.ProfilesConfig, error) { return nil, nil } if reused, err := enrichAuthLoginProfileFromHistory("cfg", &authpkg.TokenData{CorpID: "ding_same"}); reused || err != nil { t.Fatalf("nil history registry = reused=%v err=%v", reused, err) } cfg := &authpkg.ProfilesConfig{ CurrentProfile: "ding_same:user_1", OrgCurrentProfiles: map[string]string{ "ding_same": "ding_same:user_2", }, Profiles: []authpkg.Profile{ {CorpID: "ding_same", CorpName: "Same Corp", UserID: "user_1", UserName: "First"}, {CorpID: "ding_same", CorpName: "Same Corp", UserID: "user_2", UserName: "Second"}, }, } authLoadProfiles = func(string) (*authpkg.ProfilesConfig, error) { return cfg, nil } explicitData := &authpkg.TokenData{CorpID: "ding_same"} reused, err := enrichAuthLoginProfileFromHistory("cfg", explicitData, authLoginHistoryHint{Selector: "ding_same:user_1", Explicit: true}) if err != nil || !reused || explicitData.UserID != "" || explicitData.LegacyOrgScopedProfile != "ding_same:user_1" || explicitData.CorpName != "Same Corp" || explicitData.UserName != "First" { t.Fatalf("explicit history selection = %#v, reused=%v err=%v", explicitData, reused, err) } mismatchedHintData := &authpkg.TokenData{CorpID: "ding_same"} reused, err = enrichAuthLoginProfileFromHistory("cfg", mismatchedHintData, authLoginHistoryHint{Selector: "ding_other:user_9", Explicit: true}) if err != nil || reused || mismatchedHintData.UserID != "" { t.Fatalf("cross-corp explicit hint reused another identity: %#v, reused=%v err=%v", mismatchedHintData, reused, err) } orgCurrentData := &authpkg.TokenData{CorpID: "ding_same"} reused, err = enrichAuthLoginProfileFromHistory("cfg", orgCurrentData) if err != nil || reused || orgCurrentData.UserID != "" { t.Fatalf("implicit multi-account org-current was treated as identity proof: %#v, reused=%v err=%v", orgCurrentData, reused, err) } cfg.Profiles = []authpkg.Profile{cfg.Profiles[1]} soleData := &authpkg.TokenData{CorpID: "ding_same"} reused, err = enrichAuthLoginProfileFromHistory("cfg", soleData) if err != nil || !reused || soleData.UserID != "" || soleData.CorpName != "Same Corp" || soleData.UserName != "Second" { t.Fatalf("sole history selection = %#v, reused=%v err=%v", soleData, reused, err) } cfg.Profiles = []authpkg.Profile{ {CorpID: "ding_same", CorpName: "Same Corp", UserID: "user_1", UserName: "First"}, {CorpID: "ding_same", CorpName: "Same Corp", UserID: "user_2", UserName: "Second"}, } cfg.OrgCurrentProfiles = nil currentData := &authpkg.TokenData{CorpID: "ding_same"} reused, err = enrichAuthLoginProfileFromHistory("cfg", currentData) if err != nil || reused || currentData.UserID != "" { t.Fatalf("implicit multi-account current was treated as identity proof: %#v, reused=%v err=%v", currentData, reused, err) } cfg.CurrentProfile = "" ambiguousData := &authpkg.TokenData{CorpID: "ding_same"} reused, err = enrichAuthLoginProfileFromHistory("cfg", ambiguousData) if err != nil || reused || ambiguousData.UserID != "" { t.Fatalf("ambiguous history selection = %#v, reused=%v err=%v", ambiguousData, reused, err) } cfg.Profiles = []authpkg.Profile{{ Name: "external-worker", CorpID: "ding_same", CorpName: "Legacy Corp", UserName: "Legacy Worker", }} blankData := &authpkg.TokenData{CorpID: "ding_same"} reused, err = enrichAuthLoginProfileFromHistory("cfg", blankData, authLoginHistoryHint{Selector: "external-worker", Explicit: true}) if err != nil || !reused || blankData.UserID != "" || blankData.LegacyOrgScopedProfile != "external-worker" || blankData.CorpName != "Legacy Corp" || blankData.UserName != "Legacy Worker" { t.Fatalf("blank-userId history selection = %#v, reused=%v err=%v", blankData, reused, err) } contactBlankData := &authpkg.TokenData{CorpID: "ding_same", AccessToken: "new-token"} if err := enrichAuthLoginProfileFromContact( context.Background(), "cfg", &authCoverageCaller{err: errors.New("contact unavailable")}, contactBlankData, authLoginHistoryHint{Selector: "external-worker", Explicit: true}, ); err != nil { t.Fatalf("blank-userId history must keep contact best effort: %v", err) } if contactBlankData.LegacyOrgScopedProfile != "external-worker" { t.Fatalf("blank-userId contact fallback did not authorize the historical organization slot: %#v", contactBlankData) } profiles := []*authpkg.Profile{ nil, {Name: "duplicate", CorpID: "ding_same", UserID: "user_1"}, {Name: "duplicate", CorpID: "ding_same", UserID: "user_2"}, } for _, tc := range []struct { name string selector string profiles []*authpkg.Profile want *authpkg.Profile }{ {name: "empty selector", selector: "", profiles: profiles}, {name: "missing exact identity", selector: "ding_same:missing", profiles: profiles}, {name: "duplicate name", selector: "duplicate", profiles: profiles}, {name: "unmatched name", selector: "not-found", profiles: profiles}, {name: "sole organization selector", selector: "ding_same", profiles: profiles[1:2], want: profiles[1]}, } { t.Run("selector "+tc.name, func(t *testing.T) { if got := historicalProfileForSelector("ding_same", tc.selector, tc.profiles); got != tc.want { t.Fatalf("historicalProfileForSelector(%q) = %#v, want %#v", tc.selector, got, tc.want) } }) } } 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") } _, _, _ = 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 != 4 || !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) } }