Files
larksuite-cli/shortcuts/base/record_markdown_test.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())
}
}