Files
larksuite-cli/internal/output/format_test.go
梁硕 83dfb068ad feat: open-source lark-cli — the official CLI for Lark/Feishu
Change-Id: I113d9cdb5403cec347efe4595415e34a18b7decf
2026-03-28 10:36:25 +08:00

302 lines
8.1 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package output
import (
"bytes"
"encoding/json"
"strings"
"testing"
)
func TestFormatValue_JSON(t *testing.T) {
data := map[string]interface{}{"name": "Alice"}
var buf bytes.Buffer
FormatValue(&buf, data, FormatJSON)
out := buf.String()
// Should be pretty-printed JSON
if !strings.Contains(out, `"name"`) {
t.Errorf("JSON output should contain field name, got:\n%s", out)
}
if !strings.Contains(out, "Alice") {
t.Errorf("JSON output should contain value, got:\n%s", out)
}
}
func TestFormatValue_NDJSON(t *testing.T) {
data := map[string]interface{}{
"data": map[string]interface{}{
"items": []interface{}{
map[string]interface{}{"id": float64(1)},
map[string]interface{}{"id": float64(2)},
},
},
}
var buf bytes.Buffer
FormatValue(&buf, data, FormatNDJSON)
lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
if len(lines) != 2 {
t.Fatalf("NDJSON should output 2 lines, got %d:\n%s", len(lines), buf.String())
}
for _, line := range lines {
var obj map[string]interface{}
if err := json.Unmarshal([]byte(line), &obj); err != nil {
t.Errorf("each NDJSON line should be valid JSON: %s", line)
}
}
}
func TestFormatValue_Table(t *testing.T) {
data := map[string]interface{}{
"data": map[string]interface{}{
"items": []interface{}{
map[string]interface{}{"name": "Alice"},
},
},
}
var buf bytes.Buffer
FormatValue(&buf, data, FormatTable)
out := buf.String()
if !strings.Contains(out, "name") {
t.Errorf("table output should contain 'name' header, got:\n%s", out)
}
if !strings.Contains(out, "Alice") {
t.Errorf("table output should contain 'Alice', got:\n%s", out)
}
}
func TestFormatValue_CSV(t *testing.T) {
data := map[string]interface{}{
"data": map[string]interface{}{
"items": []interface{}{
map[string]interface{}{"name": "Alice"},
},
},
}
var buf bytes.Buffer
FormatValue(&buf, data, FormatCSV)
out := buf.String()
lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
if len(lines) != 2 {
t.Fatalf("CSV should have header + 1 row, got %d lines:\n%s", len(lines), out)
}
if lines[0] != "name" {
t.Errorf("CSV header should be 'name', got: %s", lines[0])
}
if lines[1] != "Alice" {
t.Errorf("CSV row should be 'Alice', got: %s", lines[1])
}
}
func TestPaginatedFormatter_JSON(t *testing.T) {
var buf bytes.Buffer
pf := NewPaginatedFormatter(&buf, FormatJSON)
pf.FormatPage([]interface{}{
map[string]interface{}{"id": float64(1)},
map[string]interface{}{"id": float64(2)},
})
lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
if len(lines) != 2 {
t.Errorf("paginated JSON should emit 2 lines (NDJSON), got %d:\n%s", len(lines), buf.String())
}
}
func TestPaginatedFormatter_NDJSON(t *testing.T) {
var buf bytes.Buffer
pf := NewPaginatedFormatter(&buf, FormatNDJSON)
pf.FormatPage([]interface{}{map[string]interface{}{"id": float64(1)}})
out := strings.TrimSpace(buf.String())
var obj map[string]interface{}
if err := json.Unmarshal([]byte(out), &obj); err != nil {
t.Errorf("NDJSON paginated output should be valid JSON: %s", out)
}
}
func TestPaginatedFormatter_Table(t *testing.T) {
var buf bytes.Buffer
pf := NewPaginatedFormatter(&buf, FormatTable)
page1 := []interface{}{map[string]interface{}{"name": "Alice"}}
page2 := []interface{}{map[string]interface{}{"name": "Bob"}}
pf.FormatPage(page1)
out1 := buf.String()
if !strings.Contains(out1, "─") {
t.Error("first table page should contain separator")
}
buf.Reset()
pf.FormatPage(page2)
out2 := buf.String()
if strings.Contains(out2, "─") {
t.Error("continuation table page should not contain separator")
}
if !strings.Contains(out2, "Bob") {
t.Error("continuation table page should contain data")
}
}
func TestPaginatedFormatter_CSV(t *testing.T) {
var buf bytes.Buffer
pf := NewPaginatedFormatter(&buf, FormatCSV)
page1 := []interface{}{map[string]interface{}{"name": "Alice"}}
page2 := []interface{}{map[string]interface{}{"name": "Bob"}}
pf.FormatPage(page1)
lines1 := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
if len(lines1) != 2 {
t.Errorf("first CSV page should have header + data, got %d lines", len(lines1))
}
buf.Reset()
pf.FormatPage(page2)
lines2 := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
if len(lines2) != 1 {
t.Errorf("continuation CSV page should have only data, got %d lines", len(lines2))
}
}
func TestPaginatedFormatter_ColumnConsistency(t *testing.T) {
// Page 1 has {a, b}, page 2 has {a, b, c} — c should be ignored in CSV
var buf bytes.Buffer
pf := NewPaginatedFormatter(&buf, FormatCSV)
pf.FormatPage([]interface{}{map[string]interface{}{"a": "1", "b": "2"}})
header := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")[0]
buf.Reset()
pf.FormatPage([]interface{}{map[string]interface{}{"a": "3", "b": "4", "c": "5"}})
dataLine := strings.TrimRight(buf.String(), "\n")
// Header and data should have same number of columns
headerCols := strings.Count(header, ",") + 1
dataCols := strings.Count(dataLine, ",") + 1
if headerCols != dataCols {
t.Errorf("column count mismatch: header has %d, data has %d\nheader: %s\ndata: %s",
headerCols, dataCols, header, dataLine)
}
}
func TestExtractItems(t *testing.T) {
// Standard Lark response
data := map[string]interface{}{
"code": float64(0),
"msg": "success",
"data": map[string]interface{}{
"items": []interface{}{
map[string]interface{}{"id": float64(1)},
map[string]interface{}{"id": float64(2)},
},
"has_more": true,
"page_token": "abc",
},
}
items := ExtractItems(data)
if len(items) != 2 {
t.Fatalf("expected 2 items, got %d", len(items))
}
// Different array field
data2 := map[string]interface{}{
"data": map[string]interface{}{
"members": []interface{}{
map[string]interface{}{"user_id": "u1"},
},
},
}
items2 := ExtractItems(data2)
if len(items2) != 1 {
t.Fatalf("expected 1 member, got %d", len(items2))
}
// Already an array
arr := []interface{}{"a", "b"}
items3 := ExtractItems(arr)
if len(items3) != 2 {
t.Fatalf("expected 2 items from raw array, got %d", len(items3))
}
// Non-response
items4 := ExtractItems("string")
if items4 != nil {
t.Fatalf("expected nil for non-response, got %v", items4)
}
// No data field and no array field
items5 := ExtractItems(map[string]interface{}{"foo": "bar"})
if items5 != nil {
t.Fatalf("expected nil for no data/array field, got %v", items5)
}
// Direct map with array field (shortcut data like {"members":[…], "total":5})
directMap := map[string]interface{}{
"members": []interface{}{map[string]interface{}{"name": "Alice"}},
"total": float64(1),
"has_more": false,
"page_token": "",
}
items6 := ExtractItems(directMap)
if len(items6) != 1 {
t.Fatalf("expected 1 item from direct map, got %d", len(items6))
}
// Direct map — plain array passed directly (e.g. calendar freebusy items)
plainArr := []interface{}{
map[string]interface{}{"start": "10:00", "end": "11:00"},
}
items7 := ExtractItems(plainArr)
if len(items7) != 1 {
t.Fatalf("expected 1 item from plain array, got %d", len(items7))
}
}
func TestFormatValue_LegacyFormats(t *testing.T) {
data := map[string]interface{}{
"data": map[string]interface{}{
"items": []interface{}{
map[string]interface{}{"name": "Alice"},
},
},
}
// "data" parses to FormatJSON with ok=false
dataFmt, dataOK := ParseFormat("data")
if dataOK {
t.Error("ParseFormat('data') should return ok=false")
}
var buf2 bytes.Buffer
FormatValue(&buf2, data, dataFmt)
out2 := buf2.String()
if !strings.Contains(out2, "items") {
t.Errorf("ParseFormat('data') → JSON should output full response, got:\n%s", out2)
}
// unknown format parses to FormatJSON with ok=false
fooFmt, fooOK := ParseFormat("foobar")
if fooOK {
t.Error("ParseFormat('foobar') should return ok=false")
}
var buf3 bytes.Buffer
FormatValue(&buf3, data, fooFmt)
out3 := buf3.String()
if !strings.Contains(out3, "items") {
t.Errorf("ParseFormat('foobar') → JSON should output full response, got:\n%s", out3)
}
}