Files
larksuite-cli/shortcuts/base/base_dashboard_execute_test.go
yballul-bytedance 0e6274d947 feat(base): add dashboard block data shortcut and workflow docs (#1067)
Change-Id: I52c471886bdb2d4b7be021ce86c34bbb78385017
2026-05-29 16:35:32 +08:00

861 lines
34 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package base
import (
"strings"
"testing"
"github.com/larksuite/cli/internal/httpmock"
)
// ── Dashboard CRUD ──────────────────────────────────────────────────
// TestBaseDashboardExecuteList tests the +dashboard-list command.
func TestBaseDashboardExecuteList(t *testing.T) {
t.Run("single page", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/base/v3/bases/app_x/dashboards",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"has_more": false,
"total": 2,
"items": []interface{}{
map[string]interface{}{"dashboard_id": "dsh_001", "name": "销售报表"},
map[string]interface{}{"dashboard_id": "dsh_002", "name": "运营看板"},
},
},
},
})
if err := runShortcut(t, BaseDashboardList, []string{"+dashboard-list", "--base-token", "app_x"}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"dsh_001"`) || !strings.Contains(got, `"dsh_002"`) {
t.Fatalf("stdout=%s", got)
}
})
}
// TestBaseDashboardExecuteGet tests the +dashboard-get command.
func TestBaseDashboardExecuteGet(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"dashboard_id": "dsh_001",
"name": "销售报表",
"theme": map[string]interface{}{"theme_style": "default"},
"blocks": []interface{}{
map[string]interface{}{"block_id": "blk_a", "block_name": "柱状图", "block_type": "column"},
},
},
},
})
if err := runShortcut(t, BaseDashboardGet, []string{"+dashboard-get", "--base-token", "app_x", "--dashboard-id", "dsh_001"}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"dsh_001"`) || !strings.Contains(got, `"销售报表"`) || !strings.Contains(got, `"dashboard"`) {
t.Fatalf("stdout=%s", got)
}
}
// TestBaseDashboardExecuteCreate tests the +dashboard-create command.
func TestBaseDashboardExecuteCreate(t *testing.T) {
t.Run("name only", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/dashboards",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"dashboard_id": "dsh_new",
"name": "新报表",
},
},
})
if err := runShortcut(t, BaseDashboardCreate, []string{"+dashboard-create", "--base-token", "app_x", "--name", "新报表"}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"dsh_new"`) || !strings.Contains(got, `"created": true`) {
t.Fatalf("stdout=%s", got)
}
})
t.Run("with theme", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/dashboards",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"dashboard_id": "dsh_themed",
"name": "主题报表",
"theme": map[string]interface{}{"theme_style": "SimpleBlue"},
},
},
})
if err := runShortcut(t, BaseDashboardCreate, []string{"+dashboard-create", "--base-token", "app_x", "--name", "主题报表", "--theme-style", "SimpleBlue"}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"dsh_themed"`) || !strings.Contains(got, `"SimpleBlue"`) {
t.Fatalf("stdout=%s", got)
}
})
}
// TestBaseDashboardExecuteUpdate tests the +dashboard-update command.
func TestBaseDashboardExecuteUpdate(t *testing.T) {
t.Run("update name", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"dashboard_id": "dsh_001",
"name": "更新后的名称",
},
},
})
if err := runShortcut(t, BaseDashboardUpdate, []string{"+dashboard-update", "--base-token", "app_x", "--dashboard-id", "dsh_001", "--name", "更新后的名称"}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"updated": true`) || !strings.Contains(got, `"更新后的名称"`) {
t.Fatalf("stdout=%s", got)
}
})
t.Run("update theme", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"dashboard_id": "dsh_001",
"name": "报表",
"theme": map[string]interface{}{"theme_style": "deepDark"},
},
},
})
if err := runShortcut(t, BaseDashboardUpdate, []string{"+dashboard-update", "--base-token", "app_x", "--dashboard-id", "dsh_001", "--theme-style", "deepDark"}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"updated": true`) || !strings.Contains(got, `"deepDark"`) {
t.Fatalf("stdout=%s", got)
}
})
}
// TestBaseDashboardExecuteDelete tests the +dashboard-delete command.
func TestBaseDashboardExecuteDelete(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "DELETE",
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001",
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{}},
})
if err := runShortcut(t, BaseDashboardDelete, []string{"+dashboard-delete", "--base-token", "app_x", "--dashboard-id", "dsh_001", "--yes"}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"deleted": true`) || !strings.Contains(got, `"dashboard_id": "dsh_001"`) {
t.Fatalf("stdout=%s", got)
}
}
// ── Dashboard Block CRUD ────────────────────────────────────────────
// TestBaseDashboardBlockExecuteList tests the +dashboard-block-list command.
func TestBaseDashboardBlockExecuteList(t *testing.T) {
t.Run("single page", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001/blocks",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"has_more": false,
"total": 2,
"items": []interface{}{
map[string]interface{}{"block_id": "blk_a", "name": "柱状图", "type": "column"},
map[string]interface{}{"block_id": "blk_b", "name": "指标卡", "type": "statistics"},
},
},
},
})
if err := runShortcut(t, BaseDashboardBlockList, []string{"+dashboard-block-list", "--base-token", "app_x", "--dashboard-id", "dsh_001"}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"blk_a"`) || !strings.Contains(got, `"blk_b"`) {
t.Fatalf("stdout=%s", got)
}
})
}
// TestBaseDashboardBlockExecuteGet tests the +dashboard-block-get command.
func TestBaseDashboardBlockExecuteGet(t *testing.T) {
t.Run("basic", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001/blocks/blk_a",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"block_id": "blk_a",
"name": "订单趋势",
"type": "column",
"layout": map[string]interface{}{"x": 0, "y": 0, "w": 12, "h": 6},
"data_config": map[string]interface{}{
"table_name": "订单表",
"count_all": true,
},
},
},
})
if err := runShortcut(t, BaseDashboardBlockGet, []string{"+dashboard-block-get", "--base-token", "app_x", "--dashboard-id", "dsh_001", "--block-id", "blk_a"}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"blk_a"`) || !strings.Contains(got, `"block"`) || !strings.Contains(got, `"订单趋势"`) {
t.Fatalf("stdout=%s", got)
}
})
t.Run("with user-id-type", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "user_id_type=union_id",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"block_id": "blk_a",
"name": "人员图表",
"type": "pie",
},
},
})
if err := runShortcut(t, BaseDashboardBlockGet, []string{"+dashboard-block-get", "--base-token", "app_x", "--dashboard-id", "dsh_001", "--block-id", "blk_a", "--user-id-type", "union_id"}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"blk_a"`) {
t.Fatalf("stdout=%s", got)
}
})
}
// TestBaseDashboardBlockExecuteGetData tests the +dashboard-block-get-data command.
func TestBaseDashboardBlockExecuteGetData(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/base/v3/bases/app_x/dashboards/blocks/blk_chart/data",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"dimensions": []interface{}{
map[string]interface{}{"field_name": "文本", "alias": "dim_text"},
},
"measures": []interface{}{
map[string]interface{}{"field_name": "Bitable_Dashboard_Count", "aggregation": "count_all", "alias": "me_count"},
},
"main_data": []interface{}{
map[string]interface{}{
"dim_text": map[string]interface{}{"value": "A"},
"me_count": map[string]interface{}{"value": 3},
},
},
},
},
})
if err := runShortcut(t, BaseDashboardBlockGetData, []string{"+dashboard-block-get-data", "--base-token", "app_x", "--block-id", "blk_chart"}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"dimensions"`) || !strings.Contains(got, `"main_data"`) || !strings.Contains(got, `"dim_text"`) {
t.Fatalf("stdout=%s", got)
}
}
// TestBaseDashboardBlockExecuteCreate tests the +dashboard-block-create command.
func TestBaseDashboardBlockExecuteCreate(t *testing.T) {
t.Run("with data-config", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001/blocks",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"block_id": "blk_new",
"name": "订单趋势",
"type": "column",
"layout": map[string]interface{}{"x": 0, "y": 0, "w": 12, "h": 6},
"data_config": map[string]interface{}{
"table_name": "订单表",
"count_all": true,
},
},
},
})
args := []string{"+dashboard-block-create", "--base-token", "app_x", "--dashboard-id", "dsh_001",
"--name", "订单趋势", "--type", "column",
"--data-config", `{"table_name":"订单表","count_all":true}`}
if err := runShortcut(t, BaseDashboardBlockCreate, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"blk_new"`) || !strings.Contains(got, `"created": true`) {
t.Fatalf("stdout=%s", got)
}
})
t.Run("statistics with series", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001/blocks",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"block_id": "blk_stat",
"name": "销售总额",
"type": "statistics",
},
},
})
args := []string{"+dashboard-block-create", "--base-token", "app_x", "--dashboard-id", "dsh_001",
"--name", "销售总额", "--type", "statistics",
"--data-config", `{"table_name":"数据表","series":[{"field_name":"数字","rollup":"SUM"}]}`}
if err := runShortcut(t, BaseDashboardBlockCreate, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"blk_stat"`) || !strings.Contains(got, `"created": true`) {
t.Fatalf("stdout=%s", got)
}
})
t.Run("without data-config", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001/blocks",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"block_id": "blk_empty",
"name": "空图表",
"type": "line",
},
},
})
args := []string{"+dashboard-block-create", "--base-token", "app_x", "--dashboard-id", "dsh_001",
"--name", "空图表", "--type", "line"}
if err := runShortcut(t, BaseDashboardBlockCreate, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"blk_empty"`) || !strings.Contains(got, `"created": true`) {
t.Fatalf("stdout=%s", got)
}
})
t.Run("invalid data-config json", func(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
args := []string{"+dashboard-block-create", "--base-token", "app_x", "--dashboard-id", "dsh_001",
"--name", "Test", "--type", "column", "--data-config", "not-json"}
if err := runShortcut(t, BaseDashboardBlockCreate, args, factory, stdout); err == nil {
t.Fatalf("expected error for invalid data-config JSON")
}
})
}
// TestBaseDashboardBlockExecuteUpdate tests the +dashboard-block-update command.
func TestBaseDashboardBlockExecuteUpdate(t *testing.T) {
t.Run("update name and data-config", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001/blocks/blk_a",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"block_id": "blk_a",
"name": "订单趋势v2",
"type": "column",
"data_config": map[string]interface{}{
"table_name": "订单表2",
"count_all": true,
},
},
},
})
args := []string{"+dashboard-block-update", "--base-token", "app_x", "--dashboard-id", "dsh_001", "--block-id", "blk_a",
"--name", "订单趋势v2",
"--data-config", `{"table_name":"订单表2","count_all":true}`}
if err := runShortcut(t, BaseDashboardBlockUpdate, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"updated": true`) || !strings.Contains(got, `"订单趋势v2"`) {
t.Fatalf("stdout=%s", got)
}
})
t.Run("update name only", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001/blocks/blk_a",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"block_id": "blk_a",
"name": "仅改名",
"type": "column",
},
},
})
args := []string{"+dashboard-block-update", "--base-token", "app_x", "--dashboard-id", "dsh_001", "--block-id", "blk_a",
"--name", "仅改名"}
if err := runShortcut(t, BaseDashboardBlockUpdate, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"updated": true`) || !strings.Contains(got, `"仅改名"`) {
t.Fatalf("stdout=%s", got)
}
})
t.Run("invalid data-config json", func(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
args := []string{"+dashboard-block-update", "--base-token", "app_x", "--dashboard-id", "dsh_001", "--block-id", "blk_a",
"--data-config", "bad-json"}
if err := runShortcut(t, BaseDashboardBlockUpdate, args, factory, stdout); err == nil {
t.Fatalf("expected error for invalid data-config JSON")
}
})
}
// TestBaseDashboardBlockExecuteDelete tests the +dashboard-block-delete command.
func TestBaseDashboardBlockExecuteDelete(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "DELETE",
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001/blocks/blk_a",
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{}},
})
if err := runShortcut(t, BaseDashboardBlockDelete, []string{"+dashboard-block-delete", "--base-token", "app_x", "--dashboard-id", "dsh_001", "--block-id", "blk_a", "--yes"}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"deleted": true`) || !strings.Contains(got, `"block_id": "blk_a"`) {
t.Fatalf("stdout=%s", got)
}
}
// ── Dry Run: Dashboard & Blocks ──────────────────────────────────────
// TestBaseDashboardDryRun_List tests the +dashboard-list --dry-run flag.
func TestBaseDashboardDryRun_List(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
if err := runShortcut(t, BaseDashboardList, []string{"+dashboard-list", "--base-token", "app_x", "--page-size", "50", "--dry-run", "--format", "pretty"}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, "GET /open-apis/base/v3/bases/app_x/dashboards") || !strings.Contains(got, "page_size=50") {
t.Fatalf("stdout=%s", got)
}
}
// TestBaseDashboardDryRun_Get tests the +dashboard-get --dry-run flag.
func TestBaseDashboardDryRun_Get(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
if err := runShortcut(t, BaseDashboardGet, []string{"+dashboard-get", "--base-token", "app_x", "--dashboard-id", "dsh_1", "--dry-run", "--format", "pretty"}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, "GET /open-apis/base/v3/bases/app_x/dashboards/dsh_1") || !strings.Contains(got, "dsh_1") {
t.Fatalf("stdout=%s", got)
}
}
// TestBaseDashboardDryRun_Create tests the +dashboard-create --dry-run flag.
func TestBaseDashboardDryRun_Create(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
args := []string{"+dashboard-create", "--base-token", "app_x", "--name", "新报表", "--theme-style", "default", "--dry-run", "--format", "pretty"}
if err := runShortcut(t, BaseDashboardCreate, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, "POST /open-apis/base/v3/bases/app_x/dashboards") || !strings.Contains(got, "\"name\":\"新报表\"") || !strings.Contains(got, "theme_style") {
t.Fatalf("stdout=%s", got)
}
}
// TestBaseDashboardDryRun_Update tests the +dashboard-update --dry-run flag.
func TestBaseDashboardDryRun_Update(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
args := []string{"+dashboard-update", "--base-token", "app_x", "--dashboard-id", "dsh_1", "--name", "更新名", "--dry-run", "--format", "pretty"}
if err := runShortcut(t, BaseDashboardUpdate, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, "PATCH /open-apis/base/v3/bases/app_x/dashboards/dsh_1") || !strings.Contains(got, "\"name\":\"更新名\"") {
t.Fatalf("stdout=%s", got)
}
}
// TestBaseDashboardDryRun_Delete tests the +dashboard-delete --dry-run flag.
func TestBaseDashboardDryRun_Delete(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
args := []string{"+dashboard-delete", "--base-token", "app_x", "--dashboard-id", "dsh_1", "--dry-run", "--format", "pretty"}
if err := runShortcut(t, BaseDashboardDelete, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, "DELETE /open-apis/base/v3/bases/app_x/dashboards/dsh_1") || !strings.Contains(got, "dsh_1") {
t.Fatalf("stdout=%s", got)
}
}
// TestBaseDashboardBlockDryRun_List tests the +dashboard-block-list --dry-run flag.
func TestBaseDashboardBlockDryRun_List(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
args := []string{"+dashboard-block-list", "--base-token", "app_x", "--dashboard-id", "dsh_1", "--page-size", "10", "--dry-run", "--format", "pretty"}
if err := runShortcut(t, BaseDashboardBlockList, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, "GET /open-apis/base/v3/bases/app_x/dashboards/dsh_1/blocks") || !strings.Contains(got, "page_size=10") {
t.Fatalf("stdout=%s", got)
}
}
// TestBaseDashboardBlockDryRun_Get tests the +dashboard-block-get --dry-run flag.
func TestBaseDashboardBlockDryRun_Get(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
args := []string{"+dashboard-block-get", "--base-token", "app_x", "--dashboard-id", "dsh_1", "--block-id", "blk_a", "--user-id-type", "union_id", "--dry-run", "--format", "pretty"}
if err := runShortcut(t, BaseDashboardBlockGet, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, "GET /open-apis/base/v3/bases/app_x/dashboards/dsh_1/blocks/blk_a") || !strings.Contains(got, "union_id") || !strings.Contains(got, "blk_a") {
t.Fatalf("stdout=%s", got)
}
}
// TestBaseDashboardBlockDryRun_GetData tests the +dashboard-block-get-data --dry-run flag.
func TestBaseDashboardBlockDryRun_GetData(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
args := []string{"+dashboard-block-get-data", "--base-token", "app_x", "--block-id", "blk_a", "--dry-run", "--format", "pretty"}
if err := runShortcut(t, BaseDashboardBlockGetData, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, "GET /open-apis/base/v3/bases/app_x/dashboards/blocks/blk_a/data") || !strings.Contains(got, "blk_a") {
t.Fatalf("stdout=%s", got)
}
}
// TestBaseDashboardBlockDryRun_Create tests the +dashboard-block-create --dry-run flag.
func TestBaseDashboardBlockDryRun_Create(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
args := []string{"+dashboard-block-create", "--base-token", "app_x", "--dashboard-id", "dsh_1", "--name", "订单趋势", "--type", "column", "--data-config", `{"table_name":"订单表","count_all":true}`, "--user-id-type", "open_id", "--dry-run", "--format", "pretty"}
if err := runShortcut(t, BaseDashboardBlockCreate, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, "POST /open-apis/base/v3/bases/app_x/dashboards/dsh_1/blocks") || !strings.Contains(got, "\"name\":\"订单趋势\"") || !strings.Contains(got, "table_name") || !strings.Contains(got, "open_id") {
t.Fatalf("stdout=%s", got)
}
}
// TestBaseDashboardBlockDryRun_Update tests the +dashboard-block-update --dry-run flag.
func TestBaseDashboardBlockDryRun_Update(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
args := []string{"+dashboard-block-update", "--base-token", "app_x", "--dashboard-id", "dsh_1", "--block-id", "blk_a", "--name", "订单趋势v2", "--data-config", `{"table_name":"订单表2","count_all":true}`, "--dry-run", "--format", "pretty"}
if err := runShortcut(t, BaseDashboardBlockUpdate, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, "PATCH /open-apis/base/v3/bases/app_x/dashboards/dsh_1/blocks/blk_a") || !strings.Contains(got, "订单趋势v2") || !strings.Contains(got, "订单表2") {
t.Fatalf("stdout=%s", got)
}
}
// TestBaseDashboardBlockDryRun_Delete tests the +dashboard-block-delete --dry-run flag.
func TestBaseDashboardBlockDryRun_Delete(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
args := []string{"+dashboard-block-delete", "--base-token", "app_x", "--dashboard-id", "dsh_1", "--block-id", "blk_a", "--dry-run", "--format", "pretty"}
if err := runShortcut(t, BaseDashboardBlockDelete, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, "DELETE /open-apis/base/v3/bases/app_x/dashboards/dsh_1/blocks/blk_a") || !strings.Contains(got, "blk_a") {
t.Fatalf("stdout=%s", got)
}
}
// ── Validator: data_config ───────────────────────────────────────────
// TestBaseDashboardBlockCreate_ValidateFails tests that data_config validation catches missing table_name.
func TestBaseDashboardBlockCreate_ValidateFails(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
// 缺 table_name 且 series 与 count_all 同时存在
args := []string{"+dashboard-block-create", "--base-token", "app_x", "--dashboard-id", "dsh_1",
"--name", "Bad", "--type", "column",
"--data-config", `{"series":[{"field_name":"金额","rollup":"sum"}],"count_all":true}`,
}
err := runShortcut(t, BaseDashboardBlockCreate, args, factory, stdout)
if err == nil {
t.Fatalf("expected validation error, got nil")
}
if !strings.Contains(err.Error(), "data_config 校验失败") || !strings.Contains(err.Error(), "table_name") {
t.Fatalf("unexpected error: %v", err)
}
}
// TestBaseDashboardBlockCreate_NoValidateFlagAllocs tests that --no-validate flag skips client-side validation.
func TestBaseDashboardBlockCreate_NoValidateFlagAllocs(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{Method: "POST", URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_1/blocks",
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{"block_id": "blk_ok", "name": "OK", "type": "column"}},
})
args := []string{"+dashboard-block-create", "--base-token", "app_x", "--dashboard-id", "dsh_1",
"--name", "OK", "--type", "column", "--no-validate",
"--data-config", `{"series":[{"field_name":"金额","rollup":"sum"}],"count_all":true}`,
}
if err := runShortcut(t, BaseDashboardBlockCreate, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
if got := stdout.String(); !strings.Contains(got, "\"blk_ok\"") || !strings.Contains(got, "\"created\": true") {
t.Fatalf("stdout=%s", got)
}
}
// TestBaseDashboardBlockCreate_InvalidRollup tests that invalid rollup values are rejected during validation.
func TestBaseDashboardBlockCreate_InvalidRollup(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
// 合法 JSON但 rollup=COUNTA不支持
args := []string{"+dashboard-block-create", "--base-token", "app_x", "--dashboard-id", "dsh_1",
"--name", "Bad", "--type", "column",
"--data-config", `{"table_name":"T","series":[{"field_name":"金额","rollup":"COUNTA"}]}`,
}
err := runShortcut(t, BaseDashboardBlockCreate, args, factory, stdout)
if err == nil {
t.Fatalf("expected validation error for invalid rollup")
}
if got := err.Error(); !strings.Contains(got, "rollup") || !strings.Contains(got, "data_config 校验失败") {
t.Fatalf("unexpected error: %v", err)
}
}
// ── Text Block Tests ────────────────────────────────────────────────
// TestBaseDashboardBlockExecuteCreate_TextType tests creating text blocks with markdown content.
func TestBaseDashboardBlockExecuteCreate_TextType(t *testing.T) {
t.Run("valid text block", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001/blocks",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"block_id": "blk_text",
"name": "说明文字",
"type": "text",
"data_config": map[string]interface{}{
"text": "# 标题\n**加粗**",
},
},
},
})
args := []string{"+dashboard-block-create", "--base-token", "app_x", "--dashboard-id", "dsh_001",
"--name", "说明文字", "--type", "text",
"--data-config", `{"text":"# 标题\n**加粗**"}`,
}
if err := runShortcut(t, BaseDashboardBlockCreate, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"blk_text"`) || !strings.Contains(got, `"created": true`) {
t.Fatalf("stdout=%s", got)
}
})
t.Run("text block missing text field", func(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
args := []string{"+dashboard-block-create", "--base-token", "app_x", "--dashboard-id", "dsh_001",
"--name", "Bad", "--type", "text",
"--data-config", `{}`,
}
err := runShortcut(t, BaseDashboardBlockCreate, args, factory, stdout)
if err == nil {
t.Fatalf("expected validation error for missing text field")
}
if got := err.Error(); !strings.Contains(got, "text") || !strings.Contains(got, "data_config 校验失败") {
t.Fatalf("unexpected error: %v", err)
}
})
}
// TestBaseDashboardBlockExecuteUpdate_TextType tests updating text block content and name.
func TestBaseDashboardBlockExecuteUpdate_TextType(t *testing.T) {
t.Run("update text content", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001/blocks/blk_text",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"block_id": "blk_text",
"name": "更新后的标题",
"type": "text",
"data_config": map[string]interface{}{
"text": "# 新内容",
},
},
},
})
args := []string{"+dashboard-block-update", "--base-token", "app_x", "--dashboard-id", "dsh_001", "--block-id", "blk_text",
"--name", "更新后的标题",
"--data-config", `{"text":"# 新内容"}`,
}
if err := runShortcut(t, BaseDashboardBlockUpdate, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"updated": true`) || !strings.Contains(got, "新内容") {
t.Fatalf("stdout=%s", got)
}
})
t.Run("update without type skips strict validation", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
// update 不传 type不做强类型校验直接透传给后端
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001/blocks/blk_text",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"block_id": "blk_text",
"type": "text",
},
},
})
args := []string{"+dashboard-block-update", "--base-token", "app_x", "--dashboard-id", "dsh_001", "--block-id", "blk_text",
"--data-config", `{"content":"xxx"}`,
}
// 不传 type本地不做强校验让后端处理
err := runShortcut(t, BaseDashboardBlockUpdate, args, factory, stdout)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := stdout.String(); !strings.Contains(got, `"updated": true`) {
t.Fatalf("stdout=%s", got)
}
})
}
// ── Dashboard Arrange ────────────────────────────────────────────────
// TestBaseDashboardExecuteArrange tests the +dashboard-arrange command for auto-arranging dashboard blocks.
func TestBaseDashboardExecuteArrange(t *testing.T) {
t.Run("arrange dashboard blocks", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001/arrange",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"dashboard_id": "dsh_001",
"name": "测试仪表盘",
"blocks": []interface{}{
map[string]interface{}{
"block_id": "cht_xxx",
"block_name": "组件1",
"block_type": "column",
"layout": map[string]interface{}{
"x": 0, "y": 0, "w": 500, "h": 400,
},
},
},
},
},
})
args := []string{"+dashboard-arrange", "--base-token", "app_x", "--dashboard-id", "dsh_001"}
if err := runShortcut(t, BaseDashboardArrange, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"arranged": true`) || !strings.Contains(got, `"dashboard_id"`) {
t.Fatalf("stdout=%s", got)
}
})
t.Run("arrange with user-id-type", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "user_id_type=union_id",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"dashboard_id": "dsh_001",
"blocks": []interface{}{},
},
},
})
args := []string{"+dashboard-arrange", "--base-token", "app_x", "--dashboard-id", "dsh_001", "--user-id-type", "union_id"}
if err := runShortcut(t, BaseDashboardArrange, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
if got := stdout.String(); !strings.Contains(got, `"arranged": true`) || !strings.Contains(got, `"dashboard_id"`) {
t.Fatalf("stdout=%s", got)
}
})
}
// TestBaseDashboardDryRun_Arrange tests the +dashboard-arrange --dry-run flag includes empty body.
func TestBaseDashboardDryRun_Arrange(t *testing.T) {
factory, stdout, _ := newExecuteFactory(t)
args := []string{"+dashboard-arrange", "--base-token", "app_x", "--dashboard-id", "dsh_001", "--user-id-type", "union_id", "--dry-run", "--format", "pretty"}
if err := runShortcut(t, BaseDashboardArrange, args, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, "POST /open-apis/base/v3/bases/app_x/dashboards/dsh_001/arrange") || !strings.Contains(got, "union_id") || !strings.Contains(got, "{}") {
t.Fatalf("stdout=%s", got)
}
}