mirror of
https://github.com/larksuite/cli.git
synced 2026-07-10 19:33:43 +08:00
Bring in the refined miaoda Spark db/file command set from the feat/miaoda-db-file-openapi work: db execute (typed errs + per-SQL-type JSON shaping), env diff/migrate, PITR recovery, changelog/audit, data import/export, db/file quota, and the 7 file-storage commands; plus the stderr spinner for slow ops and the aligned lark-apps skill references. Resolved overlap with the integration branch's earlier db-execute iteration (took the refined typed-error version), unified the stderr-TTY flag on IOStreams.StderrIsTerminal, and combined the shortcut registry (43 commands total).
133 lines
5.1 KiB
Go
133 lines
5.1 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package apps
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/larksuite/cli/errs"
|
||
"github.com/larksuite/cli/internal/httpmock"
|
||
)
|
||
|
||
const fileDeleteURL = "/open-apis/spark/v1/apps/app_x/storage/file_batch_remove"
|
||
|
||
// TestAppsFileDelete_RequiresAppIDAndPath 验证仅含空白的 --path 去空后为空时,Validate 报 --path typed 校验错误。
|
||
func TestAppsFileDelete_RequiresAppIDAndPath(t *testing.T) {
|
||
factory, stdout, _ := newAppsExecuteFactory(t)
|
||
// 传入仅含空白的 --path:满足 cobra 的 Required 检查,但 cleanDeletePaths 去空后为空,
|
||
// 触发 Validate 内的 typed --path 校验。
|
||
err := runAppsShortcut(t, AppsFileDelete,
|
||
[]string{"+file-delete", "--app-id", "app_x", "--path", " ", "--yes", "--as", "user"}, factory, stdout)
|
||
var ve *errs.ValidationError
|
||
if !errors.As(err, &ve) {
|
||
t.Fatalf("err = %T %v, want *errs.ValidationError", err, err)
|
||
}
|
||
if ve.Param != "--path" {
|
||
t.Fatalf("Param = %q, want --path", ve.Param)
|
||
}
|
||
}
|
||
|
||
// high-risk-write:无 --yes → confirmation_required(exit 10)。
|
||
func TestAppsFileDelete_RequiresConfirmation(t *testing.T) {
|
||
factory, stdout, _ := newAppsExecuteFactory(t)
|
||
err := runAppsShortcut(t, AppsFileDelete,
|
||
[]string{"+file-delete", "--app-id", "app_x", "--path", "/a.png", "--as", "user"}, factory, stdout)
|
||
if err == nil || !strings.Contains(err.Error(), "requires confirmation") {
|
||
t.Fatalf("expected confirmation_required, got %v", err)
|
||
}
|
||
}
|
||
|
||
// TestAppsFileDelete_DryRunSendsPaths 验证 dry-run 输出 POST file_batch_remove,body.paths 按序携带多个 --path。
|
||
func TestAppsFileDelete_DryRunSendsPaths(t *testing.T) {
|
||
factory, stdout, _ := newAppsExecuteFactory(t)
|
||
if err := runAppsShortcut(t, AppsFileDelete,
|
||
[]string{"+file-delete", "--app-id", "app_x", "--path", "/a.png", "--path", "/b.png", "--yes", "--dry-run", "--as", "user"}, factory, stdout); err != nil {
|
||
t.Fatalf("dry-run err=%v", err)
|
||
}
|
||
var env struct {
|
||
API []struct {
|
||
Method string `json:"method"`
|
||
URL string `json:"url"`
|
||
Body map[string]interface{} `json:"body"`
|
||
} `json:"api"`
|
||
}
|
||
_ = json.Unmarshal([]byte(stdout.String()), &env)
|
||
a := env.API[0]
|
||
if a.Method != "POST" || a.URL != fileDeleteURL {
|
||
t.Fatalf("dry-run = %s %s", a.Method, a.URL)
|
||
}
|
||
paths, _ := a.Body["paths"].([]interface{})
|
||
if len(paths) != 2 || paths[0] != "/a.png" || paths[1] != "/b.png" {
|
||
t.Fatalf("body.paths = %v", a.Body["paths"])
|
||
}
|
||
}
|
||
|
||
// 部分失败仍 ok:true;results 按下标 zip 回 path;失败项带 error{code,message}。
|
||
func TestAppsFileDelete_PartialFailureStillOK(t *testing.T) {
|
||
factory, stdout, reg := newAppsExecuteFactory(t)
|
||
reg.Register(&httpmock.Stub{
|
||
Method: "POST", URL: fileDeleteURL,
|
||
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{
|
||
"results": []interface{}{
|
||
map[string]interface{}{"status": "ok", "file": map[string]interface{}{"file_name": "a.png", "path": "/a.png"}},
|
||
map[string]interface{}{"status": "error", "error_code": "FILE_NOT_FOUND"},
|
||
},
|
||
}},
|
||
})
|
||
err := runAppsShortcut(t, AppsFileDelete,
|
||
[]string{"+file-delete", "--app-id", "app_x", "--path", "/a.png", "--path", "/missing.png", "--yes", "--as", "user"}, factory, stdout)
|
||
if err != nil {
|
||
t.Fatalf("partial failure should NOT error (ok:true semantics), got %v", err)
|
||
}
|
||
got := stdout.String()
|
||
var env struct {
|
||
Data struct {
|
||
Results []map[string]interface{} `json:"results"`
|
||
} `json:"data"`
|
||
}
|
||
if err := json.Unmarshal([]byte(got), &env); err != nil {
|
||
t.Fatalf("decode: %v\n%s", err, got)
|
||
}
|
||
if len(env.Data.Results) != 2 {
|
||
t.Fatalf("want 2 results, got %d: %s", len(env.Data.Results), got)
|
||
}
|
||
r0, r1 := env.Data.Results[0], env.Data.Results[1]
|
||
if r0["status"] != "ok" || r0["path"] != "/a.png" {
|
||
t.Errorf("result[0] = %v", r0)
|
||
}
|
||
if r1["status"] != "error" || r1["path"] != "/missing.png" {
|
||
t.Errorf("result[1] = %v (path must be back-filled by index)", r1)
|
||
}
|
||
if e, ok := r1["error"].(map[string]interface{}); !ok || e["code"] != "FILE_NOT_FOUND" {
|
||
t.Errorf("result[1].error = %v (want code FILE_NOT_FOUND)", r1["error"])
|
||
}
|
||
}
|
||
|
||
// TestAppsFileDelete_PrettySummary 验证 pretty 输出逐项 ✓/✗ 标记并汇总 "1/2 deleted"。
|
||
func TestAppsFileDelete_PrettySummary(t *testing.T) {
|
||
factory, stdout, reg := newAppsExecuteFactory(t)
|
||
reg.Register(&httpmock.Stub{
|
||
Method: "POST", URL: fileDeleteURL,
|
||
Body: map[string]interface{}{"code": 0, "data": map[string]interface{}{
|
||
"results": []interface{}{
|
||
map[string]interface{}{"status": "ok", "file": map[string]interface{}{"file_name": "a.png"}},
|
||
map[string]interface{}{"status": "error", "error_code": "FILE_NOT_FOUND"},
|
||
},
|
||
}},
|
||
})
|
||
if err := runAppsShortcut(t, AppsFileDelete,
|
||
[]string{"+file-delete", "--app-id", "app_x", "--path", "/a.png", "--path", "/missing.png", "--yes", "--format", "pretty", "--as", "user"}, factory, stdout); err != nil {
|
||
t.Fatalf("execute err=%v", err)
|
||
}
|
||
got := stdout.String()
|
||
for _, want := range []string{"✓ /a.png", "✗ /missing.png (FILE_NOT_FOUND)", "1/2 deleted"} {
|
||
if !strings.Contains(got, want) {
|
||
t.Errorf("pretty missing %q:\n%s", want, got)
|
||
}
|
||
}
|
||
}
|