Files
dingtalk-workspace-cli/internal/app/coverage_pure_helpers_test.go
玉澜 a57e6bbfeb test(app): cover pure token/profile/retry helper branches
主覆盖门禁在 Linux CI 以 -0.0008pp 惜败 overall 非回归(changed-code
已 100%)。补 25 句纯函数分支覆盖抬高 overall:
tokenResolutionError 四分支、profileSwitchProfileCells sameCorp 消歧、
authRefreshFailureError.Unwrap / withAuthRetrying / managesRuntimeOAuth、
ForceRefreshAccessToken 与 forceRefreshRejectedAccessToken 守卫、
getCachedRuntimeToken prefetch 缝。
2026-08-03 02:42:39 +08:00

136 lines
5.4 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"
"strings"
"testing"
authpkg "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
)
func TestCrossPlatformCoverageTokenResolutionErrorBranches(t *testing.T) {
if err := tokenResolutionError(nil); err != nil {
t.Fatalf("nil error must pass through: %v", err)
}
if err := tokenResolutionError(context.Canceled); !errors.Is(err, context.Canceled) {
t.Fatalf("context cancellation must pass through unwrapped: %v", err)
}
if err := tokenResolutionError(context.DeadlineExceeded); !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("deadline exceeded must pass through unwrapped: %v", err)
}
if err := tokenResolutionError(authpkg.ErrTokenDataNotFound); err == nil ||
!strings.Contains(err.Error(), "dws auth login") {
t.Fatalf("not-found must become a login guidance error: %v", err)
}
generic := errors.New("keychain locked")
if err := tokenResolutionError(generic); err == nil ||
!strings.Contains(err.Error(), "resolve access token") || !errors.Is(err, generic) {
t.Fatalf("generic error must wrap with resolve access token: %v", err)
}
}
func TestCrossPlatformCoverageProfileSwitchCellsSameCorp(t *testing.T) {
profile := authpkg.Profile{CorpID: "corp-1", CorpName: "钉钉", UserName: "Alice", UserID: "u1"}
cfg := &authpkg.ProfilesConfig{Profiles: []authpkg.Profile{
profile,
{CorpID: "corp-1", CorpName: "钉钉", UserName: "Bob", UserID: "u2"},
}}
org, _ := profileSwitchProfileCells(profile, cfg)
if !strings.Contains(org, " / Alice") {
t.Fatalf("same-corp label must disambiguate with user name: %q", org)
}
noName := authpkg.Profile{CorpID: "corp-1", CorpName: "钉钉", UserID: "u1"}
org, _ = profileSwitchProfileCells(noName, cfg)
if !strings.Contains(org, " / u1") {
t.Fatalf("blank user name must fall back to user id: %q", org)
}
blankUser := authpkg.Profile{CorpID: "corp-1", CorpName: "钉钉"}
org, _ = profileSwitchProfileCells(blankUser, cfg)
if strings.Contains(org, " / ") {
t.Fatalf("blank user name and id must not append a suffix: %q", org)
}
org, _ = profileSwitchProfileCells(profile, nil)
if strings.Contains(org, " / ") {
t.Fatalf("nil config must not append a suffix: %q", org)
}
}
func TestCrossPlatformCoverageAuthRefreshRetryHelpers(t *testing.T) {
var nilErr *authRefreshFailureError
if got := nilErr.Unwrap(); got != nil {
t.Fatalf("nil receiver Unwrap = %v, want nil", got)
}
rejection := errors.New("rejected")
refresh := errors.New("refresh boom")
wrapped := (&authRefreshFailureError{rejection: rejection, refresh: refresh}).Unwrap()
if len(wrapped) != 2 || !errors.Is(wrapped[0], rejection) || !errors.Is(wrapped[1], refresh) {
t.Fatalf("Unwrap must surface both causes: %v", wrapped)
}
if ctx := withAuthRetrying(nil); ctx == nil || !IsAuthRetrying(ctx) {
t.Fatal("withAuthRetrying(nil) must produce a retry-marked background context")
}
var nilRunner *runtimeRunner
if nilRunner.managesRuntimeOAuth(false) {
t.Fatal("nil runner must not manage runtime OAuth")
}
if (&runtimeRunner{}).managesRuntimeOAuth(true) {
t.Fatal("plugin-owned auth must opt out of runtime OAuth management")
}
if !(&runtimeRunner{}).managesRuntimeOAuth(false) {
t.Fatal("nil global flags must manage runtime OAuth")
}
if !(&runtimeRunner{globalFlags: &GlobalFlags{}}).managesRuntimeOAuth(false) {
t.Fatal("blank token must manage runtime OAuth")
}
if (&runtimeRunner{globalFlags: &GlobalFlags{Token: "tok"}}).managesRuntimeOAuth(false) {
t.Fatal("explicit token must not manage runtime OAuth")
}
}
func TestCrossPlatformCoverageForceRefreshRejectedGuards(t *testing.T) {
if _, err := ForceRefreshAccessToken(context.Background(), " "); err == nil ||
!strings.Contains(err.Error(), "config directory is empty") {
t.Fatalf("blank config dir error = %v", err)
}
testseam.Swap(t, &loadRefreshTokenData, func(string) (*authpkg.TokenData, error) { return nil, nil })
if _, err := ForceRefreshAccessToken(context.Background(), t.TempDir()); err == nil ||
!strings.Contains(err.Error(), "stored access token is empty") {
t.Fatalf("nil token data must report stored access token is empty: %v", err)
}
if _, err := forceRefreshRejectedAccessToken(context.Background(), " ", "tok"); err == nil ||
!strings.Contains(err.Error(), "config directory is empty") {
t.Fatalf("blank config dir error = %v", err)
}
if _, err := forceRefreshRejectedAccessToken(context.Background(), t.TempDir(), " "); err == nil ||
!strings.Contains(err.Error(), "rejected access token is empty") {
t.Fatalf("blank rejected token error = %v", err)
}
}
func TestCrossPlatformCoverageGetCachedRuntimeTokenSeam(t *testing.T) {
token, err := getCachedRuntimeToken(context.Background())
if err != nil && strings.TrimSpace(token) != "" {
t.Fatalf("failed token resolution must not return a token: %q / %v", token, err)
}
}