Files
wehub-resource-sync bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

1355 lines
39 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package okr
import (
"bytes"
"encoding/json"
"errors"
"strings"
"testing"
"github.com/larksuite/cli/errs"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/httpmock"
"github.com/spf13/cobra"
)
func patchTestConfig(t *testing.T) *core.CliConfig {
t.Helper()
return &core.CliConfig{
AppID: "dummy",
AppSecret: patchTestValue(),
Brand: core.BrandFeishu,
}
}
func patchTestValue() string {
return strings.Join([]string{"test", "secret"}, "-")
}
func runPatchShortcut(t *testing.T, f *cmdutil.Factory, stdout *bytes.Buffer, args []string) error {
t.Helper()
parent := &cobra.Command{Use: "okr"}
OKRPatch.Mount(parent, f)
parent.SetArgs(args)
parent.SilenceErrors = true
parent.SilenceUsage = true
if stdout != nil {
stdout.Reset()
}
return parent.Execute()
}
// --- Validate tests ---
func TestPatchValidate_MissingLevel(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--target-id", "123",
"--content", validSemiPlainJSON,
})
if err == nil {
t.Fatal("expected --level required error")
}
// Cobra required flag errors are not our typed errors, so check message
if !strings.Contains(err.Error(), "level") {
t.Fatalf("expected --level required error, got: %v", err)
}
}
func TestPatchValidate_MissingTargetID(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--content", validSemiPlainJSON,
})
if err == nil {
t.Fatal("expected --target-id required error")
}
// Cobra required flag errors are not our typed errors, so check message
if !strings.Contains(err.Error(), "target-id") {
t.Fatalf("expected --target-id required error, got: %v", err)
}
}
func TestPatchValidate_InvalidLevel(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "invalid",
"--target-id", "123",
"--content", validSemiPlainJSON,
})
if err == nil {
t.Fatal("expected error for invalid level")
}
_, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--level" {
t.Fatalf("expected param --level, got %q", validationErr.Param)
}
}
func TestPatchValidate_InvalidTargetID_NonNumeric(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "not-a-number",
"--content", validSemiPlainJSON,
})
if err == nil {
t.Fatal("expected error for invalid target-id")
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--target-id" {
t.Fatalf("expected param --target-id, got %q", validationErr.Param)
}
}
func TestPatchValidate_InvalidTargetID_Negative(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "-1",
"--content", validSemiPlainJSON,
})
if err == nil {
t.Fatal("expected error for negative target-id")
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--target-id" {
t.Fatalf("expected param --target-id, got %q", validationErr.Param)
}
}
func TestPatchValidate_InvalidTargetID_Zero(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "0",
"--content", validSemiPlainJSON,
})
if err == nil {
t.Fatal("expected error for zero target-id")
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--target-id" {
t.Fatalf("expected param --target-id, got %q", validationErr.Param)
}
}
func TestPatchValidate_InvalidStyle(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--style", "invalid",
"--content", validSemiPlainJSON,
})
if err == nil {
t.Fatal("expected error for invalid style")
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--style" {
t.Fatalf("expected param --style, got %q", validationErr.Param)
}
}
func TestPatchValidate_InvalidUserIDType(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--content", validSemiPlainJSON,
"--user-id-type", "invalid",
})
if err == nil {
t.Fatal("expected error for invalid user-id-type")
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--user-id-type" {
t.Fatalf("expected param --user-id-type, got %q", validationErr.Param)
}
}
func TestPatchValidate_NoFieldsProvided(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
})
if err == nil {
t.Fatal("expected error for no fields provided")
}
problem, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
if problem.Category != errs.CategoryValidation {
t.Fatalf("expected category %q, got %q", errs.CategoryValidation, problem.Category)
}
if problem.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("expected subtype %q, got %q", errs.SubtypeInvalidArgument, problem.Subtype)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "" {
t.Fatalf("expected empty param (error not tied to a specific field), got %q", validationErr.Param)
}
if !strings.Contains(err.Error(), "at least one of") {
t.Fatalf("expected 'at least one of' error message, got: %v", err)
}
}
func TestPatchValidate_InvalidContent_SimpleStyle(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--style", "simple",
"--content", "not-json",
})
if err == nil {
t.Fatal("expected error for invalid --content JSON")
}
problem, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
if problem.Category != errs.CategoryValidation {
t.Fatalf("expected category %q, got %q", errs.CategoryValidation, problem.Category)
}
if problem.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("expected subtype %q, got %q", errs.SubtypeInvalidArgument, problem.Subtype)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--content" {
t.Fatalf("expected param --content, got %q", validationErr.Param)
}
if !strings.Contains(err.Error(), "semi-plain JSON") {
t.Fatalf("expected semi-plain JSON error, got: %v", err)
}
}
func TestPatchValidate_InvalidContent_RichTextStyle(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--style", "richtext",
"--content", "not-json",
})
if err == nil {
t.Fatal("expected error for invalid --content JSON")
}
problem, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
if problem.Category != errs.CategoryValidation {
t.Fatalf("expected category %q, got %q", errs.CategoryValidation, problem.Category)
}
if problem.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("expected subtype %q, got %q", errs.SubtypeInvalidArgument, problem.Subtype)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--content" {
t.Fatalf("expected param --content, got %q", validationErr.Param)
}
if !strings.Contains(err.Error(), "ContentBlock JSON") {
t.Fatalf("expected ContentBlock JSON error, got: %v", err)
}
}
func TestPatchValidate_SemiPlainContent_EmptyText(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--style", "simple",
"--content", `{"text":" ","mention":[]}`,
})
if err == nil {
t.Fatal("expected error for empty text in content")
}
problem, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
if problem.Category != errs.CategoryValidation {
t.Fatalf("expected category %q, got %q", errs.CategoryValidation, problem.Category)
}
if problem.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("expected subtype %q, got %q", errs.SubtypeInvalidArgument, problem.Subtype)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--content" {
t.Fatalf("expected param --content, got %q", validationErr.Param)
}
if !strings.Contains(err.Error(), "text is required") {
t.Fatalf("expected text required error, got: %v", err)
}
}
func TestPatchValidate_SemiPlainContent_EmptyMention(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--style", "simple",
"--content", `{"text":"hello","mention":[""]}`,
})
if err == nil {
t.Fatal("expected error for empty mention in content")
}
problem, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
if problem.Category != errs.CategoryValidation {
t.Fatalf("expected category %q, got %q", errs.CategoryValidation, problem.Category)
}
if problem.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("expected subtype %q, got %q", errs.SubtypeInvalidArgument, problem.Subtype)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--content" {
t.Fatalf("expected param --content, got %q", validationErr.Param)
}
if !strings.Contains(err.Error(), "mention[0] cannot be empty") {
t.Fatalf("expected mention empty error, got: %v", err)
}
}
func TestPatchValidate_SemiPlainContent_WithDocs(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--style", "simple",
"--content", `{"text":"hello","docs":[{"title":"doc","url":"https://example.com"}]}`,
})
if err == nil {
t.Fatal("expected error for docs in simple style content")
}
problem, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
if problem.Category != errs.CategoryValidation {
t.Fatalf("expected category %q, got %q", errs.CategoryValidation, problem.Category)
}
if problem.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("expected subtype %q, got %q", errs.SubtypeInvalidArgument, problem.Subtype)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--content" {
t.Fatalf("expected param --content, got %q", validationErr.Param)
}
if !strings.Contains(err.Error(), "docs and images are not supported") {
t.Fatalf("expected docs/images not supported error, got: %v", err)
}
}
func TestPatchValidate_SemiPlainContent_WithImages(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--style", "simple",
"--content", `{"text":"hello","images":["https://example.com/img.png"]}`,
})
if err == nil {
t.Fatal("expected error for images in simple style content")
}
problem, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
if problem.Category != errs.CategoryValidation {
t.Fatalf("expected category %q, got %q", errs.CategoryValidation, problem.Category)
}
if problem.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("expected subtype %q, got %q", errs.SubtypeInvalidArgument, problem.Subtype)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--content" {
t.Fatalf("expected param --content, got %q", validationErr.Param)
}
if !strings.Contains(err.Error(), "docs and images are not supported") {
t.Fatalf("expected docs/images not supported error, got: %v", err)
}
}
func TestPatchValidate_NotesForbiddenOnKeyResult(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "key-result",
"--target-id", "123",
"--notes", validSemiPlainJSON,
})
if err == nil {
t.Fatal("expected error for notes on key-result")
}
problem, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
if problem.Category != errs.CategoryValidation {
t.Fatalf("expected category %q, got %q", errs.CategoryValidation, problem.Category)
}
if problem.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("expected subtype %q, got %q", errs.SubtypeInvalidArgument, problem.Subtype)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--notes" {
t.Fatalf("expected param --notes, got %q", validationErr.Param)
}
if !strings.Contains(err.Error(), "only supported for level=objective") {
t.Fatalf("expected notes only for objective error, got: %v", err)
}
}
func TestPatchValidate_InvalidNotes_SimpleStyle(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--style", "simple",
"--notes", "not-json",
})
if err == nil {
t.Fatal("expected error for invalid --notes JSON")
}
problem, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
if problem.Category != errs.CategoryValidation {
t.Fatalf("expected category %q, got %q", errs.CategoryValidation, problem.Category)
}
if problem.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("expected subtype %q, got %q", errs.SubtypeInvalidArgument, problem.Subtype)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--notes" {
t.Fatalf("expected param --notes, got %q", validationErr.Param)
}
}
func TestPatchValidate_InvalidNotes_RichTextStyle(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--style", "richtext",
"--notes", "not-json",
})
if err == nil {
t.Fatal("expected error for invalid --notes JSON")
}
problem, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
if problem.Category != errs.CategoryValidation {
t.Fatalf("expected category %q, got %q", errs.CategoryValidation, problem.Category)
}
if problem.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("expected subtype %q, got %q", errs.SubtypeInvalidArgument, problem.Subtype)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--notes" {
t.Fatalf("expected param --notes, got %q", validationErr.Param)
}
}
func TestPatchValidate_SemiPlainNotes_EmptyText(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--style", "simple",
"--notes", `{"text":" "}`,
})
if err == nil {
t.Fatal("expected error for empty text in notes")
}
problem, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
if problem.Category != errs.CategoryValidation {
t.Fatalf("expected category %q, got %q", errs.CategoryValidation, problem.Category)
}
if problem.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("expected subtype %q, got %q", errs.SubtypeInvalidArgument, problem.Subtype)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--notes" {
t.Fatalf("expected param --notes, got %q", validationErr.Param)
}
}
func TestPatchValidate_SemiPlainNotes_EmptyMention(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--style", "simple",
"--notes", `{"text":"hello","mention":[" "]}`,
})
if err == nil {
t.Fatal("expected error for empty mention in notes")
}
problem, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
if problem.Category != errs.CategoryValidation {
t.Fatalf("expected category %q, got %q", errs.CategoryValidation, problem.Category)
}
if problem.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("expected subtype %q, got %q", errs.SubtypeInvalidArgument, problem.Subtype)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--notes" {
t.Fatalf("expected param --notes, got %q", validationErr.Param)
}
}
func TestPatchValidate_InvalidScore_NonNumeric(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--score", "not-a-number",
})
if err == nil {
t.Fatal("expected error for invalid score")
}
problem, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
if problem.Category != errs.CategoryValidation {
t.Fatalf("expected category %q, got %q", errs.CategoryValidation, problem.Category)
}
if problem.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("expected subtype %q, got %q", errs.SubtypeInvalidArgument, problem.Subtype)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--score" {
t.Fatalf("expected param --score, got %q", validationErr.Param)
}
}
func TestPatchValidate_InvalidScore_OutOfRange(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--score", "1.5",
})
if err == nil {
t.Fatal("expected error for score out of range")
}
problem, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
if problem.Category != errs.CategoryValidation {
t.Fatalf("expected category %q, got %q", errs.CategoryValidation, problem.Category)
}
if problem.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("expected subtype %q, got %q", errs.SubtypeInvalidArgument, problem.Subtype)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--score" {
t.Fatalf("expected param --score, got %q", validationErr.Param)
}
if !strings.Contains(err.Error(), "between 0 and 1") {
t.Fatalf("expected between 0 and 1 error, got: %v", err)
}
}
func TestPatchValidate_InvalidScore_Negative(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--score", "-0.1",
})
if err == nil {
t.Fatal("expected error for negative score")
}
problem, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
if problem.Category != errs.CategoryValidation {
t.Fatalf("expected category %q, got %q", errs.CategoryValidation, problem.Category)
}
if problem.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("expected subtype %q, got %q", errs.SubtypeInvalidArgument, problem.Subtype)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--score" {
t.Fatalf("expected param --score, got %q", validationErr.Param)
}
}
func TestPatchValidate_InvalidScore_TooManyDecimals(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--score", "0.51",
})
if err == nil {
t.Fatal("expected error for score with too many decimals")
}
problem, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
if problem.Category != errs.CategoryValidation {
t.Fatalf("expected category %q, got %q", errs.CategoryValidation, problem.Category)
}
if problem.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("expected subtype %q, got %q", errs.SubtypeInvalidArgument, problem.Subtype)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--score" {
t.Fatalf("expected param --score, got %q", validationErr.Param)
}
if !strings.Contains(err.Error(), "at most one decimal place") {
t.Fatalf("expected one decimal place error, got: %v", err)
}
}
func TestPatchValidate_InvalidDeadline(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--deadline", "not-a-number",
})
if err == nil {
t.Fatal("expected error for invalid deadline")
}
problem, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
if problem.Category != errs.CategoryValidation {
t.Fatalf("expected category %q, got %q", errs.CategoryValidation, problem.Category)
}
if problem.Subtype != errs.SubtypeInvalidArgument {
t.Fatalf("expected subtype %q, got %q", errs.SubtypeInvalidArgument, problem.Subtype)
}
var validationErr *errs.ValidationError
if !errors.As(err, &validationErr) {
t.Fatalf("expected *errs.ValidationError, got: %T", err)
}
if validationErr.Param != "--deadline" {
t.Fatalf("expected param --deadline, got %q", validationErr.Param)
}
}
func TestPatchValidate_Valid_Objective_SimpleStyle(t *testing.T) {
t.Parallel()
f, stdout, _, reg := cmdutil.TestFactory(t, patchTestConfig(t))
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/okr/v2/objectives/123",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
},
})
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--style", "simple",
"--content", validSemiPlainJSON,
"--notes", validSemiPlainJSON,
"--score", "0.5",
"--deadline", "1735776000000",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestPatchValidate_Valid_Objective_RichTextStyle(t *testing.T) {
t.Parallel()
f, stdout, _, reg := cmdutil.TestFactory(t, patchTestConfig(t))
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/okr/v2/objectives/123",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
},
})
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--style", "richtext",
"--content", validContentBlockJSON,
"--notes", validContentBlockJSON,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestPatchValidate_Valid_KeyResult_SimpleStyle(t *testing.T) {
t.Parallel()
f, stdout, _, reg := cmdutil.TestFactory(t, patchTestConfig(t))
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/okr/v2/key_results/456",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
},
})
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "key-result",
"--target-id", "456",
"--style", "simple",
"--content", validSemiPlainJSON,
"--score", "1.0",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestPatchValidate_Valid_KeyResult_RichTextStyle(t *testing.T) {
t.Parallel()
f, stdout, _, reg := cmdutil.TestFactory(t, patchTestConfig(t))
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/okr/v2/key_results/456",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
},
})
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "key-result",
"--target-id", "456",
"--style", "richtext",
"--content", validContentBlockJSON,
"--deadline", "1735776000000",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestPatchValidate_Valid_ScoreBoundaryValues(t *testing.T) {
t.Parallel()
tests := []string{"0", "0.0", "1", "1.0", "0.3", "0.7"}
for _, score := range tests {
t.Run(score, func(t *testing.T) {
t.Parallel()
f, stdout, _, reg := cmdutil.TestFactory(t, patchTestConfig(t))
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/okr/v2/objectives/123",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
},
})
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--score", score,
})
if err != nil {
t.Fatalf("unexpected error for score %q: %v", score, err)
}
})
}
}
func TestPatchValidate_Valid_DefaultStyleIsSimple(t *testing.T) {
t.Parallel()
// Default style is simple, so passing semi-plain JSON without --style should work
f, stdout, _, reg := cmdutil.TestFactory(t, patchTestConfig(t))
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/okr/v2/objectives/123",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
},
})
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--content", validSemiPlainJSON,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestPatchValidate_Valid_OnlyScore(t *testing.T) {
t.Parallel()
f, stdout, _, reg := cmdutil.TestFactory(t, patchTestConfig(t))
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/okr/v2/objectives/123",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
},
})
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--score", "0.0",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestPatchValidate_Valid_OnlyDeadline(t *testing.T) {
t.Parallel()
f, stdout, _, reg := cmdutil.TestFactory(t, patchTestConfig(t))
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/okr/v2/objectives/123",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
},
})
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--deadline", "1735776000000",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
// --- DryRun tests ---
func TestPatchDryRun_Objective_Content(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--style", "simple",
"--content", validSemiPlainJSON,
"--dry-run",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
output := stdout.String()
if !strings.Contains(output, "PATCH") {
t.Fatalf("expected PATCH method in dry-run output, got: %s", output)
}
if !strings.Contains(output, "/open-apis/okr/v2/objectives/123") {
t.Fatalf("expected objective URL in dry-run output, got: %s", output)
}
if !strings.Contains(output, "content=true") {
t.Fatalf("expected content=true in dry-run output, got: %s", output)
}
}
func TestPatchDryRun_KeyResult_Score(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "key-result",
"--target-id", "456",
"--score", "0.7",
"--dry-run",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
output := stdout.String()
if !strings.Contains(output, "PATCH") {
t.Fatalf("expected PATCH method in dry-run output, got: %s", output)
}
if !strings.Contains(output, "/open-apis/okr/v2/key_results/456") {
t.Fatalf("expected key_result URL in dry-run output, got: %s", output)
}
if !strings.Contains(output, "score=true") {
t.Fatalf("expected score=true in dry-run output, got: %s", output)
}
}
func TestPatchDryRun_Objective_MultipleFields(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "789",
"--style", "simple",
"--content", validSemiPlainJSON,
"--notes", validSemiPlainJSON,
"--score", "0.5",
"--deadline", "1735776000000",
"--dry-run",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
output := stdout.String()
if !strings.Contains(output, "content=true") ||
!strings.Contains(output, "notes=true") ||
!strings.Contains(output, "score=true") ||
!strings.Contains(output, "deadline=true") {
t.Fatalf("expected all fields in dry-run output, got: %s", output)
}
if !strings.Contains(output, `"user_id_type": "open_id"`) {
t.Fatalf("expected user_id_type param in dry-run output, got: %s", output)
}
}
func TestPatchDryRun_KeyResult_WithUserIDType(t *testing.T) {
t.Parallel()
f, stdout, _, _ := cmdutil.TestFactory(t, patchTestConfig(t))
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "key-result",
"--target-id", "456",
"--score", "0.7",
"--user-id-type", "user_id",
"--dry-run",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
output := stdout.String()
if !strings.Contains(output, `"user_id_type": "user_id"`) {
t.Fatalf("expected user_id_type=user_id in dry-run output, got: %s", output)
}
}
// --- Execute tests ---
func TestPatchExecute_Objective_Success(t *testing.T) {
t.Parallel()
f, stdout, _, reg := cmdutil.TestFactory(t, patchTestConfig(t))
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/okr/v2/objectives/123",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
},
BodyFilter: func(body []byte) bool {
var data map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil {
return false
}
// Check content is present and is a ContentBlock structure
content, ok := data["content"].(map[string]interface{})
if !ok {
return false
}
blocks, ok := content["blocks"].([]interface{})
if !ok || len(blocks) == 0 {
return false
}
// Check score
score, ok := data["score"].(float64)
if !ok || score != 0.5 {
return false
}
// Check notes
notes, ok := data["notes"].(map[string]interface{})
if !ok {
return false
}
notesBlocks, ok := notes["blocks"].([]interface{})
if !ok || len(notesBlocks) == 0 {
return false
}
// Check deadline
deadline, ok := data["deadline"].(string)
if !ok || deadline != "1735776000000" {
return false
}
return true
},
})
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--style", "simple",
"--content", validSemiPlainJSON,
"--notes", validSemiPlainJSON,
"--score", "0.5",
"--deadline", "1735776000000",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
output := stdout.String()
if !strings.Contains(output, `"level": "objective"`) {
t.Fatalf("expected objective level in output, got: %s", output)
}
if !strings.Contains(output, `"target_id": "123"`) {
t.Fatalf("expected target_id in output, got: %s", output)
}
if !strings.Contains(output, `"content": true`) ||
!strings.Contains(output, `"notes": true`) ||
!strings.Contains(output, `"score": true`) ||
!strings.Contains(output, `"deadline": true`) {
t.Fatalf("expected all field patches in output, got: %s", output)
}
}
func TestPatchExecute_KeyResult_Success(t *testing.T) {
t.Parallel()
f, stdout, _, reg := cmdutil.TestFactory(t, patchTestConfig(t))
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/okr/v2/key_results/456",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
},
BodyFilter: func(body []byte) bool {
var data map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil {
return false
}
// Check content is present
content, ok := data["content"].(map[string]interface{})
if !ok {
return false
}
blocks, ok := content["blocks"].([]interface{})
if !ok || len(blocks) == 0 {
return false
}
// Check score
score, ok := data["score"].(float64)
if !ok || score != 1.0 {
return false
}
// Notes should NOT be present for key-result
if _, hasNotes := data["notes"]; hasNotes {
return false
}
return true
},
})
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "key-result",
"--target-id", "456",
"--style", "richtext",
"--content", validContentBlockJSON,
"--score", "1.0",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
output := stdout.String()
if !strings.Contains(output, `"level": "key-result"`) {
t.Fatalf("expected key-result level in output, got: %s", output)
}
if !strings.Contains(output, `"target_id": "456"`) {
t.Fatalf("expected target_id in output, got: %s", output)
}
if !strings.Contains(output, `"content": true`) ||
!strings.Contains(output, `"score": true`) {
t.Fatalf("expected field patches in output, got: %s", output)
}
if strings.Contains(output, `"notes": true`) {
t.Fatalf("unexpected notes patch in key-result output, got: %s", output)
}
}
func TestPatchExecute_OnlyScore(t *testing.T) {
t.Parallel()
f, stdout, _, reg := cmdutil.TestFactory(t, patchTestConfig(t))
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/okr/v2/objectives/123",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
},
BodyFilter: func(body []byte) bool {
var data map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil {
return false
}
// Only score should be present
if _, hasContent := data["content"]; hasContent {
return false
}
if _, hasNotes := data["notes"]; hasNotes {
return false
}
if _, hasDeadline := data["deadline"]; hasDeadline {
return false
}
score, ok := data["score"].(float64)
return ok && score == 0.3
},
})
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--score", "0.3",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
output := stdout.String()
if !strings.Contains(output, `"score": true`) {
t.Fatalf("expected score patch in output, got: %s", output)
}
if strings.Contains(output, `"content": true`) ||
strings.Contains(output, `"notes": true`) ||
strings.Contains(output, `"deadline": true`) {
t.Fatalf("unexpected field patches in output, got: %s", output)
}
}
func TestPatchExecute_APIError(t *testing.T) {
t.Parallel()
f, stdout, _, reg := cmdutil.TestFactory(t, patchTestConfig(t))
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/okr/v2/objectives/123",
Body: map[string]interface{}{
"code": 9999,
"msg": "patch error",
},
})
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--score", "0.5",
})
if err == nil {
t.Fatal("expected error for API failure")
}
prob, ok := errs.ProblemOf(err)
if !ok {
t.Fatalf("expected typed error, got: %v", err)
}
if prob.Category != errs.CategoryAPI {
t.Fatalf("expected CategoryAPI, got %q", prob.Category)
}
var apiErr *errs.APIError
if !errors.As(err, &apiErr) {
t.Fatalf("expected error to be *errs.APIError, got: %T", err)
}
if !errors.Is(err, apiErr) {
t.Fatal("errors.Is should find the APIError in the chain")
}
}
func TestPatchExecute_WithUserIDType(t *testing.T) {
t.Parallel()
f, stdout, _, reg := cmdutil.TestFactory(t, patchTestConfig(t))
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/okr/v2/key_results/789",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
},
})
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "key-result",
"--target-id", "789",
"--score", "0.8",
"--user-id-type", "union_id",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
// --- parsePatchParams tests ---
func TestParsePatchParams_ScoreRounding(t *testing.T) {
t.Parallel()
// Valid score with one decimal place is accepted (score 0.3)
f, stdout, _, reg := cmdutil.TestFactory(t, patchTestConfig(t))
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/okr/v2/objectives/123",
Body: map[string]interface{}{
"code": 0,
"msg": "ok",
},
BodyFilter: func(body []byte) bool {
var data map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil {
return false
}
score, ok := data["score"].(float64)
// 0.33 should round to 0.3
return ok && score == 0.3
},
})
err := runPatchShortcut(t, f, stdout, []string{
"+patch",
"--level", "objective",
"--target-id", "123",
"--score", "0.3",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}