Files
larksuite--cli/tests/cli_e2e/drive/drive_member_add_dryrun_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

500 lines
15 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package drive
import (
"context"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
// TestDrive_MemberAddDryRun locks in the request shape the shortcut emits
// under --dry-run: the real CLI binary is invoked end-to-end (so flag parsing,
// validation, and dry-run rendering all execute), and the printed request is
// inspected to confirm
// - HTTP method, URL template, and token path segment,
// - type query parameter (auto-inferred from a URL input, explicit for a
// bare token input),
// - explicit --type overriding URL inference,
// - member_type / member kind / perm / perm_type body fields,
// - single-member vs batch endpoint selection.
//
// Fake credentials are sufficient because --dry-run short-circuits before any
// network call.
func TestDrive_MemberAddDryRun(t *testing.T) {
// Isolate from any local CLI state: the subprocess inherits the parent
// test environment, and without an explicit config dir it could read a
// developer's real credentials/profile instead of the fake ones below.
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
t.Setenv("LARKSUITE_CLI_APP_ID", "app")
t.Setenv("LARKSUITE_CLI_APP_SECRET", "secret")
t.Setenv("LARKSUITE_CLI_BRAND", "feishu")
tests := []struct {
name string
args []string
wantURL string
wantResourceType string
wantNeedNotification string
wantMemberID string
wantMemberType string
wantPerm string
wantMemberKind string
wantPermType string
wantBatch bool
}{
{
name: "URL input auto-infers docx type",
args: []string{
"drive", "+member-add",
"--token", "https://example.feishu.cn/docx/doxcnE2E001?from=share",
"--member-id", "ou_e2e_user",
"--member-type", "openid",
"--perm", "view",
"--need-notification=false",
"--dry-run",
},
wantURL: "/open-apis/drive/v1/permissions/doxcnE2E001/members",
wantResourceType: "docx",
wantNeedNotification: "false",
wantMemberID: "ou_e2e_user",
wantMemberType: "openid",
wantPerm: "view",
wantMemberKind: "user",
},
{
name: "URL input auto-infers mindnote type from mindnotes path",
args: []string{
"drive", "+member-add",
"--token", "https://example.feishu.cn/mindnotes/mndE2E011?from=share",
"--member-id", "ou_e2e_user",
"--member-type", "openid",
"--perm", "view",
"--dry-run",
},
wantURL: "/open-apis/drive/v1/permissions/mndE2E011/members",
wantResourceType: "mindnote",
wantMemberID: "ou_e2e_user",
wantMemberType: "openid",
wantPerm: "view",
wantMemberKind: "user",
},
{
name: "bare token with explicit wiki type defaults perm_type container",
args: []string{
"drive", "+member-add",
"--token", "wikcnE2E002",
"--type", "wiki",
"--member-id", "ou_e2e_admin",
"--member-type", "openid",
"--perm", "full_access",
"--dry-run",
},
wantURL: "/open-apis/drive/v1/permissions/wikcnE2E002/members",
wantResourceType: "wiki",
wantMemberID: "ou_e2e_admin",
wantMemberType: "openid",
wantPerm: "full_access",
wantMemberKind: "user",
wantPermType: "container",
},
{
// Explicit --type must override URL inference: the /docx/ marker
// would infer type=docx, but the caller asked to grant permission
// against a wiki node. The URL token itself is still used as the
// path token.
name: "explicit --type overrides URL inference",
args: []string{
"drive", "+member-add",
"--token", "https://example.feishu.cn/docx/doxcnE2E009",
"--type", "wiki",
"--member-id", "ou_e2e_override",
"--member-type", "openid",
"--perm", "view",
"--dry-run",
},
wantURL: "/open-apis/drive/v1/permissions/doxcnE2E009/members",
wantResourceType: "wiki",
wantMemberID: "ou_e2e_override",
wantMemberType: "openid",
wantPerm: "view",
wantMemberKind: "user",
wantPermType: "container",
},
{
name: "bare token with explicit folder type",
args: []string{
"drive", "+member-add",
"--token", "fldE2E010",
"--type", "folder",
"--member-id", "ou_e2e_folder",
"--member-type", "openid",
"--perm", "view",
"--dry-run",
},
wantURL: "/open-apis/drive/v1/permissions/fldE2E010/members",
wantResourceType: "folder",
wantMemberID: "ou_e2e_folder",
wantMemberType: "openid",
wantPerm: "view",
wantMemberKind: "user",
},
{
name: "email member-type",
args: []string{
"drive", "+member-add",
"--token", "shtcnE2E003",
"--type", "sheet",
"--member-id", "user@example.com",
"--member-type", "email",
"--perm", "edit",
"--dry-run",
},
wantURL: "/open-apis/drive/v1/permissions/shtcnE2E003/members",
wantResourceType: "sheet",
wantMemberID: "user@example.com",
wantMemberType: "email",
wantPerm: "edit",
wantMemberKind: "user",
},
{
name: "unionid member-type",
args: []string{
"drive", "+member-add",
"--token", "doxcnE2E006",
"--type", "docx",
"--member-id", "on_e2e_union",
"--member-type", "unionid",
"--perm", "view",
"--dry-run",
},
wantURL: "/open-apis/drive/v1/permissions/doxcnE2E006/members",
wantResourceType: "docx",
wantMemberID: "on_e2e_union",
wantMemberType: "unionid",
wantPerm: "view",
wantMemberKind: "user",
},
{
name: "explicit-only groupid member-type",
args: []string{
"drive", "+member-add",
"--token", "doxcnE2E007",
"--type", "docx",
"--member-id", "group_e2e",
"--member-type", "groupid",
"--perm", "view",
"--dry-run",
},
wantURL: "/open-apis/drive/v1/permissions/doxcnE2E007/members",
wantResourceType: "docx",
wantMemberID: "group_e2e",
wantMemberType: "groupid",
wantPerm: "view",
wantMemberKind: "group",
},
{
name: "batch members use batch_create endpoint",
args: []string{
"drive", "+member-add",
"--token", "bascnE2E004",
"--type", "bitable",
"--member-id", "ou_a,ou_b",
"--member-type", "openid",
"--perm", "view",
"--dry-run",
},
wantURL: "/open-apis/drive/v1/permissions/bascnE2E004/members/batch_create",
wantResourceType: "bitable",
wantMemberID: "ou_a",
wantMemberType: "openid",
wantPerm: "view",
wantMemberKind: "user",
wantBatch: true,
},
{
name: "explicit groupid member-type",
args: []string{
"drive", "+member-add",
"--token", "doxcnE2E005",
"--type", "docx",
"--member-id", "grp_abc",
"--member-type", "groupid",
"--perm", "edit",
"--dry-run",
},
wantURL: "/open-apis/drive/v1/permissions/doxcnE2E005/members",
wantResourceType: "docx",
wantMemberID: "grp_abc",
wantMemberType: "groupid",
wantPerm: "edit",
wantMemberKind: "group",
},
{
name: "appid member-type is passed through",
args: []string{
"drive", "+member-add",
"--token", "doxcnE2E008",
"--type", "docx",
"--member-id", "cli_app_123",
"--member-type", "appid",
"--perm", "view",
"--dry-run",
},
wantURL: "/open-apis/drive/v1/permissions/doxcnE2E008/members",
wantResourceType: "docx",
wantMemberID: "cli_app_123",
wantMemberType: "appid",
wantPerm: "view",
},
{
name: "wikispaceid member-type requires wiki-space body type",
args: []string{
"drive", "+member-add",
"--token", "doxcnE2E012",
"--type", "docx",
"--member-id", "spc_e2e_wiki",
"--member-type", "wikispaceid",
"--member-kind", "wiki_space_editor",
"--perm", "view",
"--dry-run",
},
wantURL: "/open-apis/drive/v1/permissions/doxcnE2E012/members",
wantResourceType: "docx",
wantMemberID: "spc_e2e_wiki",
wantMemberType: "wikispaceid",
wantPerm: "view",
wantMemberKind: "wiki_space_editor",
},
}
for _, temp := range tests {
tt := temp
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: tt.args,
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
out := result.Stdout
if got := gjson.Get(out, "api.0.method").String(); got != "POST" {
t.Fatalf("method = %q, want POST\nstdout:\n%s", got, out)
}
if got := gjson.Get(out, "api.0.url").String(); got != tt.wantURL {
t.Fatalf("url = %q, want %q\nstdout:\n%s", got, tt.wantURL, out)
}
if got := gjson.Get(out, "api.0.params.type").String(); got != tt.wantResourceType {
t.Fatalf("params.type = %q, want %q\nstdout:\n%s", got, tt.wantResourceType, out)
}
notification := gjson.Get(out, "api.0.params.need_notification")
if tt.wantNeedNotification == "" {
if notification.Exists() {
t.Fatalf("need_notification should be omitted\nstdout:\n%s", out)
}
} else if got := notification.String(); got != tt.wantNeedNotification {
t.Fatalf("need_notification = %q, want %q\nstdout:\n%s", got, tt.wantNeedNotification, out)
}
bodyPath := "api.0.body"
if tt.wantBatch {
bodyPath = "api.0.body.members.0"
if count := len(gjson.Get(out, "api.0.body.members").Array()); count != 2 {
t.Fatalf("body.members count = %d, want 2\nstdout:\n%s", count, out)
}
}
if got := gjson.Get(out, bodyPath+".member_id").String(); got != tt.wantMemberID {
t.Fatalf("body.member_id = %q, want %q\nstdout:\n%s", got, tt.wantMemberID, out)
}
if got := gjson.Get(out, bodyPath+".member_type").String(); got != tt.wantMemberType {
t.Fatalf("body.member_type = %q, want %q\nstdout:\n%s", got, tt.wantMemberType, out)
}
if got := gjson.Get(out, bodyPath+".perm").String(); got != tt.wantPerm {
t.Fatalf("body.perm = %q, want %q\nstdout:\n%s", got, tt.wantPerm, out)
}
if got := gjson.Get(out, bodyPath+".type").String(); got != tt.wantMemberKind {
t.Fatalf("body.type = %q, want %q\nstdout:\n%s", got, tt.wantMemberKind, out)
}
permType := gjson.Get(out, bodyPath+".perm_type")
if tt.wantPermType == "" {
if permType.Exists() {
t.Fatalf("perm_type should be omitted\nstdout:\n%s", out)
}
} else if got := permType.String(); got != tt.wantPermType {
t.Fatalf("body.perm_type = %q, want %q\nstdout:\n%s", got, tt.wantPermType, out)
}
})
}
}
func TestDrive_MemberAddDryRunRejectsInvalidInputs(t *testing.T) {
// Isolate from any local CLI state: the subprocess inherits the parent
// test environment, and without an explicit config dir it could read a
// developer's real credentials/profile instead of the fake ones below.
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
t.Setenv("LARKSUITE_CLI_APP_ID", "app")
t.Setenv("LARKSUITE_CLI_APP_SECRET", "secret")
t.Setenv("LARKSUITE_CLI_BRAND", "feishu")
tests := []struct {
name string
args []string
defaultAs string
wantErr string
}{
{
name: "any host accepted",
args: []string{
"drive", "+member-add",
"--token", "https://google.com/docx/doxcnE2E001",
"--member-id", "ou_e2e_user",
"--member-type", "openid",
"--dry-run",
},
},
{
name: "unsupported URL path",
args: []string{
"drive", "+member-add",
"--token", "https://example.feishu.cn/calendar/calE2E001",
"--member-id", "ou_e2e_user",
"--member-type", "openid",
"--dry-run",
},
wantErr: "unsupported URL path",
},
{
name: "bare token requires explicit type",
args: []string{
"drive", "+member-add",
"--token", "unknownE2E001",
"--member-id", "ou_e2e_user",
"--member-type", "openid",
"--dry-run",
},
wantErr: "--type is required when --token is a bare token",
},
{
name: "duplicate member IDs are rejected",
args: []string{
"drive", "+member-add",
"--token", "doxcnE2E001",
"--type", "docx",
"--member-id", "ou_a,ou_b,ou_a",
"--member-type", "openid",
"--dry-run",
},
wantErr: "duplicate collaborator ID",
},
{
name: "invalid explicit type is rejected",
args: []string{
"drive", "+member-add",
"--token", "mincnE2E001",
"--type", "invalidtype",
"--member-id", "ou_e2e_user",
"--member-type", "openid",
"--dry-run",
},
wantErr: "--type must be one of: docx, doc, sheet, bitable, file, folder, wiki, mindnote, slides, minutes",
},
{
name: "member-id prefix conflicts with explicit member-type",
args: []string{
"drive", "+member-add",
"--token", "doxcnE2E001",
"--type", "docx",
"--member-id", "ou_e2e_user,oc_e2e_chat",
"--member-type", "openid",
"--dry-run",
},
wantErr: "implies --member-type openchat",
},
{
name: "explicit member-type conflicts with member-id prefix",
args: []string{
"drive", "+member-add",
"--token", "doxcnE2E001",
"--type", "docx",
"--member-id", "oc_e2e_chat",
"--member-type", "openid",
"--dry-run",
},
wantErr: "implies --member-type openchat",
},
{
name: "department collaborator requires user identity",
args: []string{
"drive", "+member-add",
"--token", "doxcnE2E001",
"--type", "docx",
"--member-id", "od_e2e_dept",
"--member-type", "opendepartmentid",
"--dry-run",
},
defaultAs: "bot",
wantErr: "--member-type=opendepartmentid requires --as user",
},
{
name: "wikispaceid requires member-kind",
args: []string{
"drive", "+member-add",
"--token", "doxcnE2E001",
"--type", "docx",
"--member-id", "spc_e2e_wiki",
"--member-type", "wikispaceid",
"--dry-run",
},
wantErr: "--member-kind is required when --member-type=wikispaceid",
},
{
name: "member-kind only applies to wikispaceid",
args: []string{
"drive", "+member-add",
"--token", "doxcnE2E001",
"--type", "docx",
"--member-id", "ou_e2e_user",
"--member-type", "openid",
"--member-kind", "wiki_space_member",
"--dry-run",
},
wantErr: "--member-kind only applies when --member-type=wikispaceid",
},
}
for _, temp := range tests {
tt := temp
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
defaultAs := tt.defaultAs
if defaultAs == "" {
defaultAs = "user"
}
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: tt.args,
DefaultAs: defaultAs,
})
require.NoError(t, err)
if tt.wantErr == "" {
result.AssertExitCode(t, 0)
return
}
result.AssertExitCode(t, 2)
output := result.Stdout + result.Stderr
require.Contains(t, output, tt.wantErr, "stdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr)
})
}
}