mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
7e9beec422
Wrap the POST /drive/v1/permissions/:token/members/apply endpoint as a user-only shortcut. --token accepts either a bare token or a document URL, with type auto-inferred from the URL path (/docx/, /sheets/, /base/, /bitable/, /file/, /wiki/, /doc/, /mindnote/, /minutes/, /slides/); an explicit --type always wins. --perm is limited to view or edit; full_access is rejected client-side to match the spec. Classifier gains two domain-specific hints for the endpoint's newly documented error codes: 1063006 (per-user-per-document quota of 5/day reached) and 1063007 (document does not accept apply requests — covers disallow-external-apply, already-has-access, and unsupported-type). test(drive): add dry-run E2E for +apply-permission Invoke the real CLI binary via clie2e.RunCmd under --dry-run and parse the rendered request JSON with gjson to lock in method, URL path (including the token segment), type query parameter (auto-inferred for docx / sheet / slides URLs, taken from explicit --type for bare tokens), perm body field, and remark presence/omission. A separate test asserts --perm full_access is rejected by the enum validator before reaching the server. Fake LARKSUITE_CLI_APP_ID / APP_SECRET / BRAND are enough because dry-run short-circuits before any API call. Update drive coverage.md to add a row and refresh metrics. test(drive): isolate E2E dry-run subprocess from local CLI config Set LARKSUITE_CLI_CONFIG_DIR to t.TempDir() in both +apply-permission dry-run tests so the subprocess can't read a developer's real credentials/profile instead of the fake env vars the tests inject. test(drive): add E2E case that exercises URL inference override Previous "bare token with explicit type wins over inference" row used a bare token, which has no URL-derived type to override. Replace it with a /docx/ URL + --type wiki combo that actually forces the explicit flag to win over URL inference, and add a separate bare-token row to keep the simpler path covered. Refresh coverage.md wording to match.
239 lines
7.4 KiB
Go
239 lines
7.4 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package drive
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/internal/cmdutil"
|
|
"github.com/larksuite/cli/internal/httpmock"
|
|
)
|
|
|
|
// ── resolvePermApplyTarget unit tests ────────────────────────────────────────
|
|
|
|
func TestResolvePermApplyTarget_BareTokenNeedsType(t *testing.T) {
|
|
t.Parallel()
|
|
_, _, err := resolvePermApplyTarget("bareToken", "")
|
|
if err == nil || !strings.Contains(err.Error(), "--type is required") {
|
|
t.Fatalf("expected --type required error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolvePermApplyTarget_BareTokenWithType(t *testing.T) {
|
|
t.Parallel()
|
|
token, docType, err := resolvePermApplyTarget("bareToken", "docx")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if token != "bareToken" || docType != "docx" {
|
|
t.Fatalf("got token=%q type=%q, want bareToken/docx", token, docType)
|
|
}
|
|
}
|
|
|
|
func TestResolvePermApplyTarget_URLInference(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
raw string
|
|
wantTok string
|
|
wantType string
|
|
}{
|
|
{"docx", "https://example.feishu.cn/docx/doxTok123?from=share", "doxTok123", "docx"},
|
|
{"sheets", "https://example.feishu.cn/sheets/shtTok456?sheet=abc", "shtTok456", "sheet"},
|
|
{"base", "https://example.feishu.cn/base/bscTok789", "bscTok789", "bitable"},
|
|
{"bitable", "https://example.feishu.cn/bitable/bscTok789", "bscTok789", "bitable"},
|
|
{"file", "https://example.feishu.cn/file/boxTok111", "boxTok111", "file"},
|
|
{"wiki", "https://example.feishu.cn/wiki/wikTok222", "wikTok222", "wiki"},
|
|
{"legacy doc", "https://example.feishu.cn/doc/docTok333", "docTok333", "doc"},
|
|
{"mindnote", "https://example.feishu.cn/mindnote/mnTok444", "mnTok444", "mindnote"},
|
|
{"slides", "https://example.feishu.cn/slides/slTok666", "slTok666", "slides"},
|
|
}
|
|
for _, temp := range tests {
|
|
tt := temp
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
token, docType, err := resolvePermApplyTarget(tt.raw, "")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if token != tt.wantTok || docType != tt.wantType {
|
|
t.Fatalf("got (%q,%q), want (%q,%q)", token, docType, tt.wantTok, tt.wantType)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolvePermApplyTarget_ExplicitTypeOverridesURL(t *testing.T) {
|
|
t.Parallel()
|
|
// Even though the URL marker is /docx/, an explicit --type wins.
|
|
token, docType, err := resolvePermApplyTarget("https://example.feishu.cn/docx/doxTok123", "wiki")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if token != "doxTok123" || docType != "wiki" {
|
|
t.Fatalf("got (%q,%q), want (doxTok123,wiki)", token, docType)
|
|
}
|
|
}
|
|
|
|
func TestResolvePermApplyTarget_UnrecognizedURL(t *testing.T) {
|
|
t.Parallel()
|
|
_, _, err := resolvePermApplyTarget("https://example.feishu.cn/unknown/xyz", "")
|
|
if err == nil || !strings.Contains(err.Error(), "could not infer token") {
|
|
t.Fatalf("expected infer error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolvePermApplyTarget_Empty(t *testing.T) {
|
|
t.Parallel()
|
|
_, _, err := resolvePermApplyTarget(" ", "docx")
|
|
if err == nil || !strings.Contains(err.Error(), "--token is required") {
|
|
t.Fatalf("expected token required error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// ── shortcut integration tests ──────────────────────────────────────────────
|
|
|
|
func TestDriveApplyPermission_ValidateMissingToken(t *testing.T) {
|
|
t.Parallel()
|
|
f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig())
|
|
err := mountAndRunDrive(t, DriveApplyPermission, []string{
|
|
"+apply-permission", "--perm", "view", "--type", "docx", "--as", "user",
|
|
}, f, stdout)
|
|
if err == nil || !strings.Contains(err.Error(), "token") {
|
|
t.Fatalf("expected token error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDriveApplyPermission_ValidateRejectsBadPerm(t *testing.T) {
|
|
t.Parallel()
|
|
f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig())
|
|
err := mountAndRunDrive(t, DriveApplyPermission, []string{
|
|
"+apply-permission",
|
|
"--token", "doxTok",
|
|
"--type", "docx",
|
|
"--perm", "full_access",
|
|
"--as", "user",
|
|
}, f, stdout)
|
|
if err == nil || !strings.Contains(err.Error(), "--perm") {
|
|
t.Fatalf("expected perm enum error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDriveApplyPermission_DryRunInfersTypeFromURL(t *testing.T) {
|
|
t.Parallel()
|
|
f, stdout, _, _ := cmdutil.TestFactory(t, driveTestConfig())
|
|
err := mountAndRunDrive(t, DriveApplyPermission, []string{
|
|
"+apply-permission",
|
|
"--token", "https://example.feishu.cn/sheets/shtTok?sheet=abc",
|
|
"--perm", "edit",
|
|
"--remark", "please",
|
|
"--dry-run", "--as", "user",
|
|
}, f, stdout)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
out := stdout.String()
|
|
for _, want := range []string{
|
|
"/open-apis/drive/v1/permissions/shtTok/members/apply",
|
|
`"POST"`,
|
|
`"sheet"`,
|
|
`"edit"`,
|
|
`"please"`,
|
|
`"shtTok"`,
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Fatalf("dry-run output missing %q:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDriveApplyPermission_ExecuteSuccess(t *testing.T) {
|
|
f, stdout, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
|
// Stub URL includes "?type=docx" — the stub only matches when the request
|
|
// URL contains that query, so this doubles as an assertion that the
|
|
// shortcut emits the type query parameter.
|
|
stub := &httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/drive/v1/permissions/doxTok123/members/apply?type=docx",
|
|
Body: map[string]interface{}{
|
|
"code": 0, "msg": "success",
|
|
"data": map[string]interface{}{"applied": true},
|
|
},
|
|
}
|
|
reg.Register(stub)
|
|
|
|
err := mountAndRunDrive(t, DriveApplyPermission, []string{
|
|
"+apply-permission",
|
|
"--token", "doxTok123",
|
|
"--type", "docx",
|
|
"--perm", "view",
|
|
"--as", "user",
|
|
}, f, stdout)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
var body map[string]interface{}
|
|
if err := json.Unmarshal(stub.CapturedBody, &body); err != nil {
|
|
t.Fatalf("parse body: %v", err)
|
|
}
|
|
if body["perm"] != "view" {
|
|
t.Fatalf("perm = %v, want view", body["perm"])
|
|
}
|
|
if _, hasRemark := body["remark"]; hasRemark {
|
|
t.Fatalf("remark should be omitted when empty, got: %v", body["remark"])
|
|
}
|
|
}
|
|
|
|
func TestDriveApplyPermission_ExecuteNotApplicableHint(t *testing.T) {
|
|
f, _, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/drive/v1/permissions/doxTok/members/apply",
|
|
Status: 400,
|
|
Body: map[string]interface{}{
|
|
"code": 1063007, "msg": "request not applicable",
|
|
},
|
|
})
|
|
|
|
err := mountAndRunDrive(t, DriveApplyPermission, []string{
|
|
"+apply-permission",
|
|
"--token", "doxTok",
|
|
"--type", "docx",
|
|
"--perm", "view",
|
|
"--as", "user",
|
|
}, f, nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for 1063007")
|
|
}
|
|
if !strings.Contains(err.Error(), "not applicable") {
|
|
t.Fatalf("expected surfaced server message, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDriveApplyPermission_ExecuteRateLimitHint(t *testing.T) {
|
|
f, _, _, reg := cmdutil.TestFactory(t, driveTestConfig())
|
|
reg.Register(&httpmock.Stub{
|
|
Method: "POST",
|
|
URL: "/open-apis/drive/v1/permissions/doxTok/members/apply",
|
|
Status: 429,
|
|
Body: map[string]interface{}{
|
|
"code": 1063006, "msg": "quota exceeded",
|
|
},
|
|
})
|
|
|
|
err := mountAndRunDrive(t, DriveApplyPermission, []string{
|
|
"+apply-permission",
|
|
"--token", "doxTok",
|
|
"--type", "docx",
|
|
"--perm", "view",
|
|
"--as", "user",
|
|
}, f, nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for 1063006")
|
|
}
|
|
}
|