207 lines
6.9 KiB
Go
207 lines
6.9 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package app
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
|
|
authpkg "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth"
|
|
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/agentproduct"
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func TestParseAgentHost(t *testing.T) {
|
|
valid := []struct {
|
|
name string
|
|
raw string
|
|
want string
|
|
}{
|
|
{name: "unset", raw: "", want: ""},
|
|
{name: "ASCII whitespace only", raw: " \t ", want: ""},
|
|
{name: "cloud", raw: "cloud", want: "cloud"},
|
|
{name: "desktop", raw: "desktop", want: "desktop"},
|
|
{name: "legacy combined label remains valid", raw: "qwenwork_cloud", want: "qwenwork_cloud"},
|
|
{name: "trim", raw: " \tcloud\t ", want: "cloud"},
|
|
{name: "generic", raw: "host-2_alpha", want: "host-2_alpha"},
|
|
{name: "leading digit", raw: "2nd_host", want: "2nd_host"},
|
|
{name: "maximum length", raw: strings.Repeat("a", maxAgentHostBytes), want: strings.Repeat("a", maxAgentHostBytes)},
|
|
}
|
|
for _, tc := range valid {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := parseAgentHost(tc.raw)
|
|
if err != nil {
|
|
t.Fatalf("parseAgentHost() error = %v", err)
|
|
}
|
|
if got != tc.want {
|
|
t.Fatalf("parseAgentHost() = %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
|
|
invalid := []struct {
|
|
name string
|
|
raw string
|
|
}{
|
|
{name: "carriage return", raw: "qwenwork_cloud\r"},
|
|
{name: "line feed", raw: "\nqwenwork_cloud"},
|
|
{name: "uppercase", raw: "Qwenwork_cloud"},
|
|
{name: "internal space", raw: "qwenwork cloud"},
|
|
{name: "internal tab", raw: "qwenwork\tcloud"},
|
|
{name: "unicode", raw: "千问办公"},
|
|
{name: "leading dash", raw: "-qwenwork"},
|
|
{name: "leading underscore", raw: "_qwenwork"},
|
|
{name: "control character", raw: "qwenwork\x00cloud"},
|
|
{name: "vertical tab", raw: "\vcloud"},
|
|
{name: "form feed", raw: "cloud\f"},
|
|
{name: "next line", raw: "cloud\u0085"},
|
|
{name: "non-breaking space", raw: "\u00a0cloud"},
|
|
{name: "ideographic space", raw: "cloud\u3000"},
|
|
{name: "too long", raw: strings.Repeat("a", maxAgentHostBytes+1)},
|
|
}
|
|
for _, tc := range invalid {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := parseAgentHost(tc.raw)
|
|
if err == nil {
|
|
t.Fatalf("parseAgentHost(%q) = %q, want error", tc.raw, got)
|
|
}
|
|
var appErr *apperrors.Error
|
|
if !errors.As(err, &appErr) {
|
|
t.Fatalf("parseAgentHost() error type = %T, want *errors.Error", err)
|
|
}
|
|
if appErr.Category != apperrors.CategoryValidation {
|
|
t.Fatalf("category = %q, want validation", appErr.Category)
|
|
}
|
|
if appErr.Reason != "invalid_agent_host" {
|
|
t.Fatalf("reason = %q, want invalid_agent_host", appErr.Reason)
|
|
}
|
|
if tc.raw != "" && strings.Contains(err.Error(), tc.raw) {
|
|
t.Fatalf("error must not echo invalid value %q: %v", tc.raw, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveIdentityHeadersAddsAgentHostBeforeEditionMerge(t *testing.T) {
|
|
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
|
|
t.Setenv(envDWSAgentHost, " qwenwork_desktop ")
|
|
t.Setenv(agentproduct.EnvName, "")
|
|
t.Setenv(envDWSChannel, "channel-test")
|
|
t.Setenv(envDingtalkAgent, "agent-test")
|
|
t.Setenv(authpkg.AgentCodeEnv, "agent-code-test")
|
|
|
|
oldEdition := edition.Get()
|
|
t.Cleanup(func() { edition.Override(oldEdition) })
|
|
|
|
mergeSawAgentHost := ""
|
|
edition.Override(&edition.Hooks{
|
|
MergeHeaders: func(headers map[string]string) map[string]string {
|
|
mergeSawAgentHost = headers[headerDWSAgentHost]
|
|
headers["claw-type"] = "test-claw"
|
|
return headers
|
|
},
|
|
})
|
|
|
|
headers := resolveIdentityHeaders()
|
|
if got := mergeSawAgentHost; got != "qwenwork_desktop" {
|
|
t.Fatalf("MergeHeaders saw agent host %q, want qwenwork_desktop", got)
|
|
}
|
|
if got := headers[headerDWSAgentHost]; got != "qwenwork_desktop" {
|
|
t.Fatalf("%s = %q, want qwenwork_desktop", headerDWSAgentHost, got)
|
|
}
|
|
if got := headers["x-dingtalk-source"]; got != "github" {
|
|
t.Fatalf("x-dingtalk-source = %q, want github", got)
|
|
}
|
|
if got := headers["x-dingtalk-dws-agent-code"]; got != "agent-code-test" {
|
|
t.Fatalf("agentCode header = %q, want agent-code-test", got)
|
|
}
|
|
if got := headers["x-dws-channel"]; got != "channel-test" {
|
|
t.Fatalf("channel header = %q, want channel-test", got)
|
|
}
|
|
if got := headers["claw-type"]; got != "test-claw" {
|
|
t.Fatalf("claw-type = %q, want test-claw", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveIdentityHeadersOmitsAbsentOrInvalidAgentHost(t *testing.T) {
|
|
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
|
|
t.Setenv(envDWSChannel, "channel-test")
|
|
|
|
oldEdition := edition.Get()
|
|
t.Cleanup(func() { edition.Override(oldEdition) })
|
|
edition.Override(&edition.Hooks{
|
|
MergeHeaders: func(headers map[string]string) map[string]string {
|
|
return headers
|
|
},
|
|
})
|
|
|
|
for _, raw := range []string{"", " \t ", "DO_NOT_ECHO"} {
|
|
t.Setenv(envDWSAgentHost, raw)
|
|
headers := resolveIdentityHeaders()
|
|
if _, ok := headers[headerDWSAgentHost]; ok {
|
|
t.Fatalf("%s must be omitted for %q: %#v", headerDWSAgentHost, raw, headers)
|
|
}
|
|
if got := headers["x-dingtalk-source"]; got != "github" {
|
|
t.Fatalf("x-dingtalk-source = %q, want github", got)
|
|
}
|
|
if got := headers["x-dws-channel"]; got != "channel-test" {
|
|
t.Fatalf("channel header = %q, want channel-test", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageRootRejectsInvalidAgentHostBeforeEditionHook(t *testing.T) {
|
|
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
|
|
const invalidValue = "DO_NOT_ECHO"
|
|
t.Setenv(envDWSAgentHost, invalidValue)
|
|
|
|
oldEdition := edition.Get()
|
|
t.Cleanup(func() { edition.Override(oldEdition) })
|
|
|
|
hookCalled := false
|
|
edition.Override(&edition.Hooks{
|
|
AfterPersistentPreRun: func(_ *cobra.Command, _ []string) error {
|
|
hookCalled = true
|
|
return nil
|
|
},
|
|
})
|
|
|
|
root := NewRootCommand()
|
|
root.SetOut(io.Discard)
|
|
root.SetErr(io.Discard)
|
|
root.SetArgs([]string{"version"})
|
|
err := root.Execute()
|
|
if err == nil {
|
|
t.Fatal("root command accepted invalid DWS_AGENT_HOST")
|
|
}
|
|
if hookCalled {
|
|
t.Fatal("edition AfterPersistentPreRun ran before DWS_AGENT_HOST validation")
|
|
}
|
|
|
|
var appErr *apperrors.Error
|
|
if !errors.As(err, &appErr) {
|
|
t.Fatalf("root error type = %T, want *errors.Error", err)
|
|
}
|
|
if appErr.Category != apperrors.CategoryValidation || appErr.Reason != "invalid_agent_host" {
|
|
t.Fatalf("root error = category %q reason %q", appErr.Category, appErr.Reason)
|
|
}
|
|
if strings.Contains(err.Error(), invalidValue) {
|
|
t.Fatalf("root error must not echo invalid value: %v", err)
|
|
}
|
|
}
|