308 lines
12 KiB
Go
308 lines
12 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
authpkg "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/personal"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/pat"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
|
)
|
|
|
|
func TestCrossPlatformCoverageAuthLoginUsesStableBlankProfileForPostLoginAuthorization(t *testing.T) {
|
|
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
|
|
oldOAuth := authOAuthLogin
|
|
oldLoadProfiles := authLoadProfiles
|
|
oldRecommend := authRunLoginRecommend
|
|
oldInteractive := authLoginInteractiveTerminal
|
|
oldResolve := authResolveProfile
|
|
t.Cleanup(func() {
|
|
authOAuthLogin = oldOAuth
|
|
authLoadProfiles = oldLoadProfiles
|
|
authRunLoginRecommend = oldRecommend
|
|
authLoginInteractiveTerminal = oldInteractive
|
|
authResolveProfile = oldResolve
|
|
})
|
|
|
|
const corpID = "corp_post_login_blank"
|
|
cfg := &authpkg.ProfilesConfig{Profiles: []authpkg.Profile{
|
|
{Name: "Fixture Organization", CorpID: corpID, CorpName: "Fixture Organization"},
|
|
{Name: "Exact Fixture", CorpID: corpID, CorpName: "Fixture Organization", UserID: "identity_exact"},
|
|
}}
|
|
wantSelector := authpkg.ProfileSelectionSelector(cfg.Profiles[0], cfg)
|
|
if wantSelector == "" || wantSelector == corpID {
|
|
t.Fatalf("blank selector = %q, want a stable account selector", wantSelector)
|
|
}
|
|
authResolveProfile = func(string, string) (*authpkg.Profile, error) {
|
|
return nil, errors.New("no implicit profile")
|
|
}
|
|
authLoadProfiles = func(string) (*authpkg.ProfilesConfig, error) { return cfg, nil }
|
|
authOAuthLogin = func(*authpkg.OAuthProvider, context.Context, bool) (*authpkg.TokenData, error) {
|
|
return &authpkg.TokenData{
|
|
AccessToken: "new-access",
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
CorpID: corpID,
|
|
}, nil
|
|
}
|
|
authLoginInteractiveTerminal = func() bool { return false }
|
|
seenSelector := ""
|
|
authRunLoginRecommend = func(context.Context, edition.ToolCaller, io.Writer, pat.LoginRecommendOptions) error {
|
|
seenSelector = authpkg.RuntimeProfile()
|
|
return nil
|
|
}
|
|
if _, _, err := authCoverageRunLogin(t, nil, "table", true, map[string]string{"recommend": "true"}); err != nil {
|
|
t.Fatalf("blank-profile login error = %v", err)
|
|
}
|
|
if seenSelector != wantSelector {
|
|
t.Fatalf("post-login runtime selector = %q, want %q", seenSelector, wantSelector)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageAuthStatusAndLogoutPreserveExactSelectors(t *testing.T) {
|
|
t.Run("status canonicalizes a known identity", func(t *testing.T) {
|
|
configDir := t.TempDir()
|
|
t.Setenv("DWS_CONFIG_DIR", configDir)
|
|
const exactSelector = "corp_status_fixture:identity_status_fixture"
|
|
if err := authpkg.SaveProfiles(configDir, &authpkg.ProfilesConfig{
|
|
Version: 2,
|
|
Profiles: []authpkg.Profile{{
|
|
Name: "Status Fixture",
|
|
CorpID: "corp_status_fixture",
|
|
UserID: "identity_status_fixture",
|
|
}},
|
|
}); err != nil {
|
|
t.Fatalf("SaveProfiles() error = %v", err)
|
|
}
|
|
|
|
oldStatus := authOAuthStatus
|
|
t.Cleanup(func() { authOAuthStatus = oldStatus })
|
|
seenSelector := ""
|
|
authOAuthStatus = func(*authpkg.OAuthProvider) (*authpkg.TokenData, error) {
|
|
seenSelector = authpkg.RuntimeProfile()
|
|
return &authpkg.TokenData{
|
|
AccessToken: "access",
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
CorpID: "corp_status_fixture",
|
|
UserID: "identity_status_fixture",
|
|
}, nil
|
|
}
|
|
cmd := newAuthStatusCommand()
|
|
_, _, _ = authCoverageRoot(cmd, "table", false)
|
|
if err := cmd.Flags().Set("profile", " Status Fixture "); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := cmd.RunE(cmd, nil); err != nil {
|
|
t.Fatalf("auth status error = %v", err)
|
|
}
|
|
if seenSelector != exactSelector {
|
|
t.Fatalf("status runtime selector = %q, want %q", seenSelector, exactSelector)
|
|
}
|
|
})
|
|
|
|
t.Run("logout keeps a blank local selector", func(t *testing.T) {
|
|
oldResolve := authResolveProfileDeletion
|
|
oldLoad := authLoadTokenForProfile
|
|
oldRevoke := authRevokeTokenForData
|
|
oldDelete := authDeleteProfileToken
|
|
t.Cleanup(func() {
|
|
authResolveProfileDeletion = oldResolve
|
|
authLoadTokenForProfile = oldLoad
|
|
authRevokeTokenForData = oldRevoke
|
|
authDeleteProfileToken = oldDelete
|
|
})
|
|
|
|
const selector = "legacy-external-worker"
|
|
authResolveProfileDeletion = func(string, string) (*authpkg.Profile, bool, error) {
|
|
return &authpkg.Profile{CorpID: "corp_logout_blank"}, true, nil
|
|
}
|
|
loadedSelector := ""
|
|
authLoadTokenForProfile = func(_ string, got string) (*authpkg.TokenData, error) {
|
|
loadedSelector = got
|
|
return &authpkg.TokenData{CorpID: "corp_logout_blank"}, nil
|
|
}
|
|
authRevokeTokenForData = func(context.Context, *authpkg.TokenData) error { return nil }
|
|
deletedSelector := ""
|
|
authDeleteProfileToken = func(_ string, got string) error {
|
|
deletedSelector = got
|
|
return nil
|
|
}
|
|
|
|
if err := logoutOneProfile(nil, context.Background(), "cfg", " "+selector+" "); err != nil {
|
|
t.Fatalf("logoutOneProfile() error = %v", err)
|
|
}
|
|
if loadedSelector != selector || deletedSelector != selector {
|
|
t.Fatalf("blank logout selectors = load %q delete %q, want %q", loadedSelector, deletedSelector, selector)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCrossPlatformCoverageAuthHistorySelectorRemainingBranches(t *testing.T) {
|
|
if got := authLoginHistorySelector("cfg", nil); got != "" {
|
|
t.Fatalf("nil history selector = %q", got)
|
|
}
|
|
|
|
oldLoad := authLoadProfiles
|
|
t.Cleanup(func() { authLoadProfiles = oldLoad })
|
|
authLoadProfiles = func(string) (*authpkg.ProfilesConfig, error) {
|
|
return nil, errors.New("profiles unavailable")
|
|
}
|
|
profile := &authpkg.Profile{CorpID: "corp_history", UserID: "identity_history"}
|
|
if got := authLoginHistorySelector("cfg", profile); got != "corp_history:identity_history" {
|
|
t.Fatalf("history selector fallback = %q", got)
|
|
}
|
|
|
|
duplicateA := &authpkg.Profile{CorpID: "corp_history", UserID: "duplicate_identity"}
|
|
duplicateB := &authpkg.Profile{CorpID: "corp_history", UserID: "duplicate_identity"}
|
|
if got := historicalProfileForSelector(
|
|
"corp_history",
|
|
"corp_history:duplicate_identity",
|
|
[]*authpkg.Profile{duplicateA, duplicateB},
|
|
); got != nil {
|
|
t.Fatalf("duplicate stable identity selected %#v", got)
|
|
}
|
|
|
|
// Whitespace keeps the raw selector from matching the stable string while
|
|
// ParseIdentitySelector still resolves its components.
|
|
exactFallback := &authpkg.Profile{CorpID: "corp_history", UserID: "fallback_identity"}
|
|
if got := historicalProfileForSelector(
|
|
"corp_history",
|
|
"corp_history : fallback_identity",
|
|
[]*authpkg.Profile{exactFallback},
|
|
); got != exactFallback {
|
|
t.Fatalf("exact history fallback = %#v, want %#v", got, exactFallback)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageProfileSwitchLegacyBlankAndNormalizedIdentityPointers(t *testing.T) {
|
|
t.Run("one legacy blank name", func(t *testing.T) {
|
|
profiles := []authpkg.Profile{
|
|
{Name: "Fixture Organization", CorpID: "corp_profile_fixture", CorpName: "Fixture Organization"},
|
|
{Name: "Exact Fixture", CorpID: "corp_profile_fixture", CorpName: "Fixture Organization", UserID: "identity_exact"},
|
|
}
|
|
cfg := &authpkg.ProfilesConfig{Profiles: profiles}
|
|
if got := profileSwitchProfileIndex(profiles, "Fixture Organization", cfg); got != 0 {
|
|
t.Fatalf("legacy blank profile index = %d, want 0", got)
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate legacy names fall through to blank-name compatibility", func(t *testing.T) {
|
|
profiles := []authpkg.Profile{
|
|
{Name: "duplicate-legacy", CorpID: "corp_profile_fixture"},
|
|
{Name: "duplicate-legacy", CorpID: "corp_profile_fixture"},
|
|
}
|
|
cfg := &authpkg.ProfilesConfig{Profiles: profiles}
|
|
if got := profileSwitchProfileIndex(profiles, "duplicate-legacy", cfg); got != 0 {
|
|
t.Fatalf("duplicate legacy fallback index = %d, want 0", got)
|
|
}
|
|
})
|
|
|
|
t.Run("normalized exact identity", func(t *testing.T) {
|
|
profiles := []authpkg.Profile{{CorpID: "corp_profile_fixture", UserID: "identity_exact"}}
|
|
cfg := &authpkg.ProfilesConfig{Profiles: profiles}
|
|
if got := profileSwitchProfileIndex(profiles, "corp_profile_fixture : identity_exact", cfg); got != 0 {
|
|
t.Fatalf("normalized exact profile index = %d, want 0", got)
|
|
}
|
|
if got := profileSwitchProfileIndex(profiles, "corp_profile_fixture : missing", cfg); got != -1 {
|
|
t.Fatalf("missing normalized exact profile index = %d, want -1", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCrossPlatformCoverageRuntimeRunnerPreservesBlankSelectorInSingleAndMultiRuns(t *testing.T) {
|
|
exact := authLogoutTestToken("corp_runner_blank")
|
|
exact.UserID = "identity_exact_runner"
|
|
other := authLogoutTestToken("corp_runner_other")
|
|
configDir := setupAuthLogoutProfiles(t, exact, other)
|
|
blank := authLogoutTestToken("corp_runner_blank")
|
|
blank.AccessToken = "access-unresolved-runner"
|
|
blank.RefreshToken = "refresh-unresolved-runner"
|
|
blank.UserID = ""
|
|
blank.UserName = ""
|
|
if err := authpkg.SaveTokenData(configDir, blank); err != nil {
|
|
t.Fatalf("SaveTokenData(blank) error = %v", err)
|
|
}
|
|
cfg, err := authpkg.LoadProfiles(configDir)
|
|
if err != nil {
|
|
t.Fatalf("LoadProfiles() error = %v", err)
|
|
}
|
|
blankSelector := ""
|
|
for _, profile := range cfg.Profiles {
|
|
if profile.CorpID == blank.CorpID && profile.UserID == "" {
|
|
blankSelector = authpkg.ProfileSelectionSelector(profile, cfg)
|
|
break
|
|
}
|
|
}
|
|
if blankSelector == "" || blankSelector == blank.CorpID {
|
|
t.Fatalf("blank runner selector = %q, want exact local selector", blankSelector)
|
|
}
|
|
|
|
runner := &runtimeRunner{fallback: multiProfileFallbackRunner{}}
|
|
invocation := executor.Invocation{
|
|
Kind: "helper_invocation",
|
|
CanonicalProduct: "contact",
|
|
Tool: "get_current_user_profile",
|
|
}
|
|
authpkg.SetRuntimeProfile(blankSelector)
|
|
result, err := runner.Run(context.Background(), invocation)
|
|
if err != nil {
|
|
t.Fatalf("single blank Run() error = %v", err)
|
|
}
|
|
content := result.Response["content"].(map[string]any)
|
|
if got := content["runtimeProfile"]; got != blankSelector {
|
|
t.Fatalf("single blank runtime profile = %#v, want %q", got, blankSelector)
|
|
}
|
|
if got := authpkg.RuntimeProfile(); got != blankSelector {
|
|
t.Fatalf("single blank runtime restoration = %q, want %q", got, blankSelector)
|
|
}
|
|
|
|
authpkg.SetRuntimeProfile(blankSelector + ",corp_runner_other")
|
|
result, err = runner.Run(context.Background(), invocation)
|
|
if err != nil {
|
|
t.Fatalf("multi blank Run() error = %v", err)
|
|
}
|
|
entries := result.Response["content"].(map[string]any)["profiles"].([]any)
|
|
if len(entries) != 2 {
|
|
t.Fatalf("multi blank profiles = %#v, want two", entries)
|
|
}
|
|
first := entries[0].(map[string]any)
|
|
if first["selector"] != blankSelector || first["profile"] != blankSelector || first["userId"] != "" {
|
|
t.Fatalf("multi blank first entry = %#v", first)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoveragePersonalBusSelectorCanonicalFallback(t *testing.T) {
|
|
authpkg.SetRuntimeProfile("")
|
|
t.Cleanup(func() { authpkg.SetRuntimeProfile("") })
|
|
identity := personal.Identity{
|
|
CorpID: "corp_event_fallback",
|
|
UserID: "identity_event_fallback",
|
|
SourceID: "open",
|
|
}
|
|
if got := personalBusProfileSelector(t.TempDir(), identity); got != "corp_event_fallback:identity_event_fallback" {
|
|
t.Fatalf("personal bus fallback selector = %q", got)
|
|
}
|
|
args := personalBusSpawnArgs(identity, "", "", " ")
|
|
if got := strings.Join(args, " "); !strings.Contains(got, "--profile corp_event_fallback:identity_event_fallback") {
|
|
t.Fatalf("personal bus default profile args = %q", got)
|
|
}
|
|
}
|