Files
larksuite--cli/shortcuts/sheets/lark_sheet_sheet_structure_test.go
wehub-resource-sync bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

333 lines
10 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package sheets
import (
"strings"
"testing"
"github.com/larksuite/cli/shortcuts/common"
)
// TestSheetStructureShortcuts_DryRun covers all 8 shortcuts in
// lark_sheet_sheet_structure (sheet-info + 7 dim-*) and verifies that the
// CLI's A1-style --range / --position / --count flags map straight through
// to the tool's `range` / `position` / `count` fields (or are normalised
// per shortcut's wire shape).
func TestSheetStructureShortcuts_DryRun(t *testing.T) {
t.Parallel()
tests := []struct {
name string
sc common.Shortcut
args []string
toolName string
wantInput map[string]interface{}
}{
{
name: "+sheet-info with include single category → narrow info_type",
sc: SheetInfo,
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--include", "row_heights,col_widths"},
toolName: "get_sheet_structure",
wantInput: map[string]interface{}{
"excel_id": testToken,
"sheet_id": testSheetID,
"info_type": "row_heights_column_widths",
},
},
{
name: "+sheet-info with mixed include → all",
sc: SheetInfo,
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--include", "row_heights,merges"},
toolName: "get_sheet_structure",
wantInput: map[string]interface{}{
"excel_id": testToken,
"sheet_id": testSheetID,
"info_type": "all",
},
},
{
name: "+dim-insert row position=6 count=3 inherit-before",
sc: DimInsert,
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--position", "6", "--count", "3", "--inherit-style", "before"},
toolName: "modify_sheet_structure",
wantInput: map[string]interface{}{
"excel_id": testToken,
"operation": "insert",
"sheet_id": testSheetID,
"position": "6",
"count": float64(3),
"side": "before",
},
},
{
name: "+dim-insert column position=C count=2",
sc: DimInsert,
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--position", "C", "--count", "2"},
toolName: "modify_sheet_structure",
wantInput: map[string]interface{}{
"excel_id": testToken,
"operation": "insert",
"sheet_id": testSheetID,
"position": "C",
"count": float64(2),
},
},
{
name: "+dim-delete column B:D",
sc: DimDelete,
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "B:D"},
toolName: "modify_sheet_structure",
wantInput: map[string]interface{}{
"excel_id": testToken,
"operation": "delete",
"sheet_id": testSheetID,
"range": "B:D",
},
},
{
name: "+dim-hide row 3:5",
sc: DimHide,
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "3:5"},
toolName: "modify_sheet_structure",
wantInput: map[string]interface{}{
"excel_id": testToken,
"operation": "hide",
"sheet_id": testSheetID,
"range": "3:5",
},
},
{
name: "+dim-unhide column AA:AC",
sc: DimUnhide,
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "AA:AC"},
toolName: "modify_sheet_structure",
wantInput: map[string]interface{}{
"excel_id": testToken,
"operation": "unhide",
"sheet_id": testSheetID,
"range": "AA:AC",
},
},
{
name: "+dim-freeze row count=2 → freeze_rows",
sc: DimFreeze,
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--dimension", "row", "--count", "2"},
toolName: "modify_sheet_structure",
wantInput: map[string]interface{}{
"excel_id": testToken,
"operation": "freeze",
"sheet_id": testSheetID,
"freeze_rows": float64(2),
},
},
{
name: "+dim-freeze count=0 → unfreeze",
sc: DimFreeze,
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--dimension", "column", "--count", "0"},
toolName: "modify_sheet_structure",
wantInput: map[string]interface{}{
"excel_id": testToken,
"operation": "unfreeze",
"sheet_id": testSheetID,
},
},
{
name: "+dim-group row 1:5 fold",
sc: DimGroup,
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "1:5", "--group-state", "fold"},
toolName: "modify_sheet_structure",
wantInput: map[string]interface{}{
"excel_id": testToken,
"operation": "group",
"sheet_id": testSheetID,
"range": "1:5",
"group_state": "fold",
},
},
{
name: "+dim-ungroup row 1:5",
sc: DimUngroup,
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "1:5"},
toolName: "modify_sheet_structure",
wantInput: map[string]interface{}{
"excel_id": testToken,
"operation": "ungroup",
"sheet_id": testSheetID,
"range": "1:5",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
body := parseDryRunBody(t, tt.sc, tt.args)
got := decodeToolInput(t, body, tt.toolName)
assertInputEquals(t, got, tt.wantInput)
})
}
}
// TestDimRange_Validation covers the A1 range parser's edge cases routed
// through +dim-hide (any --range shortcut works; we just need to exercise
// the validator).
func TestDimRange_Validation(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
want string
}{
{
name: "end before start",
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "5:3", "--dry-run"},
want: "end position is before start",
},
{
name: "mix row+column",
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "3:C", "--dry-run"},
want: "cannot mix row",
},
{
name: "invalid characters",
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "A1:B2", "--dry-run"},
want: "expected pure digits",
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
_, _, err := runShortcutCapturingErr(t, DimHide, tt.args)
requireValidation(t, err, tt.want)
})
}
}
// TestDimMove_DryRun verifies the native v3 move_dimension payload shape.
// CLI's --source-range "1:3" (1-based inclusive) is parsed into
// source.{start_index=0, end_index=2} (0-based inclusive), and sheet_id is
// carried in the path, not the body. --target "11" → destination_index=10.
func TestDimMove_DryRun(t *testing.T) {
t.Parallel()
calls := parseDryRunAPI(t, DimMove, []string{
"--url", testURL, "--sheet-id", testSheetID,
"--source-range", "1:3", "--target", "11",
})
if len(calls) != 1 {
t.Fatalf("api calls = %d, want 1", len(calls))
}
c := calls[0].(map[string]interface{})
wantURL := "/sheets/v3/spreadsheets/" + testToken + "/sheets/" + testSheetID + "/move_dimension"
if !strings.Contains(c["url"].(string), wantURL) {
t.Errorf("url = %v, want suffix %v", c["url"], wantURL)
}
body, _ := c["body"].(map[string]interface{})
src, _ := body["source"].(map[string]interface{})
if src["major_dimension"] != "ROWS" {
t.Errorf("source.major_dimension = %v, want ROWS", src["major_dimension"])
}
if src["start_index"].(float64) != 0 {
t.Errorf("start_index = %v, want 0", src["start_index"])
}
if src["end_index"].(float64) != 2 {
t.Errorf("end_index = %v, want 2 (0-based inclusive)", src["end_index"])
}
if body["destination_index"].(float64) != 10 {
t.Errorf("destination_index = %v, want 10 (target \"11\" → 0-based 10)", body["destination_index"])
}
}
// TestDimMove_Column exercises the column path: --source-range "C:F" →
// COLUMNS / start=2 / end=5; --target "H" → destination_index=7.
func TestDimMove_Column(t *testing.T) {
t.Parallel()
calls := parseDryRunAPI(t, DimMove, []string{
"--url", testURL, "--sheet-id", testSheetID,
"--source-range", "C:F", "--target", "H",
})
c := calls[0].(map[string]interface{})
body, _ := c["body"].(map[string]interface{})
src, _ := body["source"].(map[string]interface{})
if src["major_dimension"] != "COLUMNS" {
t.Errorf("major_dimension = %v, want COLUMNS", src["major_dimension"])
}
if src["start_index"].(float64) != 2 || src["end_index"].(float64) != 5 {
t.Errorf("source = %v, want start=2 end=5", src)
}
if body["destination_index"].(float64) != 7 {
t.Errorf("destination_index = %v, want 7", body["destination_index"])
}
}
// TestDimMove_MismatchedDimension verifies that mixing source row + target
// column (or vice versa) is rejected at Validate.
func TestDimMove_MismatchedDimension(t *testing.T) {
t.Parallel()
_, _, err := runShortcutCapturingErr(t, DimMove, []string{
"--url", testURL, "--sheet-id", testSheetID,
"--source-range", "1:3", "--target", "H", "--dry-run",
})
requireValidation(t, err, "must match --source-range")
}
// TestParseA1Range covers parser edge cases directly.
func TestParseA1Range(t *testing.T) {
t.Parallel()
cases := []struct {
in string
dim string
start int
end int
wantErr bool
}{
{"3:7", "row", 2, 6, false},
{"5", "row", 4, 4, false},
{"C:F", "column", 2, 5, false},
{"C", "column", 2, 2, false},
{"aa:ac", "column", 26, 28, false}, // lower-case letters accepted
{"", "", 0, 0, true},
{"3:C", "", 0, 0, true},
{"7:3", "", 0, 0, true},
{"A1", "", 0, 0, true}, // cell ref, not a row/col range
{"3:5:7", "", 0, 0, true},
{"0", "", 0, 0, true}, // rows are 1-based
}
for _, c := range cases {
t.Run(c.in, func(t *testing.T) {
t.Parallel()
dim, start, end, err := parseA1Range(c.in)
if c.wantErr {
if err == nil {
t.Errorf("parseA1Range(%q) = (%q, %d, %d, nil), want error", c.in, dim, start, end)
}
return
}
if err != nil {
t.Fatalf("parseA1Range(%q) unexpected error: %v", c.in, err)
}
if dim != c.dim || start != c.start || end != c.end {
t.Errorf("parseA1Range(%q) = (%q, %d, %d), want (%q, %d, %d)", c.in, dim, start, end, c.dim, c.start, c.end)
}
})
}
}
// TestColumnIndexToLetter exercises the corner cases of the letter helper
// (still in use by lark_sheet_workbook.go for absolute column refs).
func TestColumnIndexToLetter(t *testing.T) {
t.Parallel()
cases := []struct {
idx int
want string
}{
{0, "A"}, {25, "Z"}, {26, "AA"}, {27, "AB"}, {51, "AZ"},
{52, "BA"}, {701, "ZZ"}, {702, "AAA"},
}
for _, c := range cases {
if got := columnIndexToLetter(c.idx); got != c.want {
t.Errorf("columnIndexToLetter(%d) = %q, want %q", c.idx, got, c.want)
}
}
}