Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions shortcuts/task/shortcuts.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"strings"
"time"

"github.com/larksuite/cli/internal/output"
"github.com/larksuite/cli/shortcuts/common"
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
)
Expand Down Expand Up @@ -98,7 +99,7 @@
// Handle generic JSON payload if provided
if dataStr := runtime.Str("data"); dataStr != "" {
if err := json.Unmarshal([]byte(dataStr), &body); err != nil {
return nil, fmt.Errorf("--data must be a valid JSON object: %v", err)
return nil, output.ErrValidation("--data must be a valid JSON object: %v", err)
}
}

Expand Down Expand Up @@ -134,7 +135,7 @@
if dueStr := runtime.Str("due"); dueStr != "" {
dueObj, err := parseTaskTime(dueStr)
if err != nil {
return nil, fmt.Errorf("failed to parse due time: %v", err)
return nil, output.ErrValidation("failed to parse due time: %v", err)

Check warning on line 138 in shortcuts/task/shortcuts.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/task/shortcuts.go#L138

Added line #L138 was not covered by tests
}
body["due"] = dueObj
}
Expand All @@ -145,7 +146,7 @@

summary, _ := body["summary"].(string)
if strings.TrimSpace(summary) == "" {
return nil, fmt.Errorf("task summary is required")
return nil, output.ErrValidation("task summary is required")
}

return body, nil
Expand Down Expand Up @@ -201,7 +202,7 @@
var result map[string]interface{}
if err == nil {
if parseErr := json.Unmarshal(apiResp.RawBody, &result); parseErr != nil {
return fmt.Errorf("failed to parse response: %v", parseErr)
return output.Errorf(output.ExitAPI, "api_error", "failed to parse response: %v", parseErr)

Check warning on line 205 in shortcuts/task/shortcuts.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/task/shortcuts.go#L205

Added line #L205 was not covered by tests
}
}

Expand Down
156 changes: 156 additions & 0 deletions shortcuts/task/task_body_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package task

import (
"errors"
"testing"

"github.com/larksuite/cli/internal/output"
"github.com/spf13/cobra"

"github.com/larksuite/cli/shortcuts/common"
)

func TestBuildTaskCreateBody_StructuredErrors(t *testing.T) {
tests := []struct {
name string
data string
summary string
due string
wantCode int
wantType string
wantSubstr string
}{
Comment thread
Zhang-986 marked this conversation as resolved.
{
name: "invalid JSON data returns ErrValidation",
data: "not-json",
summary: "test",
wantCode: output.ExitValidation,
wantType: "validation",
wantSubstr: "--data must be a valid JSON object",
},
{
name: "missing summary returns ErrValidation",
data: "",
summary: "",
wantCode: output.ExitValidation,
wantType: "validation",
wantSubstr: "task summary is required",
},
{
name: "invalid due time returns ErrValidation",
data: "",
summary: "test task",
due: "not-a-valid-time",
wantCode: output.ExitValidation,
wantType: "validation",
wantSubstr: "failed to parse due time",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := &cobra.Command{}
cmd.Flags().String("data", tt.data, "")
cmd.Flags().String("summary", tt.summary, "")
cmd.Flags().String("description", "", "")
cmd.Flags().String("assignee", "", "")
cmd.Flags().String("follower", "", "")
cmd.Flags().String("due", tt.due, "")
cmd.Flags().String("tasklist-id", "", "")
cmd.Flags().String("idempotency-key", "", "")

runtime := &common.RuntimeContext{Cmd: cmd}
_, err := buildTaskCreateBody(runtime)
if err == nil {
t.Fatal("expected error, got nil")
}

var exitErr *output.ExitError
if !errors.As(err, &exitErr) {
t.Fatalf("error type = %T, want *output.ExitError; error = %v", err, err)
}
if exitErr.Code != tt.wantCode {
t.Errorf("exit code = %d, want %d", exitErr.Code, tt.wantCode)
}
if exitErr.Detail == nil {
t.Fatal("expected non-nil error detail")
}
if exitErr.Detail.Type != tt.wantType {
t.Errorf("error type = %q, want %q", exitErr.Detail.Type, tt.wantType)
}
})
}
}
Comment thread
Zhang-986 marked this conversation as resolved.

