mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
* feat(api): add --json flag as no-op alias for --format json * feat(service): add --json flag as no-op alias for --format json * feat(shortcut): add --json flag as no-op alias for --format json Skip registration when a custom --json flag already exists on the command (e.g. base shortcuts use --json for body input). Change-Id: If66236cadeea7fa81811061cce775deff51b92ce
741 lines
22 KiB
Go
741 lines
22 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package api
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"sort"
|
|
"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 TestApiCmd_FlagParsing(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
var gotOpts *APIOptions
|
|
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
|
gotOpts = opts
|
|
return nil
|
|
})
|
|
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "bot", "--dry-run"})
|
|
err := cmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if gotOpts.Method != "GET" {
|
|
t.Errorf("expected method GET, got %s", gotOpts.Method)
|
|
}
|
|
if gotOpts.Path != "/open-apis/test" {
|
|
t.Errorf("expected path /open-apis/test, got %s", gotOpts.Path)
|
|
}
|
|
if gotOpts.As != core.AsBot {
|
|
t.Errorf("expected as=bot, got %s", gotOpts.As)
|
|
}
|
|
if !gotOpts.DryRun {
|
|
t.Error("expected DryRun=true")
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_DryRun(t *testing.T) {
|
|
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
cmd := NewCmdApi(f, nil)
|
|
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "bot", "--dry-run"})
|
|
err := cmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
output := stdout.String()
|
|
if !strings.Contains(output, "Dry Run") {
|
|
t.Error("expected dry run output")
|
|
}
|
|
if !strings.Contains(output, "/open-apis/test") {
|
|
t.Error("expected path in dry run output")
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_BotMode(t *testing.T) {
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
// Register API endpoint stub
|
|
reg.Register(&httpmock.Stub{
|
|
URL: "/open-apis/test",
|
|
Body: map[string]interface{}{"code": 0, "msg": "ok", "data": map[string]interface{}{"result": "success"}},
|
|
})
|
|
|
|
cmd := NewCmdApi(f, nil)
|
|
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "bot"})
|
|
err := cmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !strings.Contains(stdout.String(), "success") {
|
|
t.Error("expected 'success' in output")
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_MissingArgs(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
cmd := NewCmdApi(f, nil)
|
|
cmd.SetArgs([]string{"GET"}) // missing path
|
|
err := cmd.Execute()
|
|
if err == nil {
|
|
t.Error("expected error for missing args")
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_InvalidParamsJSON(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
cmd := NewCmdApi(f, nil)
|
|
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "bot", "--params", "{bad"})
|
|
err := cmd.Execute()
|
|
if err == nil {
|
|
t.Error("expected validation error for invalid JSON")
|
|
}
|
|
}
|
|
|
|
func TestApiValidArgsFunction(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
cmd := NewCmdApi(f, nil)
|
|
fn := cmd.ValidArgsFunction
|
|
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
toComplete string
|
|
wantComps []string
|
|
wantDir cobra.ShellCompDirective
|
|
}{
|
|
{
|
|
name: "no args returns HTTP methods",
|
|
args: []string{},
|
|
toComplete: "",
|
|
wantComps: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
|
|
wantDir: cobra.ShellCompDirectiveNoFileComp,
|
|
},
|
|
{
|
|
name: "one arg returns nil with NoFileComp",
|
|
args: []string{"GET"},
|
|
toComplete: "",
|
|
wantComps: nil,
|
|
wantDir: cobra.ShellCompDirectiveNoFileComp,
|
|
},
|
|
{
|
|
name: "two args returns nil with NoFileComp",
|
|
args: []string{"GET", "/path"},
|
|
toComplete: "",
|
|
wantComps: nil,
|
|
wantDir: cobra.ShellCompDirectiveNoFileComp,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
comps, dir := fn(cmd, tt.args, tt.toComplete)
|
|
if dir != tt.wantDir {
|
|
t.Errorf("directive = %d, want %d", dir, tt.wantDir)
|
|
}
|
|
if tt.wantComps == nil {
|
|
if comps != nil {
|
|
t.Errorf("completions = %v, want nil", comps)
|
|
}
|
|
return
|
|
}
|
|
sort.Strings(comps)
|
|
sort.Strings(tt.wantComps)
|
|
if len(comps) != len(tt.wantComps) {
|
|
t.Errorf("completions = %v, want %v", comps, tt.wantComps)
|
|
return
|
|
}
|
|
for i := range comps {
|
|
if comps[i] != tt.wantComps[i] {
|
|
t.Errorf("completions = %v, want %v", comps, tt.wantComps)
|
|
break
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewCmdApi_StrictModeHidesAsFlag(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu, SupportedIdentities: 2,
|
|
})
|
|
|
|
cmd := NewCmdApi(f, nil)
|
|
flag := cmd.Flags().Lookup("as")
|
|
if flag == nil {
|
|
t.Fatal("expected --as flag to be registered")
|
|
}
|
|
if !flag.Hidden {
|
|
t.Fatal("expected --as flag to be hidden in strict mode")
|
|
}
|
|
if got := flag.DefValue; got != "bot" {
|
|
t.Fatalf("default value = %q, want %q", got, "bot")
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_PageLimitDefault(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
var gotOpts *APIOptions
|
|
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
|
gotOpts = opts
|
|
return nil
|
|
})
|
|
cmd.SetArgs([]string{"GET", "/open-apis/test"})
|
|
err := cmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if gotOpts.PageLimit != 10 {
|
|
t.Errorf("expected default PageLimit=10, got %d", gotOpts.PageLimit)
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_ParamsAndDataBothStdinConflict(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
cmd := NewCmdApi(f, nil)
|
|
cmd.SetArgs([]string{"POST", "/open-apis/test", "--as", "bot", "--params", "-", "--data", "-"})
|
|
err := cmd.Execute()
|
|
if err == nil {
|
|
t.Fatal("expected error when both --params and --data use stdin")
|
|
}
|
|
if !strings.Contains(err.Error(), "cannot both read from stdin") {
|
|
t.Errorf("expected stdin conflict error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_OutputAndPageAllConflict(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
var gotOpts *APIOptions
|
|
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
|
gotOpts = opts
|
|
return apiRun(opts)
|
|
})
|
|
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "bot", "--page-all", "--output", "file.bin"})
|
|
err := cmd.Execute()
|
|
if err == nil {
|
|
t.Fatal("expected error for --output + --page-all conflict")
|
|
}
|
|
if gotOpts != nil && !strings.Contains(err.Error(), "mutually exclusive") {
|
|
t.Errorf("expected 'mutually exclusive' error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_BinaryResponse_AutoSave(t *testing.T) {
|
|
f, stdout, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app-bin", AppSecret: "test-secret-bin", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
reg.Register(&httpmock.Stub{
|
|
URL: "/open-apis/drive/v1/files/xxx/download",
|
|
RawBody: []byte("fake-binary-content"),
|
|
ContentType: "application/octet-stream",
|
|
})
|
|
|
|
cmd := NewCmdApi(f, nil)
|
|
cmd.SetArgs([]string{"GET", "/open-apis/drive/v1/files/xxx/download", "--as", "bot"})
|
|
err := cmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !strings.Contains(stderr.String(), "binary response detected") {
|
|
t.Error("expected binary response hint in stderr")
|
|
}
|
|
if !strings.Contains(stdout.String(), "saved_path") {
|
|
t.Error("expected saved_path in output")
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_PageAll_NonBatchAPI_FallbackToJSON(t *testing.T) {
|
|
f, stdout, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app-pageall1", AppSecret: "test-secret-pageall1", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
// Register a non-batch API that returns scalar data (no array field)
|
|
reg.Register(&httpmock.Stub{
|
|
URL: "/open-apis/contact/v3/users/u123",
|
|
Body: map[string]interface{}{
|
|
"code": 0, "msg": "ok",
|
|
"data": map[string]interface{}{
|
|
"user_id": "u123",
|
|
"name": "Test User",
|
|
},
|
|
},
|
|
})
|
|
|
|
cmd := NewCmdApi(f, nil)
|
|
cmd.SetArgs([]string{"GET", "/open-apis/contact/v3/users/u123", "--as", "bot", "--page-all", "--format", "ndjson"})
|
|
err := cmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
// Should print fallback warning to stderr
|
|
if !strings.Contains(stderr.String(), "warning: this API does not return a list") {
|
|
t.Error("expected fallback warning in stderr")
|
|
}
|
|
if !strings.Contains(stderr.String(), "falling back to json") {
|
|
t.Error("expected 'falling back to json' in stderr")
|
|
}
|
|
// Should output JSON result to stdout
|
|
if !strings.Contains(stdout.String(), "u123") {
|
|
t.Error("expected user_id in JSON output")
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_PageAll_NonBatchAPI_ErrorStillOutputsJSON(t *testing.T) {
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app-pageall-err", AppSecret: "test-secret-pageall-err", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
// Non-batch API that returns a business error (code != 0)
|
|
reg.Register(&httpmock.Stub{
|
|
URL: "/open-apis/im/v1/chats/oc_xxx/announcement",
|
|
Body: map[string]interface{}{
|
|
"code": 230001, "msg": "no permission",
|
|
},
|
|
})
|
|
|
|
cmd := NewCmdApi(f, nil)
|
|
cmd.SetArgs([]string{"GET", "/open-apis/im/v1/chats/oc_xxx/announcement", "--as", "bot", "--page-all"})
|
|
err := cmd.Execute()
|
|
// Should return an error
|
|
if err == nil {
|
|
t.Fatal("expected an error for non-zero code")
|
|
}
|
|
// Should still output the response body so user can see the error details
|
|
if !strings.Contains(stdout.String(), "230001") {
|
|
t.Errorf("expected error response in stdout, got: %s", stdout.String())
|
|
}
|
|
if !strings.Contains(stdout.String(), "no permission") {
|
|
t.Errorf("expected error message in stdout, got: %s", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_PageAll_BatchAPI_StreamsItems(t *testing.T) {
|
|
f, stdout, stderr, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app-pageall2", AppSecret: "test-secret-pageall2", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
// Register a batch API that returns an array field
|
|
reg.Register(&httpmock.Stub{
|
|
URL: "/open-apis/contact/v3/users",
|
|
Body: map[string]interface{}{
|
|
"code": 0, "msg": "ok",
|
|
"data": map[string]interface{}{
|
|
"items": []interface{}{map[string]interface{}{"id": "1"}, map[string]interface{}{"id": "2"}},
|
|
"has_more": false,
|
|
},
|
|
},
|
|
})
|
|
|
|
cmd := NewCmdApi(f, nil)
|
|
cmd.SetArgs([]string{"GET", "/open-apis/contact/v3/users", "--as", "bot", "--page-all", "--format", "ndjson"})
|
|
err := cmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
// Should NOT print fallback warning
|
|
if strings.Contains(stderr.String(), "warning: this API does not return a list") {
|
|
t.Error("expected no fallback warning for batch API")
|
|
}
|
|
// Should stream ndjson items
|
|
if !strings.Contains(stdout.String(), `"id"`) {
|
|
t.Error("expected streamed items in output")
|
|
}
|
|
}
|
|
|
|
func TestNormalisePath_StripsQueryAndFragment(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
name string
|
|
raw string
|
|
want string
|
|
}{
|
|
{"plain path", "/open-apis/test", "/open-apis/test"},
|
|
{"with query", "/open-apis/test?admin=true", "/open-apis/test"},
|
|
{"with fragment", "/open-apis/test#section", "/open-apis/test"},
|
|
{"with both", "/open-apis/test?a=1#frag", "/open-apis/test"},
|
|
{"full URL with query", "https://open.feishu.cn/open-apis/foo?bar=1", "/open-apis/foo"},
|
|
{"short path with query", "contact/v3/users?page_size=50", "/open-apis/contact/v3/users"},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := normalisePath(tt.raw)
|
|
if got != tt.want {
|
|
t.Errorf("normalisePath(%q) = %q, want %q", tt.raw, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_JqFlag_Parsing(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
var gotOpts *APIOptions
|
|
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
|
gotOpts = opts
|
|
return nil
|
|
})
|
|
cmd.SetArgs([]string{"GET", "/open-apis/test", "--jq", ".data"})
|
|
err := cmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if gotOpts.JqExpr != ".data" {
|
|
t.Errorf("expected JqExpr=.data, got %s", gotOpts.JqExpr)
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_JqFlag_ShortForm(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
var gotOpts *APIOptions
|
|
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
|
gotOpts = opts
|
|
return nil
|
|
})
|
|
cmd.SetArgs([]string{"GET", "/open-apis/test", "-q", ".data"})
|
|
err := cmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if gotOpts.JqExpr != ".data" {
|
|
t.Errorf("expected JqExpr=.data, got %s", gotOpts.JqExpr)
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_JqAndOutputConflict(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
|
return apiRun(opts)
|
|
})
|
|
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "bot", "--jq", ".data", "--output", "file.bin"})
|
|
err := cmd.Execute()
|
|
if err == nil {
|
|
t.Fatal("expected error for --jq + --output conflict")
|
|
}
|
|
if !strings.Contains(err.Error(), "mutually exclusive") {
|
|
t.Errorf("expected 'mutually exclusive' error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_JqFilter_AppliesExpression(t *testing.T) {
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app-jq", AppSecret: "test-secret-jq", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
reg.Register(&httpmock.Stub{
|
|
URL: "/open-apis/test/jq",
|
|
Body: map[string]interface{}{
|
|
"code": 0, "msg": "ok",
|
|
"data": map[string]interface{}{
|
|
"items": []interface{}{
|
|
map[string]interface{}{"name": "Alice"},
|
|
map[string]interface{}{"name": "Bob"},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
cmd := NewCmdApi(f, nil)
|
|
cmd.SetArgs([]string{"GET", "/open-apis/test/jq", "--as", "bot", "--jq", ".data.items[].name"})
|
|
err := cmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
out := stdout.String()
|
|
if !strings.Contains(out, "Alice") || !strings.Contains(out, "Bob") {
|
|
t.Errorf("expected jq-filtered names, got: %s", out)
|
|
}
|
|
// Should NOT contain the full envelope structure
|
|
if strings.Contains(out, `"code"`) {
|
|
t.Errorf("expected jq to filter out envelope, got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_JqAndFormatConflict(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
|
return apiRun(opts)
|
|
})
|
|
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "bot", "--jq", ".data", "--format", "ndjson"})
|
|
err := cmd.Execute()
|
|
if err == nil {
|
|
t.Fatal("expected error for --jq + --format ndjson conflict")
|
|
}
|
|
if !strings.Contains(err.Error(), "mutually exclusive") {
|
|
t.Errorf("expected 'mutually exclusive' error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_JqInvalidExpression(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
|
return apiRun(opts)
|
|
})
|
|
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "bot", "--jq", "invalid["})
|
|
err := cmd.Execute()
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid jq expression")
|
|
}
|
|
if !strings.Contains(err.Error(), "invalid jq expression") {
|
|
t.Errorf("expected 'invalid jq expression' error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_PageAll_WithJq(t *testing.T) {
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app-pjq", AppSecret: "test-secret-pjq", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
reg.Register(&httpmock.Stub{
|
|
URL: "/open-apis/contact/v3/users",
|
|
Body: map[string]interface{}{
|
|
"code": 0, "msg": "ok",
|
|
"data": map[string]interface{}{
|
|
"items": []interface{}{map[string]interface{}{"id": "u1"}, map[string]interface{}{"id": "u2"}},
|
|
"has_more": false,
|
|
},
|
|
},
|
|
})
|
|
|
|
cmd := NewCmdApi(f, nil)
|
|
cmd.SetArgs([]string{"GET", "/open-apis/contact/v3/users", "--as", "bot", "--page-all", "--jq", ".data.items[].id"})
|
|
err := cmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
out := stdout.String()
|
|
if !strings.Contains(out, "u1") || !strings.Contains(out, "u2") {
|
|
t.Errorf("expected jq-filtered ids, got: %s", out)
|
|
}
|
|
if strings.Contains(out, `"code"`) {
|
|
t.Errorf("expected jq to filter out envelope, got: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_MethodUppercase(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
var gotOpts *APIOptions
|
|
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
|
gotOpts = opts
|
|
return nil
|
|
})
|
|
cmd.SetArgs([]string{"post", "/test"})
|
|
err := cmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if gotOpts.Method != "POST" {
|
|
t.Errorf("expected method POST (uppercased), got %s", gotOpts.Method)
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_FileFlagParsing(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
var gotOpts *APIOptions
|
|
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
|
gotOpts = opts
|
|
return nil
|
|
})
|
|
cmd.SetArgs([]string{"POST", "/open-apis/test", "--file", "image=photo.jpg", "--data", `{"image_type":"message"}`})
|
|
err := cmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if gotOpts.File != "image=photo.jpg" {
|
|
t.Errorf("expected File = %q, got %q", "image=photo.jpg", gotOpts.File)
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_FileAndOutputConflict(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
|
return apiRun(opts)
|
|
})
|
|
cmd.SetArgs([]string{"POST", "/open-apis/test", "--as", "bot", "--file", "photo.jpg", "--output", "out.json"})
|
|
err := cmd.Execute()
|
|
if err == nil {
|
|
t.Fatal("expected error for --file with --output")
|
|
}
|
|
if !strings.Contains(err.Error(), "mutually exclusive") {
|
|
t.Errorf("expected mutual exclusion error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_FileWithGET(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
|
return apiRun(opts)
|
|
})
|
|
cmd.SetArgs([]string{"GET", "/open-apis/test", "--as", "bot", "--file", "photo.jpg"})
|
|
err := cmd.Execute()
|
|
if err == nil {
|
|
t.Fatal("expected error for --file with GET")
|
|
}
|
|
if !strings.Contains(err.Error(), "requires POST") {
|
|
t.Errorf("expected method error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_FileStdinConflictWithData(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
|
return apiRun(opts)
|
|
})
|
|
cmd.SetArgs([]string{"POST", "/open-apis/test", "--as", "bot", "--file", "-", "--data", "-"})
|
|
err := cmd.Execute()
|
|
if err == nil {
|
|
t.Fatal("expected error for --file stdin with --data stdin")
|
|
}
|
|
if !strings.Contains(err.Error(), "cannot both read from stdin") {
|
|
t.Errorf("expected stdin conflict error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_DryRunWithFile(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
tmpFile := tmpDir + "/test.jpg"
|
|
if err := os.WriteFile(tmpFile, []byte("fake-image"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
cmd := NewCmdApi(f, nil)
|
|
cmd.SetArgs([]string{"POST", "/open-apis/im/v1/images", "--file", "image=" + tmpFile, "--data", `{"image_type":"message"}`, "--dry-run", "--as", "bot"})
|
|
err := cmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
out := stdout.String()
|
|
if !strings.Contains(out, "image") {
|
|
t.Errorf("expected dry-run output to mention file field, got: %s", out)
|
|
}
|
|
if !strings.Contains(out, "Dry Run") {
|
|
t.Errorf("expected dry-run header, got: %s", out)
|
|
}
|
|
}
|
|
|
|
// TestApiCmd_PermissionError_DerivesFirstClassFields pins that when a Lark
|
|
// API returns a missing-scope failure, the typed *errs.PermissionError
|
|
// surfaced by `lark-cli api` lifts the diagnostic signals BuildAPIError
|
|
// consumed during classification into first-class wire fields
|
|
// (MissingScopes, LogID, ConsoleURL). The wire shape is the typed envelope
|
|
// — there is no raw-payload passthrough; new Lark diagnostic fields require
|
|
// a CLI release.
|
|
func TestApiCmd_PermissionError_DerivesFirstClassFields(t *testing.T) {
|
|
f, _, _, reg := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "cli_test_perm", AppSecret: "secret", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
reg.Register(&httpmock.Stub{
|
|
URL: "/open-apis/docx/v1/documents/test",
|
|
Body: map[string]interface{}{
|
|
"code": 99991679,
|
|
"msg": "scope missing",
|
|
"log_id": "20260527-test-log",
|
|
"error": map[string]interface{}{
|
|
"permission_violations": []interface{}{
|
|
map[string]interface{}{"subject": "docx:document"},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
cmd := NewCmdApi(f, nil)
|
|
cmd.SetArgs([]string{"GET", "/open-apis/docx/v1/documents/test", "--as", "bot"})
|
|
err := cmd.Execute()
|
|
if err == nil {
|
|
t.Fatal("expected error for non-zero code")
|
|
}
|
|
|
|
var pe *errs.PermissionError
|
|
if !errors.As(err, &pe) {
|
|
t.Fatalf("expected *errs.PermissionError, got %T: %v", err, err)
|
|
}
|
|
|
|
if len(pe.MissingScopes) != 1 || pe.MissingScopes[0] != "docx:document" {
|
|
t.Errorf("MissingScopes = %v, want [docx:document]", pe.MissingScopes)
|
|
}
|
|
if pe.LogID != "20260527-test-log" {
|
|
t.Errorf("LogID = %q, want %q", pe.LogID, "20260527-test-log")
|
|
}
|
|
}
|
|
|
|
func TestApiCmd_JsonFlag_Accepted(t *testing.T) {
|
|
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
|
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
|
})
|
|
|
|
var gotOpts *APIOptions
|
|
cmd := NewCmdApi(f, func(opts *APIOptions) error {
|
|
gotOpts = opts
|
|
return nil
|
|
})
|
|
cmd.SetArgs([]string{"GET", "/open-apis/test", "--json"})
|
|
err := cmd.Execute()
|
|
if err != nil {
|
|
t.Fatalf("--json should be accepted without error, got: %v", err)
|
|
}
|
|
if gotOpts.Method != "GET" {
|
|
t.Errorf("expected method GET, got %s", gotOpts.Method)
|
|
}
|
|
}
|