Files
larksuite-cli/shortcuts/task/task_get_my_tasks_test.go
ILUO e753b15d84 fix: expose completion state in my tasks output (#1641)
* fix: expose completion state in my tasks output

* test: cover my tasks pretty completion state
2026-07-01 15:41:57 +08:00

271 lines
7.7 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package task
import (
"errors"
"strconv"
"strings"
"testing"
"time"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/httpmock"
"github.com/larksuite/cli/internal/output"
)
func TestGetMyTasks_LocalTimeFormatting(t *testing.T) {
tsMs := int64(1775174400000)
tsStr := strconv.FormatInt(tsMs, 10)
expectedDueTimeStr := time.UnixMilli(tsMs).Local().Format("2006-01-02 15:04")
expectedCreatedDateStr := time.UnixMilli(tsMs).Local().Format("2006-01-02")
expectedRFC3339 := time.UnixMilli(tsMs).Local().Format(time.RFC3339)
tests := []struct {
name string
formatFlag string
pageToken string
stubURL string
expectedOutput []string
}{
{
name: "pretty format",
formatFlag: "pretty",
stubURL: "/open-apis/task/v2/tasks",
expectedOutput: []string{
"Due: " + expectedDueTimeStr,
"Created: " + expectedCreatedDateStr,
},
},
{
name: "json format",
formatFlag: "json",
stubURL: "/open-apis/task/v2/tasks",
expectedOutput: []string{
`"due_at": "` + expectedRFC3339 + `"`,
`"created_at": "` + expectedRFC3339 + `"`,
},
},
{
name: "start from page token",
formatFlag: "json",
pageToken: "pt_001",
stubURL: "page_token=pt_001",
expectedOutput: []string{
`"guid": "task-123"`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, stdout, _, reg := taskShortcutTestFactory(t)
warmTenantToken(t, f, reg)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: tt.stubURL,
Body: map[string]interface{}{
"code": 0, "msg": "success",
"data": map[string]interface{}{
"items": []interface{}{
map[string]interface{}{
"guid": "task-123",
"summary": "Test Task",
"created_at": tsStr,
"due": map[string]interface{}{
"timestamp": tsStr,
},
"url": "https://example.com/task-123",
},
},
"has_more": false,
"page_token": "",
},
},
})
s := GetMyTasks
s.AuthTypes = []string{"bot", "user"}
args := []string{"+get-my-tasks", "--format", tt.formatFlag, "--as", "bot"}
if tt.pageToken != "" {
args = append(args, "--page-token", tt.pageToken)
}
err := runMountedTaskShortcut(t, s, args, f, stdout)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
out := stdout.String()
outNorm := strings.ReplaceAll(out, `":"`, `": "`)
for _, expected := range tt.expectedOutput {
if !strings.Contains(outNorm, expected) && !strings.Contains(out, expected) {
t.Errorf("output missing expected string (%s), got: %s", expected, out)
}
}
})
}
}
func TestGetMyTasks_IncludesCompletionStateInJSON(t *testing.T) {
tsMs := int64(1775174400000)
tsStr := strconv.FormatInt(tsMs, 10)
expectedCompletedAt := time.UnixMilli(tsMs).Local().Format(time.RFC3339)
f, stdout, _, reg := taskShortcutTestFactory(t)
warmTenantToken(t, f, reg)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/task/v2/tasks",
Body: map[string]interface{}{
"code": 0, "msg": "success",
"data": map[string]interface{}{
"items": []interface{}{
map[string]interface{}{
"guid": "task-open",
"summary": "Open Task",
"completed_at": "0",
"url": "https://example.com/task-open",
},
map[string]interface{}{
"guid": "task-done",
"summary": "Done Task",
"completed_at": tsStr,
"url": "https://example.com/task-done",
},
},
"has_more": false,
"page_token": "",
},
},
})
s := GetMyTasks
s.AuthTypes = []string{"bot", "user"}
err := runMountedTaskShortcut(t, s, []string{"+get-my-tasks", "--format", "json", "--as", "bot"}, f, stdout)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
outNorm := strings.ReplaceAll(stdout.String(), `":"`, `": "`)
for _, expected := range []string{
`"guid": "task-open"`,
`"completed": false`,
`"guid": "task-done"`,
`"completed": true`,
`"completed_at": "` + expectedCompletedAt + `"`,
} {
if !strings.Contains(outNorm, expected) {
t.Fatalf("output missing expected string (%s), got: %s", expected, stdout.String())
}
}
}
func TestGetMyTasks_IncludesCompletionStateInPretty(t *testing.T) {
tsMs := int64(1775174400000)
tsStr := strconv.FormatInt(tsMs, 10)
expectedCompletedAt := time.UnixMilli(tsMs).Local().Format("2006-01-02 15:04")
f, stdout, _, reg := taskShortcutTestFactory(t)
warmTenantToken(t, f, reg)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/task/v2/tasks",
Body: map[string]interface{}{
"code": 0, "msg": "success",
"data": map[string]interface{}{
"items": []interface{}{
map[string]interface{}{
"guid": "task-open",
"summary": "Open Task",
"completed_at": "0",
"url": "https://example.com/task-open",
},
map[string]interface{}{
"guid": "task-done",
"summary": "Done Task",
"completed_at": tsStr,
"url": "https://example.com/task-done",
},
},
"has_more": false,
"page_token": "",
},
},
})
s := GetMyTasks
s.AuthTypes = []string{"bot", "user"}
err := runMountedTaskShortcut(t, s, []string{"+get-my-tasks", "--format", "pretty", "--as", "bot"}, f, stdout)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
out := stdout.String()
for _, expected := range []string{
"[1] Open Task\n ID: task-open\n URL: https://example.com/task-open\n Completed: false\n",
"[2] Done Task\n ID: task-done\n URL: https://example.com/task-done\n Completed: true\n Completed At: " + expectedCompletedAt + "\n",
} {
if !strings.Contains(out, expected) {
t.Fatalf("output missing expected string (%s), got: %s", expected, out)
}
}
if count := strings.Count(out, "Completed At:"); count != 1 {
t.Fatalf("Completed At count = %d, want 1; output: %s", count, out)
}
}
// TestGetMyTasks_InvalidTimeFlags locks the three time-flag validation arms in
// Execute (--created_at / --due-start / --due-end). The parse runs before any
// API call, so a malformed value deterministically surfaces a typed
// *errs.ValidationError (exit 2) regardless of credentials — the command runs
// as user with a throwaway token. Each error carries the corresponding --flag
// param so the caller can point at the offending input.
func TestGetMyTasks_InvalidTimeFlags(t *testing.T) {
tests := []struct {
name string
flag string
wantParam string
}{
{name: "created_at", flag: "--created_at", wantParam: "--created_at"},
{name: "due-start", flag: "--due-start", wantParam: "--due-start"},
{name: "due-end", flag: "--due-end", wantParam: "--due-end"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, stdout, _, _ := taskShortcutTestFactory(t)
s := GetMyTasks
s.AuthTypes = []string{"bot", "user"}
args := []string{"+get-my-tasks", tt.flag, "not-a-time", "--as", "user"}
err := runMountedTaskShortcut(t, s, args, f, stdout)
if err == nil {
t.Fatalf("expected validation error for %s, got nil", tt.flag)
}
var ve *errs.ValidationError
if !errors.As(err, &ve) {
t.Fatalf("error type = %T, want *errs.ValidationError; error = %v", err, err)
}
if ve.Subtype != errs.SubtypeInvalidArgument {
t.Errorf("subtype = %q, want %q", ve.Subtype, errs.SubtypeInvalidArgument)
}
if got := output.ExitCodeOf(err); got != output.ExitValidation {
t.Errorf("exit code = %d, want %d", got, output.ExitValidation)
}
if ve.Param != tt.wantParam {
t.Errorf("param = %q, want %q", ve.Param, tt.wantParam)
}
})
}
}