Files
dingtalk-workspace-cli/internal/helpers/chat_coverage_more_test.go
2026-08-04 20:24:27 +08:00

425 lines
17 KiB
Go

package helpers
import (
"context"
"encoding/json"
"errors"
"io"
"os"
"path/filepath"
"strings"
"testing"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/spf13/cobra"
)
func newChatFlagTestCommand() *cobra.Command {
cmd := &cobra.Command{Use: "chat"}
cmd.Flags().String("forward", "", "")
cmd.Flags().String("direction", "", "")
cmd.Flags().Int("limit", 1, "")
cmd.Flags().Int("size", 2, "")
cmd.Flags().String("group", "", "")
cmd.Flags().String("conversation-id", "", "")
cmd.Flags().String("id", "", "")
cmd.Flags().String("chat", "", "")
cmd.Flags().String("open-dingtalk-id", "", "")
cmd.Flags().String("user", "", "")
cmd.Flags().String("userId", "", "")
cmd.Flags().String("agentCode", "agent", "")
cmd.Flags().String("grant-type", "once", "")
cmd.Flags().String("ttl", "", "")
cmd.Flags().String("session-id", "", "")
cmd.Flags().String("target-org-id", "", "")
cmd.Flags().Bool("all", false, "")
cmd.Flags().StringArray("permParam", nil, "")
return cmd
}
func TestCrossPlatformCoverageChatDirectionAndScalarCoverage(t *testing.T) {
search := &cobra.Command{Use: "search"}
search.Flags().String("nicks", "", "")
search.Flags().Int("limit", 20, "")
search.Flags().Int("size", 20, "")
search.Flags().String("cursor", "", "")
search.Flags().String("match-mode", "", "")
search.Flags().Bool("exclude-muted", false, "")
if err := runChatSearchCommon(search, nil); err == nil {
t.Fatal("missing nicks returned nil")
}
_ = search.Flags().Set("nicks", "One,Two")
_ = search.Flags().Set("exclude-muted", "true")
installScriptedCaller(t, &scriptedToolCaller{})
if err := runChatSearchCommon(search, nil); err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
forward, direction string
changeForward bool
changeDirection bool
fallback bool
}{
{fallback: true},
{forward: "false", changeForward: true},
{direction: "newer", changeDirection: true},
{forward: "false", direction: "newer", changeForward: true, changeDirection: true},
{direction: "older", changeDirection: true},
{forward: "true", direction: "older", changeForward: true, changeDirection: true},
{direction: " ", changeDirection: true, fallback: true},
{direction: "sideways", changeDirection: true},
} {
cmd := newChatFlagTestCommand()
if tc.changeForward {
_ = cmd.Flags().Set("forward", tc.forward)
}
if tc.changeDirection {
_ = cmd.Flags().Set("direction", tc.direction)
}
_, _ = resolveMessageForward(cmd, tc.fallback)
}
cmd := newChatFlagTestCommand()
_ = cmd.Flags().Set("size", "9")
if got := chatIntFlagOrFallback(cmd, "limit", "size"); got != 9 {
t.Fatalf("alias int = %d", got)
}
_ = chatIntFlagOrFallback(newChatFlagTestCommand(), "limit", "size")
for _, text := range []string{
"", "https://example.test/a%20b", "prefix https://example.test", "prefix http://example.test",
"contains%20escape", strings.Repeat("中", 50), "short",
} {
_ = sanitizeTitleFromText(text)
}
_ = truncateTitleToBytes("abcdef", 4, 100)
_ = truncateTitleToBytes("abcdef", 100, 100)
if _, err := marshalJSONRaw(map[string]any{"bad": make(chan int)}); err == nil {
t.Fatal("unsupported JSON value succeeded")
}
for _, raw := range []string{"", "1, 2", ",,", "1,bad"} {
_, _ = parseCSVInt64(raw)
}
for _, value := range []string{"", "123", "12a"} {
_ = isNumericUserID(value)
}
for _, raw := range []string{"{", `{}`, `{"errcode":0}`, `{"errcode":"0"}`, `{"errcode":1}`, `{"errcode":"2","errmsg":"failed"}`} {
_, _, _ = webhookErrcodeFailure(raw)
}
_, _ = splitChatIDValues([]string{"", " user ", "D-open", "d-open"})
args := map[string]any{"users": []string{"old"}}
appendStringSliceArg(args, "users", nil)
appendStringSliceArg(args, "users", []string{"new"})
appendStringSliceArg(args, "open", []string{"D1"})
_ = appendChatIDArgs(args, []string{"u1", "D1"}, "users", "open")
for _, wrap := range []bool{true, false} {
_ = normalizeAtPlaceholders("hello @u1 <@u2>", []string{"", "u1", "u2"}, wrap)
}
for _, tc := range []struct {
value string
want bool
}{
{value: "0", want: true},
{value: "test_msg_id_placeholder", want: true},
{value: "test-media", want: true},
{value: "<openMsgId>", want: true},
{value: "real-message-id", want: false},
} {
if got := isLikelyPlaceholderID(tc.value); got != tc.want {
t.Fatalf("isLikelyPlaceholderID(%q) = %v, want %v", tc.value, got, tc.want)
}
}
if got := NormalizeMessageMentions("hello @u1", []string{"u1"}, true, true); got != "<@all> hello <@u1>" {
t.Fatalf("current-user mention normalization = %q", got)
}
if got := NormalizeMessageMentions("<@all> <@u1>", []string{"u1"}, true, false); got != "@all @u1" {
t.Fatalf("bot mention normalization = %q", got)
}
if got := NormalizeMessageMentions("@alliance hello", nil, true, false); got != "@all @alliance hello" {
t.Fatalf("bot @all token detection = %q", got)
}
if got := NormalizeMessageMentions("hello @all", nil, true, false); got != "hello @all" {
t.Fatalf("trailing bot @all detection = %q", got)
}
}
func TestCrossPlatformCoverageChatContactMappingCoverage(t *testing.T) {
openIDs := map[string]string{}
names := map[string]string{}
collectContactUserMappings([]any{
map[string]any{"userId": "u1", "openDingTalkId": "D1", "name": "One"},
map[string]any{"employee": map[string]any{"staff_id": json.Number("2"), "open_dingtalk_id": "D2", "display_name": "Two"}},
map[string]any{"ignored": true},
}, openIDs, names)
if openIDs["u1"] != "D1" || openIDs["2"] != "D2" {
t.Fatalf("contact mappings = %#v", openIDs)
}
_ = stringForNestedJSONKeys(map[string]any{"employee": "bad"}, chatContactNestedUserKeys, chatUserIDJSONKeys)
_ = stringForJSONKeys(map[string]any{"ignored": "x", "userId": ""}, chatUserIDJSONKeys)
for _, value := range []any{" text ", json.Number("12"), float64(1.5), float32(2.5), int(3), int64(4), int32(5), true} {
_ = stringFromJSONScalar(value)
}
if allUserIDsMapped([]string{"u1", "missing"}, openIDs) || !allUserIDsMapped([]string{"", "u1"}, openIDs) {
t.Fatal("allUserIDsMapped result changed")
}
}
func TestCrossPlatformCoverageResolveOpenDingTalkIDsCoverage(t *testing.T) {
if id, err := resolveOpenDingTalkID(context.Background(), "D-direct"); err != nil || id != "D-direct" {
t.Fatalf("direct ID = %q, %v", id, err)
}
if _, err := resolveOpenDingTalkID(context.Background(), ""); err == nil {
t.Fatal("empty ID unexpectedly resolved")
}
if ids, err := resolveOpenDingTalkIDs(context.Background(), []string{" D1 ", ""}); err != nil || ids[0] != "D1" {
t.Fatalf("direct IDs = %#v, %v", ids, err)
}
caller := &scriptedToolCaller{steps: []scriptedToolStep{{text: `{"result":[{"userId":"u1","openDingTalkId":"D1"}]}`}}}
installScriptedCaller(t, caller)
if ids, err := resolveOpenDingTalkIDs(context.Background(), []string{"u1", "u1"}); err != nil || ids[1] != "D1" {
t.Fatalf("resolved IDs = %#v, %v", ids, err)
}
caller.steps = []scriptedToolStep{{text: `{}`}, {text: `{}`}}
caller.index = 0
_, _ = resolveOpenDingTalkID(context.Background(), "missing")
caller.steps = []scriptedToolStep{{err: errors.New("contact")}, {err: errors.New("fallback")}}
caller.index = 0
_, _ = resolveOpenDingTalkIDs(context.Background(), []string{"missing"})
caller.steps = []scriptedToolStep{{text: `{`}}
caller.index = 0
_, _ = lookupOpenDingTalkIDsByUserID(context.Background(), []string{"u1"})
caller.steps = []scriptedToolStep{
{text: `{"result":[{"userId":"u2","name":"Alice"}]}`},
{text: `{"result":[{"userId":"u2","openDingTalkId":"D2"}]}`},
}
caller.index = 0
if got, err := lookupOpenDingTalkIDsByUserID(context.Background(), []string{"", "u2"}); err != nil || got["u2"] != "D2" {
t.Fatalf("aisearch mapping = %#v, %v", got, err)
}
caller.steps = []scriptedToolStep{
{text: `{"result":[{"userId":"u3","name":"Same"}]}`},
{err: errors.New("aisearch")},
{text: `{"result":[{"userId":"u3","openDingTalkId":"D3"}]}`},
}
caller.index = 0
if got, err := lookupOpenDingTalkIDsByUserID(context.Background(), []string{"u3"}); err != nil || got["u3"] != "D3" {
t.Fatalf("contact fallback mapping = %#v, %v", got, err)
}
caller.steps = []scriptedToolStep{{text: `{}`}, {text: `{`}}
caller.index = 0
if _, err := lookupOpenDingTalkIDsByUserID(context.Background(), []string{"u4"}); err == nil {
t.Fatal("invalid fallback response returned nil")
}
caller.steps = []scriptedToolStep{
{text: `{"result":[{"userId":"u5","name":"u5"}]}`},
{text: `{}`},
{text: `{}`},
}
caller.index = 0
if got, err := lookupOpenDingTalkIDsByUserID(context.Background(), []string{"u5"}); err != nil || got["u5"] != "" {
t.Fatalf("duplicate-keyword fallback = %#v, %v", got, err)
}
caller.steps = []scriptedToolStep{
{text: `{"result":[{"userId":"u6","openDingTalkId":"D6"}]}`},
{text: `{}`},
}
caller.index = 0
if got, err := lookupOpenDingTalkIDsByUserID(context.Background(), []string{"u6", "u7"}); err != nil || got["u6"] != "D6" {
t.Fatalf("partially mapped fallback = %#v, %v", got, err)
}
caller.steps = []scriptedToolStep{{text: `{`}}
caller.index = 0
if err := lookupOpenDingTalkIDsByAisearchPerson(context.Background(), "bad", map[string]string{}, map[string]string{}); err == nil {
t.Fatal("invalid aisearch response returned nil")
}
}
func TestCrossPlatformCoverageChatTargetAndGrantCoverage(t *testing.T) {
for _, configure := range []func(*cobra.Command){
func(c *cobra.Command) { _ = c.Flags().Set("group", "group") },
func(c *cobra.Command) { _ = c.Flags().Set("open-dingtalk-id", "123") },
func(c *cobra.Command) { _ = c.Flags().Set("user", "D-open") },
func(c *cobra.Command) { _ = c.Flags().Set("user", "123") },
} {
cmd := newChatFlagTestCommand()
configure(cmd)
_, _ = buildConversationTargetArgs(cmd)
}
for _, scope := range []string{"bad", "chat.read"} {
_ = validateChatScope(scope)
}
cmd := newChatFlagTestCommand()
_ = cmd.Flags().Set("conversation-id", "cid")
_ = cmd.Flags().Set("grant-type", "bad")
_, _ = buildChatChmodArgs(cmd, "chat.read")
for _, tc := range []struct{ grantType, ttl, session string }{
{"bad", "", ""}, {"timed", "", ""}, {"timed", "1h", ""},
{"session", "", ""}, {"session", "", "session"}, {"permanent", "", "extra"},
} {
cmd := newChatFlagTestCommand()
_ = cmd.Flags().Set("grant-type", tc.grantType)
_ = cmd.Flags().Set("ttl", tc.ttl)
_ = cmd.Flags().Set("session-id", tc.session)
_, _ = buildChatGrantBaseArgs(cmd, "chat.read")
}
for _, configure := range []func(*cobra.Command){
func(c *cobra.Command) {},
func(c *cobra.Command) { _ = c.Flags().Set("conversation-id", "cid") },
func(c *cobra.Command) { _ = c.Flags().Set("open-dingtalk-id", "D1") },
func(c *cobra.Command) { _ = c.Flags().Set("user", "u1") },
func(c *cobra.Command) { _ = c.Flags().Set("permParam", "key=value") },
func(c *cobra.Command) { _ = c.Flags().Set("permParam", "bad") },
func(c *cobra.Command) { _ = c.Flags().Set("conversation-id", "cid"); _ = c.Flags().Set("user", "u1") },
} {
cmd := newChatFlagTestCommand()
configure(cmd)
_, _ = buildChatChmodArgs(cmd, "chat.read")
}
for _, values := range [][]string{nil, {"", "a=b", " a = override ", "bad"}} {
_, _ = parseChatChmodParams(values)
}
for _, configure := range []func(*cobra.Command){
func(c *cobra.Command) {},
func(c *cobra.Command) { _ = c.Flags().Set("target-org-id", "org") },
func(c *cobra.Command) { _ = c.Flags().Set("all", "true") },
func(c *cobra.Command) { _ = c.Flags().Set("target-org-id", "org"); _ = c.Flags().Set("all", "true") },
} {
cmd := newChatFlagTestCommand()
configure(cmd)
_, _ = buildChatCrossOrgDataAuthArgs(cmd)
}
cmd = newChatFlagTestCommand()
_ = cmd.Flags().Set("target-org-id", "org")
_ = cmd.Flags().Set("grant-type", "bad")
_, _ = buildChatCrossOrgDataAuthArgs(cmd)
params := map[string]string{"existing": "value"}
putChatChmodParam(params, "blank", " ")
putChatChmodParam(params, "existing", "new")
}
func TestCrossPlatformCoverageChatFileUtilityCoverage(t *testing.T) {
file := filepath.Join(t.TempDir(), "file.txt")
if err := os.WriteFile(file, []byte("content"), 0o600); err != nil {
t.Fatal(err)
}
_, _ = fileMD5Hex(file)
_, _ = fileMD5Hex(filepath.Join(t.TempDir(), "missing"))
_, _ = fileMD5Hex(t.TempDir())
for _, raw := range []string{
`{`, `{}`,
`{"resourceUrls":["https://upload"],"uploadKey":"key","headers":{"x-test":1,"blank":null},"ossHeaders":{"x-other":"yes"}}`,
`{"result":{"resourceUrl":"https://upload","key":"key"}}`,
} {
_, _, _, _ = parseConversationFileUploadInfo(raw)
}
_, _ = buildConversationLocalFileMeta(filepath.Join(t.TempDir(), "missing"), "", "")
_, _ = buildConversationLocalFileMeta(t.TempDir(), "", "")
meta, err := buildConversationLocalFileMeta(file, "custom.bin", "provided")
if err != nil || meta.FileType != "bin" {
t.Fatalf("file meta = %#v, %v", meta, err)
}
_, _ = buildConversationLocalFileMeta(file, "", "")
previousMD5 := chatFileMD5
chatFileMD5 = func(string) (string, error) { return "", errors.New("md5") }
t.Cleanup(func() { chatFileMD5 = previousMD5 })
if _, err := buildConversationLocalFileMeta(file, "", ""); err == nil {
t.Fatal("MD5 failure returned nil")
}
chatFileMD5 = previousMD5
_, _ = buildConversationFileContent(1, 2, meta)
for _, raw := range []string{`{`, `{}`, `{"result":{"dentryId":1,"spaceId":"2"}}`} {
_, _, _ = parseConversationFileSendIDs(raw)
}
for _, value := range []any{
map[string]any{"id": json.Number("1")},
map[string]any{"nested": map[string]any{"id": "2"}},
[]any{map[string]any{"id": 3}},
`{"id":4}`, `[ {"id":5} ]`, "not-json", true,
} {
_, _ = findInt64Field(value, "id")
}
for _, value := range []any{json.Number("bad"), float64(-1), int64(3), int(4), "5", "bad", true} {
_, _ = int64FromJSONScalar(value)
}
_ = cloneStringAnyMap(map[string]any{"key": "value"})
_ = unmarshalJSONUseNumber(`{"n":1}`, &map[string]any{})
_ = firstStringField(map[string]any{"one": "", "two": 2}, "one", "two")
previous := deps
caller := &helpersCoreCaller{format: "json"}
InitDeps(caller)
deps.Out.w = io.Discard
deps.Out.errW = io.Discard
t.Cleanup(func() { deps = previous })
caller.format = "raw"
}
func TestCrossPlatformCoverageUploadConversationLocalFileCoverage(t *testing.T) {
oldPut := httpPutFile
t.Cleanup(func() { httpPutFile = oldPut })
meta := conversationLocalFileMeta{LocalPath: "file", FileName: "file.txt", FileSize: 4, MD5: "md5"}
for _, tc := range []struct {
name string
steps []scriptedToolStep
put error
}{
{"success", []scriptedToolStep{{text: `{"resourceUrl":"https://upload","uploadKey":"key"}`}, {text: `{"ok":true}`}}, nil},
{"init-error", []scriptedToolStep{{err: errors.New("init")}}, nil},
{"parse-error", []scriptedToolStep{{text: `{}`}}, nil},
{"put-error", []scriptedToolStep{{text: `{"resourceUrl":"https://upload","uploadKey":"key"}`}}, errors.New("put")},
{"commit-error", []scriptedToolStep{{text: `{"resourceUrl":"https://upload","uploadKey":"key"}`}, {err: errors.New("commit")}}, nil},
} {
t.Run(tc.name, func(t *testing.T) {
caller := &scriptedToolCaller{steps: tc.steps}
installScriptedCaller(t, caller)
httpPutFile = func(context.Context, string, map[string]string, string, int64) error { return tc.put }
_, _ = uploadConversationLocalFile(context.Background(), map[string]any{"target": "id"}, meta, "uuid")
})
}
}
func TestCrossPlatformCoverageGuardGroupOwnerRemovalCoverage(t *testing.T) {
oldArgs := os.Args
os.Args = []string{"dws", "chat"}
t.Cleanup(func() { os.Args = oldArgs })
for _, tc := range []struct {
name string
remove []string
steps []scriptedToolStep
}{
{"owner-open", []string{"D-owner"}, []scriptedToolStep{{text: `{"result":{"list":[{"memberRoleType":1,"openDingtalkId":"D-owner"}]}}`}}},
{"other-open", []string{"D-other"}, []scriptedToolStep{{text: `{"result":{"list":[{"memberRoleType":1,"openDingtalkId":"D-owner"}]}}`}}},
{"owner-user", []string{"u1"}, []scriptedToolStep{
{text: `{"result":{"hasMore":true,"nextCursor":"next","list":[]}}`},
{text: `{"result":{"list":[{"memberRoleType":1,"openDingtalkId":"D-owner"}]}}`},
{text: `{"result":[{"userId":"u1","openDingTalkId":"D-owner"}]}`},
}},
{"group-error", []string{"u1"}, []scriptedToolStep{{err: errors.New("group")}}},
{"group-invalid", []string{"u1"}, []scriptedToolStep{{text: `{`}}},
} {
t.Run(tc.name, func(t *testing.T) {
caller := &scriptedToolCaller{steps: tc.steps}
installScriptedCaller(t, caller)
err := guardGroupOwnerRemoval(context.Background(), "group", tc.remove)
if tc.name == "owner-open" {
var typed *apperrors.Error
if !errors.As(err, &typed) || typed.Reason != "group_owner_in_remove_list" || !strings.Contains(typed.Hint, "从 --users 中移除群主") || strings.Contains(typed.Hint, "先执行 dws chat group transfer-owner") {
t.Fatalf("owner removal hint = %v", err)
}
}
})
}
caller := &scriptedToolCaller{steps: []scriptedToolStep{{text: `{}`}}}
installScriptedCaller(t, caller)
if owner, err := groupOwnerOpenDingTalkID(context.Background(), "group"); err != nil || owner != "" {
t.Fatalf("missing owner = %q, %v", owner, err)
}
}