mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
Add BuildResourceURL helper and wire it into doc/sheets/drive/base/wiki create paths so callers always receive a clickable link, even when the backend response (MCP degraded path or upstream OpenAPI) returns an empty URL field. The fallback uses the brand-standard host (www.feishu.cn / www.larksuite.com), which redirects to the tenant domain. Affected entries: - docs +create v1 / v2 - sheets +create - drive +create-folder / +import / +upload (newly exposes url) - wiki +node-create (newly exposes url) drive +create-shortcut is intentionally skipped because the URL form depends on the underlying file kind, which the shortcut payload does not carry.
333 lines
9.2 KiB
Go
333 lines
9.2 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package drive
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/larksuite/cli/internal/cmdutil"
|
|
"github.com/larksuite/cli/internal/core"
|
|
"github.com/larksuite/cli/internal/httpmock"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
func TestValidateDriveCreateFolderSpecRejectsInvalidInputs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
spec driveCreateFolderSpec
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "empty name",
|
|
spec: driveCreateFolderSpec{},
|
|
wantErr: "--name must not be empty",
|
|
},
|
|
{
|
|
name: "name too long",
|
|
spec: driveCreateFolderSpec{
|
|
Name: strings.Repeat("a", 257),
|
|
},
|
|
wantErr: "maximum of 256 bytes",
|
|
},
|
|
{
|
|
name: "invalid folder token",
|
|
spec: driveCreateFolderSpec{
|
|
Name: "Reports",
|
|
FolderToken: "../bad",
|
|
},
|
|
wantErr: "--folder-token",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
err := validateDriveCreateFolderSpec(tt.spec)
|
|
if err == nil {
|
|
t.Fatal("expected validation error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Fatalf("error = %q, want substring %q", err.Error(), tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDriveCreateFolderDryRunIncludesCreateRequest(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cmd := &cobra.Command{Use: "drive +create-folder"}
|
|
cmd.Flags().String("name", "", "")
|
|
cmd.Flags().String("folder-token", "", "")
|
|
if err := cmd.Flags().Set("name", " Weekly Reports "); err != nil {
|
|
t.Fatalf("set --name: %v", err)
|
|
}
|
|
if err := cmd.Flags().Set("folder-token", " fld_parent "); err != nil {
|
|
t.Fatalf("set --folder-token: %v", err)
|
|
}
|
|
|
|
runtime := common.TestNewRuntimeContextWithIdentity(cmd, nil, core.AsBot)
|
|
dry := DriveCreateFolder.DryRun(context.Background(), runtime)
|
|
if dry == nil {
|
|
t.Fatal("DryRun returned nil")
|
|
}
|
|
|
|
data, err := json.Marshal(dry)
|
|
if err != nil {
|
|
t.Fatalf("marshal dry run: %v", err)
|
|
}
|
|
|
|
var got struct {
|
|
API []struct {
|
|
Method string `json:"method"`
|
|
URL string `json:"url"`
|
|
Body map[string]interface{} `json:"body"`
|
|
} `json:"api"`
|
|
}
|
|
if err := json.Unmarshal(data, &got); err != nil {
|
|
t.Fatalf("unmarshal dry run json: %v", err)
|
|
}
|
|
if len(got.API) != 1 {
|
|
t.Fatalf("expected 1 API call, got %d", len(got.API))
|
|
}
|
|
if got.API[0].Method != "POST" || got.API[0].URL != "/open-apis/drive/v1/files/create_folder" {
|
|
t.Fatalf("unexpected dry-run API call: %#v", got.API[0])
|
|
}
|
|
if got.API[0].Body["name"] != "Weekly Reports" {
|
|
t.Fatalf("name = %#v, want %q", got.API[0].Body["name"], "Weekly Reports")
|
|
}
|
|
if got.API[0].Body["folder_token"] != "fld_parent" {
|
|
t.Fatalf("folder_token = %#v, want %q", got.API[0].Body["folder_token"], "fld_parent")
|
|
}
|
|
}
|
|
|
|
func TestDriveCreateFolderBotAutoGrantSuccess(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, drivePermissionGrantTestConfig(t, "ou_current_user"))
|
|
|
|
createStub := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/drive/v1/files/create_folder",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"msg": "ok",
|
|
"data": map[string]interface{}{
|
|
"token": "fld_created",
|
|
"url": "https://example.feishu.cn/drive/folder/fld_created",
|
|
},
|
|
},
|
|
}
|
|
reg.Register(createStub)
|
|
|
|
permStub := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/drive/v1/permissions/fld_created/members",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"msg": "ok",
|
|
},
|
|
}
|
|
reg.Register(permStub)
|
|
|
|
err := mountAndRunDrive(t, DriveCreateFolder, []string{
|
|
"+create-folder",
|
|
"--name", " Weekly Reports ",
|
|
"--folder-token", " fld_parent ",
|
|
"--as", "bot",
|
|
}, f, stdout)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
body := decodeCapturedJSONBody(t, createStub)
|
|
if body["name"] != "Weekly Reports" {
|
|
t.Fatalf("name = %#v, want %q", body["name"], "Weekly Reports")
|
|
}
|
|
if body["folder_token"] != "fld_parent" {
|
|
t.Fatalf("folder_token = %#v, want %q", body["folder_token"], "fld_parent")
|
|
}
|
|
|
|
data := decodeDriveEnvelope(t, stdout)
|
|
if data["folder_token"] != "fld_created" {
|
|
t.Fatalf("folder_token = %#v, want %q", data["folder_token"], "fld_created")
|
|
}
|
|
if data["parent_folder_token"] != "fld_parent" {
|
|
t.Fatalf("parent_folder_token = %#v, want %q", data["parent_folder_token"], "fld_parent")
|
|
}
|
|
if data["name"] != "Weekly Reports" {
|
|
t.Fatalf("name = %#v, want %q", data["name"], "Weekly Reports")
|
|
}
|
|
if data["url"] != "https://example.feishu.cn/drive/folder/fld_created" {
|
|
t.Fatalf("url = %#v, want folder url", data["url"])
|
|
}
|
|
|
|
grant, _ := data["permission_grant"].(map[string]interface{})
|
|
if grant["status"] != common.PermissionGrantGranted {
|
|
t.Fatalf("permission_grant.status = %#v, want %q", grant["status"], common.PermissionGrantGranted)
|
|
}
|
|
if grant["user_open_id"] != "ou_current_user" {
|
|
t.Fatalf("permission_grant.user_open_id = %#v, want %q", grant["user_open_id"], "ou_current_user")
|
|
}
|
|
if grant["message"] != "Granted the current CLI user full_access (可管理权限) on the new folder." {
|
|
t.Fatalf("permission_grant.message = %#v", grant["message"])
|
|
}
|
|
|
|
permBody := decodeCapturedJSONBody(t, permStub)
|
|
if permBody["member_type"] != "openid" || permBody["member_id"] != "ou_current_user" || permBody["perm"] != "full_access" || permBody["type"] != "user" {
|
|
t.Fatalf("unexpected permission request body: %#v", permBody)
|
|
}
|
|
}
|
|
|
|
func TestDriveCreateFolderUsesRootWhenParentIsOmitted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, drivePermissionGrantTestConfig(t, "ou_current_user"))
|
|
|
|
createStub := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/drive/v1/files/create_folder",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"msg": "ok",
|
|
"data": map[string]interface{}{
|
|
"token": "fld_root_child",
|
|
},
|
|
},
|
|
}
|
|
reg.Register(createStub)
|
|
|
|
err := mountAndRunDrive(t, DriveCreateFolder, []string{
|
|
"+create-folder",
|
|
"--name", "Inbox",
|
|
"--as", "user",
|
|
}, f, stdout)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
body := decodeCapturedJSONBody(t, createStub)
|
|
if body["folder_token"] != "" {
|
|
t.Fatalf("folder_token = %#v, want empty string for root create", body["folder_token"])
|
|
}
|
|
|
|
data := decodeDriveEnvelope(t, stdout)
|
|
if data["folder_token"] != "fld_root_child" {
|
|
t.Fatalf("folder_token = %#v, want %q", data["folder_token"], "fld_root_child")
|
|
}
|
|
if data["parent_folder_token"] != "" {
|
|
t.Fatalf("parent_folder_token = %#v, want empty string", data["parent_folder_token"])
|
|
}
|
|
if _, ok := data["permission_grant"]; ok {
|
|
t.Fatalf("did not expect permission_grant in user mode output: %#v", data)
|
|
}
|
|
}
|
|
|
|
func TestDriveCreateFolderFallbackURLWhenBackendOmitsIt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, drivePermissionGrantTestConfig(t, ""))
|
|
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/drive/v1/files/create_folder",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"msg": "ok",
|
|
"data": map[string]interface{}{
|
|
"token": "fld_created",
|
|
// "url" deliberately omitted to exercise the fallback.
|
|
},
|
|
},
|
|
})
|
|
|
|
err := mountAndRunDrive(t, DriveCreateFolder, []string{
|
|
"+create-folder",
|
|
"--name", "Weekly Reports",
|
|
"--as", "user",
|
|
}, f, stdout)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
data := decodeDriveEnvelope(t, stdout)
|
|
if got, want := data["url"], "https://www.feishu.cn/drive/folder/fld_created"; got != want {
|
|
t.Fatalf("url = %#v, want %q (brand-standard fallback)", got, want)
|
|
}
|
|
}
|
|
|
|
func TestDriveCreateFolderFallbackURLWhenBackendURLIsWhitespace(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, drivePermissionGrantTestConfig(t, ""))
|
|
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/drive/v1/files/create_folder",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"msg": "ok",
|
|
"data": map[string]interface{}{
|
|
"token": "fld_created",
|
|
"url": " ", // whitespace-only must trigger fallback, not pass through.
|
|
},
|
|
},
|
|
})
|
|
|
|
err := mountAndRunDrive(t, DriveCreateFolder, []string{
|
|
"+create-folder",
|
|
"--name", "Weekly Reports",
|
|
"--as", "user",
|
|
}, f, stdout)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
data := decodeDriveEnvelope(t, stdout)
|
|
if got, want := data["url"], "https://www.feishu.cn/drive/folder/fld_created"; got != want {
|
|
t.Fatalf("url = %#v, want %q (whitespace-only backend URL must yield fallback)", got, want)
|
|
}
|
|
}
|
|
|
|
func TestDriveCreateFolderRejectsCreateResponseWithoutToken(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, drivePermissionGrantTestConfig(t, "ou_current_user"))
|
|
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/drive/v1/files/create_folder",
|
|
Body: map[string]interface{}{
|
|
"code": 0,
|
|
"msg": "ok",
|
|
"data": map[string]interface{}{
|
|
"url": "https://example.feishu.cn/drive/folder/unknown",
|
|
},
|
|
},
|
|
})
|
|
|
|
err := mountAndRunDrive(t, DriveCreateFolder, []string{
|
|
"+create-folder",
|
|
"--name", "Broken Folder",
|
|
"--as", "bot",
|
|
}, f, stdout)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "returned no folder token") {
|
|
t.Fatalf("err = %v, want missing folder token error", err)
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("stdout should be empty on error, got %s", stdout.String())
|
|
}
|
|
}
|