From a5a6a0f2cedcd9d239e83f5eedfc450d67a79014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8A=E5=AE=98=E7=8E=84?= Date: Thu, 23 Jul 2026 13:01:10 +0800 Subject: [PATCH] test(auth): close legacy compatibility coverage gaps --- .../app/event_profile_selector_compat_test.go | 21 ++ internal/auth/access_token_snapshot_test.go | 3 +- .../auth_legacy_platform_coverage_test.go | 297 ++++++++++++++++++ internal/auth/oauth_provider_coverage_test.go | 4 + 4 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 internal/auth/auth_legacy_platform_coverage_test.go diff --git a/internal/app/event_profile_selector_compat_test.go b/internal/app/event_profile_selector_compat_test.go index 267af36d..5a99c882 100644 --- a/internal/app/event_profile_selector_compat_test.go +++ b/internal/app/event_profile_selector_compat_test.go @@ -72,6 +72,27 @@ func TestPersonalBusProfileSelectorPrefersExplicitRuntimeSelector(t *testing.T) } } +func TestCrossPlatformCoveragePersonalBusProfileSelectorFallsBackToMatchingIdentity(t *testing.T) { + configDir, cfg, _, exactSelector := seedPersonalBusProfileSelectorConfig(t) + cfg.Profiles = append(cfg.Profiles, authpkg.Profile{ + Name: "Other Current", + CorpID: "corp_event_other_fixture", + CorpName: "Other Fixture Organization", + UserID: "identity_event_other_fixture", + }) + cfg.CurrentProfile = authpkg.ProfileSelectionSelector(cfg.Profiles[2], cfg) + if err := authpkg.SaveProfiles(configDir, cfg); err != nil { + t.Fatalf("SaveProfiles() error = %v", err) + } + authpkg.SetRuntimeProfile("") + t.Cleanup(func() { authpkg.SetRuntimeProfile("") }) + + identity := personal.Identity{CorpID: cfg.Profiles[1].CorpID, UserID: cfg.Profiles[1].UserID} + if got := personalBusProfileSelector(configDir, identity); got != exactSelector { + t.Fatalf("personalBusProfileSelector() = %q, want identity fallback %q", got, exactSelector) + } +} + func seedPersonalBusProfileSelectorConfig(t *testing.T) (string, *authpkg.ProfilesConfig, string, string) { t.Helper() configDir := t.TempDir() diff --git a/internal/auth/access_token_snapshot_test.go b/internal/auth/access_token_snapshot_test.go index 6595da74..00655b26 100644 --- a/internal/auth/access_token_snapshot_test.go +++ b/internal/auth/access_token_snapshot_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "io" + "log/slog" "net/http" "net/url" "testing" @@ -94,7 +95,7 @@ func TestCrossPlatformCoverageOAuthProviderLoginReauthorizesAfterLoadFailureAndR return nil } - provider := NewOAuthProvider(t.TempDir(), nil) + provider := NewOAuthProvider(t.TempDir(), slog.New(slog.NewTextHandler(io.Discard, nil))) provider.Output = io.Discard _, err := provider.Login(context.Background(), false) if !errors.Is(err, targetErr) { diff --git a/internal/auth/auth_legacy_platform_coverage_test.go b/internal/auth/auth_legacy_platform_coverage_test.go new file mode 100644 index 00000000..f23beee0 --- /dev/null +++ b/internal/auth/auth_legacy_platform_coverage_test.go @@ -0,0 +1,297 @@ +// 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 auth + +import ( + "errors" + "strings" + "testing" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/keychain" +) + +// The native coverage jobs intentionally execute only TestCrossPlatformCoverage +// entry points. Keep the compatibility assertions below as independently named +// regression tests for the stable make target, and exercise the same functions +// here as isolated subtests so their cleanup hooks run between cases. +func TestCrossPlatformCoverageAuthLegacyCompatibilityRegressions(t *testing.T) { + t.Setenv(keychain.DisableKeychainEnv, "1") + t.Setenv(keychain.StorageDirEnv, t.TempDir()) + tests := []struct { + name string + run func(*testing.T) + }{ + {"prepare unique global owner", TestPrepareLoginPersistenceRepairsOnlySafeGlobalOwner}, + {"token load isolation matrix", TestTokenLoadIsolationMatrix}, + {"reject future profiles before remote work", TestPersistingLoginFlowsRejectFutureProfilesBeforeRemoteWork}, + {"fresh UID-less isolation", TestFreshUIDLessExactLoginCannotOverwriteExistingUnresolvedProfile}, + {"reject mismatched exact switch", TestSetCurrentProfileRejectsMismatchedExactIdentitySlotBesideBlankProfile}, + {"reject unreadable previous identity", TestUsePreviousProfileRejectsUnreadableExactIdentityBesideBlankProfile}, + {"reauthorization guidance without profile", TestLegacyRefreshReauthorizationGuidanceWithoutProfileStillExplainsLogin}, + {"repair v3 unresolved profile", TestPrepareLoginPersistenceV3UnresolvedProfileRepair}, + {"device ignores unrelated unreadable profile", TestDeviceLoginIgnoresUnreadableUnrelatedProfile}, + {"reject unreadable global before login", TestPrepareLoginPersistenceUnreadableGlobalFailsClosedBeforeRemote}, + {"device validates resolved target", TestDeviceFlowChecksResolvedTargetBeforeSave}, + {"accept recoverable credential material", TestPrepareLoginPersistenceRequiresCredentialMaterialButNotValidity}, + {"auth code validates resolved target", TestExchangeAuthCodeChecksResolvedTargetBeforeSave}, + {"standalone exchange prepares and marks fresh", TestExchangeCodeForTokenPreparesBeforeRemoteAndMarksFresh}, + } + for _, test := range tests { + t.Run(test.name, test.run) + } +} + +func TestCrossPlatformCoverageHalfMigratedGlobalRepairRemainingEdges(t *testing.T) { + t.Setenv(keychain.DisableKeychainEnv, "1") + t.Setenv(keychain.StorageDirEnv, t.TempDir()) + + t.Run("nil global token", func(t *testing.T) { + isolateHalfMigratedRepairHooks(t) + profilesLoadLegacy = func() (*TokenData, error) { return nil, nil } + cfg := &ProfilesConfig{Version: profilesVersion, Profiles: []Profile{{CorpID: "corp_nil_global"}}} + if err := repairHalfMigratedGlobalTokenLocked(cfg); err != nil { + t.Fatalf("repairHalfMigratedGlobalTokenLocked() error = %v", err) + } + }) + + t.Run("global token without organization", func(t *testing.T) { + isolateHalfMigratedRepairHooks(t) + profilesLoadLegacy = func() (*TokenData, error) { + return &TokenData{AccessToken: "legacy"}, nil + } + cfg := &ProfilesConfig{Version: profilesVersion, Profiles: []Profile{{CorpID: "corp_other"}}} + if err := repairHalfMigratedGlobalTokenLocked(cfg); err != nil { + t.Fatalf("repairHalfMigratedGlobalTokenLocked() error = %v", err) + } + }) + + t.Run("orphan global organization", func(t *testing.T) { + isolateHalfMigratedRepairHooks(t) + profilesLoadLegacy = func() (*TokenData, error) { + return &TokenData{AccessToken: "legacy", CorpID: "corp_orphan"}, nil + } + cfg := &ProfilesConfig{Version: profilesVersion, Profiles: []Profile{{CorpID: "corp_other"}}} + if err := repairHalfMigratedGlobalTokenLocked(cfg); err != nil { + t.Fatalf("repairHalfMigratedGlobalTokenLocked() error = %v", err) + } + }) + + t.Run("identity repair write failure", func(t *testing.T) { + isolateHalfMigratedRepairHooks(t) + failure := errors.New("identity repair write failure") + const corpID, userID = "corp_identity_repair", "user_identity_repair" + profilesLoadLegacy = func() (*TokenData, error) { + return &TokenData{AccessToken: "legacy", CorpID: corpID}, nil + } + profilesLoadCorp = func(string) (*TokenData, error) { + return &TokenData{AccessToken: "organization", CorpID: corpID}, nil + } + profilesSaveIdentity = func(string, string, *TokenData) error { return failure } + cfg := &ProfilesConfig{Version: profilesVersion, Profiles: []Profile{{CorpID: corpID, UserID: userID}}} + if err := repairHalfMigratedGlobalTokenLocked(cfg); !errors.Is(err, failure) { + t.Fatalf("repairHalfMigratedGlobalTokenLocked() error = %v, want %v", err, failure) + } + }) + + t.Run("organization repair write failure", func(t *testing.T) { + isolateHalfMigratedRepairHooks(t) + failure := errors.New("organization repair write failure") + const corpID = "corp_organization_repair" + profilesLoadLegacy = func() (*TokenData, error) { + return &TokenData{AccessToken: "legacy", CorpID: corpID}, nil + } + profilesSaveCorp = func(string, *TokenData) error { return failure } + cfg := &ProfilesConfig{Version: profilesVersion, Profiles: []Profile{{CorpID: corpID}}} + if err := repairHalfMigratedGlobalTokenLocked(cfg); !errors.Is(err, failure) { + t.Fatalf("repairHalfMigratedGlobalTokenLocked() error = %v, want %v", err, failure) + } + }) + + t.Run("nil profile is not canonical", func(t *testing.T) { + if loginProfileHasUsableCanonicalToken(nil, nil, nil) { + t.Fatal("nil profile was treated as a usable canonical token") + } + }) + + t.Run("global write preflight reports unreadable slot", func(t *testing.T) { + oldGet := authKeychainGet + failure := errors.New("global ciphertext is unreadable") + authKeychainGet = func(_, account string) (string, error) { + if account == keychain.AccountToken { + return "", failure + } + return "", nil + } + t.Cleanup(func() { authKeychainGet = oldGet }) + + err := preflightTokenWritePersistence(t.TempDir(), &TokenData{AccessToken: "fresh"}) + if !errors.Is(err, failure) { + t.Fatalf("preflightTokenWritePersistence() error = %v, want %v", err, failure) + } + }) +} + +func TestCrossPlatformCoverageLegacyProfileGuardRemainingEdges(t *testing.T) { + t.Run("empty v3 registry normalizes and persists", func(t *testing.T) { + oldLoad := profilesLoad + oldSave := profilesSave + cfg := &ProfilesConfig{Version: profilesUnresolvedSelectorVersion} + profilesLoad = func(string) (*ProfilesConfig, error) { return cfg, nil } + saves := 0 + profilesSave = func(string, *ProfilesConfig) error { + saves++ + return nil + } + t.Cleanup(func() { + profilesLoad = oldLoad + profilesSave = oldSave + }) + + if err := ensureProfilesMigrationLocked(t.TempDir()); err != nil { + t.Fatalf("ensureProfilesMigrationLocked() error = %v", err) + } + if cfg.Version != profilesVersion || saves != 1 { + t.Fatalf("normalized registry = version %d saves %d", cfg.Version, saves) + } + }) + + if normalizeProfilesVersionForSelectors(nil) { + t.Fatal("nil profile registry reported a version change") + } + future := &ProfilesConfig{Version: profilesMaxVersion + 1} + if normalizeProfilesVersionForSelectors(future) { + t.Fatal("future profile registry reported a version change") + } + if profilesConfigContainsUnresolvedSelector(nil) { + t.Fatal("nil profile registry contained an unresolved selector") + } + reserved := unresolvedProfileSelector("corp_org_current_guard") + if !profilesConfigContainsUnresolvedSelector(&ProfilesConfig{ + OrgCurrentProfiles: map[string]string{"corp_org_current_guard": reserved}, + }) { + t.Fatal("reserved organization-current selector was not detected") + } + if selectorConflictsWithOrganizationGrammar(nil, "corp") { + t.Fatal("nil profile registry reported an organization-selector conflict") + } + if err := validateIdentityOnlyProfileToken(Profile{}); !errors.Is(err, ErrTokenDataNotFound) { + t.Fatalf("validateIdentityOnlyProfileToken(blank) error = %v", err) + } + + oldLoadIdentity := profilesLoadIdentity + profilesLoadIdentity = func(string, string) (*TokenData, error) { return nil, nil } + t.Cleanup(func() { profilesLoadIdentity = oldLoadIdentity }) + if err := validateIdentityOnlyProfileToken(Profile{CorpID: "corp_nil_identity", UserID: "user_nil_identity"}); !errors.Is(err, ErrTokenDataNotFound) { + t.Fatalf("validateIdentityOnlyProfileToken(nil token) error = %v", err) + } +} + +func TestCrossPlatformCoverageTokenPersistenceRemainingEdges(t *testing.T) { + t.Setenv(keychain.DisableKeychainEnv, "1") + t.Setenv(keychain.StorageDirEnv, t.TempDir()) + + plan := planTokenPersistenceWrites(&ProfilesConfig{}, nil, "") + if !plan.WriteGlobal || plan.CorpID != "" { + t.Fatalf("nil-token write plan = %#v", plan) + } + if err := SaveLoginTokenData(t.TempDir(), nil); err == nil { + t.Fatal("SaveLoginTokenData(nil) succeeded") + } + + futureDir := t.TempDir() + writeFutureProfilesForLoginPreflight(t, futureDir) + err := SaveLoginTokenData(futureDir, &TokenData{AccessToken: "fresh"}) + if err == nil || !strings.Contains(err.Error(), "newer than supported") { + t.Fatalf("SaveLoginTokenData(future schema) error = %v", err) + } + + t.Run("nil identity token", func(t *testing.T) { + isolateTokenProfileLoadHooks(t) + tokenLoadKeychainIdentity = func(string, string) (*TokenData, error) { return nil, nil } + _, err := tokenLoadProfileIdentity(Profile{CorpID: "corp_nil_identity", UserID: "user_nil_identity"}) + if !errors.Is(err, ErrTokenDataNotFound) { + t.Fatalf("tokenLoadProfileIdentity() error = %v", err) + } + }) + + t.Run("nil organization fallback", func(t *testing.T) { + isolateTokenProfileLoadHooks(t) + tokenLoadKeychainForCorpID = func(string) (*TokenData, error) { return nil, nil } + _, err := tokenLoadProfileIdentity(Profile{CorpID: "corp_nil_org", UserID: "user_nil_org"}) + if !errors.Is(err, ErrTokenDataNotFound) { + t.Fatalf("tokenLoadProfileIdentity() error = %v", err) + } + }) + + t.Run("organization fallback belongs to another organization", func(t *testing.T) { + isolateTokenProfileLoadHooks(t) + tokenLoadKeychainForCorpID = func(string) (*TokenData, error) { + return &TokenData{AccessToken: "wrong", CorpID: "corp_other", UserID: "user_wrong_org"}, nil + } + _, err := tokenLoadProfileIdentity(Profile{CorpID: "corp_expected", UserID: "user_wrong_org"}) + if err == nil || !strings.Contains(err.Error(), "contains token for corpId") { + t.Fatalf("tokenLoadProfileIdentity() error = %v", err) + } + }) + + t.Run("identity repair write failure", func(t *testing.T) { + isolateTokenProfileLoadHooks(t) + failure := errors.New("identity repair write failure") + const corpID, userID = "corp_identity_save", "user_identity_save" + tokenLoadKeychainForCorpID = func(string) (*TokenData, error) { + return &TokenData{AccessToken: "organization", CorpID: corpID, UserID: userID}, nil + } + tokenSaveKeychainForIdentity = func(string, string, *TokenData) error { return failure } + _, err := tokenLoadProfileIdentity(Profile{CorpID: corpID, UserID: userID}) + if !errors.Is(err, failure) { + t.Fatalf("tokenLoadProfileIdentity() error = %v, want %v", err, failure) + } + }) +} + +func isolateHalfMigratedRepairHooks(t *testing.T) { + t.Helper() + oldLoadLegacy := profilesLoadLegacy + oldLoadCorp := profilesLoadCorp + oldLoadIdentity := profilesLoadIdentity + oldSaveCorp := profilesSaveCorp + oldSaveIdentity := profilesSaveIdentity + t.Cleanup(func() { + profilesLoadLegacy = oldLoadLegacy + profilesLoadCorp = oldLoadCorp + profilesLoadIdentity = oldLoadIdentity + profilesSaveCorp = oldSaveCorp + profilesSaveIdentity = oldSaveIdentity + }) + profilesLoadLegacy = func() (*TokenData, error) { return nil, ErrTokenDataNotFound } + profilesLoadCorp = func(string) (*TokenData, error) { return nil, ErrTokenDataNotFound } + profilesLoadIdentity = func(string, string) (*TokenData, error) { return nil, ErrTokenDataNotFound } + profilesSaveCorp = func(string, *TokenData) error { return nil } + profilesSaveIdentity = func(string, string, *TokenData) error { return nil } +} + +func isolateTokenProfileLoadHooks(t *testing.T) { + t.Helper() + oldLoadIdentity := tokenLoadKeychainIdentity + oldLoadCorp := tokenLoadKeychainForCorpID + oldSaveIdentity := tokenSaveKeychainForIdentity + t.Cleanup(func() { + tokenLoadKeychainIdentity = oldLoadIdentity + tokenLoadKeychainForCorpID = oldLoadCorp + tokenSaveKeychainForIdentity = oldSaveIdentity + }) + tokenLoadKeychainIdentity = func(string, string) (*TokenData, error) { return nil, ErrTokenDataNotFound } + tokenLoadKeychainForCorpID = func(string) (*TokenData, error) { return nil, ErrTokenDataNotFound } + tokenSaveKeychainForIdentity = func(string, string, *TokenData) error { return nil } +} diff --git a/internal/auth/oauth_provider_coverage_test.go b/internal/auth/oauth_provider_coverage_test.go index 8448affc..43213ab4 100644 --- a/internal/auth/oauth_provider_coverage_test.go +++ b/internal/auth/oauth_provider_coverage_test.go @@ -1089,6 +1089,10 @@ func TestCrossPlatformCoverageOAuthHelperRemainingEdges(t *testing.T) { if _, err := p.exchangeCode(context.Background(), "code"); !errors.Is(err, fail) { t.Fatalf("direct exchange request error = %v", err) } + oauthHTTPClient = networkClient + if _, err := ExchangeCodeForToken(context.Background(), p.configDir, "code"); !errors.Is(err, fail) { + t.Fatalf("exchange wrapper request error = %v", err) + } p.httpClient = responseClient("{") if _, err := p.exchangeCode(context.Background(), "code"); err == nil { t.Fatal("malformed direct exchange succeeded")