mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
303 lines
9.6 KiB
Go
303 lines
9.6 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package base
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
extcs "github.com/larksuite/cli/extension/contentsafety"
|
|
"github.com/larksuite/cli/internal/cmdutil"
|
|
"github.com/larksuite/cli/internal/core"
|
|
"github.com/larksuite/cli/internal/output"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
type recordMarkdownCSTestProvider struct {
|
|
alert *extcs.Alert
|
|
}
|
|
|
|
func (p *recordMarkdownCSTestProvider) Name() string { return "test" }
|
|
func (p *recordMarkdownCSTestProvider) Scan(_ context.Context, _ extcs.ScanRequest) (*extcs.Alert, error) {
|
|
return p.alert, nil
|
|
}
|
|
|
|
func newRecordMarkdownTestRuntime(stdout, stderr *bytes.Buffer) *common.RuntimeContext {
|
|
parentCmd := &cobra.Command{Use: "lark-cli"}
|
|
baseCmd := &cobra.Command{Use: "base"}
|
|
cmd := &cobra.Command{Use: "+record-list"}
|
|
cmd.Flags().String("format", "markdown", "")
|
|
parentCmd.AddCommand(baseCmd)
|
|
baseCmd.AddCommand(cmd)
|
|
return &common.RuntimeContext{
|
|
Config: &core.CliConfig{Brand: core.BrandFeishu},
|
|
Cmd: cmd,
|
|
Factory: &cmdutil.Factory{IOStreams: &cmdutil.IOStreams{Out: stdout, ErrOut: stderr}},
|
|
}
|
|
}
|
|
|
|
func TestRenderRecordMarkdownEmptyResult(t *testing.T) {
|
|
got, err := renderRecordMarkdown(map[string]interface{}{
|
|
"fields": []interface{}{"Name", "Age"},
|
|
"record_id_list": []interface{}{},
|
|
"data": []interface{}{},
|
|
"has_more": false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("err=%v", err)
|
|
}
|
|
for _, want := range []string{
|
|
"| _record_id | Name | Age |",
|
|
"Meta: count=0; has_more=false",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("output missing %q:\n%s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRenderRecordMarkdownEscapesTableCells(t *testing.T) {
|
|
got, err := renderRecordMarkdown(map[string]interface{}{
|
|
"fields": []interface{}{"Name|Label", "Note"},
|
|
"record_id_list": []interface{}{"rec_1"},
|
|
"data": []interface{}{[]interface{}{"A|B", "line1\nline2"}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("err=%v", err)
|
|
}
|
|
for _, want := range []string{
|
|
"| _record_id | Name\\|Label | Note |",
|
|
"| rec_1 | A\\|B | line1<br>line2 |",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("output missing %q:\n%s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRenderRecordGetMarkdownSingleRecordUsesKVLayout(t *testing.T) {
|
|
got, err := renderRecordGetMarkdown(map[string]interface{}{
|
|
"fields": []interface{}{"Name|Label", "Note"},
|
|
"record_id_list": []interface{}{"rec_1"},
|
|
"data": []interface{}{[]interface{}{"A|B", "line1\nline2"}},
|
|
"has_more": false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("err=%v", err)
|
|
}
|
|
for _, want := range []string{
|
|
"- `_record_id`: rec_1",
|
|
"- `Name|Label`: A|B",
|
|
"- `Note`: line1\nline2",
|
|
"Meta: count=1; has_more=false",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("output missing %q:\n%s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRenderRecordGetMarkdownSingleMissingRecordUsesNotFoundLayout(t *testing.T) {
|
|
got, err := renderRecordGetMarkdown(map[string]interface{}{
|
|
"fields": []interface{}{"Name", "Note"},
|
|
"record_id_list": []interface{}{"rec_missing"},
|
|
"data": []interface{}{[]interface{}{nil, nil}},
|
|
"record_not_found": []interface{}{"rec_missing"},
|
|
"has_more": false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("err=%v", err)
|
|
}
|
|
for _, want := range []string{
|
|
"Record not found.",
|
|
"- `_record_id`: rec_missing",
|
|
"Meta: count=1; has_more=false; record_not_found=1",
|
|
"Missing records: rec_missing",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("output missing %q:\n%s", want, got)
|
|
}
|
|
}
|
|
if strings.Contains(got, "- `Name`:") {
|
|
t.Fatalf("missing record layout should not render business fields:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestRenderRecordMarkdownIncludesMissingRecords(t *testing.T) {
|
|
got, err := renderRecordMarkdown(map[string]interface{}{
|
|
"fields": []interface{}{"Name"},
|
|
"record_id_list": []interface{}{"rec_1", "rec_missing"},
|
|
"data": []interface{}{[]interface{}{"Alice"}, []interface{}{nil}},
|
|
"record_not_found": []interface{}{"rec_missing"},
|
|
"has_more": false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("err=%v", err)
|
|
}
|
|
for _, want := range []string{
|
|
"Meta: count=2; has_more=false; record_not_found=1",
|
|
"Missing records: rec_missing",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("output missing %q:\n%s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRenderRecordMarkdownTruncatesIgnoredFields(t *testing.T) {
|
|
ignored := make([]interface{}, maxRecordMarkdownIgnoredFields+2)
|
|
for i := range ignored {
|
|
ignored[i] = fmt.Sprintf("Field%d", i+1)
|
|
}
|
|
got, err := renderRecordMarkdown(map[string]interface{}{
|
|
"fields": []interface{}{"Name"},
|
|
"record_id_list": []interface{}{"rec_1"},
|
|
"data": []interface{}{[]interface{}{"Alice"}},
|
|
"ignored_fields": ignored,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("err=%v", err)
|
|
}
|
|
if !strings.Contains(got, fmt.Sprintf("ignored_fields=%d", len(ignored))) ||
|
|
!strings.Contains(got, fmt.Sprintf("...(%d total)", len(ignored))) ||
|
|
strings.Contains(got, "Field22") {
|
|
t.Fatalf("ignored field truncation mismatch:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestOutputRecordMarkdownContentSafetyWarnKeepsStdoutClean(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONTENT_SAFETY_MODE", "warn")
|
|
extcs.Register(&recordMarkdownCSTestProvider{
|
|
alert: &extcs.Alert{Provider: "test", MatchedRules: []string{"r1"}},
|
|
})
|
|
defer extcs.Register(nil)
|
|
|
|
stdout := &bytes.Buffer{}
|
|
stderr := &bytes.Buffer{}
|
|
err := outputRecordMarkdown(newRecordMarkdownTestRuntime(stdout, stderr), map[string]interface{}{
|
|
"fields": []interface{}{"Name"},
|
|
"record_id_list": []interface{}{"rec_1"},
|
|
"data": []interface{}{[]interface{}{"Alice"}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("err=%v", err)
|
|
}
|
|
if got := stdout.String(); !strings.Contains(got, "| rec_1 | Alice |") || strings.Contains(got, "content safety") {
|
|
t.Fatalf("stdout should contain only markdown data, got:\n%s", got)
|
|
}
|
|
if got := stderr.String(); !strings.Contains(got, "warning: content safety alert") {
|
|
t.Fatalf("stderr missing content safety warning:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestOutputRecordMarkdownContentSafetyBlockDoesNotWriteStdout(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONTENT_SAFETY_MODE", "block")
|
|
extcs.Register(&recordMarkdownCSTestProvider{
|
|
alert: &extcs.Alert{Provider: "test", MatchedRules: []string{"r1"}},
|
|
})
|
|
defer extcs.Register(nil)
|
|
|
|
stdout := &bytes.Buffer{}
|
|
stderr := &bytes.Buffer{}
|
|
err := outputRecordMarkdown(newRecordMarkdownTestRuntime(stdout, stderr), map[string]interface{}{
|
|
"fields": []interface{}{"Name"},
|
|
"record_id_list": []interface{}{"rec_1"},
|
|
"data": []interface{}{[]interface{}{"Alice"}},
|
|
})
|
|
var csErr *errs.ContentSafetyError
|
|
if !errors.As(err, &csErr) {
|
|
t.Fatalf("err=%v, want typed content safety error", err)
|
|
}
|
|
if len(csErr.Rules) != 1 || csErr.Rules[0] != "r1" {
|
|
t.Fatalf("rules=%v", csErr.Rules)
|
|
}
|
|
if stdout.Len() > 0 {
|
|
t.Fatalf("block mode should not write stdout, got:\n%s", stdout.String())
|
|
}
|
|
if stderr.Len() > 0 {
|
|
t.Fatalf("block mode should not write warning to stderr, got:\n%s", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestOutputRecordMarkdownFallsBackToJSONWhenRenderFails(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONTENT_SAFETY_MODE", "off")
|
|
|
|
stdout := &bytes.Buffer{}
|
|
stderr := &bytes.Buffer{}
|
|
err := outputRecordMarkdown(newRecordMarkdownTestRuntime(stdout, stderr), map[string]interface{}{
|
|
"records": map[string]interface{}{
|
|
"schema": []interface{}{"Name"},
|
|
"rows": []interface{}{[]interface{}{"Alice"}},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("err=%v", err)
|
|
}
|
|
if strings.Contains(stdout.String(), "markdown render failed") {
|
|
t.Fatalf("stdout should not contain fallback warning:\n%s", stdout.String())
|
|
}
|
|
var env output.Envelope
|
|
if err := json.Unmarshal(stdout.Bytes(), &env); err != nil {
|
|
t.Fatalf("stdout should be JSON fallback, got err=%v stdout=%s", err, stdout.String())
|
|
}
|
|
if !env.OK || !strings.Contains(stdout.String(), `"records"`) {
|
|
t.Fatalf("stdout missing JSON fallback data:\n%s", stdout.String())
|
|
}
|
|
if got := stderr.String(); !strings.Contains(got, "warning: record markdown render failed, falling back to json") {
|
|
t.Fatalf("stderr missing fallback warning:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestOutputRecordMarkdownDefaultFormatAllowsJqJSONFallback(t *testing.T) {
|
|
t.Setenv("LARKSUITE_CLI_CONTENT_SAFETY_MODE", "off")
|
|
|
|
stdout := &bytes.Buffer{}
|
|
stderr := &bytes.Buffer{}
|
|
runtime := newRecordMarkdownTestRuntime(stdout, stderr)
|
|
runtime.JqExpr = ".data.record_id_list[0]"
|
|
err := outputRecordMarkdown(runtime, map[string]interface{}{
|
|
"fields": []interface{}{"Name"},
|
|
"record_id_list": []interface{}{"rec_1"},
|
|
"data": []interface{}{[]interface{}{"Alice"}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("err=%v", err)
|
|
}
|
|
if got := strings.TrimSpace(stdout.String()); got != "rec_1" {
|
|
t.Fatalf("stdout jq fallback mismatch: %q", got)
|
|
}
|
|
if stderr.Len() > 0 {
|
|
t.Fatalf("stderr should be empty, got:\n%s", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestOutputRecordMarkdownExplicitFormatRejectsJq(t *testing.T) {
|
|
stdout := &bytes.Buffer{}
|
|
stderr := &bytes.Buffer{}
|
|
runtime := newRecordMarkdownTestRuntime(stdout, stderr)
|
|
runtime.JqExpr = ".data"
|
|
if err := runtime.Cmd.Flags().Set("format", "markdown"); err != nil {
|
|
t.Fatalf("set format: %v", err)
|
|
}
|
|
err := outputRecordMarkdown(runtime, map[string]interface{}{
|
|
"fields": []interface{}{"Name"},
|
|
"record_id_list": []interface{}{"rec_1"},
|
|
"data": []interface{}{[]interface{}{"Alice"}},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "--jq and --format markdown are mutually exclusive") {
|
|
t.Fatalf("err=%v, want jq markdown conflict", err)
|
|
}
|
|
if stdout.Len() > 0 {
|
|
t.Fatalf("stdout should be empty, got:\n%s", stdout.String())
|
|
}
|
|
}
|