Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,802 changes: 2,486 additions & 316 deletions shortcuts/base/base_execute_test.go

Large diffs are not rendered by default.

27 changes: 22 additions & 5 deletions shortcuts/base/base_form_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@ var BaseFormDelete = common.Shortcut{
Scopes: []string{"base:form:delete"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Flags: []common.Flag{
{Name: "base-token", Desc: "Base app token (base_token)", Required: true},
{Name: "table-id", Desc: "table ID", Required: true},
{Name: "form-id", Desc: "form ID", Required: true},
Flags: appendDeleteApprovalFlags(
baseTokenFlag(true),
common.Flag{Name: "table-id", Desc: "table ID", Required: true},
common.Flag{Name: "form-id", Desc: "form ID", Required: true},
),
Tips: []string{
"Use +form-list or +form-get first when the form target is ambiguous.",
baseHighRiskYesTip,
"Use --prepare-approval to create the approval URL, or pass --auth-code to execute the delete.",
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
return common.NewDryRunAPI().
Expand All @@ -33,8 +38,20 @@ var BaseFormDelete = common.Shortcut{
baseToken := runtime.Str("base-token")
tableId := runtime.Str("table-id")
formId := runtime.Str("form-id")
stop, err := handleDeleteApproval(runtime, deleteApprovalSpec{
Action: "form_delete",
BaseToken: baseToken,
ResourceType: "form",
ResourceID: formId,
})
if err != nil {
return err
}
if stop {
return nil
}

_, err := baseV3Call(runtime, "DELETE",
_, err = baseV3Call(runtime, "DELETE",
baseV3Path("bases", baseToken, "tables", tableId, "forms", formId), nil, nil)
if err != nil {
return err
Expand Down
16 changes: 2 additions & 14 deletions shortcuts/base/base_form_execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
func TestBaseFormExecuteList(t *testing.T) {
t.Run("single page", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
registerTokenStub(reg)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/forms",
Expand All @@ -39,7 +38,6 @@ func TestBaseFormExecuteList(t *testing.T) {

t.Run("auto pagination", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
registerTokenStub(reg)
// First page: has_more=true
reg.Register(&httpmock.Stub{
Method: "GET",
Expand Down Expand Up @@ -86,7 +84,6 @@ func TestBaseFormExecuteList(t *testing.T) {

func TestBaseFormExecuteGet(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
registerTokenStub(reg)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/forms/vew_form1",
Expand All @@ -110,7 +107,6 @@ func TestBaseFormExecuteGet(t *testing.T) {
func TestBaseFormExecuteCreate(t *testing.T) {
t.Run("name only", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
registerTokenStub(reg)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/forms",
Expand All @@ -133,7 +129,6 @@ func TestBaseFormExecuteCreate(t *testing.T) {

t.Run("with description", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
registerTokenStub(reg)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/forms",
Expand All @@ -158,7 +153,6 @@ func TestBaseFormExecuteCreate(t *testing.T) {

t.Run("with description link", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
registerTokenStub(reg)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/forms",
Expand All @@ -185,7 +179,6 @@ func TestBaseFormExecuteCreate(t *testing.T) {
func TestBaseFormExecuteUpdate(t *testing.T) {
t.Run("update name", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
registerTokenStub(reg)
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/forms/vew_form1",
Expand All @@ -208,7 +201,6 @@ func TestBaseFormExecuteUpdate(t *testing.T) {

t.Run("update with description", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
registerTokenStub(reg)
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/forms/vew_form1",
Expand All @@ -234,13 +226,13 @@ func TestBaseFormExecuteUpdate(t *testing.T) {

func TestBaseFormExecuteDelete(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
registerTokenStub(reg)
registerDeleteApprovalValidationStub(reg)
reg.Register(&httpmock.Stub{
Method: "DELETE",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/forms/vew_form1",
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{}},
})
if err := runShortcut(t, BaseFormDelete, []string{"+form-delete", "--base-token", "app_x", "--table-id", "tbl_x", "--form-id", "vew_form1", "--yes"}, factory, stdout); err != nil {
if err := runShortcut(t, BaseFormDelete, []string{"+form-delete", "--base-token", "app_x", "--table-id", "tbl_x", "--form-id", "vew_form1", "--auth-code", "larkauth_v1_demo", "--yes"}, factory, stdout); err != nil {
t.Fatalf("err=%v", err)
}
if got := stdout.String(); !strings.Contains(got, `"deleted": true`) || !strings.Contains(got, `"form_id": "vew_form1"`) {
Expand All @@ -250,7 +242,6 @@ func TestBaseFormExecuteDelete(t *testing.T) {

func TestBaseFormQuestionsExecuteList(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
registerTokenStub(reg)
reg.Register(&httpmock.Stub{
Method: "GET",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/forms/vew_form1/questions",
Expand All @@ -276,7 +267,6 @@ func TestBaseFormQuestionsExecuteList(t *testing.T) {
func TestBaseFormQuestionsExecuteCreate(t *testing.T) {
t.Run("create questions", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
registerTokenStub(reg)
reg.Register(&httpmock.Stub{
Method: "POST",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/forms/vew_form1/questions",
Expand Down Expand Up @@ -311,7 +301,6 @@ func TestBaseFormQuestionsExecuteCreate(t *testing.T) {

func TestBaseFormQuestionsExecuteUpdate(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
registerTokenStub(reg)
reg.Register(&httpmock.Stub{
Method: "PATCH",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/forms/vew_form1/questions",
Expand All @@ -337,7 +326,6 @@ func TestBaseFormQuestionsExecuteUpdate(t *testing.T) {
func TestBaseFormQuestionsExecuteDelete(t *testing.T) {
t.Run("delete questions", func(t *testing.T) {
factory, stdout, reg := newExecuteFactory(t)
registerTokenStub(reg)
reg.Register(&httpmock.Stub{
Method: "DELETE",
URL: "/open-apis/base/v3/bases/app_x/tables/tbl_x/forms/vew_form1/questions",
Expand Down
96 changes: 96 additions & 0 deletions shortcuts/base/delete_approval.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package base

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"strings"

"github.com/larksuite/cli/shortcuts/common"
)

const deleteApprovalValidatePath = "/space/api/base/delete/validate-auth-code"
const deleteApprovalRequestPath = "/space/api/base/delete/requests"

type deleteApprovalSpec struct {
Action string
BaseToken string
ResourceType string
ResourceID string
}

func deleteApprovalFlags() []common.Flag {
return []common.Flag{
{Name: "auth-code", Desc: "one-time delete authorization code returned by the Base delete approval page"},
{Name: "prepare-approval", Type: "bool", Desc: "create a delete approval request instead of executing the delete"},
}
}

func appendDeleteApprovalFlags(flags ...common.Flag) []common.Flag {
return append(flags, deleteApprovalFlags()...)
}

func handleDeleteApproval(runtime *common.RuntimeContext, spec deleteApprovalSpec) (bool, error) {
if runtime.Bool("prepare-approval") {
return true, prepareDeleteApproval(runtime, spec)
}
if strings.TrimSpace(runtime.Str("auth-code")) == "" {
return false, baseFlagErrorf("--auth-code is required for %s; run with --prepare-approval first to create an approval URL", spec.Action)
}
return false, validateDeleteApproval(runtime, spec)
}

func prepareDeleteApproval(runtime *common.RuntimeContext, spec deleteApprovalSpec) error {
data, err := runtime.CallAPITyped(http.MethodPost, deleteApprovalRequestPath, nil, map[string]interface{}{
"action": spec.Action,
"base_token": spec.BaseToken,
"resource_type": spec.ResourceType,
"resource_id": spec.ResourceID,
})
if err != nil {
return err
}
runtime.Out(map[string]interface{}{
"approval_required": true,
"request_id": common.GetString(data, "request_id"),
"approval_url": common.GetString(data, "approval_url"),
"expires_at": data["expires_at"],
"request_digest": common.GetString(data, "request_digest"),
"action": spec.Action,
"base_token": spec.BaseToken,
"resource_type": spec.ResourceType,
"resource_id": spec.ResourceID,
}, nil)
return nil
}

func validateDeleteApproval(runtime *common.RuntimeContext, spec deleteApprovalSpec) error {
data, err := runtime.CallAPITyped(http.MethodPost, deleteApprovalValidatePath, nil, map[string]interface{}{
"auth_code": strings.TrimSpace(runtime.Str("auth-code")),
"action": spec.Action,
"base_token": spec.BaseToken,
"resource_type": spec.ResourceType,
"resource_id": spec.ResourceID,
"request_digest": deleteApprovalDigest(spec),
})
if err != nil {
return err
}
if code := common.GetString(data, "error_code"); code != "" {
msg := common.GetString(data, "error_message")
if msg == "" {
msg = code
}
return baseValidationErrorf("delete authorization failed: %s", msg)
}
return nil
}

func deleteApprovalDigest(spec deleteApprovalSpec) string {
sum := sha256.Sum256([]byte(fmt.Sprintf("%s|%s|%s|%s", spec.Action, spec.BaseToken, spec.ResourceType, spec.ResourceID)))
return hex.EncodeToString(sum[:])
}
Comment on lines +1 to +96

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for test files covering delete_approval functions
rg -l 'handleDeleteApproval|prepareDeleteApproval|validateDeleteApproval|deleteApprovalDigest' shortcuts/base/ --type=go -g '*_test.go'

# Check for direct tests of delete_approval functions
rg -n 'func Test.*[Dd]elete[Aa]pproval|func Test.*HandleDeleteApproval|func Test.*DeleteApprovalDigest' shortcuts/base/ --type=go

Repository: larksuite/cli

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# List base shortcut files and tests
git ls-files shortcuts/base | sed -n '1,200p'

printf '\n--- deleteApproval references ---\n'
rg -n 'deleteApproval|prepare-approval|auth-code|request_digest|delete/validate-auth-code|delete/requests' shortcuts/base --type=go

printf '\n--- test files in shortcuts/base ---\n'
rg -n '^func Test' shortcuts/base --type=go -g '*_test.go'

Repository: larksuite/cli

Length of output: 22113


🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -a '*_test.go' shortcuts/base | sed -n '1,200p'

Repository: larksuite/cli

Length of output: 440


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect delete-approval-related tests and the validation stub
sed -n '1,180p' shortcuts/base/base_execute_test.go

printf '\n--- table/record/view delete test regions ---\n'
sed -n '1160,1285p' shortcuts/base/base_execute_test.go
printf '\n---\n'
sed -n '2880,2975p' shortcuts/base/base_execute_test.go
printf '\n---\n'
sed -n '2020,2085p' shortcuts/base/base_execute_test.go

printf '\n--- delete approval consumers ---\n'
sed -n '160,235p' shortcuts/base/table_ops.go
sed -n '170,215p' shortcuts/base/view_ops.go
sed -n '490,535p' shortcuts/base/record_ops.go
sed -n '165,205p' shortcuts/base/field_ops.go

Repository: larksuite/cli

Length of output: 24431


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path('shortcuts/base/base_execute_test.go')
text = path.read_text()

targets = [
    'func TestBaseTableExecuteReadAndDelete',
    'func TestBaseRecordExecuteReadCreateDelete',
    'func TestBaseViewExecuteReadCreateDeleteAndFilter',
    'func TestBaseFieldExecuteCRUD',
    'func registerDeleteApprovalValidationStub',
]

for target in targets:
    idx = text.find(target)
    print(f'\n=== {target} ===')
    if idx == -1:
        print('NOT FOUND')
        continue
    start = max(0, idx - 200)
    end = min(len(text), idx + 2200)
    print(text[start:end])
PY

Repository: larksuite/cli

Length of output: 12405


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- prepare-approval occurrences ---'
rg -n 'prepare-approval|delete/requests|delete/validate-auth-code|request_digest|auth-code' shortcuts/base --type=go

printf '\n%s\n' '--- delete approval helper tests elsewhere ---'
rg -n 'handleDeleteApproval|prepareDeleteApproval|validateDeleteApproval|deleteApprovalDigest|registerDeleteApprovalValidationStub' shortcuts --type=go

Repository: larksuite/cli

Length of output: 6173


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --fixed-strings '--auth-code is required' shortcuts/base --type=go
printf '\n---\n'
rg -n --fixed-strings '--prepare-approval' shortcuts/base --type=go
printf '\n---\n'
rg -n 'delete approval|approval URL|request_digest|error_code' shortcuts/base/*_test.go --type=go

Repository: larksuite/cli

Length of output: 577


Add targeted tests for the delete-approval helper branches.
base_execute_test.go covers the delete commands’ happy paths, but delete_approval.go still needs direct coverage for --prepare-approval, missing --auth-code, validation error_code responses, and deleteApprovalDigest.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/base/delete_approval.go` around lines 1 - 96, Add targeted unit
tests around deleteApproval helpers to cover the untested branches in
handleDeleteApproval, prepareDeleteApproval, validateDeleteApproval, and
deleteApprovalDigest. Verify --prepare-approval returns the approval request
path output, missing --auth-code triggers the expected baseFlagErrorf branch,
validateDeleteApproval surfaces an error_code/error_message response as a
baseValidationErrorf, and deleteApprovalDigest is stable for the same
deleteApprovalSpec inputs. Use the existing deleteApprovalSpec and
RuntimeContext behavior to locate and exercise these branches directly.

Source: Coding guidelines

9 changes: 7 additions & 2 deletions shortcuts/base/field_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@ var BaseFieldDelete = common.Shortcut{
Risk: "high-risk-write",
Scopes: []string{"base:field:delete"},
AuthTypes: authTypes(),
Flags: []common.Flag{baseTokenFlag(true), tableRefFlag(true), fieldRefFlag(true)},
DryRun: dryRunFieldDelete,
Flags: appendDeleteApprovalFlags(baseTokenFlag(true), tableRefFlag(true), fieldRefFlag(true)),
Tips: []string{
baseHighRiskYesTip,
`Example: lark-cli base +field-delete --base-token <base_token> --table-id <table_id> --field-id "Status" --yes`,
"Use --prepare-approval to create the approval URL, or pass --auth-code to execute the delete.",
},
DryRun: dryRunFieldDelete,
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
return executeFieldDelete(runtime)
},
Expand Down
14 changes: 13 additions & 1 deletion shortcuts/base/field_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,19 @@ func executeFieldDelete(runtime *common.RuntimeContext) error {
baseToken := runtime.Str("base-token")
tableIDValue := baseTableID(runtime)
fieldRef := runtime.Str("field-id")
_, err := baseV3Call(runtime, "DELETE", baseV3Path("bases", baseToken, "tables", tableIDValue, "fields", fieldRef), nil, nil)
stop, err := handleDeleteApproval(runtime, deleteApprovalSpec{
Action: "field_delete",
BaseToken: baseToken,
ResourceType: "field",
ResourceID: fieldRef,
})
if err != nil {
return err
}
if stop {
return nil
}
_, err = baseV3Call(runtime, "DELETE", baseV3Path("bases", baseToken, "tables", tableIDValue, "fields", fieldRef), nil, nil)
if err != nil {
return err
}
Expand Down
19 changes: 16 additions & 3 deletions shortcuts/base/record_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,25 @@ import (
var BaseRecordDelete = common.Shortcut{
Service: "base",
Command: "+record-delete",
Description: "Delete a record by ID",
Description: "Delete one or more records by ID",
Risk: "high-risk-write",
Scopes: []string{"base:record:delete"},
AuthTypes: authTypes(),
Flags: []common.Flag{baseTokenFlag(true), tableRefFlag(true), recordRefFlag(true)},
DryRun: dryRunRecordDelete,
Flags: appendDeleteApprovalFlags(
baseTokenFlag(true),
tableRefFlag(true),
common.Flag{Name: "record-id", Type: "string_array", Desc: "record ID (repeatable)"},
common.Flag{Name: "json", Desc: `JSON object with record_id_list, e.g. {"record_id_list":["rec_xxx"]}`},
),
Tips: []string{
baseHighRiskYesTip,
`Example: lark-cli base +record-delete --base-token <base_token> --table-id <table_id> --record-id <record_id_1> --record-id <record_id_2> --yes`,
"Use --prepare-approval to create the approval URL, or pass --auth-code to execute the delete.",
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
return validateRecordSelection(runtime)
},
DryRun: dryRunRecordDelete,
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
return executeRecordDelete(runtime)
},
Expand Down
Loading
Loading