Files
dingtalk-workspace-cli/internal/auth/auth_extra_test.go
T

350 lines
9.5 KiB
Go

package auth
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
)
// ─── endpoints.go ──────────────────────────────────────────────────────
func TestClientID_RuntimeOverride(t *testing.T) {
t.Setenv("DWS_CLIENT_ID", "env-id")
SetClientID("flag-id")
t.Cleanup(func() { SetClientID("") })
if got := ClientID(); got != "flag-id" {
t.Fatalf("expected flag-id, got %s", got)
}
}
func TestClientID_EnvFallback(t *testing.T) {
SetClientID("")
t.Setenv("DWS_CONFIG_DIR", t.TempDir()) // Use temp dir to avoid reading persisted config
t.Setenv("DWS_CLIENT_ID", "env-id")
if got := ClientID(); got != "env-id" {
t.Fatalf("expected env-id, got %s", got)
}
}
func TestClientID_Default(t *testing.T) {
SetClientID("")
t.Setenv("DWS_CONFIG_DIR", t.TempDir()) // Use temp dir to avoid reading persisted config
t.Setenv("DWS_CLIENT_ID", "")
// When DefaultClientID is a placeholder (starts with "<"), ClientID() returns empty string
if strings.HasPrefix(DefaultClientID, "<") {
if got := ClientID(); got != "" {
t.Fatalf("expected empty string for placeholder, got %s", got)
}
} else {
if got := ClientID(); got != DefaultClientID {
t.Fatalf("expected default, got %s", got)
}
}
}
func TestClientSecret_RuntimeOverride(t *testing.T) {
t.Setenv("DWS_CLIENT_SECRET", "env-secret")
SetClientSecret("flag-secret")
t.Cleanup(func() { SetClientSecret("") })
if got := ClientSecret(); got != "flag-secret" {
t.Fatalf("expected flag-secret, got %s", got)
}
}
func TestClientSecret_EnvFallback(t *testing.T) {
SetClientSecret("")
t.Setenv("DWS_CONFIG_DIR", t.TempDir()) // Use temp dir to avoid reading persisted config
t.Setenv("DWS_CLIENT_SECRET", "env-secret")
if got := ClientSecret(); got != "env-secret" {
t.Fatalf("expected env-secret, got %s", got)
}
}
func TestClientSecret_Default(t *testing.T) {
SetClientSecret("")
t.Setenv("DWS_CONFIG_DIR", t.TempDir()) // Use temp dir to avoid reading persisted config
t.Setenv("DWS_CLIENT_SECRET", "")
if got := ClientSecret(); got != DefaultClientSecret {
t.Fatalf("expected default, got %s", got)
}
}
func TestClientID_ConcurrentAccess(t *testing.T) {
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(2)
go func(n int) {
defer wg.Done()
SetClientID(fmt.Sprintf("id-%d", n))
}(i)
go func() {
defer wg.Done()
_ = ClientID()
}()
}
wg.Wait()
SetClientID("")
}
// ─── identity.go ───────────────────────────────────────────────────────
func TestGenerateUUID_Format(t *testing.T) {
t.Parallel()
uuid := generateUUID()
parts := strings.Split(uuid, "-")
if len(parts) != 5 {
t.Fatalf("expected 5 parts, got %d: %s", len(parts), uuid)
}
if len(uuid) != 36 {
t.Fatalf("expected 36 chars, got %d: %s", len(uuid), uuid)
}
// Version 4 marker
if uuid[14] != '4' {
t.Fatalf("expected version 4 at position 14, got %c", uuid[14])
}
}
func TestIdentity_LoadMissing(t *testing.T) {
t.Parallel()
id := Load(t.TempDir())
if id != nil {
t.Fatal("expected nil for missing identity")
}
}
func TestIdentity_EnsureExistsCreatesNew(t *testing.T) {
t.Parallel()
dir := t.TempDir()
id := EnsureExists(dir)
if id == nil || id.AgentID == "" {
t.Fatal("expected non-nil identity with agentID")
}
if id.Source != "dws" {
t.Fatalf("expected source dws, got %s", id.Source)
}
}
func TestIdentity_EnsureExistsLoadsExisting(t *testing.T) {
t.Parallel()
dir := t.TempDir()
id1 := EnsureExists(dir)
id2 := EnsureExists(dir)
if id1.AgentID != id2.AgentID {
t.Fatalf("expected same ID, got %s vs %s", id1.AgentID, id2.AgentID)
}
}
func TestIdentity_LoadInvalidJSON(t *testing.T) {
t.Parallel()
dir := t.TempDir()
_ = os.WriteFile(filepath.Join(dir, identityFile), []byte("not json"), 0o600)
if Load(dir) != nil {
t.Fatal("expected nil for invalid JSON")
}
}
func TestIdentity_LoadEmptyAgentID(t *testing.T) {
t.Parallel()
dir := t.TempDir()
data, _ := json.Marshal(Identity{AgentID: "", Source: "dws"})
_ = os.WriteFile(filepath.Join(dir, identityFile), data, 0o600)
if Load(dir) != nil {
t.Fatal("expected nil for empty agentID")
}
}
func TestIdentity_Headers(t *testing.T) {
t.Parallel()
id := &Identity{AgentID: "test-uuid", Source: "dws"}
h := id.Headers()
if h["x-dws-agent-id"] != "test-uuid" {
t.Fatalf("wrong agent-id header: %s", h["x-dws-agent-id"])
}
if h["x-dws-source"] != "dws" {
t.Fatalf("wrong source header: %s", h["x-dws-source"])
}
if _, ok := h["x-dingtalk-scenario-code"]; !ok {
t.Fatal("missing scenario-code header")
}
}
func TestIdentity_Headers_Nil(t *testing.T) {
t.Parallel()
var id *Identity
if h := id.Headers(); h != nil {
t.Fatalf("expected nil headers for nil identity, got %v", h)
}
}
// ─── manager.go ────────────────────────────────────────────────────────
func TestManager_SaveAndGetToken(t *testing.T) {
t.Parallel()
dir := t.TempDir()
m := NewManager(dir, nil)
if err := m.SaveToken("test-token"); err != nil {
t.Fatalf("SaveToken error: %v", err)
}
token, source, err := m.GetToken()
if err != nil {
t.Fatalf("GetToken error: %v", err)
}
if token != "test-token" {
t.Fatalf("expected test-token, got %s", token)
}
if source != "file" {
t.Fatalf("expected file source, got %s", source)
}
}
func TestManager_GetToken_Missing(t *testing.T) {
t.Parallel()
m := NewManager(t.TempDir(), nil)
_, _, err := m.GetToken()
if err == nil {
t.Fatal("expected error for missing token")
}
}
func TestManager_SaveAndGetMCPURL(t *testing.T) {
t.Parallel()
dir := t.TempDir()
m := NewManager(dir, nil)
if err := m.SaveMCPURL("https://example.com/mcp"); err != nil {
t.Fatalf("SaveMCPURL error: %v", err)
}
url, err := m.GetMCPURL()
if err != nil {
t.Fatalf("GetMCPURL error: %v", err)
}
if url != "https://example.com/mcp" {
t.Fatalf("expected url, got %s", url)
}
}
func TestManager_GetMCPURL_Missing(t *testing.T) {
t.Parallel()
m := NewManager(t.TempDir(), nil)
_, err := m.GetMCPURL()
if err == nil {
t.Fatal("expected error for missing MCP URL")
}
}
// ─── token.go ──────────────────────────────────────────────────────────
func TestTokenData_IsAccessTokenValid(t *testing.T) {
t.Parallel()
tests := []struct {
name string
data *TokenData
want bool
}{
{"nil", nil, false},
{"empty token", &TokenData{}, false},
{"expired", &TokenData{AccessToken: "t", ExpiresAt: time.Now().Add(-time.Hour)}, false},
{"valid", &TokenData{AccessToken: "t", ExpiresAt: time.Now().Add(time.Hour)}, true},
{"within buffer", &TokenData{AccessToken: "t", ExpiresAt: time.Now().Add(3 * time.Minute)}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.data.IsAccessTokenValid(); got != tt.want {
t.Fatalf("IsAccessTokenValid() = %v, want %v", got, tt.want)
}
})
}
}
func TestTokenData_IsRefreshTokenValid(t *testing.T) {
t.Parallel()
tests := []struct {
name string
data *TokenData
want bool
}{
{"nil", nil, false},
{"empty", &TokenData{}, false},
{"expired", &TokenData{RefreshToken: "r", RefreshExpAt: time.Now().Add(-time.Hour)}, false},
{"valid", &TokenData{RefreshToken: "r", RefreshExpAt: time.Now().Add(time.Hour)}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.data.IsRefreshTokenValid(); got != tt.want {
t.Fatalf("IsRefreshTokenValid() = %v, want %v", got, tt.want)
}
})
}
}
func TestTokenData_HasPersistentCode(t *testing.T) {
t.Parallel()
if (&TokenData{}).HasPersistentCode() {
t.Fatal("expected false for empty")
}
if !(&TokenData{PersistentCode: "code"}).HasPersistentCode() {
t.Fatal("expected true")
}
var nilData *TokenData
if nilData.HasPersistentCode() {
t.Fatal("expected false for nil")
}
}
func TestRevokeTokenRemote(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
// Can't easily test since LogoutURL is a const. Just test that it doesn't panic with real URL.
}
// ─── oauth_helpers.go ──────────────────────────────────────────────────
type tokenResponse struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
PersistentCode string `json:"persistentCode"`
ExpiresIn int64 `json:"expiresIn"`
}
func TestBuildTokenData_DefaultExpiry(t *testing.T) {
resp := tokenResponse{
AccessToken: "access-123",
RefreshToken: "refresh-456",
ExpiresIn: 0,
}
data := buildTokenDataFromResponse(resp)
if data == nil {
t.Fatal("expected non-nil token data")
}
if data.AccessToken != "access-123" {
t.Fatalf("wrong access token: %s", data.AccessToken)
}
}
func buildTokenDataFromResponse(resp tokenResponse) *TokenData {
if resp.AccessToken == "" {
return nil
}
now := time.Now()
expiresIn := resp.ExpiresIn
if expiresIn <= 0 {
expiresIn = 7200
}
return &TokenData{
AccessToken: resp.AccessToken,
RefreshToken: resp.RefreshToken,
ExpiresAt: now.Add(time.Duration(expiresIn) * time.Second),
RefreshExpAt: now.Add(30 * 24 * time.Hour),
PersistentCode: resp.PersistentCode,
}
}