func TestBuildTaskUpdateBody_StructuredErrors(t *testing.T) {
tests := []struct {
name string
data string
summary string
due string
wantCode int
wantType string
wantSubstr string
}{
{
name: "invalid JSON data returns ErrValidation",
data: "not-json",
summary: "",
due: "",
wantCode: output.ExitValidation,
wantType: "validation",
wantSubstr: "--data must be a valid JSON object",
},
{
name: "no fields to update returns ErrValidation",
data: "",
summary: "",
due: "",
wantCode: output.ExitValidation,
wantType: "validation",
wantSubstr: "no fields to update",
},
{
name: "invalid due time returns ErrValidation",
data: "",
summary: "",
due: "not-a-valid-time",
wantCode: output.ExitValidation,
wantType: "validation",
wantSubstr: "failed to parse due time",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := &cobra.Command{}
cmd.Flags().String("data", tt.data, "")
cmd.Flags().String("summary", tt.summary, "")
cmd.Flags().String("description", "", "")
cmd.Flags().String("due", tt.due, "")

runtime := &common.RuntimeContext{Cmd: cmd}
_, err := buildTaskUpdateBody(runtime)
if err == nil {
t.Fatal("expected error, got nil")
}

var exitErr *output.ExitError
if !errors.As(err, &exitErr) {
t.Fatalf("error type = %T, want *output.ExitError; error = %v", err, err)
}
if exitErr.Code != tt.wantCode {
t.Errorf("exit code = %d, want %d", exitErr.Code, tt.wantCode)
}
if exitErr.Detail == nil {
t.Fatal("expected non-nil error detail")
}
if exitErr.Detail.Type != tt.wantType {
t.Errorf("error type = %q, want %q", exitErr.Detail.Type, tt.wantType)
}
})
}
}
14 changes: 8 additions & 6 deletions shortcuts/task/task_query_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"strconv"
"strings"
"time"

"github.com/larksuite/cli/internal/output"
)

func splitAndTrimCSV(input string) []string {
Expand Down Expand Up @@ -44,7 +46,7 @@
}
startSecInt, err = strconv.ParseInt(startSec, 10, 64)
if err != nil {
return "", "", fmt.Errorf("invalid start timestamp: %w", err)
return "", "", output.ErrValidation("invalid start timestamp: %v", err)

Check warning on line 49 in shortcuts/task/task_query_helpers.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/task/task_query_helpers.go#L49

Added line #L49 was not covered by tests
}
hasStart = true
startMillis = startSec + "000"
Expand All @@ -56,13 +58,13 @@
}
endSecInt, err = strconv.ParseInt(endSec, 10, 64)
if err != nil {
return "", "", fmt.Errorf("invalid end timestamp: %w", err)
return "", "", output.ErrValidation("invalid end timestamp: %v", err)

Check warning on line 61 in shortcuts/task/task_query_helpers.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/task/task_query_helpers.go#L61

Added line #L61 was not covered by tests
}
hasEnd = true
endMillis = endSec + "000"
}
if hasStart && hasEnd && startSecInt > endSecInt {
return "", "", fmt.Errorf("start time must be earlier than or equal to end time")
return "", "", output.ErrValidation("start time must be earlier than or equal to end time")
}
return startMillis, endMillis, nil
}
Expand All @@ -89,7 +91,7 @@
}
startSecInt, err = strconv.ParseInt(startSec, 10, 64)
if err != nil {
return "", "", fmt.Errorf("invalid start timestamp: %w", err)
return "", "", output.ErrValidation("invalid start timestamp: %v", err)

Check warning on line 94 in shortcuts/task/task_query_helpers.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/task/task_query_helpers.go#L94

Added line #L94 was not covered by tests
}
hasStart = true
startTime = time.Unix(startSecInt, 0).Local().Format(time.RFC3339)
Expand All @@ -101,13 +103,13 @@
}
endSecInt, err = strconv.ParseInt(endSec, 10, 64)
if err != nil {
return "", "", fmt.Errorf("invalid end timestamp: %w", err)
return "", "", output.ErrValidation("invalid end timestamp: %v", err)

Check warning on line 106 in shortcuts/task/task_query_helpers.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/task/task_query_helpers.go#L106

Added line #L106 was not covered by tests
}
hasEnd = true
endTime = time.Unix(endSecInt, 0).Local().Format(time.RFC3339)
}
if hasStart && hasEnd && startSecInt > endSecInt {
return "", "", fmt.Errorf("start time must be earlier than or equal to end time")
return "", "", output.ErrValidation("start time must be earlier than or equal to end time")
}
return startTime, endTime, nil
}
Expand Down
24 changes: 24 additions & 0 deletions shortcuts/task/task_query_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
package task

