3182 lines
122 KiB
Go
3182 lines
122 KiB
Go
package auth
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/keychain"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/config"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
|
)
|
|
|
|
type errorReader struct{}
|
|
|
|
func (errorReader) Read([]byte) (int, error) { return 0, errors.New("read") }
|
|
|
|
type duplicateFailAfterWriter struct {
|
|
remaining int
|
|
}
|
|
|
|
func (w *duplicateFailAfterWriter) Write(p []byte) (int, error) {
|
|
if w.remaining <= 0 {
|
|
return 0, errors.New("write")
|
|
}
|
|
if len(p) > w.remaining {
|
|
n := w.remaining
|
|
w.remaining = 0
|
|
return n, errors.New("write")
|
|
}
|
|
w.remaining -= len(p)
|
|
return len(p), nil
|
|
}
|
|
|
|
type duplicateFakePortableTemp struct {
|
|
name string
|
|
writeErr error
|
|
chmodErr error
|
|
syncErr error
|
|
closeErr error
|
|
closeCall int
|
|
}
|
|
|
|
func (f *duplicateFakePortableTemp) Write(p []byte) (int, error) {
|
|
if f.writeErr != nil {
|
|
return 0, f.writeErr
|
|
}
|
|
return len(p), nil
|
|
}
|
|
func (f *duplicateFakePortableTemp) Name() string { return f.name }
|
|
func (f *duplicateFakePortableTemp) Chmod(os.FileMode) error { return f.chmodErr }
|
|
func (f *duplicateFakePortableTemp) Sync() error { return f.syncErr }
|
|
func (f *duplicateFakePortableTemp) Close() error { f.closeCall++; return f.closeErr }
|
|
|
|
type fakeSecureTemp struct {
|
|
name string
|
|
writeErr error
|
|
syncErr error
|
|
closeErr error
|
|
}
|
|
|
|
func (f *fakeSecureTemp) Write(p []byte) (int, error) {
|
|
if f.writeErr != nil {
|
|
return 0, f.writeErr
|
|
}
|
|
return len(p), nil
|
|
}
|
|
func (f *fakeSecureTemp) Name() string { return f.name }
|
|
func (f *fakeSecureTemp) Sync() error { return f.syncErr }
|
|
func (f *fakeSecureTemp) Close() error { return f.closeErr }
|
|
|
|
type failAfterWriter struct {
|
|
remaining int
|
|
}
|
|
|
|
func (w *failAfterWriter) Write(p []byte) (int, error) {
|
|
if w.remaining <= 0 {
|
|
return 0, errors.New("write")
|
|
}
|
|
if len(p) > w.remaining {
|
|
n := w.remaining
|
|
w.remaining = 0
|
|
return n, errors.New("write")
|
|
}
|
|
w.remaining -= len(p)
|
|
return len(p), nil
|
|
}
|
|
|
|
type fakePortableTemp struct {
|
|
name string
|
|
writeErr error
|
|
chmodErr error
|
|
syncErr error
|
|
closeErr error
|
|
closeCall int
|
|
}
|
|
|
|
func (f *fakePortableTemp) Write(p []byte) (int, error) {
|
|
if f.writeErr != nil {
|
|
return 0, f.writeErr
|
|
}
|
|
return len(p), nil
|
|
}
|
|
func (f *fakePortableTemp) Name() string { return f.name }
|
|
func (f *fakePortableTemp) Chmod(os.FileMode) error { return f.chmodErr }
|
|
func (f *fakePortableTemp) Sync() error { return f.syncErr }
|
|
func (f *fakePortableTemp) Close() error { f.closeCall++; return f.closeErr }
|
|
|
|
func TestCrossPlatformCoverageAppTokenEdges(t *testing.T) {
|
|
if err := SaveAppTokenData(&AppTokenData{}); err == nil {
|
|
t.Fatal("empty client ID save succeeded")
|
|
}
|
|
if _, err := LoadAppTokenData(""); err == nil {
|
|
t.Fatal("empty client ID load succeeded")
|
|
}
|
|
if err := DeleteAppTokenData(""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, err := LoadAppTokenData("missing"); err != nil || got != nil {
|
|
t.Fatalf("missing app token = %#v, %v", got, err)
|
|
}
|
|
data := &AppTokenData{ClientID: "app-one", AccessToken: "cached", ExpiresAt: time.Now().Add(time.Hour)}
|
|
if err := SaveAppTokenData(data); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loaded, err := LoadAppTokenData("app-one")
|
|
if err != nil || loaded.AccessToken != "cached" || loaded.UpdatedAt.IsZero() {
|
|
t.Fatalf("loaded app token = %#v, %v", loaded, err)
|
|
}
|
|
if err := keychain.Set(keychain.Service, appTokenPrefix+"malformed", "{"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := LoadAppTokenData("malformed"); err == nil {
|
|
t.Fatal("malformed app token succeeded")
|
|
}
|
|
if err := DeleteAppTokenData("app-one"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
oldClient := appTokenHTTPClient
|
|
t.Cleanup(func() { appTokenHTTPClient = oldClient })
|
|
response := func(status int, body string) {
|
|
appTokenHTTPClient = &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
StatusCode: status,
|
|
Body: io.NopCloser(strings.NewReader(body)),
|
|
Header: make(http.Header),
|
|
}, nil
|
|
})}
|
|
}
|
|
response(http.StatusOK, "{\"accessToken\":\"fresh\",\"expireIn\":0}")
|
|
token, expires, err := FetchAppToken(context.Background(), "key", "secret")
|
|
if err != nil || token != "fresh" || expires <= 0 {
|
|
t.Fatalf("FetchAppToken = %q %d %v", token, expires, err)
|
|
}
|
|
response(http.StatusBadGateway, strings.Repeat("x", 250))
|
|
if _, _, err := FetchAppToken(context.Background(), "key", "secret"); err == nil || !strings.Contains(err.Error(), "...") {
|
|
t.Fatalf("HTTP app token error = %v", err)
|
|
}
|
|
response(http.StatusOK, "{")
|
|
if _, _, err := FetchAppToken(context.Background(), "key", "secret"); err == nil {
|
|
t.Fatal("malformed app token response succeeded")
|
|
}
|
|
response(http.StatusOK, "{}")
|
|
if _, _, err := FetchAppToken(context.Background(), "key", "secret"); err == nil {
|
|
t.Fatal("empty app token response succeeded")
|
|
}
|
|
appTokenHTTPClient = &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
|
return nil, errors.New("network")
|
|
})}
|
|
if _, _, err := FetchAppToken(context.Background(), "key", "secret"); err == nil {
|
|
t.Fatal("network app token request succeeded")
|
|
}
|
|
appTokenHTTPClient = &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
|
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(errorReader{}), Header: make(http.Header)}, nil
|
|
})}
|
|
if _, _, err := FetchAppToken(context.Background(), "key", "secret"); err == nil {
|
|
t.Fatal("app token read failure succeeded")
|
|
}
|
|
|
|
provider := &AppTokenProvider{}
|
|
if _, err := provider.GetToken(context.Background()); err == nil {
|
|
t.Fatal("missing app credentials succeeded")
|
|
}
|
|
cached := &AppTokenData{ClientID: "cache-key", AccessToken: "cached", ExpiresAt: time.Now().Add(time.Hour)}
|
|
if err := SaveAppTokenData(cached); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
provider = &AppTokenProvider{AppKey: "cache-key", AppSecret: "secret"}
|
|
if got, err := provider.GetToken(context.Background()); err != nil || got != "cached" {
|
|
t.Fatalf("cached provider token = %q, %v", got, err)
|
|
}
|
|
response(http.StatusOK, "{\"accessToken\":\"provider-fresh\",\"expireIn\":7200}")
|
|
provider = &AppTokenProvider{AppKey: "fresh-key", AppSecret: "secret"}
|
|
if got, err := provider.GetToken(context.Background()); err != nil || got != "provider-fresh" {
|
|
t.Fatalf("fresh provider token = %q, %v", got, err)
|
|
}
|
|
response(http.StatusInternalServerError, "down")
|
|
provider = &AppTokenProvider{AppKey: "failed-key", AppSecret: "secret"}
|
|
if _, err := provider.GetToken(context.Background()); err == nil {
|
|
t.Fatal("failed provider fetch succeeded")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDeviceFlowRequestAndOutputEdges(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
p := &DeviceFlowProvider{Output: &buf, clientID: "client", scope: "scope", httpClient: http.DefaultClient}
|
|
p.SetScope("custom")
|
|
(*DeviceFlowProvider)(nil).SetScope("ignored")
|
|
if p.scope != "custom" || p.output() != &buf || (*DeviceFlowProvider)(nil).output() != io.Discard {
|
|
t.Fatal("device flow setters/output failed")
|
|
}
|
|
|
|
type deviceMode string
|
|
var mode deviceMode
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch mode {
|
|
case "malformed":
|
|
_, _ = io.WriteString(w, "{")
|
|
case "service-error":
|
|
writeServiceResult(w, false, nil, "bad", "failed")
|
|
case "bad-result":
|
|
_, _ = io.WriteString(w, "{\"success\":true,\"result\":\"bad\"}")
|
|
case "empty":
|
|
writeServiceResult(w, true, DeviceAuthResponse{}, "", "")
|
|
case "defaults":
|
|
writeServiceResult(w, true, DeviceAuthResponse{DeviceCode: "dc", UserCode: "uc", Interval: 99, ExpiresIn: -1}, "", "")
|
|
case "token-ok":
|
|
writeServiceResult(w, true, DeviceTokenResponse{AuthCode: "code"}, "", "")
|
|
case "poll-fail":
|
|
_ = json.NewEncoder(w).Encode(DevicePollResponse{Success: false, Code: "bad", Message: "failed"})
|
|
case "poll-business":
|
|
_ = json.NewEncoder(w).Encode(DevicePollResponse{Success: false, Data: DevicePollData{Status: StatusRejected}})
|
|
default:
|
|
http.Error(w, "down", http.StatusBadGateway)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
p.SetBaseURL(srv.URL)
|
|
p.SetTerminalBaseURL(srv.URL)
|
|
p.httpClient = srv.Client()
|
|
|
|
for _, m := range []deviceMode{"malformed", "service-error", "bad-result", "empty"} {
|
|
mode = m
|
|
if _, err := p.requestDeviceCode(context.Background()); err == nil {
|
|
t.Errorf("requestDeviceCode mode %q succeeded", m)
|
|
}
|
|
}
|
|
mode = "defaults"
|
|
if got, err := p.requestDeviceCode(context.Background()); err != nil || got.Interval != defaultPollInterval || got.ExpiresIn != 900 {
|
|
t.Fatalf("request defaults = %#v, %v", got, err)
|
|
}
|
|
for _, m := range []deviceMode{"malformed", "service-error", "bad-result"} {
|
|
mode = m
|
|
if _, err := p.pollDeviceToken(context.Background(), "dc"); err == nil {
|
|
t.Errorf("pollDeviceToken mode %q succeeded", m)
|
|
}
|
|
}
|
|
mode = "token-ok"
|
|
if got, err := p.pollDeviceToken(context.Background(), "dc"); err != nil || got.AuthCode != "code" {
|
|
t.Fatalf("poll token = %#v, %v", got, err)
|
|
}
|
|
mode = "malformed"
|
|
if _, err := p.pollDeviceStatus(context.Background(), "flow"); err == nil {
|
|
t.Fatal("malformed poll status succeeded")
|
|
}
|
|
mode = "poll-fail"
|
|
if _, err := p.pollDeviceStatus(context.Background(), "flow"); err == nil {
|
|
t.Fatal("failed poll status succeeded")
|
|
}
|
|
mode = "poll-business"
|
|
if got, err := p.pollDeviceStatus(context.Background(), "flow"); err != nil || got.Data.Status != StatusRejected {
|
|
t.Fatalf("business poll status = %#v, %v", got, err)
|
|
}
|
|
|
|
badClient := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
|
return nil, errors.New("network")
|
|
})}
|
|
p.httpClient = badClient
|
|
if _, err := p.postForm(context.Background(), "://bad", url.Values{}); err == nil {
|
|
t.Fatal("invalid form URL succeeded")
|
|
}
|
|
if _, err := p.postForm(context.Background(), "https://example.test", url.Values{}); err == nil {
|
|
t.Fatal("form network error succeeded")
|
|
}
|
|
if _, err := p.doGet(context.Background(), "://bad"); err == nil {
|
|
t.Fatal("invalid GET URL succeeded")
|
|
}
|
|
if _, err := p.doGet(context.Background(), "https://example.test"); err == nil {
|
|
t.Fatal("GET network error succeeded")
|
|
}
|
|
readClient := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
|
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(errorReader{}), Header: make(http.Header)}, nil
|
|
})}
|
|
p.httpClient = readClient
|
|
if _, err := p.postForm(context.Background(), "https://example.test", url.Values{}); err == nil {
|
|
t.Fatal("form read error succeeded")
|
|
}
|
|
if _, err := p.doGet(context.Background(), "https://example.test"); err == nil {
|
|
t.Fatal("GET read error succeeded")
|
|
}
|
|
|
|
if got := truncateBody([]byte("short"), 10); got != "short" {
|
|
t.Fatalf("short truncate = %q", got)
|
|
}
|
|
if got := truncateBody([]byte("long-value"), 4); got != "long...(truncated)" {
|
|
t.Fatalf("long truncate = %q", got)
|
|
}
|
|
dfPrintStep(&buf, 1, "step", 2)
|
|
dfPrintDeviceCodeBox(&buf, &DeviceAuthResponse{UserCode: "code", VerificationURI: "url", VerificationURIComplete: "complete", ExpiresIn: 1})
|
|
dfPrintBox(&buf, []string{strings.Repeat("x", 55)})
|
|
dfPrintPollResult(&buf, "authorized", "ok")
|
|
dfPrintPollResult(&buf, "pending", "wait")
|
|
dfPrintPollResult(&buf, "unknown", "bad")
|
|
dfPrintWarning(&buf, "warning")
|
|
if isInvalidGrantError(nil) || !isInvalidGrantError(errors.New("invalid_grant")) ||
|
|
!isInvalidGrantError(errors.New("code expired")) || isInvalidGrantError(errors.New("other")) {
|
|
t.Fatal("invalid grant detection failed")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDeviceFlowWaitImmediateEdges(t *testing.T) {
|
|
p := &DeviceFlowProvider{Output: io.Discard}
|
|
if _, err := p.waitForAuthorizationByFlowID(context.Background(), &DeviceAuthResponse{ExpiresIn: 0}); err == nil {
|
|
t.Fatal("flow deadline succeeded")
|
|
}
|
|
if _, err := p.waitForAuthorizationByDeviceCode(context.Background(), &DeviceAuthResponse{ExpiresIn: 0}); err == nil {
|
|
t.Fatal("device deadline succeeded")
|
|
}
|
|
oldAfter := deviceFlowAfter
|
|
t.Cleanup(func() { deviceFlowAfter = oldAfter })
|
|
deviceFlowAfter = func(time.Duration) <-chan time.Time {
|
|
return make(chan time.Time)
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
if _, err := p.waitForAuthorizationByFlowID(ctx, &DeviceAuthResponse{ExpiresIn: 10, Interval: 1, FlowID: "f"}); err == nil {
|
|
t.Fatal("canceled flow wait succeeded")
|
|
}
|
|
if _, err := p.waitForAuthorizationByDeviceCode(ctx, &DeviceAuthResponse{ExpiresIn: 10, Interval: 1, DeviceCode: "d"}); err == nil {
|
|
t.Fatal("canceled device wait succeeded")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDeviceFlowLoginRetry(t *testing.T) {
|
|
oldClient := oauthHTTPClient
|
|
oldAfter := deviceFlowAfter
|
|
oldBrowser := deviceOpenBrowser
|
|
t.Cleanup(func() {
|
|
oauthHTTPClient = oldClient
|
|
deviceFlowAfter = oldAfter
|
|
deviceOpenBrowser = oldBrowser
|
|
SetClientID("")
|
|
SetClientSecret("")
|
|
resetClientIDFromMCP()
|
|
SetClientID("")
|
|
})
|
|
deviceFlowAfter = func(time.Duration) <-chan time.Time {
|
|
ch := make(chan time.Time, 1)
|
|
ch <- time.Now()
|
|
return ch
|
|
}
|
|
deviceOpenBrowser = func(string) error { return errors.New("browser") }
|
|
var exchanges int
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch {
|
|
case strings.HasSuffix(r.URL.Path, ClientIDPath):
|
|
_ = json.NewEncoder(w).Encode(ClientIDResponse{Success: true, Result: "device-client"})
|
|
case strings.HasSuffix(r.URL.Path, DeviceCodePath):
|
|
writeServiceResult(w, true, DeviceAuthResponse{
|
|
DeviceCode: "dc", UserCode: "uc", VerificationURI: "https://verify",
|
|
VerificationURIComplete: "https://verify/full", ExpiresIn: 60, Interval: 1, FlowID: "flow",
|
|
}, "", "")
|
|
case strings.HasSuffix(r.URL.Path, DevicePollPath):
|
|
_ = json.NewEncoder(w).Encode(DevicePollResponse{Success: true, Data: DevicePollData{Status: StatusApproved, AuthCode: "code"}})
|
|
case strings.HasSuffix(r.URL.Path, MCPOAuthTokenPath):
|
|
exchanges++
|
|
if exchanges < 3 {
|
|
http.Error(w, "invalid_grant code expired", http.StatusBadRequest)
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"accessToken": "device-access", "refreshToken": "refresh", "expiresIn": 7200,
|
|
"corpId": "corp-device", "userId": "user-device",
|
|
})
|
|
case strings.HasSuffix(r.URL.Path, CLIAuthEnabledPath):
|
|
_ = json.NewEncoder(w).Encode(CLIAuthStatus{Success: true, Result: &CLIAuthResult{CLIAuthEnabled: true}})
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
dir := setupMCPConfigDir(t, srv.URL)
|
|
oauthHTTPClient = srv.Client()
|
|
p := NewDeviceFlowProvider(dir, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
|
p.SetBaseURL(srv.URL)
|
|
p.SetTerminalBaseURL(srv.URL)
|
|
p.httpClient = srv.Client()
|
|
p.Output = io.Discard
|
|
token, err := p.Login(context.Background())
|
|
if err != nil || token.AccessToken != "device-access" || exchanges != 3 {
|
|
t.Fatalf("device Login = %#v, %v, exchanges=%d", token, err, exchanges)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoveragePortablePathHelpers(t *testing.T) {
|
|
if _, err := cleanPortableName("../escape"); err == nil {
|
|
t.Fatal("unsafe portable name succeeded")
|
|
}
|
|
if got, err := cleanPortableName("./a/b"); err != nil || got != "a/b" {
|
|
t.Fatalf("clean portable name = %q, %v", got, err)
|
|
}
|
|
root := t.TempDir()
|
|
if _, err := safeJoin(root, "../escape"); err == nil {
|
|
t.Fatal("unsafe join succeeded")
|
|
}
|
|
if got, err := safeJoin(root, "a/b"); err != nil || got != filepath.Join(root, "a", "b") {
|
|
t.Fatalf("safe join = %q, %v", got, err)
|
|
}
|
|
if rel := mustPortableRel(root, filepath.Join(root, "a", "b")); rel != "a/b" {
|
|
t.Fatalf("portable rel = %q", rel)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
tw := tar.NewWriter(&buf)
|
|
if err := writePortableBytes(tw, "file", []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := tw.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tr := tar.NewReader(bytes.NewReader(buf.Bytes()))
|
|
hdr, err := tr.Next()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
target := filepath.Join(t.TempDir(), "nested", "file")
|
|
if err := extractPortableEntry(target, hdr, tr); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if data, err := os.ReadFile(target); err != nil || string(data) != "x" {
|
|
t.Fatalf("extracted file = %q, %v", data, err)
|
|
}
|
|
if err := extractPortableEntry(target, &tar.Header{Typeflag: tar.TypeSymlink}, strings.NewReader("")); err == nil {
|
|
t.Fatal("unsupported portable entry succeeded")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageProfilesLifecycleEdges(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfg, err := LoadProfiles(dir)
|
|
if err != nil || cfg.Version != 1 || len(cfg.Profiles) != 0 {
|
|
t.Fatalf("missing profiles = %#v, %v", cfg, err)
|
|
}
|
|
if err := os.WriteFile(ProfilesPath(dir), []byte("{"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err = LoadProfiles(dir)
|
|
if err != nil || len(cfg.Profiles) != 0 {
|
|
t.Fatalf("corrupt profiles = %#v, %v", cfg, err)
|
|
}
|
|
if matches, _ := filepath.Glob(ProfilesPath(dir) + ".corrupt-*"); len(matches) != 1 {
|
|
t.Fatalf("quarantined profiles = %#v", matches)
|
|
}
|
|
if err := SaveProfiles(dir, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cfg = &ProfilesConfig{
|
|
PrimaryProfile: "missing",
|
|
CurrentProfile: "missing",
|
|
PreviousProfile: "missing",
|
|
Profiles: []Profile{
|
|
{CorpID: " corp-a ", CorpName: "Acme"},
|
|
{CorpID: "corp-a", Name: "duplicate"},
|
|
{CorpID: ""},
|
|
},
|
|
}
|
|
normalizeProfilesConfig(cfg)
|
|
if len(cfg.Profiles) != 1 || cfg.Profiles[0].Name != "Acme" ||
|
|
cfg.PrimaryProfile != "" || cfg.CurrentProfile != "" || cfg.PreviousProfile != "" {
|
|
t.Fatalf("normalized profiles = %#v", cfg)
|
|
}
|
|
|
|
if err := upsertProfileFromToken(dir, cfg, nil, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := upsertProfileFromToken(dir, cfg, &TokenData{}, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Now()
|
|
first := &TokenData{
|
|
CorpID: "corp-a", CorpName: "Acme Updated", UserID: "u1", UserName: "User",
|
|
ClientID: "client", ExpiresAt: now.Add(time.Hour), RefreshExpAt: now.Add(24 * time.Hour),
|
|
}
|
|
if err := UpsertProfileFromToken(dir, first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second := &TokenData{CorpID: "corp-b", CorpName: "Acme Updated", AccessToken: "b"}
|
|
if err := UpsertProfileFromTokenWithCurrent(dir, second, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := SaveTokenDataKeychainForIdentity(first.CorpID, first.UserID, first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := SaveTokenDataKeychainForCorpID(second.CorpID, second); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = DeleteTokenDataKeychainForIdentity(first.CorpID, first.UserID)
|
|
_ = DeleteTokenDataKeychainForCorpID(second.CorpID)
|
|
})
|
|
profilesForAmbiguity, err := LoadProfiles(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i := range profilesForAmbiguity.Profiles {
|
|
profilesForAmbiguity.Profiles[i].Name = "profile-" + profilesForAmbiguity.Profiles[i].CorpID
|
|
profilesForAmbiguity.Profiles[i].CorpName = "Shared Corp"
|
|
}
|
|
if err := SaveProfiles(dir, profilesForAmbiguity); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, err := ResolveProfile(dir, "Shared Corp"); err == nil || got != nil {
|
|
t.Fatalf("ambiguous corp name = %#v, %v", got, err)
|
|
}
|
|
if got, err := ResolveProfile(dir, "corp-b"); err != nil || got.CorpID != "corp-b" {
|
|
t.Fatalf("explicit profile = %#v, %v", got, err)
|
|
}
|
|
if got, err := ResolveProfile(dir, "missing"); err == nil || got != nil {
|
|
t.Fatalf("missing profile = %#v, %v", got, err)
|
|
}
|
|
if got, err := ResolveProfile(dir, ""); err != nil || got == nil {
|
|
t.Fatalf("default profile = %#v, %v", got, err)
|
|
}
|
|
|
|
if _, err := SetCurrentProfile(dir, "missing"); err == nil {
|
|
t.Fatal("setting missing profile succeeded")
|
|
}
|
|
if _, err := SetCurrentProfile(dir, "corp-a"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, err := UsePreviousProfile(dir); err != nil || got.CorpID != "corp-b" {
|
|
t.Fatalf("previous profile = %#v, %v", got, err)
|
|
}
|
|
if err := MarkProfileStatus(dir, "", ProfileStatusExpired); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := MarkProfileStatus(dir, "missing", ProfileStatusExpired); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := MarkProfileStatus(dir, "corp-a", ProfileStatusExpired); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if removed, err := RemoveProfile(dir, "corp-b"); err != nil || removed.CorpID != "corp-b" {
|
|
t.Fatalf("removed profile = %#v, %v", removed, err)
|
|
}
|
|
if _, err := RemoveProfile(dir, "missing"); err == nil {
|
|
t.Fatal("removing missing profile succeeded")
|
|
}
|
|
if _, err := RemoveProfile(dir, "corp-a"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := UsePreviousProfile(dir); err == nil {
|
|
t.Fatal("empty previous profile succeeded")
|
|
}
|
|
|
|
nameCfg := &ProfilesConfig{Profiles: []Profile{
|
|
{CorpID: "one", Name: "Acme"},
|
|
{CorpID: "two", Name: "Acme-12345678"},
|
|
{CorpID: "three", Name: "Acme-12345678-2"},
|
|
}}
|
|
if got := chooseProfileName(nameCfg, &TokenData{CorpID: "corp-12345678", CorpName: "Acme"}); got != "Acme-2" {
|
|
t.Fatalf("collision profile name = %q", got)
|
|
}
|
|
if chooseProfileName(&ProfilesConfig{}, &TokenData{}) != "profile" {
|
|
t.Fatal("fallback profile name failed")
|
|
}
|
|
if shouldRefreshProfileName(nil, first) || shouldRefreshProfileName(&Profile{}, nil) ||
|
|
!shouldRefreshProfileName(&Profile{}, first) {
|
|
t.Fatal("profile refresh-name decisions failed")
|
|
}
|
|
if shortCorpID("short") != "short" || shortCorpID("corp-12345678") != "12345678" {
|
|
t.Fatal("profile formatting helpers failed")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageProfilesMigrationAndMirror(t *testing.T) {
|
|
dir := t.TempDir()
|
|
_ = DeleteTokenDataKeychain()
|
|
legacy := &TokenData{CorpID: "legacy-corp", CorpName: "Legacy", AccessToken: "token"}
|
|
if err := SaveTokenDataKeychain(legacy); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := EnsureProfilesMigration(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := LoadProfiles(dir)
|
|
if err != nil || len(cfg.Profiles) != 1 {
|
|
t.Fatalf("migrated profiles = %#v, %v", cfg, err)
|
|
}
|
|
if err := SyncLegacyTokenMirror(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := DeleteTokenDataKeychainForCorpID("legacy-corp"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := SyncLegacyTokenMirror(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
emptyDir := t.TempDir()
|
|
_ = DeleteTokenDataKeychain()
|
|
if err := EnsureProfilesMigration(emptyDir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := SyncLegacyTokenMirror(emptyDir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := resolveProfileForLoad(emptyDir, "missing"); err == nil {
|
|
t.Fatal("missing load profile succeeded")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageTokenEditionHooksAndCleanup(t *testing.T) {
|
|
oldHooks := edition.Get()
|
|
t.Cleanup(func() {
|
|
edition.Override(oldHooks)
|
|
SetRuntimeProfile("")
|
|
})
|
|
dir := t.TempDir()
|
|
var saved []byte
|
|
deleted := false
|
|
edition.Override(&edition.Hooks{
|
|
SaveToken: func(_ string, data []byte) error {
|
|
saved = append([]byte(nil), data...)
|
|
return nil
|
|
},
|
|
LoadToken: func(string) ([]byte, error) { return saved, nil },
|
|
DeleteToken: func(string) error {
|
|
deleted = true
|
|
return nil
|
|
},
|
|
})
|
|
data := &TokenData{AccessToken: "hook-token"}
|
|
if err := SaveTokenData(dir, data); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, err := LoadTokenData(dir); err != nil || got.AccessToken != "hook-token" {
|
|
t.Fatalf("hook load = %#v, %v", got, err)
|
|
}
|
|
if _, err := LoadTokenDataForProfile(dir, "profile"); err == nil {
|
|
t.Fatal("hook profile selection succeeded")
|
|
}
|
|
if err := DeleteTokenDataForProfile(dir, "profile"); err == nil {
|
|
t.Fatal("hook profile delete succeeded")
|
|
}
|
|
if err := DeleteAllTokenData(dir); err != nil || !deleted {
|
|
t.Fatalf("hook delete all = %v, deleted=%v", err, deleted)
|
|
}
|
|
edition.Override(&edition.Hooks{LoadToken: func(string) ([]byte, error) { return []byte("{"), nil }})
|
|
if _, err := LoadTokenData(dir); err == nil {
|
|
t.Fatal("malformed hook token succeeded")
|
|
}
|
|
edition.Override(&edition.Hooks{LoadToken: func(string) ([]byte, error) { return nil, errors.New("load") }})
|
|
if _, err := LoadTokenData(dir); err == nil {
|
|
t.Fatal("hook token read error succeeded")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageTokenAndKeychainEdges(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := SaveTokenDataKeychainForCorpID("", &TokenData{}); err == nil {
|
|
t.Fatal("empty corp token save succeeded")
|
|
}
|
|
if _, err := LoadTokenDataKeychainForCorpID(""); err == nil {
|
|
t.Fatal("empty corp token load succeeded")
|
|
}
|
|
if err := DeleteTokenDataKeychainForCorpID(""); err == nil {
|
|
t.Fatal("empty corp token delete succeeded")
|
|
}
|
|
if TokenDataExistsKeychainForCorpID("") {
|
|
t.Fatal("empty corp token exists")
|
|
}
|
|
if _, err := loadTokenDataKeychainAccount("missing-account"); err == nil {
|
|
t.Fatal("missing keychain account loaded")
|
|
}
|
|
if err := keychain.Set(keychain.Service, "bad-token", "{"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := loadTokenDataKeychainAccount("bad-token"); err == nil {
|
|
t.Fatal("malformed keychain token loaded")
|
|
}
|
|
if err := SaveClientSecret("", "secret"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := SaveClientSecret("client", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := SaveClientSecret("client", "secret"); err != nil || LoadClientSecret("client") != "secret" {
|
|
t.Fatal("client secret round trip failed")
|
|
}
|
|
if LoadClientSecret("") != "" || LoadClientSecret("missing") != "" {
|
|
t.Fatal("missing client secret was returned")
|
|
}
|
|
if err := DeleteClientSecret(""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := DeleteClientSecret("client"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
migrationOnce = sync.Once{}
|
|
migrationDone = false
|
|
EnsureMigration(dir, slog.Default())
|
|
if !IsMigrationDone() {
|
|
t.Fatal("migration state not recorded")
|
|
}
|
|
|
|
badDir := filepath.Join(t.TempDir(), "file")
|
|
if err := os.WriteFile(badDir, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := WriteTokenMarker(badDir); err == nil {
|
|
t.Fatal("token marker in file dir succeeded")
|
|
}
|
|
if err := DeleteTokenMarker(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := DeleteAllTokenData(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageAppConfigDeleteReloadEdges(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.Mkdir(GetAppConfigPath(dir), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := LoadAppConfig(dir); err == nil {
|
|
t.Fatal("directory app config loaded")
|
|
}
|
|
if err := os.RemoveAll(GetAppConfigPath(dir)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(GetAppConfigPath(dir), []byte("{"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := LoadAppConfig(dir); err == nil {
|
|
t.Fatal("malformed app config loaded")
|
|
}
|
|
if err := os.Remove(GetAppConfigPath(dir)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
config := &AppConfig{ClientID: "client", ClientSecret: PlainSecret("secret")}
|
|
if err := SaveAppConfig(dir, config); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, err := ReloadAppConfig(dir); err != nil || got.ClientID != "client" {
|
|
t.Fatalf("reload app config = %#v, %v", got, err)
|
|
}
|
|
if err := DeleteAppConfig(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if HasAppConfig(dir) {
|
|
t.Fatal("app config remains after delete")
|
|
}
|
|
if err := DeleteAppConfig(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageBasicAuthCoverageEdges(t *testing.T) {
|
|
t.Run("channel and status", func(t *testing.T) {
|
|
t.Setenv(AgentCodeEnv, " agent ")
|
|
if !AgentCodeEnvPresent() || !HostOwnsPATFlow() {
|
|
t.Fatal("host-owned PAT flow was not detected")
|
|
}
|
|
for _, status := range []string{StatusApproved, StatusRejected, StatusExpired, StatusPending, StatusCancelled} {
|
|
if ParseDeviceFlowStatus(status, true) != status {
|
|
t.Fatalf("status %q was not preserved", status)
|
|
}
|
|
}
|
|
if ParseDeviceFlowStatus("", false) != StatusExpired || ParseDeviceFlowStatus("future", true) != "future" {
|
|
t.Fatal("device status fallback failed")
|
|
}
|
|
})
|
|
|
|
t.Run("endpoints and sources", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
t.Setenv("DWS_CONFIG_DIR", dir)
|
|
t.Setenv("DWS_CLIENT_ID", "")
|
|
t.Setenv("DWS_CLIENT_SECRET", "")
|
|
resetAppConfigCache()
|
|
SetClientID("")
|
|
SetClientSecret("")
|
|
resetClientIDFromMCP()
|
|
oldHome := authUserHomeDir
|
|
oldEdition := edition.Get()
|
|
t.Cleanup(func() {
|
|
authUserHomeDir = oldHome
|
|
edition.Override(oldEdition)
|
|
SetClientID("")
|
|
SetClientSecret("")
|
|
resetClientIDFromMCP()
|
|
resetAppConfigCache()
|
|
})
|
|
|
|
if err := os.WriteFile(filepath.Join(dir, "terminal_url"), []byte("https://terminal.test"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if GetTerminalBaseURL() != "https://terminal.test" || !strings.Contains(GetDeveloperSettingsURL(), "terminal.test") {
|
|
t.Fatal("terminal endpoint override failed")
|
|
}
|
|
if GetUserAccessTokenURL() != UserAccessTokenURL || GetRefreshTokenURL() != UserAccessTokenURL || GetRevokeTokenURL() != "" {
|
|
t.Fatal("direct token endpoints failed")
|
|
}
|
|
SetClientIDFromMCP("mcp-client")
|
|
if !strings.HasSuffix(GetUserAccessTokenURL(), MCPOAuthTokenPath) || !strings.HasSuffix(GetRefreshTokenURL(), MCPRefreshTokenPath) || !strings.HasSuffix(GetRevokeTokenURL(), MCPRevokeTokenPath) {
|
|
t.Fatal("MCP token endpoints failed")
|
|
}
|
|
resetClientIDFromMCP()
|
|
SetClientID("")
|
|
|
|
SetClientSecret("runtime-secret")
|
|
if resolveCredentialSource() != "flag" || !HasValidClientSecret() {
|
|
t.Fatal("runtime credential source failed")
|
|
}
|
|
SetClientSecret("")
|
|
if HasValidClientSecret() {
|
|
t.Fatal("placeholder secret was accepted")
|
|
}
|
|
t.Setenv("DWS_CLIENT_ID", "env-id")
|
|
if resolveCredentialSource() != "env" {
|
|
t.Fatal("environment credential source failed")
|
|
}
|
|
t.Setenv("DWS_CLIENT_ID", "")
|
|
resetAppConfigCache()
|
|
if resolveCredentialSource() != "default" {
|
|
t.Fatal("default credential source failed")
|
|
}
|
|
|
|
edition.Override(&edition.Hooks{Name: "coverage", AuthClientID: "edition-id"})
|
|
if ClientID() != "edition-id" {
|
|
t.Fatal("edition client ID failed")
|
|
}
|
|
edition.Override(oldEdition)
|
|
resetAppConfigCache()
|
|
if err := os.WriteFile(GetAppConfigPath(dir), []byte(`{"clientId":"app-id","clientSecret":"plain"}`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := ReloadAppConfig(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resetAppConfigCache()
|
|
if resolveCredentialSource() != "app" {
|
|
t.Fatal("app credential source failed")
|
|
}
|
|
|
|
t.Setenv("DWS_CONFIG_DIR", "")
|
|
authUserHomeDir = func() (string, error) { return "", errors.New("home") }
|
|
if getDefaultConfigDir() != ".dws" {
|
|
t.Fatal("home-directory fallback failed")
|
|
}
|
|
})
|
|
|
|
t.Run("strict credentials", func(t *testing.T) {
|
|
t.Setenv(EnvClientID, "env-id")
|
|
t.Setenv(EnvClientSecret, "env-secret")
|
|
if id, secret, idSource, secretSource, err := ResolveAppCredentialsStrict(t.TempDir()); err != nil || id != "env-id" || secret != "env-secret" || idSource != CredentialSourceEnv || secretSource != CredentialSourceEnv {
|
|
t.Fatalf("env strict credentials = %q %q %q %q %v", id, secret, idSource, secretSource, err)
|
|
}
|
|
if EnvHalfSet() {
|
|
t.Fatal("complete env pair reported as half set")
|
|
}
|
|
t.Setenv(EnvClientSecret, "")
|
|
if !EnvHalfSet() {
|
|
t.Fatal("half-set env pair was not detected")
|
|
}
|
|
t.Setenv(EnvClientID, "")
|
|
|
|
badPath := filepath.Join(t.TempDir(), "config-file")
|
|
if err := os.WriteFile(badPath, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, _, _, err := ResolveAppCredentialsStrict(badPath); err == nil {
|
|
t.Fatal("unreadable app config succeeded")
|
|
}
|
|
missing := t.TempDir()
|
|
if _, _, _, _, err := ResolveAppCredentialsStrict(missing); !errors.Is(err, ErrAppConfigMissing) {
|
|
t.Fatalf("missing app config error = %v", err)
|
|
}
|
|
writeConfig := func(t *testing.T, value string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(GetAppConfigPath(dir), []byte(value), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return dir
|
|
}
|
|
if _, _, _, _, err := ResolveAppCredentialsStrict(writeConfig(t, `{"clientId":"","clientSecret":"secret"}`)); !errors.Is(err, ErrClientIDEmpty) {
|
|
t.Fatalf("empty client ID error = %v", err)
|
|
}
|
|
if _, _, _, _, err := ResolveAppCredentialsStrict(writeConfig(t, `{"clientId":"id","clientSecret":""}`)); !errors.Is(err, ErrClientSecretEmpty) {
|
|
t.Fatalf("empty client secret error = %v", err)
|
|
}
|
|
if _, _, _, _, err := ResolveAppCredentialsStrict(writeConfig(t, `{"clientId":"id","clientSecret":{"source":"file","id":"/missing/secret"}}`)); !errors.Is(err, ErrSecretResolve) {
|
|
t.Fatalf("secret resolve error = %v", err)
|
|
}
|
|
secretFile := filepath.Join(t.TempDir(), "secret")
|
|
if err := os.WriteFile(secretFile, []byte(" file-secret \n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dir := writeConfig(t, `{"clientId":"id","clientSecret":{"source":"file","id":`+fmt.Sprintf("%q", secretFile)+`}}`)
|
|
if id, secret, idSource, secretSource, err := ResolveAppCredentialsStrict(dir); err != nil || id != "id" || secret != "file-secret" || idSource != CredentialSourceAppConfig || secretSource != CredentialSourcePlainConfig {
|
|
t.Fatalf("file strict credentials = %q %q %q %q %v", id, secret, idSource, secretSource, err)
|
|
}
|
|
})
|
|
|
|
t.Run("manager and secret", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
m := NewManager(dir, slog.Default())
|
|
if err := m.SaveToken("1234567890"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := m.SaveMCPURL("https://mcp.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if token, source, err := m.GetToken(); err != nil || token != "1234567890" || source != "file" || !m.IsAuthenticated() {
|
|
t.Fatalf("manager token = %q %q %v", token, source, err)
|
|
}
|
|
if rawURL, err := m.GetMCPURL(); err != nil || rawURL != "https://mcp.test" {
|
|
t.Fatalf("manager MCP URL = %q %v", rawURL, err)
|
|
}
|
|
if ok, source, masked := m.Status(); !ok || source != "file" || masked != "1234...7890" {
|
|
t.Fatalf("manager status = %v %q %q", ok, source, masked)
|
|
}
|
|
if maskToken("short") != "****" {
|
|
t.Fatal("short token masking failed")
|
|
}
|
|
if err := m.DeleteToken(); err != nil || m.IsAuthenticated() {
|
|
t.Fatal("manager token deletion failed")
|
|
}
|
|
if ok, _, _ := m.Status(); ok {
|
|
t.Fatal("missing manager token reported authenticated")
|
|
}
|
|
if _, err := NewManager(t.TempDir(), nil).GetMCPURL(); err == nil {
|
|
t.Fatal("missing manager MCP URL succeeded")
|
|
}
|
|
|
|
fileDir := filepath.Join(t.TempDir(), "file")
|
|
if err := os.WriteFile(fileDir, []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
badManager := NewManager(filepath.Join(fileDir, "child"), nil)
|
|
if err := badManager.SaveToken("x"); err == nil {
|
|
t.Fatal("token mkdir failure succeeded")
|
|
}
|
|
if err := badManager.SaveMCPURL("x"); err == nil {
|
|
t.Fatal("MCP URL mkdir failure succeeded")
|
|
}
|
|
writeDir := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(writeDir, tokenFileName), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := NewManager(writeDir, nil).SaveToken("x"); err == nil {
|
|
t.Fatal("token write failure succeeded")
|
|
}
|
|
if err := os.Mkdir(filepath.Join(writeDir, "mcp_url"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := NewManager(writeDir, nil).SaveMCPURL("x"); err == nil {
|
|
t.Fatal("MCP URL write failure succeeded")
|
|
}
|
|
deleteDir := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(deleteDir, tokenFileName), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(deleteDir, tokenFileName, "child"), []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := NewManager(deleteDir, nil).DeleteToken(); err == nil {
|
|
t.Fatal("token remove failure succeeded")
|
|
}
|
|
|
|
if !PlainSecret("").IsZero() || PlainSecret("x").IsZero() {
|
|
t.Fatal("secret zero detection failed")
|
|
}
|
|
secretPath := filepath.Join(t.TempDir(), "secret")
|
|
if err := os.WriteFile(secretPath, []byte(" value \n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if value, err := ResolveSecret(SecretInput{Ref: &SecretRef{Source: "file", ID: secretPath}}); err != nil || value != "value" {
|
|
t.Fatalf("file secret = %q %v", value, err)
|
|
}
|
|
if _, err := ResolveSecret(SecretInput{Ref: &SecretRef{Source: "file", ID: filepath.Join(t.TempDir(), "missing")}}); err == nil {
|
|
t.Fatal("missing file secret succeeded")
|
|
}
|
|
if _, err := ResolveSecret(SecretInput{Ref: &SecretRef{Source: "future", ID: "x"}}); err == nil {
|
|
t.Fatal("unknown secret source succeeded")
|
|
}
|
|
ref := SecretInput{Ref: &SecretRef{Source: "file", ID: secretPath}}
|
|
if stored, err := StoreSecret("id", ref); err != nil || stored.Ref != ref.Ref {
|
|
t.Fatal("existing secret reference changed")
|
|
}
|
|
})
|
|
|
|
t.Run("headers and stale token", func(t *testing.T) {
|
|
applyEditionEnterpriseCredentialHeaders(nil)
|
|
oldEdition := edition.Get()
|
|
t.Cleanup(func() { edition.Override(oldEdition) })
|
|
edition.Override(&edition.Hooks{Name: "coverage"})
|
|
req, _ := http.NewRequest(http.MethodGet, "https://example.test", nil)
|
|
applyEditionEnterpriseCredentialHeaders(req)
|
|
edition.Override(&edition.Hooks{Name: "coverage", EnterpriseCredentialHeaders: func(map[string]string) map[string]string {
|
|
return map[string]string{" X-Test ": " value ", "": "ignored", "Empty": " "}
|
|
}})
|
|
applyEditionEnterpriseCredentialHeaders(req)
|
|
if req.Header.Get("X-Test") != "value" || req.Header.Get("Empty") != "" {
|
|
t.Fatalf("enterprise headers = %v", req.Header)
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
if err := MarkAccessTokenStale(dir); err == nil {
|
|
t.Fatal("missing token stale mark succeeded")
|
|
}
|
|
if err := SaveTokenData(dir, &TokenData{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := MarkAccessTokenStale(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data := &TokenData{AccessToken: "access", ExpiresAt: time.Now().Add(time.Hour)}
|
|
if err := SaveTokenData(dir, data); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := MarkAccessTokenStale(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loaded, err := LoadTokenData(dir)
|
|
if err != nil || !loaded.ExpiresAt.Before(time.Now()) {
|
|
t.Fatalf("stale token = %#v %v", loaded, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCrossPlatformCoveragePortableAuthBundleCoverageEdges(t *testing.T) {
|
|
makeBundle := func(t *testing.T, entries []tar.Header, bodies []string) []byte {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
gz := gzip.NewWriter(&buf)
|
|
tw := tar.NewWriter(gz)
|
|
for i := range entries {
|
|
hdr := entries[i]
|
|
body := bodies[i]
|
|
if hdr.Typeflag == 0 {
|
|
hdr.Typeflag = tar.TypeReg
|
|
}
|
|
if hdr.Typeflag == tar.TypeReg {
|
|
hdr.Size = int64(len(body))
|
|
}
|
|
if err := tw.WriteHeader(&hdr); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if body != "" {
|
|
if _, err := tw.Write([]byte(body)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
if err := tw.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := gz.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
t.Run("platform and populated states", func(t *testing.T) {
|
|
oldGOOS := portableRuntimeGOOS
|
|
oldGet := authKeychainGet
|
|
oldExists := authKeychainExists
|
|
t.Cleanup(func() {
|
|
portableRuntimeGOOS = oldGOOS
|
|
authKeychainGet = oldGet
|
|
authKeychainExists = oldExists
|
|
})
|
|
authKeychainGet = func(string, string) (string, error) { return "", nil }
|
|
authKeychainExists = func(string, string) bool { return false }
|
|
portableRuntimeGOOS = func() string { return "linux" }
|
|
if !PortableExportSupported() {
|
|
t.Fatal("Linux portable export was disabled")
|
|
}
|
|
portableRuntimeGOOS = func() string { return "darwin" }
|
|
t.Setenv(keychain.DisableKeychainEnv, "")
|
|
if PortableExportSupported() {
|
|
t.Fatal("macOS system-keychain export was enabled")
|
|
}
|
|
t.Setenv(keychain.DisableKeychainEnv, "1")
|
|
if !PortableExportSupported() {
|
|
t.Fatal("macOS file-keychain export was disabled")
|
|
}
|
|
|
|
keyDir := t.TempDir()
|
|
t.Setenv(keychain.StorageDirEnv, keyDir)
|
|
configDir := t.TempDir()
|
|
if PortableAuthTargetPopulated(configDir) || PortableAuthSourceReady() {
|
|
t.Fatal("empty portable auth target reported populated")
|
|
}
|
|
if err := os.WriteFile(ProfilesPath(configDir), []byte("{}"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !PortableAuthTargetPopulated(configDir) {
|
|
t.Fatal("profiles target was not detected")
|
|
}
|
|
if err := os.Remove(ProfilesPath(configDir)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(configDir, "app.json"), []byte("{}"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !PortableAuthTargetPopulated(configDir) {
|
|
t.Fatal("app target was not detected")
|
|
}
|
|
if err := os.Remove(filepath.Join(configDir, "app.json")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
storageDir := keychain.StorageDir(keychain.Service)
|
|
if err := os.MkdirAll(storageDir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(storageDir, keychain.AccountToken+".enc"), []byte("encrypted"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
authKeychainGet = func(string, string) (string, error) {
|
|
return `{"corp_id":"corp","access_token":"portable"}`, nil
|
|
}
|
|
if !PortableAuthTargetPopulated(configDir) || !PortableAuthSourceReady() {
|
|
t.Fatal("encrypted token source was not detected")
|
|
}
|
|
})
|
|
|
|
t.Run("export and import", func(t *testing.T) {
|
|
oldGOOS := portableRuntimeGOOS
|
|
oldGet := authKeychainGet
|
|
t.Cleanup(func() {
|
|
portableRuntimeGOOS = oldGOOS
|
|
authKeychainGet = oldGet
|
|
})
|
|
authKeychainGet = func(string, string) (string, error) { return "", nil }
|
|
portableRuntimeGOOS = func() string { return "linux" }
|
|
keyRoot := t.TempDir()
|
|
t.Setenv(keychain.StorageDirEnv, keyRoot)
|
|
t.Setenv(keychain.DisableKeychainEnv, "1")
|
|
configDir := t.TempDir()
|
|
if err := ExportPortableAuthBundle(configDir, nil); err == nil {
|
|
t.Fatal("nil portable writer succeeded")
|
|
}
|
|
portableRuntimeGOOS = func() string { return "darwin" }
|
|
t.Setenv(keychain.DisableKeychainEnv, "")
|
|
if err := ExportPortableAuthBundle(configDir, io.Discard); err == nil {
|
|
t.Fatal("unsupported portable export succeeded")
|
|
}
|
|
portableRuntimeGOOS = func() string { return "linux" }
|
|
t.Setenv(keychain.DisableKeychainEnv, "1")
|
|
t.Setenv(keychain.StorageDirEnv, filepath.Join(t.TempDir(), "missing"))
|
|
if err := ExportPortableAuthBundle(configDir, io.Discard); err == nil {
|
|
t.Fatal("missing keychain export succeeded")
|
|
}
|
|
emptyKeyRoot := t.TempDir()
|
|
t.Setenv(keychain.StorageDirEnv, emptyKeyRoot)
|
|
if err := os.MkdirAll(keychain.StorageDir(keychain.Service), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ExportPortableAuthBundle(configDir, io.Discard); err == nil {
|
|
t.Fatal("empty keychain export succeeded")
|
|
}
|
|
|
|
readyKeyRoot := t.TempDir()
|
|
t.Setenv(keychain.StorageDirEnv, readyKeyRoot)
|
|
keyDir := keychain.StorageDir(keychain.Service)
|
|
if err := os.MkdirAll(filepath.Join(keyDir, "nested"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(keyDir, keychain.AccountToken+".enc"), []byte("encrypted"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
authKeychainGet = func(string, string) (string, error) {
|
|
return `{"corp_id":"corp","access_token":"portable"}`, nil
|
|
}
|
|
if err := os.WriteFile(filepath.Join(keyDir, "nested", "value"), []byte("nested"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(filepath.Join(keyDir, "dek"), filepath.Join(keyDir, "link")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for name, value := range map[string]string{
|
|
"app.json": "{}",
|
|
profilesJSONFile: "{}",
|
|
"mcp_url": "https://mcp.test",
|
|
config.ManagedMCPURLRegionFileName: "https://mcp.test",
|
|
"terminal_url": "https://terminal.test",
|
|
} {
|
|
if err := os.WriteFile(filepath.Join(configDir, name), []byte(value), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := os.Mkdir(filepath.Join(configDir, "app-dir.json"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var bundle bytes.Buffer
|
|
if err := ExportPortableAuthBundle(configDir, &bundle); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
importKeyRoot := t.TempDir()
|
|
t.Setenv(keychain.StorageDirEnv, importKeyRoot)
|
|
importDir := t.TempDir()
|
|
report, err := ImportPortableAuthBundle(importDir, bytes.NewReader(bundle.Bytes()))
|
|
if err != nil || report.BundleOS != "linux" || report.OSMismatch {
|
|
t.Fatalf("portable import report = %#v %v", report, err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(importDir, "app.json")); err != nil {
|
|
t.Fatal("config file was not imported")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(importDir, config.ManagedMCPURLRegionFileName)); err != nil {
|
|
t.Fatal("managed MCP region marker was not imported")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(keychain.StorageDir(keychain.Service), keychain.AccountToken+".enc")); err != nil {
|
|
t.Fatal("encrypted token was not imported")
|
|
}
|
|
})
|
|
|
|
t.Run("import validation", func(t *testing.T) {
|
|
oldGOOS := portableRuntimeGOOS
|
|
t.Cleanup(func() {
|
|
portableRuntimeGOOS = oldGOOS
|
|
})
|
|
portableRuntimeGOOS = func() string { return "linux" }
|
|
|
|
t.Setenv(keychain.StorageDirEnv, t.TempDir())
|
|
if _, err := ImportPortableAuthBundle(t.TempDir(), nil); err == nil {
|
|
t.Fatal("nil portable reader succeeded")
|
|
}
|
|
if _, err := ImportPortableAuthBundle(t.TempDir(), strings.NewReader("not gzip")); err == nil {
|
|
t.Fatal("invalid gzip bundle succeeded")
|
|
}
|
|
var corrupt bytes.Buffer
|
|
gz := gzip.NewWriter(&corrupt)
|
|
_, _ = gz.Write([]byte("not a tar stream"))
|
|
_ = gz.Close()
|
|
if _, err := ImportPortableAuthBundle(t.TempDir(), bytes.NewReader(corrupt.Bytes())); err == nil {
|
|
t.Fatal("invalid tar bundle succeeded")
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
header tar.Header
|
|
body string
|
|
wantErr bool
|
|
}{
|
|
{"bad manifest", tar.Header{Name: portableAuthManifest}, "{", true},
|
|
{"unsafe parent", tar.Header{Name: "../evil"}, "x", true},
|
|
{"unsafe absolute", tar.Header{Name: "/evil"}, "x", true},
|
|
{"unsupported path", tar.Header{Name: "other/value"}, "x", true},
|
|
{"unsupported type", tar.Header{Name: "config/link", Typeflag: tar.TypeSymlink, Linkname: "x"}, "", true},
|
|
{"directory", tar.Header{Name: "config/nested", Typeflag: tar.TypeDir}, "", false},
|
|
{"no manifest", tar.Header{Name: "config/value"}, "x", false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := ImportPortableAuthBundle(t.TempDir(), bytes.NewReader(makeBundle(t, []tar.Header{tc.header}, []string{tc.body})))
|
|
if (err != nil) != tc.wantErr {
|
|
t.Fatalf("import error = %v, want error %v", err, tc.wantErr)
|
|
}
|
|
})
|
|
}
|
|
manifest := `{"version":1,"os":"other-os","keychain_service":"dws-cli"}`
|
|
report, err := ImportPortableAuthBundle(t.TempDir(), bytes.NewReader(makeBundle(t, []tar.Header{{Name: portableAuthManifest}}, []string{manifest})))
|
|
if err != nil || report.BundleOS != "other-os" || !report.OSMismatch {
|
|
t.Fatalf("mismatched OS report = %#v %v", report, err)
|
|
}
|
|
})
|
|
|
|
t.Run("helper failures", func(t *testing.T) {
|
|
if _, err := cleanPortableName("."); err == nil {
|
|
t.Fatal("empty portable name succeeded")
|
|
}
|
|
if _, err := cleanPortableName("../x"); err == nil {
|
|
t.Fatal("parent portable name succeeded")
|
|
}
|
|
if got, err := cleanPortableName(" config/value "); err != nil || got != "config/value" {
|
|
t.Fatalf("clean portable name = %q %v", got, err)
|
|
}
|
|
for _, rel := range []string{"", ".", "..", "../x", "/absolute"} {
|
|
if _, err := safeJoin(t.TempDir(), rel); err == nil {
|
|
t.Errorf("unsafe relative path %q succeeded", rel)
|
|
}
|
|
}
|
|
safeRoot := t.TempDir()
|
|
if got, err := safeJoin(safeRoot, "nested/value"); err != nil || got != filepath.Join(safeRoot, "nested", "value") {
|
|
t.Fatalf("safe join = %q %v", got, err)
|
|
}
|
|
oldInRoot := portablePathInRoot
|
|
portablePathInRoot = func(string, string) bool { return false }
|
|
if _, err := safeJoin(safeRoot, "value"); err == nil {
|
|
t.Fatal("out-of-root path succeeded")
|
|
}
|
|
portablePathInRoot = oldInRoot
|
|
|
|
if _, err := portableConfigFiles("["); err == nil {
|
|
t.Fatal("invalid config glob succeeded")
|
|
}
|
|
dir := t.TempDir()
|
|
file := filepath.Join(dir, "app.json")
|
|
if err := os.WriteFile(file, []byte("{}"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
oldStat := portableStat
|
|
portableStat = func(string) (os.FileInfo, error) { return nil, errors.New("stat") }
|
|
if files, err := portableConfigFiles(dir); err != nil || len(files) != 0 {
|
|
t.Fatalf("stat-skipped config files = %v %v", files, err)
|
|
}
|
|
portableStat = oldStat
|
|
oldRel := portableRel
|
|
portableRel = func(string, string) (string, error) { return "", errors.New("rel") }
|
|
if _, err := portableConfigFiles(dir); err == nil {
|
|
t.Fatal("config relative-path failure succeeded")
|
|
}
|
|
if got := mustPortableRel("root", "file"); got != "file" {
|
|
t.Fatalf("portable relative fallback = %q", got)
|
|
}
|
|
portableRel = oldRel
|
|
|
|
oldMarshal := portableJSONMarshal
|
|
portableJSONMarshal = func(any, string, string) ([]byte, error) { return nil, errors.New("marshal") }
|
|
if err := writePortableManifest(tar.NewWriter(io.Discard), portableAuthBundleManifest{}); err == nil {
|
|
t.Fatal("manifest marshal failure succeeded")
|
|
}
|
|
portableJSONMarshal = oldMarshal
|
|
|
|
closed := tar.NewWriter(io.Discard)
|
|
_ = closed.Close()
|
|
if err := writePortableBytes(closed, "value", []byte("x"), 0o600); err == nil {
|
|
t.Fatal("closed tar byte write succeeded")
|
|
}
|
|
dataFail := &failAfterWriter{remaining: 512}
|
|
if err := writePortableBytes(tar.NewWriter(dataFail), "value", []byte("x"), 0o600); err == nil {
|
|
t.Fatal("tar data write failure succeeded")
|
|
}
|
|
|
|
if err := addPortableDir(tar.NewWriter(io.Discard), filepath.Join(t.TempDir(), "missing"), "keychain"); err == nil {
|
|
t.Fatal("missing portable directory succeeded")
|
|
}
|
|
walkDir := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(walkDir, "nested"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
portableRel = func(string, string) (string, error) { return "", errors.New("rel") }
|
|
if err := addPortableDir(tar.NewWriter(io.Discard), walkDir, "keychain"); err == nil {
|
|
t.Fatal("directory relative-path failure succeeded")
|
|
}
|
|
portableRel = oldRel
|
|
|
|
if err := addPortableFile(tar.NewWriter(io.Discard), filepath.Join(t.TempDir(), "missing"), "value"); err == nil {
|
|
t.Fatal("missing portable file succeeded")
|
|
}
|
|
if err := addPortableFile(tar.NewWriter(io.Discard), t.TempDir(), "dir"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
oldOpen := portableOpen
|
|
portableOpen = func(string) (io.ReadCloser, error) { return nil, errors.New("open") }
|
|
if err := addPortableFile(tar.NewWriter(io.Discard), file, "value"); err == nil {
|
|
t.Fatal("portable file open failure succeeded")
|
|
}
|
|
portableOpen = oldOpen
|
|
if err := addPortableFile(closed, file, "value"); err == nil {
|
|
t.Fatal("closed tar file write succeeded")
|
|
}
|
|
portableOpen = func(string) (io.ReadCloser, error) { return io.NopCloser(errorReader{}), nil }
|
|
if err := addPortableFile(tar.NewWriter(io.Discard), file, "value"); err == nil {
|
|
t.Fatal("portable file copy failure succeeded")
|
|
}
|
|
portableOpen = oldOpen
|
|
|
|
oldMkdir := portableMkdirAll
|
|
oldChmod := portableChmod
|
|
oldCreate := portableCreateTemp
|
|
oldRename := portableRename
|
|
oldRemove := portableRemove
|
|
t.Cleanup(func() {
|
|
portableStat = oldStat
|
|
portableRel = oldRel
|
|
portableOpen = oldOpen
|
|
portableJSONMarshal = oldMarshal
|
|
portablePathInRoot = oldInRoot
|
|
portableMkdirAll = oldMkdir
|
|
portableChmod = oldChmod
|
|
portableCreateTemp = oldCreate
|
|
portableRename = oldRename
|
|
portableRemove = oldRemove
|
|
})
|
|
portableMkdirAll = func(string, os.FileMode) error { return errors.New("mkdir") }
|
|
if err := extractPortableEntry(filepath.Join(t.TempDir(), "dir"), &tar.Header{Typeflag: tar.TypeDir}, strings.NewReader("")); err == nil {
|
|
t.Fatal("portable directory mkdir failure succeeded")
|
|
}
|
|
if err := extractPortableEntry(filepath.Join(t.TempDir(), "file"), &tar.Header{Typeflag: tar.TypeReg}, strings.NewReader("x")); err == nil {
|
|
t.Fatal("portable file mkdir failure succeeded")
|
|
}
|
|
portableMkdirAll = oldMkdir
|
|
portableChmod = func(string, os.FileMode) error { return errors.New("chmod") }
|
|
if err := extractPortableEntry(t.TempDir(), &tar.Header{Typeflag: tar.TypeDir}, strings.NewReader("")); err == nil {
|
|
t.Fatal("portable directory chmod failure succeeded")
|
|
}
|
|
portableChmod = oldChmod
|
|
portableCreateTemp = func(string, string) (portableTempFile, error) { return nil, errors.New("create") }
|
|
if err := extractPortableEntry(filepath.Join(t.TempDir(), "file"), &tar.Header{Typeflag: tar.TypeReg}, strings.NewReader("x")); err == nil {
|
|
t.Fatal("portable temp create failure succeeded")
|
|
}
|
|
fake := func() *fakePortableTemp { return &fakePortableTemp{name: filepath.Join(t.TempDir(), "tmp")} }
|
|
for name, configure := range map[string]func(*fakePortableTemp){
|
|
"chmod": func(f *fakePortableTemp) { f.chmodErr = errors.New("chmod") },
|
|
"write": func(f *fakePortableTemp) { f.writeErr = errors.New("write") },
|
|
"sync": func(f *fakePortableTemp) { f.syncErr = errors.New("sync") },
|
|
"close": func(f *fakePortableTemp) { f.closeErr = errors.New("close") },
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
f := fake()
|
|
configure(f)
|
|
portableCreateTemp = func(string, string) (portableTempFile, error) { return f, nil }
|
|
if err := extractPortableEntry(filepath.Join(t.TempDir(), "file"), &tar.Header{Typeflag: tar.TypeReg}, strings.NewReader("x")); err == nil {
|
|
t.Fatalf("portable temp %s failure succeeded", name)
|
|
}
|
|
})
|
|
}
|
|
f := fake()
|
|
portableCreateTemp = func(string, string) (portableTempFile, error) { return f, nil }
|
|
portableRename = func(string, string) error { return errors.New("rename") }
|
|
portableRemove = func(string) error { return errors.New("remove") }
|
|
if err := extractPortableEntry(filepath.Join(t.TempDir(), "file"), &tar.Header{Typeflag: tar.TypeReg}, strings.NewReader("x")); err == nil {
|
|
t.Fatal("portable rename failure succeeded")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCrossPlatformCoverageTokenStorageAndRevocationCoverageEdges(t *testing.T) {
|
|
oldMarshalIndent := tokenJSONMarshalIndent
|
|
oldMarshal := tokenJSONMarshal
|
|
oldMkdir := tokenMkdirAll
|
|
oldRead := tokenReadFile
|
|
oldWrite := tokenWriteFile
|
|
oldRename := tokenRename
|
|
oldRemove := tokenRemove
|
|
oldGlob := tokenGlob
|
|
oldSaveCorp := tokenSaveKeychainForCorpID
|
|
oldSaveIdentity := tokenSaveKeychainForIdentity
|
|
oldSaveLegacy := tokenSaveKeychain
|
|
oldLoadCorp := tokenLoadKeychainForCorpID
|
|
oldLoadIdentity := tokenLoadKeychainIdentity
|
|
oldLoadLegacy := tokenLoadKeychain
|
|
oldExists := tokenKeychainExists
|
|
oldDeleteCorp := tokenDeleteKeychainForCorpID
|
|
oldDeleteIdentity := tokenDeleteKeychainIdentity
|
|
oldDeleteLegacy := tokenDeleteKeychain
|
|
oldRemoveAuthEntries := tokenRemoveAuthTokenEntries
|
|
oldLoadSecure := tokenLoadSecure
|
|
oldDeleteSecure := tokenDeleteSecure
|
|
oldEnsureProfiles := profilesEnsureMigration
|
|
oldResolve := tokenResolveProfile
|
|
oldResolveDeletion := tokenResolveDeletion
|
|
oldResolveSelection := tokenResolveSelection
|
|
oldUpsert := tokenUpsertProfile
|
|
oldRemoveProfile := tokenRemoveProfile
|
|
oldSync := tokenSyncLegacyMirror
|
|
oldSyncOrganization := tokenSyncOrganizationMirror
|
|
oldLoadProfiles := tokenLoadProfiles
|
|
oldSaveProfiles := tokenSaveProfiles
|
|
oldWriteMarker := tokenWriteMarker
|
|
oldWriteManualMarker := tokenWriteManualMarker
|
|
oldDeleteMarker := tokenDeleteMarker
|
|
oldParseURL := tokenParseURL
|
|
oldNewRequest := tokenNewRequest
|
|
oldDefaultDir := tokenDefaultConfigDir
|
|
oldLoadData := tokenLoadData
|
|
oldRevokeURL := tokenRevokeURL
|
|
oldMCPBaseURL := tokenMCPBaseURL
|
|
oldLogoutURL := tokenLogoutURL
|
|
oldLogoutContinue := tokenLogoutContinueURL
|
|
oldLogoutClient := tokenLogoutHTTPClient
|
|
oldRevokeClient := tokenRevokeHTTPClient
|
|
oldEdition := edition.Get()
|
|
t.Cleanup(func() {
|
|
tokenJSONMarshalIndent = oldMarshalIndent
|
|
tokenJSONMarshal = oldMarshal
|
|
tokenMkdirAll = oldMkdir
|
|
tokenReadFile = oldRead
|
|
tokenWriteFile = oldWrite
|
|
tokenRename = oldRename
|
|
tokenRemove = oldRemove
|
|
tokenGlob = oldGlob
|
|
tokenSaveKeychainForCorpID = oldSaveCorp
|
|
tokenSaveKeychainForIdentity = oldSaveIdentity
|
|
tokenSaveKeychain = oldSaveLegacy
|
|
tokenLoadKeychainForCorpID = oldLoadCorp
|
|
tokenLoadKeychainIdentity = oldLoadIdentity
|
|
tokenLoadKeychain = oldLoadLegacy
|
|
tokenKeychainExists = oldExists
|
|
tokenDeleteKeychainForCorpID = oldDeleteCorp
|
|
tokenDeleteKeychainIdentity = oldDeleteIdentity
|
|
tokenDeleteKeychain = oldDeleteLegacy
|
|
tokenRemoveAuthTokenEntries = oldRemoveAuthEntries
|
|
tokenLoadSecure = oldLoadSecure
|
|
tokenDeleteSecure = oldDeleteSecure
|
|
profilesEnsureMigration = oldEnsureProfiles
|
|
tokenResolveProfile = oldResolve
|
|
tokenResolveDeletion = oldResolveDeletion
|
|
tokenResolveSelection = oldResolveSelection
|
|
tokenUpsertProfile = oldUpsert
|
|
tokenRemoveProfile = oldRemoveProfile
|
|
tokenSyncLegacyMirror = oldSync
|
|
tokenSyncOrganizationMirror = oldSyncOrganization
|
|
tokenLoadProfiles = oldLoadProfiles
|
|
tokenSaveProfiles = oldSaveProfiles
|
|
tokenWriteMarker = oldWriteMarker
|
|
tokenWriteManualMarker = oldWriteManualMarker
|
|
tokenDeleteMarker = oldDeleteMarker
|
|
tokenParseURL = oldParseURL
|
|
tokenNewRequest = oldNewRequest
|
|
tokenDefaultConfigDir = oldDefaultDir
|
|
tokenLoadData = oldLoadData
|
|
tokenRevokeURL = oldRevokeURL
|
|
tokenMCPBaseURL = oldMCPBaseURL
|
|
tokenLogoutURL = oldLogoutURL
|
|
tokenLogoutContinueURL = oldLogoutContinue
|
|
tokenLogoutHTTPClient = oldLogoutClient
|
|
tokenRevokeHTTPClient = oldRevokeClient
|
|
edition.Override(oldEdition)
|
|
SetRuntimeProfile("")
|
|
SetClientID("")
|
|
resetClientIDFromMCP()
|
|
})
|
|
|
|
t.Run("marker", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
tokenMkdirAll = func(string, os.FileMode) error { return errors.New("mkdir") }
|
|
if err := WriteTokenMarker(dir); err == nil {
|
|
t.Fatal("marker mkdir failure succeeded")
|
|
}
|
|
tokenMkdirAll = oldMkdir
|
|
tokenWriteFile = func(string, []byte, os.FileMode) error { return errors.New("write") }
|
|
if err := WriteTokenMarker(dir); err == nil {
|
|
t.Fatal("marker write failure succeeded")
|
|
}
|
|
tokenWriteFile = oldWrite
|
|
tokenRename = func(string, string) error { return errors.New("rename") }
|
|
if err := WriteTokenMarker(dir); err == nil {
|
|
t.Fatal("marker rename failure succeeded")
|
|
}
|
|
tokenRename = oldRename
|
|
tokenRemove = func(string) error { return errors.New("remove") }
|
|
if err := DeleteTokenMarker(dir); err == nil {
|
|
t.Fatal("marker delete failure succeeded")
|
|
}
|
|
tokenRemove = oldRemove
|
|
})
|
|
|
|
t.Run("save branches", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
data := &TokenData{CorpID: "corp", AccessToken: "access"}
|
|
fail := errors.New("fail")
|
|
tokenSaveKeychainForCorpID = func(string, *TokenData) error { return fail }
|
|
if err := saveTokenDataLocked(dir, data); !errors.Is(err, fail) {
|
|
t.Fatalf("corp keychain error = %v", err)
|
|
}
|
|
tokenSaveKeychainForCorpID = oldSaveCorp
|
|
tokenUpsertProfile = func(string, *TokenData, bool) error { return fail }
|
|
if err := saveTokenDataLocked(dir, data); !errors.Is(err, fail) {
|
|
t.Fatalf("profile upsert error = %v", err)
|
|
}
|
|
tokenUpsertProfile = oldUpsert
|
|
tokenSaveKeychain = func(*TokenData) error { return fail }
|
|
if err := saveTokenDataLocked(dir, data); !errors.Is(err, fail) {
|
|
t.Fatalf("legacy keychain error = %v", err)
|
|
}
|
|
tokenSaveKeychain = oldSaveLegacy
|
|
SetRuntimeProfile("other")
|
|
tokenSyncLegacyMirror = func(string) error { return fail }
|
|
if err := saveTokenDataLocked(dir, data); !errors.Is(err, fail) {
|
|
t.Fatalf("legacy mirror error = %v", err)
|
|
}
|
|
tokenSyncLegacyMirror = oldSync
|
|
tokenWriteMarker = func(string) error { return fail }
|
|
if err := saveTokenDataLocked(dir, data); !errors.Is(err, fail) {
|
|
t.Fatalf("corp marker error = %v", err)
|
|
}
|
|
SetRuntimeProfile("")
|
|
tokenWriteManualMarker = func(string) error { return fail }
|
|
if err := saveTokenDataLocked(dir, &TokenData{}); !errors.Is(err, fail) {
|
|
t.Fatalf("legacy marker error = %v", err)
|
|
}
|
|
tokenWriteMarker = oldWriteMarker
|
|
tokenWriteManualMarker = oldWriteManualMarker
|
|
|
|
edition.Override(&edition.Hooks{Name: "coverage", SaveToken: func(string, []byte) error { return nil }})
|
|
if err := saveTokenDataLocked(dir, data); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tokenJSONMarshalIndent = func(any, string, string) ([]byte, error) { return nil, fail }
|
|
if err := saveTokenViaHook(edition.Get(), dir, data); err == nil {
|
|
t.Fatal("hook marshal failure succeeded")
|
|
}
|
|
tokenJSONMarshalIndent = oldMarshalIndent
|
|
edition.Override(oldEdition)
|
|
})
|
|
|
|
t.Run("load branches", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
fail := errors.New("fail")
|
|
selected := &Profile{CorpID: "corp"}
|
|
tokenResolveProfile = func(string, string) (*Profile, error) { return nil, fail }
|
|
if _, err := LoadTokenDataForProfile(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("profile resolution error = %v", err)
|
|
}
|
|
tokenResolveProfile = func(string, string) (*Profile, error) { return selected, nil }
|
|
tokenLoadKeychainForCorpID = func(string) (*TokenData, error) { return nil, fail }
|
|
if _, err := LoadTokenDataForProfile(dir, "explicit"); !errors.Is(err, fail) {
|
|
t.Fatalf("explicit profile load error = %v", err)
|
|
}
|
|
tokenLoadKeychainForCorpID = func(string) (*TokenData, error) { return nil, ErrTokenDataNotFound }
|
|
tokenLoadKeychain = func() (*TokenData, error) { return &TokenData{CorpID: "corp", AccessToken: "legacy"}, nil }
|
|
if got, err := LoadTokenDataForProfile(dir, ""); err != nil || got.AccessToken != "legacy" {
|
|
t.Fatalf("matching legacy fallback = %#v %v", got, err)
|
|
}
|
|
tokenLoadKeychain = func() (*TokenData, error) { return &TokenData{CorpID: "other"}, nil }
|
|
if _, err := LoadTokenDataForProfile(dir, ""); !errors.Is(err, ErrTokenDataNotFound) {
|
|
t.Fatalf("mismatched legacy fallback error = %v", err)
|
|
}
|
|
|
|
tokenResolveProfile = func(string, string) (*Profile, error) { return nil, nil }
|
|
tokenKeychainExists = func() bool { return true }
|
|
tokenLoadKeychain = func() (*TokenData, error) { return nil, fail }
|
|
if _, err := LoadTokenDataForProfile(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("legacy slot load error = %v", err)
|
|
}
|
|
tokenKeychainExists = func() bool { return false }
|
|
tokenLoadSecure = func(string) (*TokenData, error) { return nil, fail }
|
|
if _, err := LoadTokenDataForProfile(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("secure load error = %v", err)
|
|
}
|
|
legacy := &TokenData{AccessToken: "secure"}
|
|
tokenLoadSecure = func(string) (*TokenData, error) { return legacy, nil }
|
|
tokenSaveKeychain = func(*TokenData) error { return fail }
|
|
if got, err := LoadTokenDataForProfile(dir, ""); err != nil || got != legacy {
|
|
t.Fatalf("failed migration load = %#v %v", got, err)
|
|
}
|
|
tokenSaveKeychain = func(*TokenData) error { return nil }
|
|
tokenLoadKeychain = func() (*TokenData, error) { return nil, ErrTokenDataNotFound }
|
|
tokenReadFile = func(string) ([]byte, error) { return nil, os.ErrNotExist }
|
|
tokenWriteManualMarker = func(string) error { return nil }
|
|
deleted := false
|
|
tokenDeleteSecure = func(string) error { deleted = true; return nil }
|
|
if _, err := LoadTokenDataForProfile(dir, ""); err != nil || !deleted {
|
|
t.Fatal("successful secure migration did not clean up")
|
|
}
|
|
|
|
edition.Override(&edition.Hooks{Name: "coverage", LoadToken: func(string) ([]byte, error) { return nil, fail }})
|
|
if _, err := LoadTokenDataForProfile(dir, "selected"); err == nil {
|
|
t.Fatal("hook profile selection succeeded")
|
|
}
|
|
if _, err := LoadTokenDataForProfile(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("hook load error = %v", err)
|
|
}
|
|
edition.Override(&edition.Hooks{Name: "coverage", LoadToken: func(string) ([]byte, error) { return []byte("{"), nil }})
|
|
if _, err := LoadTokenDataForProfile(dir, ""); err == nil {
|
|
t.Fatal("malformed hook token succeeded")
|
|
}
|
|
edition.Override(oldEdition)
|
|
})
|
|
|
|
t.Run("delete branches", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
fail := errors.New("fail")
|
|
selected := &Profile{CorpID: "corp", UserID: "user"}
|
|
profilesEnsureMigration = func(string) error { return fail }
|
|
if err := deleteTokenDataForProfileLocked(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("delete migration error = %v", err)
|
|
}
|
|
profilesEnsureMigration = func(string) error { return nil }
|
|
tokenLoadProfiles = func(string) (*ProfilesConfig, error) { return nil, fail }
|
|
if err := deleteTokenDataForProfileLocked(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("delete profiles load error = %v", err)
|
|
}
|
|
cfg := &ProfilesConfig{
|
|
CurrentProfile: ProfileSelector(*selected),
|
|
OrgCurrentProfiles: map[string]string{"corp": ProfileSelector(*selected)},
|
|
Profiles: []Profile{*selected},
|
|
}
|
|
tokenLoadProfiles = func(string) (*ProfilesConfig, error) { return cfg, nil }
|
|
tokenResolveDeletion = func(*ProfilesConfig, string) (*Profile, bool, error) { return nil, false, fail }
|
|
if err := deleteTokenDataForProfileLocked(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("delete resolution error = %v", err)
|
|
}
|
|
tokenResolveDeletion = func(*ProfilesConfig, string) (*Profile, bool, error) { return selected, true, nil }
|
|
tokenLoadKeychainIdentity = func(string, string) (*TokenData, error) {
|
|
return &TokenData{CorpID: "corp", UserID: "user"}, nil
|
|
}
|
|
tokenLoadKeychainForCorpID = func(string) (*TokenData, error) {
|
|
return &TokenData{CorpID: "corp", UserID: "user"}, nil
|
|
}
|
|
tokenLoadKeychain = func() (*TokenData, error) {
|
|
return &TokenData{CorpID: "corp", UserID: "user"}, nil
|
|
}
|
|
tokenReadFile = func(string) ([]byte, error) { return nil, os.ErrNotExist }
|
|
tokenSaveProfiles = func(string, *ProfilesConfig) error { return nil }
|
|
tokenSaveKeychainForIdentity = func(string, string, *TokenData) error { return nil }
|
|
tokenSaveKeychainForCorpID = func(string, *TokenData) error { return nil }
|
|
tokenSaveKeychain = func(*TokenData) error { return nil }
|
|
tokenWriteMarker = func(string) error { return nil }
|
|
tokenDeleteMarker = func(string) error { return nil }
|
|
tokenRemoveProfile = func(string, string) (*Profile, error) { return selected, nil }
|
|
tokenSyncLegacyMirror = func(string) error { return nil }
|
|
tokenSyncOrganizationMirror = func(Profile) error { return nil }
|
|
tokenDeleteSecure = func(string) error { return nil }
|
|
tokenDeleteKeychainForCorpID = func(string) error { return nil }
|
|
tokenDeleteKeychainIdentity = func(string, string) error { return fail }
|
|
if err := deleteTokenDataForProfileLocked(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("delete identity keychain error = %v", err)
|
|
}
|
|
tokenDeleteKeychainIdentity = func(string, string) error { return nil }
|
|
cfg.OrgCurrentProfiles = map[string]string{"corp": ProfileSelector(*selected)}
|
|
tokenRemoveProfile = func(string, string) (*Profile, error) {
|
|
cfg.OrgCurrentProfiles = nil
|
|
return selected, nil
|
|
}
|
|
tokenDeleteKeychainForCorpID = func(string) error { return fail }
|
|
if err := deleteTokenDataForProfileLocked(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("delete corp keychain error = %v", err)
|
|
}
|
|
tokenDeleteKeychainForCorpID = func(string) error { return nil }
|
|
cfg.OrgCurrentProfiles = map[string]string{"corp": ProfileSelector(*selected)}
|
|
tokenRemoveProfile = func(string, string) (*Profile, error) { return nil, fail }
|
|
if err := deleteTokenDataForProfileLocked(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("remove profile error = %v", err)
|
|
}
|
|
tokenRemoveProfile = func(string, string) (*Profile, error) { return selected, nil }
|
|
cfg.OrgCurrentProfiles = nil
|
|
tokenSyncLegacyMirror = func(string) error { return fail }
|
|
if err := deleteTokenDataForProfileLocked(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("sync mirror error = %v", err)
|
|
}
|
|
tokenSyncLegacyMirror = func(string) error { return nil }
|
|
tokenDeleteSecure = func(string) error { return fail }
|
|
if err := deleteTokenDataForProfileLocked(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("secure cleanup error = %v", err)
|
|
}
|
|
|
|
cfg.CurrentProfile = ""
|
|
cfg.Profiles = nil
|
|
tokenDeleteKeychain = func() error { return fail }
|
|
tokenDeleteMarker = func(string) error { return nil }
|
|
if err := deleteTokenDataForProfileLocked(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("legacy keychain delete error = %v", err)
|
|
}
|
|
tokenDeleteKeychain = func() error { return nil }
|
|
tokenDeleteSecure = func(string) error { return fail }
|
|
if err := deleteTokenDataForProfileLocked(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("legacy secure delete error = %v", err)
|
|
}
|
|
|
|
edition.Override(&edition.Hooks{Name: "coverage", DeleteToken: func(string) error { return fail }})
|
|
if err := DeleteTokenDataForProfile(dir, "selected"); err == nil {
|
|
t.Fatal("hook profile deletion succeeded")
|
|
}
|
|
if err := DeleteTokenDataForProfile(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("hook token deletion error = %v", err)
|
|
}
|
|
if err := DeleteAllTokenData(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("hook delete-all error = %v", err)
|
|
}
|
|
edition.Override(oldEdition)
|
|
})
|
|
|
|
t.Run("delete all", func(t *testing.T) {
|
|
fail := errors.New("fail")
|
|
base := func() string {
|
|
tokenRemoveAuthTokenEntries = func(string) error { return nil }
|
|
tokenRemove = func(string) error { return os.ErrNotExist }
|
|
tokenGlob = func(string) ([]string, error) { return nil, nil }
|
|
tokenDeleteSecure = func(string) error { return nil }
|
|
tokenDeleteMarker = func(string) error { return nil }
|
|
return t.TempDir()
|
|
}
|
|
dir := base()
|
|
tokenRemoveAuthTokenEntries = func(string) error { return fail }
|
|
if err := DeleteAllTokenData(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("delete-all auth namespace error = %v", err)
|
|
}
|
|
dir = base()
|
|
tokenRemove = func(string) error { return fail }
|
|
if err := DeleteAllTokenData(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("delete-all profiles error = %v", err)
|
|
}
|
|
dir = base()
|
|
corrupt := filepath.Join(dir, "profiles.json.corrupt-x")
|
|
tokenGlob = func(string) ([]string, error) { return []string{corrupt}, nil }
|
|
tokenRemove = func(name string) error {
|
|
if name == corrupt {
|
|
return fail
|
|
}
|
|
return os.ErrNotExist
|
|
}
|
|
if err := DeleteAllTokenData(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("delete-all quarantine error = %v", err)
|
|
}
|
|
dir = base()
|
|
tokenDeleteSecure = func(string) error { return fail }
|
|
if err := DeleteAllTokenData(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("delete-all secure error = %v", err)
|
|
}
|
|
dir = base()
|
|
tokenDeleteMarker = func(string) error { return fail }
|
|
if err := DeleteAllTokenData(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("delete-all marker error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("remote revoke", func(t *testing.T) {
|
|
resetClientIDFromMCP()
|
|
SetClientID("client")
|
|
fail := errors.New("fail")
|
|
tokenLoadData = func(string) (*TokenData, error) {
|
|
return &TokenData{AccessToken: "access", ClientID: "client", Source: "direct"}, nil
|
|
}
|
|
tokenParseURL = func(string) (*url.URL, error) { return nil, fail }
|
|
if err := RevokeTokenRemote(context.Background()); !errors.Is(err, fail) {
|
|
t.Fatalf("logout parse error = %v", err)
|
|
}
|
|
tokenParseURL = oldParseURL
|
|
tokenLogoutURL = "https://logout.test"
|
|
tokenNewRequest = func(context.Context, string, string, io.Reader) (*http.Request, error) { return nil, fail }
|
|
if err := RevokeTokenRemote(context.Background()); !errors.Is(err, fail) {
|
|
t.Fatalf("logout request error = %v", err)
|
|
}
|
|
tokenNewRequest = oldNewRequest
|
|
tokenLogoutHTTPClient = &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { return nil, fail })}
|
|
if err := RevokeTokenRemote(context.Background()); !errors.Is(err, fail) {
|
|
t.Fatalf("logout network error = %v", err)
|
|
}
|
|
response := func(status int) *http.Client {
|
|
return &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
|
return &http.Response{StatusCode: status, Body: io.NopCloser(strings.NewReader("")), Header: make(http.Header)}, nil
|
|
})}
|
|
}
|
|
for _, status := range []int{http.StatusOK, http.StatusFound} {
|
|
tokenLogoutHTTPClient = response(status)
|
|
if err := RevokeTokenRemote(context.Background()); err != nil {
|
|
t.Fatalf("logout status %d = %v", status, err)
|
|
}
|
|
}
|
|
tokenLogoutHTTPClient = response(http.StatusBadGateway)
|
|
if err := RevokeTokenRemote(context.Background()); err == nil {
|
|
t.Fatal("bad logout status succeeded")
|
|
}
|
|
|
|
SetClientIDFromMCP("mcp-client")
|
|
tokenLoadData = func(string) (*TokenData, error) { return nil, fail }
|
|
if err := RevokeTokenRemote(context.Background()); err != nil {
|
|
t.Fatal("missing token revoke should be a no-op")
|
|
}
|
|
tokenMCPBaseURL = func() string { return "https://revoke.test" }
|
|
tokenLoadData = func(string) (*TokenData, error) {
|
|
return &TokenData{AccessToken: "access", ClientID: "mcp-client", Source: "mcp"}, nil
|
|
}
|
|
tokenJSONMarshal = func(any) ([]byte, error) { return nil, fail }
|
|
if err := RevokeTokenRemote(context.Background()); !errors.Is(err, fail) {
|
|
t.Fatalf("revoke marshal error = %v", err)
|
|
}
|
|
tokenJSONMarshal = oldMarshal
|
|
tokenNewRequest = func(context.Context, string, string, io.Reader) (*http.Request, error) { return nil, fail }
|
|
if err := RevokeTokenRemote(context.Background()); !errors.Is(err, fail) {
|
|
t.Fatalf("revoke request error = %v", err)
|
|
}
|
|
tokenNewRequest = oldNewRequest
|
|
tokenRevokeHTTPClient = &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) { return nil, fail })}
|
|
if err := RevokeTokenRemote(context.Background()); !errors.Is(err, fail) {
|
|
t.Fatalf("revoke network error = %v", err)
|
|
}
|
|
tokenRevokeHTTPClient = response(http.StatusBadGateway)
|
|
if err := RevokeTokenRemote(context.Background()); err == nil {
|
|
t.Fatal("bad revoke status succeeded")
|
|
}
|
|
tokenRevokeHTTPClient = response(http.StatusOK)
|
|
if err := RevokeTokenRemote(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSmallRemainingAuthEdges(t *testing.T) {
|
|
t.Run("app token hooks", func(t *testing.T) {
|
|
oldIndent := appTokenMarshalIndent
|
|
oldMarshal := appTokenMarshal
|
|
oldRequest := appTokenNewRequest
|
|
oldSet := appTokenKeychainSet
|
|
oldGet := appTokenKeychainGet
|
|
oldClient := appTokenHTTPClient
|
|
t.Cleanup(func() {
|
|
appTokenMarshalIndent = oldIndent
|
|
appTokenMarshal = oldMarshal
|
|
appTokenNewRequest = oldRequest
|
|
appTokenKeychainSet = oldSet
|
|
appTokenKeychainGet = oldGet
|
|
appTokenHTTPClient = oldClient
|
|
})
|
|
fail := errors.New("fail")
|
|
appTokenMarshalIndent = func(any, string, string) ([]byte, error) { return nil, fail }
|
|
if err := SaveAppTokenData(&AppTokenData{ClientID: "id"}); !errors.Is(err, fail) {
|
|
t.Fatalf("app token marshal error = %v", err)
|
|
}
|
|
appTokenMarshalIndent = oldIndent
|
|
appTokenKeychainSet = func(string, string, string) error { return fail }
|
|
if err := SaveAppTokenData(&AppTokenData{ClientID: "id"}); !errors.Is(err, fail) {
|
|
t.Fatalf("app token keychain error = %v", err)
|
|
}
|
|
appTokenKeychainSet = oldSet
|
|
appTokenKeychainGet = func(string, string) (string, error) { return "", nil }
|
|
if got, err := LoadAppTokenData("id"); err != nil || got != nil {
|
|
t.Fatalf("empty app token = %#v %v", got, err)
|
|
}
|
|
appTokenKeychainGet = func(string, string) (string, error) { return "", fail }
|
|
if got, err := LoadAppTokenData("id"); err != nil || got != nil {
|
|
t.Fatalf("missing app token = %#v %v", got, err)
|
|
}
|
|
appTokenKeychainGet = oldGet
|
|
appTokenMarshal = func(any) ([]byte, error) { return nil, fail }
|
|
if _, _, err := FetchAppToken(context.Background(), "id", "secret"); !errors.Is(err, fail) {
|
|
t.Fatalf("app token request marshal error = %v", err)
|
|
}
|
|
appTokenMarshal = oldMarshal
|
|
appTokenNewRequest = func(context.Context, string, string, io.Reader) (*http.Request, error) { return nil, fail }
|
|
if _, _, err := FetchAppToken(context.Background(), "id", "secret"); !errors.Is(err, fail) {
|
|
t.Fatalf("app token request creation error = %v", err)
|
|
}
|
|
appTokenNewRequest = oldRequest
|
|
|
|
appTokenKeychainGet = func(string, string) (string, error) { return "{", nil }
|
|
appTokenHTTPClient = &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
|
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"accessToken":"fresh","expireIn":7200}`)), Header: make(http.Header)}, nil
|
|
})}
|
|
appTokenKeychainSet = func(string, string, string) error { return fail }
|
|
if got, err := (&AppTokenProvider{AppKey: "id", AppSecret: "secret"}).GetToken(context.Background()); err != nil || got != "fresh" {
|
|
t.Fatalf("uncached app token with save failure = %q %v", got, err)
|
|
}
|
|
})
|
|
|
|
t.Run("defaults and strict keychain", func(t *testing.T) {
|
|
oldID := defaultAuthClientID
|
|
oldSecret := defaultAuthSecret
|
|
oldEdition := edition.Get()
|
|
t.Cleanup(func() {
|
|
defaultAuthClientID = oldID
|
|
defaultAuthSecret = oldSecret
|
|
edition.Override(oldEdition)
|
|
SetClientID("")
|
|
SetClientSecret("")
|
|
resetAppConfigCache()
|
|
})
|
|
dir := t.TempDir()
|
|
t.Setenv("DWS_CONFIG_DIR", dir)
|
|
t.Setenv("DWS_CLIENT_ID", "")
|
|
t.Setenv("DWS_CLIENT_SECRET", "")
|
|
SetClientID("")
|
|
SetClientSecret("")
|
|
resetAppConfigCache()
|
|
defaultAuthClientID = "built-in-id"
|
|
defaultAuthSecret = "built-in-secret"
|
|
if ClientID() != "built-in-id" || ClientSecret() != "built-in-secret" {
|
|
t.Fatal("built-in credential fallback failed")
|
|
}
|
|
|
|
if err := SaveAppConfig(dir, &AppConfig{ClientID: "saved-id", ClientSecret: PlainSecret("saved-secret")}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resetAppConfigCache()
|
|
if got := ClientSecret(); got != "saved-secret" {
|
|
t.Fatalf("persisted client secret = %q", got)
|
|
}
|
|
|
|
account := secretAccountKey("strict-id")
|
|
if err := keychain.Set(keychain.Service, account, "strict-secret"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
config := `{"clientId":"strict-id","clientSecret":{"source":"keychain","id":` + fmt.Sprintf("%q", account) + `}}`
|
|
if err := os.WriteFile(GetAppConfigPath(dir), []byte(config), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, secret, _, source, err := ResolveAppCredentialsStrict(dir); err != nil || secret != "strict-secret" || source != CredentialSourceKeychain {
|
|
t.Fatalf("strict keychain credentials = %q %q %v", secret, source, err)
|
|
}
|
|
})
|
|
|
|
t.Run("secret hooks", func(t *testing.T) {
|
|
oldGet := secretKeychainGet
|
|
oldSet := secretKeychainSet
|
|
t.Cleanup(func() { secretKeychainGet = oldGet; secretKeychainSet = oldSet })
|
|
var input SecretInput
|
|
if err := input.UnmarshalJSON([]byte("true")); err == nil {
|
|
t.Fatal("invalid secret JSON succeeded")
|
|
}
|
|
fail := errors.New("fail")
|
|
secretKeychainGet = func(string, string) (string, error) { return "", fail }
|
|
if _, err := ResolveSecret(SecretInput{Ref: &SecretRef{Source: "keychain", ID: "id"}}); !errors.Is(err, fail) {
|
|
t.Fatalf("secret keychain get error = %v", err)
|
|
}
|
|
secretKeychainSet = func(string, string, string) error { return fail }
|
|
if _, err := StoreSecret("id", PlainSecret("secret")); !errors.Is(err, fail) {
|
|
t.Fatalf("secret keychain set error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("portable export failures", func(t *testing.T) {
|
|
oldGOOS := portableRuntimeGOOS
|
|
oldGlob := portableGlob
|
|
oldMarshal := portableJSONMarshal
|
|
oldWalk := portableWalkDir
|
|
oldOpen := portableOpen
|
|
oldInRoot := portablePathInRoot
|
|
t.Cleanup(func() {
|
|
portableRuntimeGOOS = oldGOOS
|
|
portableGlob = oldGlob
|
|
portableJSONMarshal = oldMarshal
|
|
portableWalkDir = oldWalk
|
|
portableOpen = oldOpen
|
|
portablePathInRoot = oldInRoot
|
|
})
|
|
portableRuntimeGOOS = func() string { return "linux" }
|
|
keyRoot := t.TempDir()
|
|
t.Setenv(keychain.StorageDirEnv, keyRoot)
|
|
keyDir := keychain.StorageDir(keychain.Service)
|
|
if err := os.MkdirAll(keyDir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(keyDir, keychain.AccountToken+".enc"), []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
configDir := t.TempDir()
|
|
portableGlob = func(string) ([]string, error) { return nil, errors.New("glob") }
|
|
if err := ExportPortableAuthBundle(configDir, io.Discard); err == nil {
|
|
t.Fatal("portable config scan failure succeeded")
|
|
}
|
|
portableGlob = oldGlob
|
|
portableJSONMarshal = func(any, string, string) ([]byte, error) { return nil, errors.New("marshal") }
|
|
if err := ExportPortableAuthBundle(configDir, io.Discard); err == nil {
|
|
t.Fatal("portable manifest failure succeeded")
|
|
}
|
|
portableJSONMarshal = oldMarshal
|
|
portableWalkDir = func(string, fs.WalkDirFunc) error { return errors.New("walk") }
|
|
if err := ExportPortableAuthBundle(configDir, io.Discard); err == nil {
|
|
t.Fatal("portable keychain walk failure succeeded")
|
|
}
|
|
portableWalkDir = oldWalk
|
|
configFile := filepath.Join(configDir, "app.json")
|
|
if err := os.WriteFile(configFile, []byte("{}"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
portableOpen = func(name string) (io.ReadCloser, error) {
|
|
if name == configFile {
|
|
return nil, errors.New("open")
|
|
}
|
|
return oldOpen(name)
|
|
}
|
|
if err := ExportPortableAuthBundle(configDir, io.Discard); err == nil {
|
|
t.Fatal("portable config add failure succeeded")
|
|
}
|
|
portableOpen = oldOpen
|
|
|
|
portablePathInRoot = func(string, string) bool { return false }
|
|
var buf bytes.Buffer
|
|
gz := gzip.NewWriter(&buf)
|
|
tw := tar.NewWriter(gz)
|
|
_ = tw.WriteHeader(&tar.Header{Name: "config/value", Typeflag: tar.TypeReg, Size: 1})
|
|
_, _ = tw.Write([]byte("x"))
|
|
_ = tw.Close()
|
|
_ = gz.Close()
|
|
if _, err := ImportPortableAuthBundle(t.TempDir(), bytes.NewReader(buf.Bytes())); err == nil {
|
|
t.Fatal("portable safe-join failure succeeded")
|
|
}
|
|
})
|
|
|
|
if tokenLogoutHTTPClient.CheckRedirect == nil {
|
|
t.Fatal("logout redirect guard is missing")
|
|
}
|
|
if err := tokenLogoutHTTPClient.CheckRedirect(nil, nil); !errors.Is(err, http.ErrUseLastResponse) {
|
|
t.Fatalf("logout redirect guard = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageProfilesCoverageEdges(t *testing.T) {
|
|
oldAcquire := profilesAcquireDualLock
|
|
oldRead := profilesReadFile
|
|
oldRename := profilesRename
|
|
oldMkdir := profilesMkdirAll
|
|
oldMarshal := profilesMarshalIndent
|
|
oldWrite := profilesWriteFile
|
|
oldRemove := profilesRemove
|
|
oldLoad := profilesLoad
|
|
oldSave := profilesSave
|
|
oldEnsure := profilesEnsureMigration
|
|
oldSync := profilesSyncLegacyMirror
|
|
oldExists := profilesTokenExists
|
|
oldLoadLegacy := profilesLoadLegacy
|
|
oldSaveCorp := profilesSaveCorp
|
|
oldExistsCorp := profilesTokenExistsCorp
|
|
oldLoadCorp := profilesLoadCorp
|
|
oldSaveLegacy := profilesSaveLegacy
|
|
oldMarker := profilesWriteMarker
|
|
oldDeleteLegacy := profilesDeleteLegacy
|
|
oldDeleteMarker := profilesDeleteMarker
|
|
t.Cleanup(func() {
|
|
profilesAcquireDualLock = oldAcquire
|
|
profilesReadFile = oldRead
|
|
profilesRename = oldRename
|
|
profilesMkdirAll = oldMkdir
|
|
profilesMarshalIndent = oldMarshal
|
|
profilesWriteFile = oldWrite
|
|
profilesRemove = oldRemove
|
|
profilesLoad = oldLoad
|
|
profilesSave = oldSave
|
|
profilesEnsureMigration = oldEnsure
|
|
profilesSyncLegacyMirror = oldSync
|
|
profilesTokenExists = oldExists
|
|
profilesLoadLegacy = oldLoadLegacy
|
|
profilesSaveCorp = oldSaveCorp
|
|
profilesTokenExistsCorp = oldExistsCorp
|
|
profilesLoadCorp = oldLoadCorp
|
|
profilesSaveLegacy = oldSaveLegacy
|
|
profilesWriteMarker = oldMarker
|
|
profilesDeleteLegacy = oldDeleteLegacy
|
|
profilesDeleteMarker = oldDeleteMarker
|
|
})
|
|
fail := errors.New("fail")
|
|
|
|
profilesAcquireDualLock = func(context.Context, string) (*DualLock, error) { return nil, fail }
|
|
if err := withProfilesLock(t.TempDir(), func() error { return nil }); !errors.Is(err, fail) {
|
|
t.Fatalf("profiles lock error = %v", err)
|
|
}
|
|
profilesAcquireDualLock = oldAcquire
|
|
|
|
profilesReadFile = func(string) ([]byte, error) { return nil, fail }
|
|
if _, err := LoadProfiles(t.TempDir()); !errors.Is(err, fail) {
|
|
t.Fatalf("profiles read error = %v", err)
|
|
}
|
|
profilesReadFile = oldRead
|
|
|
|
dir := t.TempDir()
|
|
profilesMkdirAll = func(string, os.FileMode) error { return fail }
|
|
if err := SaveProfiles(dir, &ProfilesConfig{}); !errors.Is(err, fail) {
|
|
t.Fatalf("profiles mkdir error = %v", err)
|
|
}
|
|
profilesMkdirAll = oldMkdir
|
|
profilesMarshalIndent = func(any, string, string) ([]byte, error) { return nil, fail }
|
|
if err := SaveProfiles(dir, &ProfilesConfig{}); !errors.Is(err, fail) {
|
|
t.Fatalf("profiles marshal error = %v", err)
|
|
}
|
|
profilesMarshalIndent = oldMarshal
|
|
profilesWriteFile = func(string, []byte, os.FileMode) error { return fail }
|
|
if err := SaveProfiles(dir, &ProfilesConfig{}); !errors.Is(err, fail) {
|
|
t.Fatalf("profiles write error = %v", err)
|
|
}
|
|
profilesWriteFile = oldWrite
|
|
profilesRename = func(string, string) error { return fail }
|
|
profilesRemove = func(string) error { return fail }
|
|
if err := SaveProfiles(dir, &ProfilesConfig{}); !errors.Is(err, fail) {
|
|
t.Fatalf("profiles rename error = %v", err)
|
|
}
|
|
profilesRename = oldRename
|
|
profilesRemove = oldRemove
|
|
|
|
profilesLoad = func(string) (*ProfilesConfig, error) { return nil, fail }
|
|
if err := ensureProfilesMigrationLocked(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("migration profile load error = %v", err)
|
|
}
|
|
if err := upsertProfileFromTokenWithCurrentLocked(dir, &TokenData{}, true); !errors.Is(err, fail) {
|
|
t.Fatalf("upsert profile load error = %v", err)
|
|
}
|
|
profilesLoad = func(string) (*ProfilesConfig, error) { return &ProfilesConfig{}, nil }
|
|
profilesTokenExists = func() bool { return true }
|
|
for name, token := range map[string]*TokenData{"nil": nil, "empty corp": {}} {
|
|
t.Run("migration "+name, func(t *testing.T) {
|
|
profilesLoadLegacy = func() (*TokenData, error) { return token, nil }
|
|
if err := ensureProfilesMigrationLocked(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
}
|
|
profilesLoadLegacy = func() (*TokenData, error) { return nil, fail }
|
|
if err := ensureProfilesMigrationLocked(dir); err != nil {
|
|
t.Fatal("legacy token read errors should be ignored")
|
|
}
|
|
legacy := &TokenData{CorpID: "corp"}
|
|
profilesLoadLegacy = func() (*TokenData, error) { return legacy, nil }
|
|
profilesSaveCorp = func(string, *TokenData) error { return fail }
|
|
if err := ensureProfilesMigrationLocked(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("migration corp save error = %v", err)
|
|
}
|
|
profilesSaveCorp = oldSaveCorp
|
|
profilesSave = func(string, *ProfilesConfig) error { return fail }
|
|
if err := ensureProfilesMigrationLocked(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("migration profile save error = %v", err)
|
|
}
|
|
|
|
cfg := &ProfilesConfig{
|
|
PrimaryProfile: "primary", CurrentProfile: "current", PreviousProfile: "previous",
|
|
Profiles: []Profile{{Name: "primary", CorpID: "primary"}, {Name: "current", CorpID: "current"}, {Name: "previous", CorpID: "previous"}},
|
|
}
|
|
profilesEnsureMigration = func(string) error { return fail }
|
|
if _, err := ResolveProfile(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("resolve migration error = %v", err)
|
|
}
|
|
if _, err := resolveProfileForLoad(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("load resolution migration error = %v", err)
|
|
}
|
|
if _, err := setCurrentProfileLocked(dir, "current"); !errors.Is(err, fail) {
|
|
t.Fatalf("set-current migration error = %v", err)
|
|
}
|
|
if _, err := usePreviousProfileLocked(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("previous migration error = %v", err)
|
|
}
|
|
profilesEnsureMigration = func(string) error { return nil }
|
|
profilesLoad = func(string) (*ProfilesConfig, error) { return nil, fail }
|
|
if _, err := ResolveProfile(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("resolve load error = %v", err)
|
|
}
|
|
if _, err := resolveProfileForLoad(dir, ""); !errors.Is(err, fail) {
|
|
t.Fatalf("profile-for-load read error = %v", err)
|
|
}
|
|
if _, err := setCurrentProfileLocked(dir, "current"); !errors.Is(err, fail) {
|
|
t.Fatalf("set-current read error = %v", err)
|
|
}
|
|
if _, err := usePreviousProfileLocked(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("previous read error = %v", err)
|
|
}
|
|
if _, err := removeProfileLocked(dir, "current"); !errors.Is(err, fail) {
|
|
t.Fatalf("remove read error = %v", err)
|
|
}
|
|
if err := markProfileStatusLocked(dir, "current", "expired"); !errors.Is(err, fail) {
|
|
t.Fatalf("mark status read error = %v", err)
|
|
}
|
|
if err := syncLegacyTokenMirrorLocked(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("mirror read error = %v", err)
|
|
}
|
|
profilesLoad = func(string) (*ProfilesConfig, error) {
|
|
return &ProfilesConfig{
|
|
CurrentProfile: "current",
|
|
PreviousProfile: "missing",
|
|
Profiles: []Profile{{CorpID: "current"}},
|
|
}, nil
|
|
}
|
|
if _, err := usePreviousProfileLocked(dir); err == nil || !strings.Contains(err.Error(), "not found") {
|
|
t.Fatalf("missing previous profile error = %v", err)
|
|
}
|
|
|
|
profilesLoad = func(string) (*ProfilesConfig, error) {
|
|
clone := *cfg
|
|
clone.Profiles = append([]Profile(nil), cfg.Profiles...)
|
|
return &clone, nil
|
|
}
|
|
profilesSave = func(string, *ProfilesConfig) error { return nil }
|
|
if got, err := ResolveProfile(dir, ""); err != nil || got == nil || got.CorpID != "current" {
|
|
t.Fatalf("current profile resolution = %#v %v", got, err)
|
|
}
|
|
primaryOnly := &ProfilesConfig{PrimaryProfile: "primary", Profiles: []Profile{{Name: "primary", CorpID: "primary"}}}
|
|
profilesLoad = func(string) (*ProfilesConfig, error) { return primaryOnly, nil }
|
|
if got, err := ResolveProfile(dir, ""); err != nil || got != nil {
|
|
t.Fatalf("primary-only profile resolution = %#v %v", got, err)
|
|
}
|
|
profilesLoad = func(string) (*ProfilesConfig, error) { return &ProfilesConfig{}, nil }
|
|
if got, err := ResolveProfile(dir, ""); err != nil || got != nil {
|
|
t.Fatalf("empty profile resolution = %#v %v", got, err)
|
|
}
|
|
|
|
profilesLoad = func(string) (*ProfilesConfig, error) { return cfg, nil }
|
|
profilesTokenExistsCorp = func(string) bool { return false }
|
|
if got, err := resolveProfileForLoad(dir, ""); err != nil || got == nil || got.CorpID != "current" {
|
|
t.Fatalf("profile-for-load current fallback = %#v %v", got, err)
|
|
}
|
|
cfgNoCurrent := &ProfilesConfig{PrimaryProfile: "primary", Profiles: []Profile{{CorpID: "primary"}}}
|
|
profilesLoad = func(string) (*ProfilesConfig, error) { return cfgNoCurrent, nil }
|
|
if got, err := resolveProfileForLoad(dir, ""); err != nil || got != nil {
|
|
t.Fatalf("profile-for-load primary-only result = %#v %v", got, err)
|
|
}
|
|
|
|
profilesLoad = func(string) (*ProfilesConfig, error) { return cfg, nil }
|
|
profilesSave = func(string, *ProfilesConfig) error { return fail }
|
|
if _, err := setCurrentProfileLocked(dir, "current"); !errors.Is(err, fail) {
|
|
t.Fatalf("set-current save error = %v", err)
|
|
}
|
|
if _, err := usePreviousProfileLocked(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("previous save error = %v", err)
|
|
}
|
|
if _, err := removeProfileLocked(dir, "current"); !errors.Is(err, fail) {
|
|
t.Fatalf("remove save error = %v", err)
|
|
}
|
|
cfg = &ProfilesConfig{
|
|
PrimaryProfile: "primary", CurrentProfile: "current", PreviousProfile: "previous",
|
|
Profiles: []Profile{{Name: "primary", CorpID: "primary"}, {Name: "current", CorpID: "current"}, {Name: "previous", CorpID: "previous"}},
|
|
}
|
|
profilesSave = func(string, *ProfilesConfig) error { return nil }
|
|
profilesLoadCorp = func(corpID string) (*TokenData, error) {
|
|
return &TokenData{CorpID: corpID, AccessToken: "x"}, nil
|
|
}
|
|
profilesSaveCorp = func(string, *TokenData) error { return nil }
|
|
profilesSyncLegacyMirror = func(string) error { return fail }
|
|
if _, err := setCurrentProfileLocked(dir, "current"); !errors.Is(err, fail) {
|
|
t.Fatalf("set-current mirror error = %v", err)
|
|
}
|
|
if _, err := usePreviousProfileLocked(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("previous mirror error = %v", err)
|
|
}
|
|
|
|
profilesLoad = func(string) (*ProfilesConfig, error) { return cfg, nil }
|
|
profilesLoadCorp = func(string) (*TokenData, error) { return &TokenData{AccessToken: "x"}, nil }
|
|
profilesSaveLegacy = func(*TokenData) error { return fail }
|
|
if err := syncLegacyTokenMirrorLocked(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("mirror save error = %v", err)
|
|
}
|
|
profilesSaveLegacy = func(*TokenData) error { return nil }
|
|
profilesWriteMarker = func(string) error { return fail }
|
|
if err := syncLegacyTokenMirrorLocked(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("mirror marker error = %v", err)
|
|
}
|
|
profilesLoadCorp = func(string) (*TokenData, error) { return nil, fail }
|
|
if err := syncLegacyTokenMirrorLocked(dir); err != nil {
|
|
t.Fatal("transient mirror read error should be ignored")
|
|
}
|
|
profilesLoadCorp = func(string) (*TokenData, error) { return nil, nil }
|
|
deletedLegacy, deletedMarker := false, false
|
|
profilesDeleteLegacy = func() error { deletedLegacy = true; return fail }
|
|
profilesDeleteMarker = func(string) error { deletedMarker = true; return fail }
|
|
if err := syncLegacyTokenMirrorLocked(dir); !errors.Is(err, fail) || !deletedLegacy || deletedMarker {
|
|
t.Fatalf("empty mirror cleanup = %v %v %v", err, deletedLegacy, deletedMarker)
|
|
}
|
|
profilesDeleteLegacy = func() error { return nil }
|
|
if err := syncLegacyTokenMirrorLocked(dir); !errors.Is(err, fail) || !deletedMarker {
|
|
t.Fatalf("empty mirror marker cleanup = %v %v", err, deletedMarker)
|
|
}
|
|
|
|
normalizeProfilesConfig(nil)
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSecureIdentityKeychainAndLockEdges(t *testing.T) {
|
|
t.Run("secure store", func(t *testing.T) {
|
|
oldGetMAC := secureGetMAC
|
|
oldMkdir := secureMkdirAll
|
|
oldStat := secureStat
|
|
oldChmod := secureChmod
|
|
oldMarshal := secureMarshalIndent
|
|
oldEncrypt := secureEncrypt
|
|
oldCreateTemp := secureCreateTemp
|
|
oldRemove := secureRemove
|
|
oldRename := secureRename
|
|
oldRead := secureReadFile
|
|
oldDecrypt := secureDecrypt
|
|
oldUnmarshal := secureUnmarshal
|
|
t.Cleanup(func() {
|
|
secureGetMAC = oldGetMAC
|
|
secureMkdirAll = oldMkdir
|
|
secureStat = oldStat
|
|
secureChmod = oldChmod
|
|
secureMarshalIndent = oldMarshal
|
|
secureEncrypt = oldEncrypt
|
|
secureCreateTemp = oldCreateTemp
|
|
secureRemove = oldRemove
|
|
secureRename = oldRename
|
|
secureReadFile = oldRead
|
|
secureDecrypt = oldDecrypt
|
|
secureUnmarshal = oldUnmarshal
|
|
cachedMACOnce = sync.Once{}
|
|
cachedMAC, cachedMACErr = "", nil
|
|
})
|
|
resetMAC := func() {
|
|
cachedMACOnce = sync.Once{}
|
|
cachedMAC, cachedMACErr = "", nil
|
|
}
|
|
fail := errors.New("fail")
|
|
secureGetMAC = func() (string, error) { return "", fail }
|
|
resetMAC()
|
|
if _, err := resolvePassword(); !errors.Is(err, fail) {
|
|
t.Fatalf("password MAC error = %v", err)
|
|
}
|
|
if err := SaveSecureTokenData(t.TempDir(), &TokenData{}); !errors.Is(err, fail) {
|
|
t.Fatalf("secure save password error = %v", err)
|
|
}
|
|
resetMAC()
|
|
if _, err := LoadSecureTokenData(t.TempDir()); !errors.Is(err, fail) {
|
|
t.Fatalf("secure load password error = %v", err)
|
|
}
|
|
secureGetMAC = func() (string, error) { return "00:11:22:33:44:55", nil }
|
|
resetMAC()
|
|
dir := t.TempDir()
|
|
secureMkdirAll = func(string, os.FileMode) error { return fail }
|
|
if err := SaveSecureTokenData(dir, &TokenData{}); !errors.Is(err, fail) {
|
|
t.Fatalf("secure mkdir error = %v", err)
|
|
}
|
|
secureMkdirAll = oldMkdir
|
|
if err := os.Chmod(dir, 0o777); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secureChmod = func(string, os.FileMode) error { return fail }
|
|
if err := SaveSecureTokenData(dir, &TokenData{}); !errors.Is(err, fail) {
|
|
t.Fatalf("secure chmod error = %v", err)
|
|
}
|
|
secureChmod = oldChmod
|
|
if err := os.Chmod(dir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secureMarshalIndent = func(any, string, string) ([]byte, error) { return nil, fail }
|
|
if err := SaveSecureTokenData(dir, &TokenData{}); !errors.Is(err, fail) {
|
|
t.Fatalf("secure marshal error = %v", err)
|
|
}
|
|
secureMarshalIndent = oldMarshal
|
|
secureEncrypt = func([]byte, []byte) ([]byte, error) { return nil, fail }
|
|
if err := SaveSecureTokenData(dir, &TokenData{}); !errors.Is(err, fail) {
|
|
t.Fatalf("secure encrypt error = %v", err)
|
|
}
|
|
secureEncrypt = oldEncrypt
|
|
secureCreateTemp = func(string, string) (secureTempFile, error) { return nil, fail }
|
|
if err := SaveSecureTokenData(dir, &TokenData{}); !errors.Is(err, fail) {
|
|
t.Fatalf("secure temp open error = %v", err)
|
|
}
|
|
for name, configure := range map[string]func(*fakeSecureTemp){
|
|
"write": func(f *fakeSecureTemp) { f.writeErr = fail },
|
|
"sync": func(f *fakeSecureTemp) { f.syncErr = fail },
|
|
"close": func(f *fakeSecureTemp) { f.closeErr = fail },
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
f := &fakeSecureTemp{name: filepath.Join(dir, secureDataFile+".tmp-fake")}
|
|
configure(f)
|
|
secureCreateTemp = func(string, string) (secureTempFile, error) { return f, nil }
|
|
if err := SaveSecureTokenData(dir, &TokenData{}); !errors.Is(err, fail) {
|
|
t.Fatalf("secure %s error = %v", name, err)
|
|
}
|
|
})
|
|
}
|
|
secureCreateTemp = func(string, string) (secureTempFile, error) {
|
|
return &fakeSecureTemp{name: filepath.Join(dir, secureDataFile+".tmp-fake")}, nil
|
|
}
|
|
secureRename = func(string, string) error { return fail }
|
|
secureRemove = func(string) error { return fail }
|
|
if err := SaveSecureTokenData(dir, &TokenData{}); !errors.Is(err, fail) {
|
|
t.Fatalf("secure rename error = %v", err)
|
|
}
|
|
secureRename = oldRename
|
|
secureCreateTemp = oldCreateTemp
|
|
secureRemove = oldRemove
|
|
|
|
secureReadFile = func(string) ([]byte, error) { return nil, fail }
|
|
if _, err := LoadSecureTokenData(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("secure read error = %v", err)
|
|
}
|
|
secureReadFile = func(string) ([]byte, error) { return []byte("cipher"), nil }
|
|
secureDecrypt = func([]byte, []byte) ([]byte, error) { return nil, fail }
|
|
if _, err := LoadSecureTokenData(dir); !errors.Is(err, ErrTokenDecryption) {
|
|
t.Fatalf("secure decrypt error = %v", err)
|
|
}
|
|
secureDecrypt = func([]byte, []byte) ([]byte, error) { return []byte("{}"), nil }
|
|
secureUnmarshal = func([]byte, any) error { return fail }
|
|
if _, err := LoadSecureTokenData(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("secure unmarshal error = %v", err)
|
|
}
|
|
secureRemove = func(string) error { return fail }
|
|
if err := DeleteSecureData(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("secure delete error = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("keychain store", func(t *testing.T) {
|
|
oldMarshal := authKeychainMarshal
|
|
oldUnmarshal := authKeychainUnmarshal
|
|
oldSet := authKeychainSet
|
|
oldGet := authKeychainGet
|
|
oldMigrate := authKeychainMigrate
|
|
t.Cleanup(func() {
|
|
authKeychainMarshal = oldMarshal
|
|
authKeychainUnmarshal = oldUnmarshal
|
|
authKeychainSet = oldSet
|
|
authKeychainGet = oldGet
|
|
authKeychainMigrate = oldMigrate
|
|
migrationOnce = sync.Once{}
|
|
})
|
|
fail := errors.New("fail")
|
|
authKeychainMarshal = func(any, string, string) ([]byte, error) { return nil, fail }
|
|
if err := saveTokenDataKeychainAccount("account", &TokenData{}); !errors.Is(err, fail) {
|
|
t.Fatalf("keychain marshal error = %v", err)
|
|
}
|
|
authKeychainMarshal = oldMarshal
|
|
authKeychainSet = func(string, string, string) error { return fail }
|
|
if err := saveTokenDataKeychainAccount("account", &TokenData{}); !errors.Is(err, fail) {
|
|
t.Fatalf("keychain set error = %v", err)
|
|
}
|
|
if err := SaveClientSecret("id", "secret"); !errors.Is(err, fail) {
|
|
t.Fatalf("client-secret set error = %v", err)
|
|
}
|
|
authKeychainSet = oldSet
|
|
authKeychainGet = func(string, string) (string, error) { return "", fail }
|
|
if _, err := loadTokenDataKeychainAccount("account"); !errors.Is(err, fail) {
|
|
t.Fatalf("keychain get error = %v", err)
|
|
}
|
|
if LoadClientSecret("id") != "" {
|
|
t.Fatal("client-secret get error returned a value")
|
|
}
|
|
authKeychainGet = func(string, string) (string, error) { return "{}", nil }
|
|
authKeychainUnmarshal = func([]byte, any) error { return fail }
|
|
if _, err := loadTokenDataKeychainAccount("account"); !errors.Is(err, fail) {
|
|
t.Fatalf("keychain unmarshal error = %v", err)
|
|
}
|
|
|
|
for name, result := range map[string]*keychain.MigrationResult{
|
|
"migrated": {Migrated: true, FromPath: "from", BackupPath: "backup"},
|
|
"relogin": {NeedRelogin: true, Error: fail},
|
|
"error": {Error: fail},
|
|
} {
|
|
t.Run("migration "+name, func(t *testing.T) {
|
|
migrationOnce = sync.Once{}
|
|
authKeychainMigrate = func(string) *keychain.MigrationResult { return result }
|
|
EnsureMigration(t.TempDir(), slog.Default())
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("identity", func(t *testing.T) {
|
|
oldRead := identityReadFile
|
|
oldUnmarshal := identityUnmarshal
|
|
oldMkdir := identityMkdirAll
|
|
oldMarshal := identityMarshalIndent
|
|
oldWrite := identityWriteFile
|
|
oldRand := identityRandRead
|
|
oldEdition := edition.Get()
|
|
t.Cleanup(func() {
|
|
identityReadFile = oldRead
|
|
identityUnmarshal = oldUnmarshal
|
|
identityMkdirAll = oldMkdir
|
|
identityMarshalIndent = oldMarshal
|
|
identityWriteFile = oldWrite
|
|
identityRandRead = oldRand
|
|
edition.Override(oldEdition)
|
|
})
|
|
id := &Identity{MachineID: "machine"}
|
|
id.migrate()
|
|
if id.AgentID != "machine" || id.Source != "dws" || id.Agents == nil {
|
|
t.Fatalf("identity migration = %#v", id)
|
|
}
|
|
id.Agents = nil
|
|
if got := id.ResolveAgentID(t.TempDir(), "agent", "test"); got == "" || id.Agents == nil {
|
|
t.Fatal("identity agent map was not initialized")
|
|
}
|
|
edition.Override(&edition.Hooks{Name: "coverage", ScenarioCode: "custom-scenario"})
|
|
if id.Headers()["x-dingtalk-scenario-code"] != "custom-scenario" {
|
|
t.Fatal("identity scenario override failed")
|
|
}
|
|
fail := errors.New("fail")
|
|
identityMkdirAll = func(string, os.FileMode) error { return fail }
|
|
if err := save(t.TempDir(), id); !errors.Is(err, fail) {
|
|
t.Fatalf("identity mkdir error = %v", err)
|
|
}
|
|
identityMkdirAll = oldMkdir
|
|
identityMarshalIndent = func(any, string, string) ([]byte, error) { return nil, fail }
|
|
if err := save(t.TempDir(), id); !errors.Is(err, fail) {
|
|
t.Fatalf("identity marshal error = %v", err)
|
|
}
|
|
identityMarshalIndent = oldMarshal
|
|
identityWriteFile = func(string, []byte, os.FileMode) error { return fail }
|
|
if err := save(t.TempDir(), id); !errors.Is(err, fail) {
|
|
t.Fatalf("identity write error = %v", err)
|
|
}
|
|
if formatDerivedAgentID("x") != "dwsa_00000000000x" || base62Encode([]byte{0}) != "0" {
|
|
t.Fatal("identity short encoding failed")
|
|
}
|
|
identityRandRead = func([]byte) (int, error) { return 0, fail }
|
|
if generateUUID() != "00000000-0000-4000-8000-000000000000" {
|
|
t.Fatal("identity UUID fallback failed")
|
|
}
|
|
})
|
|
|
|
t.Run("file lock", func(t *testing.T) {
|
|
oldMkdir := fileLockMkdirAll
|
|
oldOpen := fileLockOpenFile
|
|
oldTry := fileLockTry
|
|
oldNow := fileLockNow
|
|
oldSleep := fileLockSleep
|
|
oldProcess := dualAcquireProcessLock
|
|
oldToken := dualAcquireTokenLock
|
|
t.Cleanup(func() {
|
|
fileLockMkdirAll = oldMkdir
|
|
fileLockOpenFile = oldOpen
|
|
fileLockTry = oldTry
|
|
fileLockNow = oldNow
|
|
fileLockSleep = oldSleep
|
|
dualAcquireProcessLock = oldProcess
|
|
dualAcquireTokenLock = oldToken
|
|
processLocks = sync.Map{}
|
|
})
|
|
fail := errors.New("fail")
|
|
key := processLockKey("bad-type")
|
|
processLocks.Store(key, "not-a-channel")
|
|
release, waited, err := acquireProcessLock(context.Background(), "bad-type")
|
|
if err != nil || waited {
|
|
t.Fatalf("bad process lock recovery = %v %v", waited, err)
|
|
}
|
|
release()
|
|
held, _, err := acquireProcessLock(context.Background(), "held")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
if _, waited, err := acquireProcessLock(ctx, "held"); err == nil || !waited {
|
|
t.Fatalf("canceled process wait = %v %v", waited, err)
|
|
}
|
|
held()
|
|
|
|
fileLockMkdirAll = func(string, os.FileMode) error { return fail }
|
|
if _, err := acquireTokenLock(t.TempDir()); !errors.Is(err, fail) {
|
|
t.Fatalf("token lock mkdir error = %v", err)
|
|
}
|
|
fileLockMkdirAll = oldMkdir
|
|
fileLockOpenFile = func(string, int, os.FileMode) (*os.File, error) { return nil, fail }
|
|
if _, err := acquireTokenLock(t.TempDir()); !errors.Is(err, fail) {
|
|
t.Fatalf("token lock open error = %v", err)
|
|
}
|
|
fileLockOpenFile = oldOpen
|
|
fileLockTry = func(*os.File) error { return fail }
|
|
base := time.Now()
|
|
calls := 0
|
|
fileLockNow = func() time.Time {
|
|
calls++
|
|
if calls < 3 {
|
|
return base
|
|
}
|
|
return base.Add(time.Hour)
|
|
}
|
|
slept := false
|
|
fileLockSleep = func(time.Duration) { slept = true }
|
|
if _, err := acquireTokenLock(t.TempDir()); err == nil || !slept {
|
|
t.Fatalf("token lock timeout = %v, slept=%v", err, slept)
|
|
}
|
|
|
|
dualAcquireProcessLock = func(context.Context, string) (func(), bool, error) { return nil, false, fail }
|
|
if _, err := AcquireDualLock(context.Background(), t.TempDir()); !errors.Is(err, fail) {
|
|
t.Fatalf("dual process lock error = %v", err)
|
|
}
|
|
released := false
|
|
dualAcquireProcessLock = func(context.Context, string) (func(), bool, error) {
|
|
return func() { released = true }, true, nil
|
|
}
|
|
dualAcquireTokenLock = func(string) (*tokenFileLock, error) { return nil, fail }
|
|
if _, err := AcquireDualLock(context.Background(), t.TempDir()); !errors.Is(err, fail) || !released {
|
|
t.Fatalf("dual file lock error = %v released=%v", err, released)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCrossPlatformCoverageAppConfigHookCoverageEdges(t *testing.T) {
|
|
oldStore := appConfigStoreSecret
|
|
oldMarshal := appConfigMarshalIndent
|
|
oldWrite := appConfigAtomicWrite
|
|
oldRead := appConfigReadFile
|
|
oldRemove := appConfigRemove
|
|
oldLoad := appConfigLoad
|
|
oldResolve := appConfigResolveSecret
|
|
oldBeforeLock := appConfigBeforeResolveLock
|
|
oldEdition := edition.Get()
|
|
t.Cleanup(func() {
|
|
appConfigStoreSecret = oldStore
|
|
appConfigMarshalIndent = oldMarshal
|
|
appConfigAtomicWrite = oldWrite
|
|
appConfigReadFile = oldRead
|
|
appConfigRemove = oldRemove
|
|
appConfigLoad = oldLoad
|
|
appConfigResolveSecret = oldResolve
|
|
appConfigBeforeResolveLock = oldBeforeLock
|
|
edition.Override(oldEdition)
|
|
resetAppConfigCache()
|
|
})
|
|
fail := errors.New("fail")
|
|
dir := t.TempDir()
|
|
appConfigStoreSecret = func(string, SecretInput) (SecretInput, error) { return SecretInput{}, fail }
|
|
if err := SaveAppConfig(dir, &AppConfig{ClientID: "id", ClientSecret: PlainSecret("secret")}); !errors.Is(err, fail) {
|
|
t.Fatalf("app config secret-store error = %v", err)
|
|
}
|
|
appConfigStoreSecret = oldStore
|
|
appConfigMarshalIndent = func(any, string, string) ([]byte, error) { return nil, fail }
|
|
if err := SaveAppConfig(dir, &AppConfig{ClientID: "id"}); !errors.Is(err, fail) {
|
|
t.Fatalf("app config marshal error = %v", err)
|
|
}
|
|
appConfigMarshalIndent = oldMarshal
|
|
appConfigAtomicWrite = func(string, []byte) error { return fail }
|
|
if err := SaveAppConfig(dir, &AppConfig{ClientID: "id"}); !errors.Is(err, fail) {
|
|
t.Fatalf("app config write error = %v", err)
|
|
}
|
|
appConfigAtomicWrite = oldWrite
|
|
|
|
cleanupLegacySiblingAppConfig(dir, nil)
|
|
edition.Override(&edition.Hooks{Name: "coverage"})
|
|
cleanupLegacySiblingAppConfig(dir, &AppConfig{ClientID: "id"})
|
|
legacyPath := filepath.Join(dir, appConfigFile)
|
|
if err := os.WriteFile(legacyPath, []byte("{"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cleanupLegacySiblingAppConfig(dir, &AppConfig{ClientID: "id"})
|
|
if err := os.WriteFile(legacyPath, []byte(`{"clientId":"other"}`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cleanupLegacySiblingAppConfig(dir, &AppConfig{ClientID: "id"})
|
|
if err := os.WriteFile(legacyPath, []byte(`{"clientId":"id"}`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
appConfigRemove = func(string) error { return fail }
|
|
cleanupLegacySiblingAppConfig(dir, &AppConfig{ClientID: "id"})
|
|
appConfigReadFile = func(string) ([]byte, error) { return nil, fail }
|
|
cleanupLegacySiblingAppConfig(dir, &AppConfig{ClientID: "id"})
|
|
appConfigReadFile = oldRead
|
|
|
|
appConfigRemove = func(string) error { return fail }
|
|
if err := DeleteAppConfig(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("app config delete error = %v", err)
|
|
}
|
|
appConfigRemove = oldRemove
|
|
appConfigLoad = func(string) (*AppConfig, error) { return nil, fail }
|
|
if _, err := ReloadAppConfig(dir); !errors.Is(err, fail) {
|
|
t.Fatalf("app config reload error = %v", err)
|
|
}
|
|
resetAppConfigCache()
|
|
appConfigLoad = oldLoad
|
|
if err := os.WriteFile(GetAppConfigPath(dir), []byte(`{"clientId":"id","clientSecret":"secret"}`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := ReloadAppConfig(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resetAppConfigCache()
|
|
appConfigResolveSecret = func(SecretInput) (string, error) { return "", fail }
|
|
if id, secret := ResolveAppCredentials(dir); id != "id" || secret != "" {
|
|
t.Fatalf("app config resolve failure = %q %q", id, secret)
|
|
}
|
|
resetAppConfigCache()
|
|
appConfigBeforeResolveLock = func() {
|
|
cachedResolvedMu.Lock()
|
|
cachedResolvedID = "raced-id"
|
|
cachedResolvedSecret = "raced-secret"
|
|
cachedResolvedValid = true
|
|
cachedResolvedMu.Unlock()
|
|
}
|
|
if id, secret := ResolveAppCredentials(dir); id != "raced-id" || secret != "raced-secret" {
|
|
t.Fatalf("app config double-check = %q %q", id, secret)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageDeviceFlowHighLevelCoverageEdges(t *testing.T) {
|
|
oldFetch := deviceFetchClientID
|
|
oldLogin := deviceLoginOnce
|
|
oldRequest := deviceRequestCode
|
|
oldWait := deviceWaitAuth
|
|
oldExchange := deviceExchangeCode
|
|
oldCheck := deviceCheckCLIAuth
|
|
oldAdmins := deviceGetAdmins
|
|
oldSaveToken := deviceSaveToken
|
|
oldHasApp := deviceHasAppConfig
|
|
oldSaveApp := deviceSaveAppConfig
|
|
oldPollStatus := devicePollStatus
|
|
oldPollToken := devicePollToken
|
|
oldAfter := deviceFlowAfter
|
|
oldBrowser := deviceOpenBrowser
|
|
t.Cleanup(func() {
|
|
deviceFetchClientID = oldFetch
|
|
deviceLoginOnce = oldLogin
|
|
deviceRequestCode = oldRequest
|
|
deviceWaitAuth = oldWait
|
|
deviceExchangeCode = oldExchange
|
|
deviceCheckCLIAuth = oldCheck
|
|
deviceGetAdmins = oldAdmins
|
|
deviceSaveToken = oldSaveToken
|
|
deviceHasAppConfig = oldHasApp
|
|
deviceSaveAppConfig = oldSaveApp
|
|
devicePollStatus = oldPollStatus
|
|
devicePollToken = oldPollToken
|
|
deviceFlowAfter = oldAfter
|
|
deviceOpenBrowser = oldBrowser
|
|
SetClientID("")
|
|
SetClientSecret("")
|
|
resetClientIDFromMCP()
|
|
})
|
|
fail := errors.New("fail")
|
|
p := &DeviceFlowProvider{configDir: t.TempDir(), clientID: "client", Output: io.Discard, logger: slog.Default(), NoBrowser: true}
|
|
|
|
SetClientID("runtime-id")
|
|
SetClientSecret("runtime-secret")
|
|
deviceLoginOnce = func(p *DeviceFlowProvider, _ context.Context, _ int) (*TokenData, error) {
|
|
if p.clientID != "runtime-id" {
|
|
t.Fatalf("runtime device client ID = %q", p.clientID)
|
|
}
|
|
return &TokenData{AccessToken: "access"}, nil
|
|
}
|
|
if _, err := p.Login(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
SetClientID("")
|
|
SetClientSecret("")
|
|
deviceFetchClientID = func(context.Context) (string, error) { return "", fail }
|
|
if _, err := p.Login(context.Background()); !errors.Is(err, fail) {
|
|
t.Fatalf("device client-ID fetch error = %v", err)
|
|
}
|
|
deviceFetchClientID = func(context.Context) (string, error) { return "mcp-id", nil }
|
|
deviceLoginOnce = func(*DeviceFlowProvider, context.Context, int) (*TokenData, error) { return nil, fail }
|
|
if _, err := p.Login(context.Background()); !errors.Is(err, fail) {
|
|
t.Fatalf("device login error = %v", err)
|
|
}
|
|
|
|
deviceRequestCode = func(*DeviceFlowProvider, context.Context) (*DeviceAuthResponse, error) { return nil, fail }
|
|
if _, err := p.loginOnce(context.Background(), 1); !errors.Is(err, fail) {
|
|
t.Fatalf("device-code request error = %v", err)
|
|
}
|
|
authResp := &DeviceAuthResponse{DeviceCode: "device", UserCode: "user", VerificationURIComplete: "https://example.test", Interval: 1, ExpiresIn: 10}
|
|
deviceRequestCode = func(*DeviceFlowProvider, context.Context) (*DeviceAuthResponse, error) { return authResp, nil }
|
|
deviceOpenBrowser = func(string) error { return fail }
|
|
p.NoBrowser = false
|
|
deviceWaitAuth = func(*DeviceFlowProvider, context.Context, *DeviceAuthResponse) (*DeviceTokenResponse, error) {
|
|
return nil, fail
|
|
}
|
|
if _, err := p.loginOnce(context.Background(), 1); !errors.Is(err, fail) {
|
|
t.Fatalf("device wait error = %v", err)
|
|
}
|
|
p.NoBrowser = true
|
|
deviceWaitAuth = func(*DeviceFlowProvider, context.Context, *DeviceAuthResponse) (*DeviceTokenResponse, error) {
|
|
return &DeviceTokenResponse{AuthCode: "code"}, nil
|
|
}
|
|
deviceExchangeCode = func(*OAuthProvider, context.Context, string) (*TokenData, error) { return nil, fail }
|
|
if _, err := p.loginOnce(context.Background(), 1); !errors.Is(err, fail) {
|
|
t.Fatalf("device exchange error = %v", err)
|
|
}
|
|
token := &TokenData{AccessToken: "access"}
|
|
deviceExchangeCode = func(*OAuthProvider, context.Context, string) (*TokenData, error) { return token, nil }
|
|
deviceCheckCLIAuth = func(*OAuthProvider, context.Context, string) (*CLIAuthStatus, error) { return nil, fail }
|
|
if _, err := p.loginOnce(context.Background(), 1); !errors.Is(err, fail) {
|
|
t.Fatalf("device CLI-auth check error = %v", err)
|
|
}
|
|
|
|
denials := []struct {
|
|
name string
|
|
channel string
|
|
status *CLIAuthStatus
|
|
}{
|
|
{"user forbidden", "", &CLIAuthStatus{Success: true, Result: &CLIAuthResult{UserScope: "forbidden"}}},
|
|
{"user excluded", "", &CLIAuthStatus{Success: true, Result: &CLIAuthResult{UserScope: "specified"}}},
|
|
{"channel excluded", "bad", &CLIAuthStatus{Success: true, Result: &CLIAuthResult{ChannelScope: "specified", AllowedChannels: []string{"good"}}}},
|
|
{"channel required", "", &CLIAuthStatus{Success: false, ErrorCode: "CHANNEL_REQUIRED"}},
|
|
{"enterprise default", "", &CLIAuthStatus{Success: false, ErrorCode: "ENTERPRISE_NOT_AUTHORIZED"}},
|
|
{"enterprise message", "", &CLIAuthStatus{Success: false, ErrorCode: "ENTERPRISE_NOT_AUTHORIZED", ErrorMsg: " custom "}},
|
|
{"no auth", "", &CLIAuthStatus{Success: false, ErrorCode: "NO_AUTH"}},
|
|
}
|
|
for _, tc := range denials {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Setenv("DWS_CHANNEL", tc.channel)
|
|
deviceCheckCLIAuth = func(*OAuthProvider, context.Context, string) (*CLIAuthStatus, error) { return tc.status, nil }
|
|
if _, err := p.loginOnce(context.Background(), 1); err == nil {
|
|
t.Fatal("denied device login succeeded")
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Setenv("DWS_CHANNEL", "")
|
|
deviceCheckCLIAuth = func(*OAuthProvider, context.Context, string) (*CLIAuthStatus, error) {
|
|
return &CLIAuthStatus{Success: true, Result: &CLIAuthResult{}}, nil
|
|
}
|
|
deviceGetAdmins = func(context.Context, string) (*SuperAdminResponse, error) {
|
|
return &SuperAdminResponse{Success: true, Result: []SuperAdmin{{Name: "Admin"}}}, nil
|
|
}
|
|
if _, err := p.loginOnce(context.Background(), 1); err == nil {
|
|
t.Fatal("disabled CLI auth device login succeeded")
|
|
}
|
|
deviceCheckCLIAuth = func(*OAuthProvider, context.Context, string) (*CLIAuthStatus, error) {
|
|
return &CLIAuthStatus{Success: true, Result: &CLIAuthResult{CLIAuthEnabled: true}}, nil
|
|
}
|
|
deviceSaveToken = func(string, *TokenData) error { return fail }
|
|
if _, err := p.loginOnce(context.Background(), 1); !errors.Is(err, fail) {
|
|
t.Fatalf("device token save error = %v", err)
|
|
}
|
|
deviceSaveToken = func(string, *TokenData) error { return nil }
|
|
deviceHasAppConfig = func(string) bool { return false }
|
|
appSaved := false
|
|
deviceSaveAppConfig = func(string, *AppConfig) error { appSaved = true; return fail }
|
|
if _, err := p.loginOnce(context.Background(), 1); err != nil || !appSaved {
|
|
t.Fatalf("successful device login = %v appSaved=%v", err, appSaved)
|
|
}
|
|
|
|
deviceFlowAfter = func(time.Duration) <-chan time.Time {
|
|
ch := make(chan time.Time, 1)
|
|
ch <- time.Now()
|
|
return ch
|
|
}
|
|
flowAuth := &DeviceAuthResponse{FlowID: "flow", Interval: 1, ExpiresIn: 30}
|
|
flowCases := []struct {
|
|
name string
|
|
responses []*DevicePollResponse
|
|
errors []error
|
|
wantErr bool
|
|
}{
|
|
{"network then approved", []*DevicePollResponse{nil, {Success: true, Data: DevicePollData{Status: StatusApproved, AuthCode: "code"}}}, []error{fail, nil}, false},
|
|
{"pending then approved", []*DevicePollResponse{{Success: true, Data: DevicePollData{Status: StatusPending}}, {Success: true, Data: DevicePollData{Status: StatusApproved}}}, nil, false},
|
|
{"rejected", []*DevicePollResponse{{Success: true, Data: DevicePollData{Status: StatusRejected}}}, nil, true},
|
|
{"expired", []*DevicePollResponse{{Success: true, Data: DevicePollData{Status: StatusExpired}}}, nil, true},
|
|
{"unknown then approved", []*DevicePollResponse{{Success: true, Data: DevicePollData{Status: "FUTURE"}}, {Success: true, Data: DevicePollData{Status: StatusApproved}}}, nil, false},
|
|
}
|
|
for _, tc := range flowCases {
|
|
t.Run("flow "+tc.name, func(t *testing.T) {
|
|
call := 0
|
|
devicePollStatus = func(*DeviceFlowProvider, context.Context, string) (*DevicePollResponse, error) {
|
|
i := call
|
|
call++
|
|
if i < len(tc.errors) && tc.errors[i] != nil {
|
|
return nil, tc.errors[i]
|
|
}
|
|
return tc.responses[i], nil
|
|
}
|
|
_, err := p.waitForAuthorizationByFlowID(context.Background(), flowAuth)
|
|
if (err != nil) != tc.wantErr {
|
|
t.Fatalf("flow wait error = %v want %v", err, tc.wantErr)
|
|
}
|
|
})
|
|
}
|
|
deviceAuth := &DeviceAuthResponse{DeviceCode: "device", Interval: 30, ExpiresIn: 30}
|
|
deviceCases := []struct {
|
|
name string
|
|
responses []*DeviceTokenResponse
|
|
errors []error
|
|
wantErr bool
|
|
}{
|
|
{"network then success", []*DeviceTokenResponse{nil, {AuthCode: "code"}}, []error{fail, nil}, false},
|
|
{"pending then success", []*DeviceTokenResponse{{Error: "authorization_pending"}, {AuthCode: "code"}}, nil, false},
|
|
{"slow then success", []*DeviceTokenResponse{{Error: "slow_down"}, {AuthCode: "code"}}, nil, false},
|
|
{"denied", []*DeviceTokenResponse{{Error: "access_denied"}}, nil, true},
|
|
{"expired", []*DeviceTokenResponse{{Error: "expired_token"}}, nil, true},
|
|
{"unknown then success", []*DeviceTokenResponse{{Error: "future"}, {AuthCode: "code"}}, nil, false},
|
|
}
|
|
for _, tc := range deviceCases {
|
|
t.Run("device "+tc.name, func(t *testing.T) {
|
|
call := 0
|
|
devicePollToken = func(*DeviceFlowProvider, context.Context, string) (*DeviceTokenResponse, error) {
|
|
i := call
|
|
call++
|
|
if i < len(tc.errors) && tc.errors[i] != nil {
|
|
return nil, tc.errors[i]
|
|
}
|
|
return tc.responses[i], nil
|
|
}
|
|
_, err := p.waitForAuthorizationByDeviceCode(context.Background(), deviceAuth)
|
|
if (err != nil) != tc.wantErr {
|
|
t.Fatalf("device wait error = %v want %v", err, tc.wantErr)
|
|
}
|
|
})
|
|
}
|
|
|
|
statusClient := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
|
return &http.Response{StatusCode: http.StatusBadGateway, Body: io.NopCloser(strings.NewReader("down")), Header: make(http.Header)}, nil
|
|
})}
|
|
p.httpClient = statusClient
|
|
p.baseURL = "https://example.test"
|
|
p.terminalBaseURL = "https://example.test"
|
|
if _, err := p.requestDeviceCode(context.Background()); err == nil {
|
|
t.Fatal("device-code HTTP error succeeded")
|
|
}
|
|
if _, err := p.pollDeviceToken(context.Background(), "device"); err == nil {
|
|
t.Fatal("device-token HTTP error succeeded")
|
|
}
|
|
if _, err := p.pollDeviceStatus(context.Background(), "flow"); err == nil {
|
|
t.Fatal("device-status HTTP error succeeded")
|
|
}
|
|
if _, err := p.postForm(context.Background(), "https://example.test", url.Values{}); err == nil {
|
|
t.Fatal("form HTTP status error succeeded")
|
|
}
|
|
if _, err := p.doGet(context.Background(), "https://example.test"); err == nil {
|
|
t.Fatal("GET HTTP status error succeeded")
|
|
}
|
|
dfPrintBox(io.Discard, []string{"short"})
|
|
}
|
|
|
|
func TestCrossPlatformCoverageMultiAccountSelectorAndIdentityLoadEdges(t *testing.T) {
|
|
cfg := &ProfilesConfig{
|
|
Profiles: []Profile{
|
|
{Name: "local-a", CorpID: "corp-a", CorpName: "Shared", UserID: "u1", UserName: "Alice"},
|
|
{Name: "duplicate", CorpID: "corp-a", CorpName: "Shared", UserID: "u2", UserName: "Alice"},
|
|
{Name: "duplicate", CorpID: "corp-b", CorpName: "Shared", UserID: "u3", UserName: "Bob"},
|
|
{Name: "solo", CorpID: "corp-c", CorpName: "Solo", UserID: "u4", UserName: "Carol"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
selector string
|
|
wantErr bool
|
|
}{
|
|
{"", true},
|
|
{"missing:u1", true},
|
|
{"Shared:u1", true},
|
|
{"corp-a:u1", false},
|
|
{"corp-a:Alice", true},
|
|
{"corp-a:missing", true},
|
|
{"corp-a", true},
|
|
{"corp-c", false},
|
|
{"Solo", false},
|
|
{"local-a", false},
|
|
{"duplicate", true},
|
|
{"missing", true},
|
|
} {
|
|
_, _, err := resolveProfileSelection("", cfg, tc.selector)
|
|
if (err != nil) != tc.wantErr {
|
|
t.Fatalf("resolveProfileSelection(%q) error = %v", tc.selector, err)
|
|
}
|
|
}
|
|
if _, _, err := resolveProfileSelection("", nil, "x"); err == nil {
|
|
t.Fatal("nil profile config selection succeeded")
|
|
}
|
|
cfg.OrgCurrentProfiles = map[string]string{"corp-a": "corp-a:u2"}
|
|
if got, _, err := resolveProfileSelection("", cfg, "corp-a"); err != nil || got.UserID != "u2" {
|
|
t.Fatalf("organization current selection = %#v %v", got, err)
|
|
}
|
|
|
|
for _, tc := range []struct {
|
|
selector string
|
|
wantErr bool
|
|
exact bool
|
|
}{
|
|
{"", true, false},
|
|
{"corp-a:u1", false, true},
|
|
{"corp-a", false, false},
|
|
{"Solo", false, false},
|
|
{"local-a", false, true},
|
|
{"duplicate", true, true},
|
|
{"missing", true, false},
|
|
} {
|
|
_, exact, err := resolveProfileDeletionSelection(cfg, tc.selector)
|
|
if (err != nil) != tc.wantErr || (!tc.wantErr && exact != tc.exact) {
|
|
t.Fatalf("resolveProfileDeletionSelection(%q) = exact %v, %v", tc.selector, exact, err)
|
|
}
|
|
}
|
|
if _, _, err := resolveProfileDeletionSelection(nil, "x"); err == nil {
|
|
t.Fatal("nil deletion config succeeded")
|
|
}
|
|
|
|
for _, selector := range []string{"", "corp-a", "missing", "Solo", "Shared"} {
|
|
_, _ = resolveOrganizationCorpID(cfg, selector)
|
|
}
|
|
if got, err := resolveOrganizationCorpID(nil, "x"); err != nil || got != "" {
|
|
t.Fatalf("nil organization resolution = %q %v", got, err)
|
|
}
|
|
if _, _, err := resolveOrganizationDefault(cfg, "missing", "missing", nil); err == nil {
|
|
t.Fatal("empty organization default succeeded")
|
|
}
|
|
if got, _, err := resolveOrganizationDefault(cfg, "corp-c", "Solo", profilesForCorpID(cfg, "corp-c")); err != nil || got.UserID != "u4" {
|
|
t.Fatalf("single account organization default = %#v %v", got, err)
|
|
}
|
|
if _, _, err := resolveOrganizationDefault(&ProfilesConfig{}, "corp-a", "corp-a", profilesForCorpID(cfg, "corp-a")); err == nil {
|
|
t.Fatal("ambiguous organization default succeeded")
|
|
}
|
|
if got := profileSelectorCandidates([]*Profile{nil, &cfg.Profiles[1], &cfg.Profiles[0]}); len(got) != 2 || got[0] != "corp-a:u1" {
|
|
t.Fatalf("selector candidates = %#v", got)
|
|
}
|
|
for selector, want := range map[string]bool{
|
|
"": false,
|
|
"corp-a:u1": true,
|
|
"Shared:Bob": true,
|
|
"corp-c": true,
|
|
"local-a": true,
|
|
"definitely-missing": false,
|
|
} {
|
|
if got := profileSelectorReferenceExists(cfg, selector); got != want {
|
|
t.Fatalf("profileSelectorReferenceExists(%q) = %v", selector, got)
|
|
}
|
|
}
|
|
if profileSelectorReferenceExists(nil, "corp-a") {
|
|
t.Fatal("nil profile selector reference exists")
|
|
}
|
|
|
|
oldAcquire := profilesAcquireDualLock
|
|
oldEnsure := profilesEnsureMigration
|
|
oldLoad := profilesLoad
|
|
t.Cleanup(func() {
|
|
profilesAcquireDualLock = oldAcquire
|
|
profilesEnsureMigration = oldEnsure
|
|
profilesLoad = oldLoad
|
|
})
|
|
profilesAcquireDualLock = func(context.Context, string) (*DualLock, error) { return &DualLock{}, nil }
|
|
profilesEnsureMigration = func(string) error { return nil }
|
|
profilesLoad = func(string) (*ProfilesConfig, error) { return cfg, nil }
|
|
if got, exact, err := ResolveProfileDeletionScope("cfg", "corp-a:u1"); err != nil || !exact || got.UserID != "u1" {
|
|
t.Fatalf("deletion scope = %#v %v %v", got, exact, err)
|
|
}
|
|
profilesEnsureMigration = func(string) error { return errors.New("migration") }
|
|
if _, _, err := ResolveProfileDeletionScope("cfg", "corp-a"); err == nil {
|
|
t.Fatal("deletion scope migration failure succeeded")
|
|
}
|
|
profilesEnsureMigration = func(string) error { return nil }
|
|
profilesLoad = func(string) (*ProfilesConfig, error) { return nil, errors.New("load") }
|
|
if _, _, err := ResolveProfileDeletionScope("cfg", "corp-a"); err == nil {
|
|
t.Fatal("deletion scope load failure succeeded")
|
|
}
|
|
|
|
oldLoadIdentity := profilesLoadIdentity
|
|
oldLoadCorp := profilesLoadCorp
|
|
oldSaveIdentity := profilesSaveIdentity
|
|
t.Cleanup(func() {
|
|
profilesLoadIdentity = oldLoadIdentity
|
|
profilesLoadCorp = oldLoadCorp
|
|
profilesSaveIdentity = oldSaveIdentity
|
|
})
|
|
profile := Profile{CorpID: "corp-a", UserID: "u1"}
|
|
profilesLoadIdentity = func(string, string) (*TokenData, error) { return &TokenData{AccessToken: "identity"}, nil }
|
|
if got, err := loadTokenForProfileIdentity(profile); err != nil || got.AccessToken != "identity" {
|
|
t.Fatalf("identity token load = %#v %v", got, err)
|
|
}
|
|
fail := errors.New("fail")
|
|
profilesLoadIdentity = func(string, string) (*TokenData, error) { return nil, fail }
|
|
if _, err := loadTokenForProfileIdentity(profile); !errors.Is(err, fail) {
|
|
t.Fatalf("identity load failure = %v", err)
|
|
}
|
|
profilesLoadIdentity = func(string, string) (*TokenData, error) { return nil, ErrTokenDataNotFound }
|
|
profilesLoadCorp = func(string) (*TokenData, error) { return nil, ErrTokenDataNotFound }
|
|
if _, err := loadTokenForProfileIdentity(profile); !errors.Is(err, ErrTokenDataNotFound) {
|
|
t.Fatalf("missing organization mirror = %v", err)
|
|
}
|
|
profilesLoadCorp = func(string) (*TokenData, error) { return nil, fail }
|
|
if _, err := loadTokenForProfileIdentity(profile); !errors.Is(err, fail) {
|
|
t.Fatalf("organization mirror failure = %v", err)
|
|
}
|
|
for _, data := range []*TokenData{
|
|
{CorpID: "corp-a"},
|
|
{CorpID: "corp-a", UserID: "other"},
|
|
} {
|
|
profilesLoadCorp = func(string) (*TokenData, error) { return data, nil }
|
|
if _, err := loadTokenForProfileIdentity(profile); err == nil {
|
|
t.Fatalf("invalid organization mirror %#v succeeded", data)
|
|
}
|
|
}
|
|
profilesLoadCorp = func(string) (*TokenData, error) {
|
|
return &TokenData{CorpID: "corp-a", UserID: "u1", AccessToken: "mirror"}, nil
|
|
}
|
|
profilesSaveIdentity = func(string, string, *TokenData) error { return fail }
|
|
if _, err := loadTokenForProfileIdentity(profile); !errors.Is(err, fail) {
|
|
t.Fatalf("identity repair save failure = %v", err)
|
|
}
|
|
profilesSaveIdentity = func(string, string, *TokenData) error { return nil }
|
|
if got, err := loadTokenForProfileIdentity(profile); err != nil || got.AccessToken != "mirror" {
|
|
t.Fatalf("identity repair = %#v %v", got, err)
|
|
}
|
|
if _, err := loadTokenForProfileIdentity(Profile{CorpID: "corp-a"}); err == nil {
|
|
t.Fatal("unresolved profile loaded organization token owned by an exact identity")
|
|
}
|
|
profilesLoadCorp = func(string) (*TokenData, error) {
|
|
return &TokenData{CorpID: "corp-a", AccessToken: "organization"}, nil
|
|
}
|
|
if got, err := loadTokenForProfileIdentity(Profile{CorpID: "corp-a"}); err != nil || got.AccessToken != "organization" {
|
|
t.Fatalf("organization-only token load = %#v %v", got, err)
|
|
}
|
|
}
|