Files
dingtalk-workspace-cli/internal/helpers/chat_favorites_test.go

232 lines
7.7 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 helpers
import (
"context"
"io"
"reflect"
"strings"
"testing"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
type chatFavoritesCall struct {
productID string
toolName string
args map[string]any
}
type chatFavoritesCaller struct {
calls []chatFavoritesCall
}
func (c *chatFavoritesCaller) CallTool(_ context.Context, productID, toolName string, args map[string]any) (*edition.ToolResult, error) {
c.calls = append(c.calls, chatFavoritesCall{productID: productID, toolName: toolName, args: args})
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: `{}`}}}, nil
}
func (*chatFavoritesCaller) Format() string { return "json" }
func (*chatFavoritesCaller) DryRun() bool { return false }
func (*chatFavoritesCaller) Fields() string { return "" }
func (*chatFavoritesCaller) JQ() string { return "" }
func executeChatFavoritesCommand(t *testing.T, caller *chatFavoritesCaller, args ...string) error {
t.Helper()
previousDeps := deps
t.Cleanup(func() { deps = previousDeps })
InitDeps(caller)
deps.Out.w = io.Discard
root := newChatCommand()
root.SilenceErrors = true
root.SilenceUsage = true
root.SetArgs(args)
return root.Execute()
}
func TestChatFavoritesCommandsRegistered(t *testing.T) {
root := newChatCommand()
tests := []struct {
name string
flags []string
}{
{name: "add-favorite", flags: []string{"open-message-id", "open-conversation-id"}},
{name: "remove-favorite", flags: []string{"open-message-id", "open-conversation-id"}},
{name: "list-favorites", flags: []string{"cursor", "size"}},
}
for _, tt := range tests {
cmd, remaining, err := root.Find([]string{"message", tt.name})
if err != nil || len(remaining) != 0 {
t.Fatalf("dws chat message %s not registered: cmd=%v remaining=%v err=%v", tt.name, cmd, remaining, err)
}
for _, flag := range tt.flags {
if cmd.Flags().Lookup(flag) == nil {
t.Errorf("chat message %s: missing flag --%s", tt.name, flag)
}
}
}
}
func TestChatListMyGroupsValidatesReviewedRoleEnum(t *testing.T) {
caller := &chatFavoritesCaller{}
err := executeChatFavoritesCommand(t, caller, "group", "list-my-groups", "--role", "owner")
if err == nil || !strings.Contains(err.Error(), "OWNER or ADMIN") {
t.Fatalf("error = %v, want reviewed role validation", err)
}
if len(caller.calls) != 0 {
t.Fatalf("tool call count = %d, want 0", len(caller.calls))
}
caller = &chatFavoritesCaller{}
if err := executeChatFavoritesCommand(t, caller, "group", "list-my-groups", "--role", "OWNER", "--limit", "10"); err != nil {
t.Fatalf("list-my-groups returned error: %v", err)
}
if len(caller.calls) != 1 || caller.calls[0].toolName != "list_owned_or_admin_groups" {
t.Fatalf("tool calls = %#v, want one list_owned_or_admin_groups call", caller.calls)
}
want := map[string]any{"roleFilter": "OWNER", "limit": 10}
if !reflect.DeepEqual(caller.calls[0].args, want) {
t.Fatalf("tool args = %#v, want %#v", caller.calls[0].args, want)
}
}
func TestChatFavoriteMutationMappings(t *testing.T) {
tests := []struct {
name string
command string
toolName string
}{
{name: "add", command: "add-favorite", toolName: "add_message_favorite"},
{name: "remove", command: "remove-favorite", toolName: "remove_message_favorite"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
caller := &chatFavoritesCaller{}
err := executeChatFavoritesCommand(t, caller,
"message", tt.command,
"--open-message-id", "msg-1",
"--open-conversation-id", "cid-1",
)
if err != nil {
t.Fatalf("chat message %s returned error: %v", tt.command, err)
}
want := chatFavoritesCall{
productID: "im",
toolName: tt.toolName,
args: map[string]any{
"openMessageId": "msg-1",
"openConversationId": "cid-1",
},
}
if len(caller.calls) != 1 || !reflect.DeepEqual(caller.calls[0], want) {
t.Fatalf("calls = %#v, want %#v", caller.calls, want)
}
})
}
}
func TestChatFavoriteMutationsRequireBothIDs(t *testing.T) {
tests := []struct {
name string
args []string
}{
{name: "add missing message", args: []string{"message", "add-favorite", "--open-conversation-id", "cid-1"}},
{name: "add missing conversation", args: []string{"message", "add-favorite", "--open-message-id", "msg-1"}},
{name: "remove missing message", args: []string{"message", "remove-favorite", "--open-conversation-id", "cid-1"}},
{name: "remove missing conversation", args: []string{"message", "remove-favorite", "--open-message-id", "msg-1"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
caller := &chatFavoritesCaller{}
err := executeChatFavoritesCommand(t, caller, tt.args...)
if err == nil {
t.Fatal("command without all required IDs returned nil error")
}
if len(caller.calls) != 0 {
t.Fatalf("remote calls = %d, want 0", len(caller.calls))
}
})
}
}
func TestChatListFavoritesSuppliesOpenDefaults(t *testing.T) {
caller := &chatFavoritesCaller{}
if err := executeChatFavoritesCommand(t, caller, "message", "list-favorites"); err != nil {
t.Fatalf("list-favorites returned error: %v", err)
}
want := chatFavoritesCall{
productID: "im",
toolName: "list_message_favorites",
args: map[string]any{
"cursor": int64(0),
"size": "20",
},
}
if len(caller.calls) != 1 || !reflect.DeepEqual(caller.calls[0], want) {
t.Fatalf("calls = %#v, want %#v", caller.calls, want)
}
}
func TestChatListFavoritesMapsExplicitPagination(t *testing.T) {
caller := &chatFavoritesCaller{}
err := executeChatFavoritesCommand(t, caller,
"message", "list-favorites", "--cursor", "42", "--size", "30")
if err != nil {
t.Fatalf("list-favorites returned error: %v", err)
}
want := map[string]any{"cursor": int64(42), "size": "30"}
if len(caller.calls) != 1 || caller.calls[0].productID != "im" || caller.calls[0].toolName != "list_message_favorites" || !reflect.DeepEqual(caller.calls[0].args, want) {
t.Fatalf("calls = %#v, want im/list_message_favorites %#v", caller.calls, want)
}
}
func TestChatListFavoritesRejectsInvalidSize(t *testing.T) {
for _, size := range []string{"-1", "0", "31"} {
t.Run(size, func(t *testing.T) {
caller := &chatFavoritesCaller{}
err := executeChatFavoritesCommand(t, caller, "message", "list-favorites", "--size", size)
if err == nil || !strings.Contains(err.Error(), "--size must be between 1 and 30") {
t.Fatalf("error = %v, want size validation error", err)
}
if got := apperrors.ExitCode(err); got != 3 {
t.Fatalf("exit code = %d, want validation code 3", got)
}
if len(caller.calls) != 0 {
t.Fatalf("remote calls = %d, want 0", len(caller.calls))
}
})
}
}
func TestChatListFavoritesRejectsNegativeCursor(t *testing.T) {
caller := &chatFavoritesCaller{}
err := executeChatFavoritesCommand(t, caller, "message", "list-favorites", "--cursor", "-1")
if err == nil || !strings.Contains(err.Error(), "--cursor must be greater than or equal to 0") {
t.Fatalf("error = %v, want cursor validation error", err)
}
if len(caller.calls) != 0 {
t.Fatalf("remote calls = %d, want 0", len(caller.calls))
}
}