313 lines
11 KiB
Go
313 lines
11 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/apiclient"
|
|
authpkg "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/mcptypes"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func appRPCServer(t *testing.T, initOK, listOK bool) *httptest.Server {
|
|
t.Helper()
|
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
ID int `json:"id"`
|
|
Method string `json:"method"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch req.Method {
|
|
case "initialize":
|
|
if !initOK {
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"jsonrpc": "2.0", "id": req.ID, "error": map[string]any{"code": -32601, "message": "init"}})
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"jsonrpc": "2.0", "id": req.ID, "result": map[string]any{"protocolVersion": "2025-03-26"}})
|
|
case "tools/list":
|
|
if !listOK {
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"jsonrpc": "2.0", "id": req.ID, "error": map[string]any{"code": -1, "message": "list"}})
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": req.ID,
|
|
"result": map[string]any{
|
|
"tools": []any{map[string]any{
|
|
"name": "tool",
|
|
"description": "desc",
|
|
"inputSchema": map[string]any{
|
|
"properties": map[string]any{"id": map[string]any{"type": "string"}},
|
|
"required": []any{"id", 1, ""},
|
|
},
|
|
}},
|
|
},
|
|
})
|
|
default:
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"jsonrpc": "2.0", "id": req.ID, "result": map[string]any{}})
|
|
}
|
|
}))
|
|
}
|
|
|
|
func TestCrossPlatformCoveragePluginAuthCoverage(t *testing.T) {
|
|
fallback := pluginAuthFromServerDescriptor(mcptypes.ServerDescriptor{Key: "fallback", Endpoint: "%", AuthHeaders: map[string]string{"Authorization": "token"}})
|
|
if fallback == nil || fallback.Token != "token" || len(fallback.TrustedDomains) != 0 {
|
|
t.Fatalf("fallback plugin auth = %#v", fallback)
|
|
}
|
|
got := pluginAuthFromServerDescriptor(mcptypes.ServerDescriptor{Key: "server", Endpoint: "https://x.test", CLI: mcptypes.CLIOverlay{ID: "cli"}, AuthHeaders: map[string]string{"Authorization": "Bearer token", "X": "Y"}})
|
|
if got == nil || got.Token != "token" || got.ExtraHeaders["X"] != "Y" || len(got.TrustedDomains) != 2 {
|
|
t.Fatalf("plugin auth = %#v", got)
|
|
}
|
|
if anonymous := pluginAuthFromServerDescriptor(mcptypes.ServerDescriptor{Key: "none"}); anonymous == nil || anonymous.Token != "" {
|
|
t.Fatalf("anonymous plugin ownership = %#v", anonymous)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageRawAPIAndTokenCoverage(t *testing.T) {
|
|
oldProvider := newAccessTokenProvider
|
|
oldManager := newLegacyTokenManager
|
|
t.Cleanup(func() {
|
|
newAccessTokenProvider = oldProvider
|
|
newLegacyTokenManager = oldManager
|
|
})
|
|
cmd := &cobra.Command{Use: "api"}
|
|
var out, errOut bytes.Buffer
|
|
cmd.SetOut(&out)
|
|
cmd.SetErr(&errOut)
|
|
cmd.SetContext(context.Background())
|
|
|
|
invalid := []struct {
|
|
args []string
|
|
gf GlobalFlags
|
|
af apiFlags
|
|
}{
|
|
{[]string{"GET", "/x?a=1"}, GlobalFlags{}, apiFlags{}},
|
|
{[]string{"TRACE", "/x"}, GlobalFlags{}, apiFlags{}},
|
|
{[]string{"GET", "../x"}, GlobalFlags{}, apiFlags{}},
|
|
{[]string{"GET", "/x"}, GlobalFlags{}, apiFlags{params: "x\x00"}},
|
|
{[]string{"POST", "/x"}, GlobalFlags{}, apiFlags{data: "x\x00"}},
|
|
{[]string{"POST", "/x"}, GlobalFlags{}, apiFlags{params: "-", data: "-"}},
|
|
{[]string{"GET", "/x"}, GlobalFlags{Output: "x"}, apiFlags{pageAll: true}},
|
|
{[]string{"GET", "/x"}, GlobalFlags{}, apiFlags{params: "{"}},
|
|
{[]string{"POST", "/x"}, GlobalFlags{}, apiFlags{data: "{"}},
|
|
{[]string{"GET", "https://evil.test/x"}, GlobalFlags{Token: "t"}, apiFlags{}},
|
|
}
|
|
for _, tc := range invalid {
|
|
if err := runAPI(cmd, tc.args, &tc.gf, &tc.af); err == nil {
|
|
t.Fatalf("invalid API %#v succeeded", tc)
|
|
}
|
|
}
|
|
for _, raw := range []string{"", "a=1&empty=&=x", "a=1&b=2"} {
|
|
if got := parseQueryStringToJSON(raw); got == "" {
|
|
t.Fatalf("query JSON %q empty", raw)
|
|
}
|
|
}
|
|
if got, err := resolveRawAPIToken(context.Background(), " token "); err != nil || got != "token" {
|
|
t.Fatalf("explicit raw token = %q, %v", got, err)
|
|
}
|
|
authpkg.SetClientID("")
|
|
authpkg.SetClientSecret("")
|
|
if _, err := resolveRawAPIToken(context.Background(), ""); err == nil {
|
|
t.Fatal("missing app credentials succeeded")
|
|
}
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if strings.Contains(r.URL.Path, "fail") {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = io.WriteString(w, `{"error":"bad"}`)
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"items": []any{1}, "hasMore": false})
|
|
}))
|
|
defer server.Close()
|
|
host := strings.TrimPrefix(server.URL, "http://")
|
|
host = strings.Split(host, ":")[0]
|
|
apiclient.AllowedHosts[host] = true
|
|
t.Cleanup(func() { delete(apiclient.AllowedHosts, host) })
|
|
gf := &GlobalFlags{Token: "token", DryRun: true, Format: "json", Timeout: 1}
|
|
af := &apiFlags{baseURL: server.URL}
|
|
if err := runAPI(cmd, []string{"GET", "/ok"}, gf, af); err != nil || out.Len() == 0 {
|
|
t.Fatalf("API dry run = %q, %v", out.String(), err)
|
|
}
|
|
out.Reset()
|
|
gf.DryRun = false
|
|
if err := runAPI(cmd, []string{"GET", "/ok"}, gf, af); err != nil || out.Len() == 0 {
|
|
t.Fatalf("API request = %q, %v", out.String(), err)
|
|
}
|
|
out.Reset()
|
|
af.pageAll = true
|
|
if err := runAPI(cmd, []string{"GET", "/ok"}, gf, af); err != nil || out.Len() == 0 {
|
|
t.Fatalf("API pagination = %q, %v", out.String(), err)
|
|
}
|
|
client := apiclient.NewClient("token", server.URL)
|
|
if err := runPaginated(context.Background(), client, apiclient.RawAPIRequest{Method: "GET", Path: "/fail"}, &apiFlags{}, apiclient.ResponseOptions{Out: io.Discard, ErrOut: io.Discard}); err == nil {
|
|
t.Fatal("failed pagination succeeded")
|
|
}
|
|
|
|
if _, err := ResolveAuxiliaryAccessToken(context.Background(), "", ""); err == nil {
|
|
t.Fatal("empty auxiliary config succeeded")
|
|
}
|
|
if got, err := ResolveAuxiliaryAccessToken(context.Background(), "ignored", " explicit "); err != nil || got != "explicit" {
|
|
t.Fatalf("explicit auxiliary token = %q, %v", got, err)
|
|
}
|
|
dir := t.TempDir()
|
|
newAccessTokenProvider = func(string) accessTokenGetter { return fakeAccessTokenGetter{token: "saved"} }
|
|
newLegacyTokenManager = func(string) legacyTokenGetter { return fakeLegacyTokenGetter{} }
|
|
if got, err := resolveAccessTokenFromDir(context.Background(), dir); err != nil || got != "saved" {
|
|
t.Fatalf("saved access token = %q, %v", got, err)
|
|
}
|
|
newAccessTokenProvider = func(string) accessTokenGetter { return fakeAccessTokenGetter{} }
|
|
missing := t.TempDir()
|
|
if got, err := resolveAccessTokenFromDir(context.Background(), missing); got != "" || !errors.Is(err, authpkg.ErrTokenDataNotFound) {
|
|
t.Fatalf("missing access token = %q, %v", got, err)
|
|
}
|
|
if _, err := ResolveAuxiliaryAccessToken(context.Background(), missing, ""); err == nil {
|
|
t.Fatal("missing auxiliary credentials succeeded")
|
|
}
|
|
if _, err := ForceRefreshAccessToken(context.Background(), ""); err == nil {
|
|
t.Fatal("empty force refresh config succeeded")
|
|
}
|
|
if _, err := ForceRefreshAccessToken(context.Background(), missing); err == nil {
|
|
t.Fatal("missing force refresh token succeeded")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageRootUtilityAndTimingCoverage(t *testing.T) {
|
|
_ = resolveVerbosity(nil)
|
|
for _, flags := range []struct {
|
|
debug bool
|
|
verbose bool
|
|
format string
|
|
json bool
|
|
}{{}, {verbose: true}, {debug: true}, {format: "json"}, {format: "table"}, {json: true}} {
|
|
flagCmd := &cobra.Command{Use: "flags"}
|
|
flagCmd.Flags().Bool("debug", flags.debug, "")
|
|
flagCmd.Flags().Bool("verbose", flags.verbose, "")
|
|
flagCmd.Flags().String("format", flags.format, "")
|
|
flagCmd.Flags().Bool("json", false, "")
|
|
if flags.json {
|
|
_ = flagCmd.Flags().Set("json", "true")
|
|
}
|
|
_ = resolveVerbosity(flagCmd)
|
|
_ = commandRequestsJSONErrors(flagCmd)
|
|
_ = wantsJSONErrors(flagCmd)
|
|
}
|
|
_ = commandRequestsJSONErrors(nil)
|
|
_ = wantsJSONErrors(nil)
|
|
if got, changed := normalizeProfileFlagArgs([]string{"--profile", "a,", "b"}); !changed || len(got) == 0 {
|
|
t.Fatalf("profile args = %#v, %v", got, changed)
|
|
}
|
|
if _, changed := normalizeProfileFlagArgs([]string{"--profile"}); changed {
|
|
t.Fatal("incomplete profile flag changed")
|
|
}
|
|
if preparseProfileFlag([]string{"--profile=a"}) != "a" || preparseProfileFlag([]string{"--profile", "b"}) != "b" || preparseProfileFlag(nil) != "" {
|
|
t.Fatal("profile preparse mismatch")
|
|
}
|
|
if !argsChanged([]string{"a"}, []string{"b"}) || argsChanged([]string{"a"}, []string{"a"}) {
|
|
t.Fatal("argsChanged mismatch")
|
|
}
|
|
cmd := &cobra.Command{Use: "root"}
|
|
cmd.SetContext(context.Background())
|
|
cmd.Flags().String("output", "", "")
|
|
if err := configureOutputSink(cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := filepath.Join(t.TempDir(), "nested", "out.txt")
|
|
_ = cmd.Flags().Set("output", path)
|
|
if err := configureOutputSink(cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, _ = io.WriteString(cmd.OutOrStdout(), "data")
|
|
if err := closeOutputSink(cmd); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
local := &cobra.Command{Use: "local"}
|
|
local.SetContext(context.Background())
|
|
local.SetOut(io.Discard)
|
|
local.Flags().String("output", "", "")
|
|
if err := configureOutputSink(local); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := validateOptionalPath("--x", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := validateOptionalPath("--x", "bad\x00path"); err == nil {
|
|
t.Fatal("unsafe path succeeded")
|
|
}
|
|
|
|
root := &cobra.Command{Use: "root", Short: "root"}
|
|
bindPersistentFlags(root, &GlobalFlags{})
|
|
root.AddCommand(&cobra.Command{Use: "alpha", Short: "alpha"}, &cobra.Command{Use: "hidden", Hidden: true})
|
|
configureRootHelp(root)
|
|
var help bytes.Buffer
|
|
root.SetOut(&help)
|
|
_ = root.Help()
|
|
renderRootGlobalFlags(root)
|
|
_ = visiblePersistentFlags(root)
|
|
for _, command := range root.Commands() {
|
|
_ = commandShort(command)
|
|
}
|
|
_ = visibleMCPRootCommands(root)
|
|
_ = visibleUtilityRootCommands(root)
|
|
|
|
tc := NewTimingCollector()
|
|
tc.Record("a", time.Microsecond)
|
|
tc.Record("b", 2*time.Second)
|
|
for _, d := range []time.Duration{time.Nanosecond, time.Microsecond, time.Millisecond, time.Second} {
|
|
_ = formatDuration(d)
|
|
}
|
|
for _, debug := range []bool{false, true} {
|
|
if debug {
|
|
t.Setenv(PerfDebugEnv, "1")
|
|
} else {
|
|
t.Setenv(PerfDebugEnv, "")
|
|
}
|
|
tc.PrintIfEnabled()
|
|
}
|
|
var timingOut bytes.Buffer
|
|
tc.Print(&timingOut)
|
|
if timingOut.Len() == 0 {
|
|
t.Fatal("timing output empty")
|
|
}
|
|
t.Setenv("HOME", t.TempDir())
|
|
if defaultPerfReportPath() == "" {
|
|
t.Fatal("default perf path empty")
|
|
}
|
|
t.Setenv(PerfReportEnv, "auto")
|
|
tc.WriteReportIfEnabled("v", "cmd")
|
|
if _, err := LoadLatestReport(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv(PerfReportEnv, "")
|
|
tc.WriteReportIfEnabled("v", "cmd")
|
|
_ = exeRelativeConfigDir()
|
|
|
|
merged := mergeTopLevelCommands([]*cobra.Command{{Use: "a"}, {Use: "a"}, {Use: "b"}, nil})
|
|
if len(merged) != 2 {
|
|
t.Fatalf("merged commands = %#v", merged)
|
|
}
|
|
dedupRoot := &cobra.Command{Use: "root"}
|
|
dedupRoot.AddCommand(&cobra.Command{Use: "same"}, &cobra.Command{Use: "same"})
|
|
deduplicateCommands(dedupRoot)
|
|
addPluginCommandsSafe(dedupRoot, []*cobra.Command{{Use: "same"}, {Use: "new"}})
|
|
_ = newCompletionCommand(dedupRoot)
|
|
_ = newCatalogCommand()
|
|
_ = newConfigCommand()
|
|
_ = newCacheCommand()
|
|
_ = newVersionCommand()
|
|
_ = newAPICommand(&GlobalFlags{})
|
|
_ = NewRootCommand(context.Background())
|
|
}
|