import (
"errors"
"strings"
"testing"

"github.com/larksuite/cli/internal/output"
)

func TestSplitAndTrimCSV(t *testing.T) {
Expand Down Expand Up @@ -95,6 +98,18 @@ func TestParseTimeRangeMillisAndRequireSearchFilter(t *testing.T) {
if err == nil {
t.Fatalf("parseTimeRangeMillis(%q) expected error, got nil", tt.input)
}
if tt.name == "reversed range fails fast" {
var exitErr *output.ExitError
if !errors.As(err, &exitErr) {
t.Fatalf("error type = %T, want *output.ExitError; error = %v", err, err)
}
if exitErr.Code != output.ExitValidation {
t.Errorf("exit code = %d, want %d", exitErr.Code, output.ExitValidation)
}
if exitErr.Detail == nil || exitErr.Detail.Type != "validation" {
t.Errorf("error detail type = %q, want %q", exitErr.Detail.Type, "validation")
}
}
return
}
if err != nil {
Expand Down Expand Up @@ -260,6 +275,15 @@ func TestRenderRelatedTasksPretty(t *testing.T) {
if err == nil {
t.Fatal("expected error, got nil")
}
if tt.name == "reversed range fails fast" {
var exitErr *output.ExitError
if !errors.As(err, &exitErr) {
t.Fatalf("error type = %T, want *output.ExitError; error = %v", err, err)
}
if exitErr.Code != output.ExitValidation {
t.Errorf("exit code = %d, want %d", exitErr.Code, output.ExitValidation)
}
}
Comment thread
Zhang-986 marked this conversation as resolved.
return
}
if err != nil {
Expand Down
8 changes: 4 additions & 4 deletions shortcuts/task/task_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
var result map[string]interface{}
if err == nil {
if parseErr := json.Unmarshal(apiResp.RawBody, &result); parseErr != nil {
return fmt.Errorf("failed to parse response for task %s: %v", taskId, parseErr)
return output.Errorf(output.ExitAPI, "api_error", "failed to parse response for task %s: %v", taskId, parseErr)

Check warning on line 79 in shortcuts/task/task_update.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/task/task_update.go#L79

Added line #L79 was not covered by tests
}
}

Expand Down Expand Up @@ -133,7 +133,7 @@

if dataStr := runtime.Str("data"); dataStr != "" {
if err := json.Unmarshal([]byte(dataStr), &taskObj); err != nil {
return nil, fmt.Errorf("--data must be a valid JSON object: %v", err)
return nil, output.ErrValidation("--data must be a valid JSON object: %v", err)
}
Comment thread
Zhang-986 marked this conversation as resolved.
// If data is provided, assume keys are update fields
for k := range taskObj {
Expand All @@ -158,7 +158,7 @@
if dueStr := runtime.Str("due"); dueStr != "" {
dueObj, err := parseTaskTime(dueStr)
if err != nil {
return nil, fmt.Errorf("failed to parse due time: %v", err)
return nil, output.ErrValidation("failed to parse due time: %v", err)
}
taskObj["due"] = dueObj
if !contains(updateFields, "due") {
Expand All @@ -167,7 +167,7 @@
}

if len(updateFields) == 0 {
return nil, fmt.Errorf("no fields to update")
return nil, output.ErrValidation("no fields to update")
}

return map[string]interface{}{
Expand Down
5 changes: 2 additions & 3 deletions shortcuts/task/task_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
func parseRelativeTime(s string) (time.Time, error) {
matches := relativeTimeRe.FindStringSubmatch(s)
if len(matches) == 0 {
return time.Time{}, fmt.Errorf("invalid relative time format: %s", s)
return time.Time{}, output.ErrValidation("invalid relative time format: %s", s)
}

sign := matches[1]
Expand All @@ -50,9 +50,8 @@
return now.Add(time.Duration(amount) * time.Minute), nil
case "h":
return now.Add(time.Duration(amount) * time.Hour), nil
default:
return time.Time{}, fmt.Errorf("unknown unit: %s", unit)
}
panic(fmt.Sprintf("unreachable: relativeTimeRe matched unexpected unit %q", unit))

Check warning on line 54 in shortcuts/task/task_util.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/task/task_util.go#L54

Added line #L54 was not covered by tests
}

const (
Expand Down
Loading
Loading