mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
Compare commits
3 Commits
v1.0.79-be
...
feat/bot-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27b38f8bb9 | ||
|
|
3958bc047e | ||
|
|
215fe8a614 |
@@ -55,6 +55,7 @@ func BaseSecurityHeaders() http.Header {
|
||||
if v := envvars.AgentTrace(); v != "" {
|
||||
h.Set(HeaderAgentTrace, v)
|
||||
}
|
||||
h.Set("x-tt-env", "ppe_bot_user_id")
|
||||
return h
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ func ResolveEndpoints(brand LarkBrand) Endpoints {
|
||||
}
|
||||
default:
|
||||
return Endpoints{
|
||||
Open: "https://open.feishu.cn",
|
||||
Open: "https://open.feishu-pre.cn",
|
||||
Accounts: "https://accounts.feishu.cn",
|
||||
MCP: "https://mcp.feishu.cn",
|
||||
AppLink: "https://applink.feishu.cn",
|
||||
|
||||
68
shortcuts/minutes/bot_identity_test.go
Normal file
68
shortcuts/minutes/bot_identity_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Tests pinning bot-identity support for `minutes +detail` (minute metadata,
|
||||
// artifacts, and transcript all flow under a tenant access token).
|
||||
|
||||
package minutes
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
)
|
||||
|
||||
func TestMinutesDetailSupportsUserAndBotIdentity(t *testing.T) {
|
||||
want := []string{"user", "bot"}
|
||||
if !reflect.DeepEqual(MinutesDetail.AuthTypes, want) {
|
||||
t.Fatalf("MinutesDetail.AuthTypes = %v, want %v", MinutesDetail.AuthTypes, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetail_DryRun_BotIdentity(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig())
|
||||
err := detailMountAndRun(t, MinutesDetail, []string{"+detail", "--minute-tokens", "tok001", "--dry-run", "--as", "bot"}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error under --as bot: %v", err)
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "/open-apis/minutes/v1/minutes/") {
|
||||
t.Errorf("dry-run should show minutes API path, got: %s", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetail_DryRun_BotIdentity_Transcript(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig())
|
||||
err := detailMountAndRun(t, MinutesDetail, []string{"+detail", "--minute-tokens", "tok001", "--transcript", "--dry-run", "--as", "bot"}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error under --as bot: %v", err)
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "artifacts") {
|
||||
t.Errorf("dry-run should show artifacts API path when --transcript is set, got: %s", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinutesApplyPermissionSupportsUserAndBotIdentity(t *testing.T) {
|
||||
want := []string{"user", "bot"}
|
||||
if !reflect.DeepEqual(MinutesApplyPermission.AuthTypes, want) {
|
||||
t.Fatalf("MinutesApplyPermission.AuthTypes = %v, want %v", MinutesApplyPermission.AuthTypes, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyPermission_DryRun_BotIdentity(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig())
|
||||
err := mountAndRun(t, MinutesApplyPermission, []string{
|
||||
"+apply-permission", "--minute-token", "obcnexampleminute", "--perm", "view", "--dry-run", "--as", "bot",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error under --as bot: %v", err)
|
||||
}
|
||||
out := stdout.String()
|
||||
if !strings.Contains(out, "/open-apis/minutes/v1/minutes/obcnexampleminute/permissions/apply") {
|
||||
t.Errorf("dry-run should show apply-permission API path, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, `"perm": "view"`) && !strings.Contains(out, `"perm":"view"`) {
|
||||
t.Errorf("dry-run should show perm body, got: %s", out)
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ var MinutesApplyPermission = common.Shortcut{
|
||||
Description: "Apply for view or edit permission on a minute",
|
||||
Risk: "write",
|
||||
Scopes: []string{"minutes:permission:apply"},
|
||||
AuthTypes: []string{"user"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
Flags: []common.Flag{
|
||||
{Name: "minute-token", Desc: "minute token", Required: true},
|
||||
{Name: "perm", Desc: "permission to apply for", Required: true, Enum: []string{"view", "edit"}},
|
||||
|
||||
@@ -285,7 +285,7 @@ var MinutesDetail = common.Shortcut{
|
||||
Description: "Query minute details with selective artifact flags (summary, todo, chapter, transcript, keyword)",
|
||||
Risk: "read",
|
||||
Scopes: []string{"minutes:minutes.basic:read", "minutes:minutes.artifacts:read"},
|
||||
AuthTypes: []string{"user"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: []common.Flag{
|
||||
{Name: "minute-tokens", Desc: "minute tokens, comma-separated for batch", Required: true},
|
||||
|
||||
145
shortcuts/vc/bot_identity_test.go
Normal file
145
shortcuts/vc/bot_identity_test.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Tests pinning bot-identity support for the vc read shortcuts
|
||||
// (+detail / +notes / +recording).
|
||||
|
||||
package vc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/credential"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AuthTypes contracts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestVCReadShortcutsSupportUserAndBotIdentity(t *testing.T) {
|
||||
want := []string{"user", "bot"}
|
||||
cases := map[string][]string{
|
||||
"+detail": VCDetail.AuthTypes,
|
||||
"+notes": VCNotes.AuthTypes,
|
||||
"+recording": VCRecording.AuthTypes,
|
||||
}
|
||||
for cmd, got := range cases {
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("%s AuthTypes = %v, want %v", cmd, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bot dry-run: the meeting/recording paths flow under bot identity
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestDetail_DryRun_BotIdentity(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig())
|
||||
err := mountAndRun(t, VCDetail, []string{"+detail", "--meeting-ids", "m001", "--dry-run", "--as", "bot"}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error under --as bot: %v", err)
|
||||
}
|
||||
out := stdout.String()
|
||||
if !strings.Contains(out, "/open-apis/vc/v1/meetings/{meeting_id}") {
|
||||
t.Errorf("dry-run should show meeting.get API, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "recording") {
|
||||
t.Errorf("dry-run should show recording API, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecording_DryRun_BotIdentity(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig())
|
||||
err := mountAndRun(t, VCRecording, []string{"+recording", "--meeting-ids", "m001", "--dry-run", "--as", "bot"}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error under --as bot: %v", err)
|
||||
}
|
||||
out := stdout.String()
|
||||
if !strings.Contains(out, "recording") {
|
||||
t.Errorf("dry-run should show recording API, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotes_DryRun_BotIdentity(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig())
|
||||
err := mountAndRun(t, VCNotes, []string{"+notes", "--meeting-ids", "m001", "--dry-run", "--as", "bot"}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error under --as bot: %v", err)
|
||||
}
|
||||
out := stdout.String()
|
||||
if !strings.Contains(out, "/open-apis/vc/v1/notes/{note_id}") {
|
||||
t.Errorf("dry-run should show note.get API, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// calendar-event-ids also flows under bot: a bot has a primary calendar, so the
|
||||
// primary-calendar -> meeting_id -> recording/notes chain is expected to work.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestRecording_DryRun_BotIdentity_CalendarEventIDs(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig())
|
||||
err := mountAndRun(t, VCRecording, []string{"+recording", "--calendar-event-ids", "evt001", "--dry-run", "--as", "bot"}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error under --as bot: %v", err)
|
||||
}
|
||||
out := stdout.String()
|
||||
if !strings.Contains(out, "mget_instance_relation_info") {
|
||||
t.Errorf("dry-run should show the primary-calendar resolution step, got: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "recording") {
|
||||
t.Errorf("dry-run should show recording API, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotes_DryRun_BotIdentity_CalendarEventIDs(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig())
|
||||
err := mountAndRun(t, VCNotes, []string{"+notes", "--calendar-event-ids", "evt001", "--dry-run", "--as", "bot"}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error under --as bot: %v", err)
|
||||
}
|
||||
out := stdout.String()
|
||||
if !strings.Contains(out, "mget_instance_relation_info") {
|
||||
t.Errorf("dry-run should show the primary-calendar resolution step, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Identity-aware preflight: bot resolves TAT (empty local scopes in this stub),
|
||||
// so an under-scoped UAT must not make --as bot fail. Reverting to
|
||||
// auth.GetStoredToken(user) would break this.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestRecording_BotIdentityAwareScopePreflight(t *testing.T) {
|
||||
cfg := defaultConfig()
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, cfg)
|
||||
f.Credential = credential.NewCredentialProvider(nil, nil, &recordingIdentityTokenResolver{
|
||||
uatScopes: "calendar:calendar:read", // deliberately missing vc:record:readonly
|
||||
tatScopes: "", // bot/tenant: no local scope metadata
|
||||
}, nil)
|
||||
|
||||
err := mountAndRun(t, VCRecording, []string{"+recording", "--meeting-ids", "m001", "--dry-run", "--as", "bot"}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("bot preflight must resolve tenant token, not the under-scoped user token; got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// recordingIdentityTokenResolver returns different scopes for UAT vs TAT so
|
||||
// bot identity-aware preflight can be pinned separately from user preflight.
|
||||
type recordingIdentityTokenResolver struct {
|
||||
uatScopes string
|
||||
tatScopes string
|
||||
}
|
||||
|
||||
func (r *recordingIdentityTokenResolver) ResolveToken(_ context.Context, req credential.TokenSpec) (*credential.TokenResult, error) {
|
||||
scopes := r.uatScopes
|
||||
if req.Type == credential.TokenTypeTAT {
|
||||
scopes = r.tatScopes
|
||||
}
|
||||
return &credential.TokenResult{Token: "test-token", Scopes: scopes}, nil
|
||||
}
|
||||
@@ -164,7 +164,7 @@ var VCDetail = common.Shortcut{
|
||||
Description: "Get meeting details including note_id and minute_token by meeting IDs",
|
||||
Risk: "read",
|
||||
Scopes: []string{"vc:meeting.meetingevent:read", "vc:record:readonly"},
|
||||
AuthTypes: []string{"user"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: []common.Flag{
|
||||
{Name: "meeting-ids", Desc: "meeting IDs, comma-separated for batch", Required: true},
|
||||
|
||||
@@ -536,7 +536,7 @@ var VCNotes = common.Shortcut{
|
||||
Description: "Query meeting notes (via meeting-ids, minute-tokens, or calendar-event-ids)",
|
||||
Risk: "read",
|
||||
Scopes: []string{"vc:note:read"}, // minimum scope; additional per-flag scopes checked in Validate
|
||||
AuthTypes: []string{"user"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
Hidden: true, // hidden from --help; prefer vc +detail, minutes +detail, or note +detail
|
||||
HasFormat: true,
|
||||
Flags: []common.Flag{
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/credential"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
@@ -91,13 +92,13 @@ var VCRecording = common.Shortcut{
|
||||
Description: "Query minute_token from meeting-ids or calendar-event-ids",
|
||||
Risk: "read",
|
||||
Scopes: []string{"vc:record:readonly"},
|
||||
AuthTypes: []string{"user"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: []common.Flag{
|
||||
{Name: "meeting-ids", Desc: "meeting IDs, comma-separated for batch"},
|
||||
{Name: "calendar-event-ids", Desc: "calendar event instance IDs, comma-separated for batch"},
|
||||
},
|
||||
Validate: func(_ context.Context, runtime *common.RuntimeContext) error {
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
if err := common.ExactlyOneTyped(runtime, "meeting-ids", "calendar-event-ids"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -116,18 +117,14 @@ var VCRecording = common.Shortcut{
|
||||
case runtime.Str("calendar-event-ids") != "":
|
||||
required = scopesRecordingCalendarEventIDs
|
||||
}
|
||||
appID := runtime.Config.AppID
|
||||
userOpenID := runtime.UserOpenId()
|
||||
if appID != "" && userOpenID != "" {
|
||||
stored := auth.GetStoredToken(appID, userOpenID)
|
||||
if stored != nil {
|
||||
if missing := auth.MissingScopes(stored.Scope, required); len(missing) > 0 {
|
||||
return errs.NewPermissionError(errs.SubtypeMissingScope,
|
||||
"missing required scope(s): %s", strings.Join(missing, ", ")).
|
||||
WithHint("run `lark-cli auth login --scope %q` in the background. It blocks and outputs a verification URL — retrieve the URL and open it in a browser to complete login.", strings.Join(missing, " ")).
|
||||
WithMissingScopes(missing...).
|
||||
WithIdentity(string(runtime.As()))
|
||||
}
|
||||
result, err := runtime.Factory.Credential.ResolveToken(ctx, credential.NewTokenSpec(runtime.As(), runtime.Config.AppID))
|
||||
if err == nil && result != nil && result.Scopes != "" {
|
||||
if missing := auth.MissingScopes(result.Scopes, required); len(missing) > 0 {
|
||||
return errs.NewPermissionError(errs.SubtypeMissingScope,
|
||||
"missing required scope(s): %s", strings.Join(missing, ", ")).
|
||||
WithHint("run `lark-cli auth login --scope %q` in the background. It blocks and outputs a verification URL — retrieve the URL and open it in a browser to complete login.", strings.Join(missing, " ")).
|
||||
WithMissingScopes(missing...).
|
||||
WithIdentity(string(runtime.As()))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -10,14 +10,12 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
keyring "github.com/zalando/go-keyring"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/auth"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/credential"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
@@ -141,27 +139,15 @@ func TestRecording_BatchLimit_CalendarEventIDs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRecording_Validate_MissingScope(t *testing.T) {
|
||||
keyring.MockInit() // use in-memory keyring to avoid macOS keychain popups
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
|
||||
cfg := defaultConfig()
|
||||
// Store a token that intentionally lacks the vc:record:readonly scope.
|
||||
token := &auth.StoredUAToken{
|
||||
UserOpenId: cfg.UserOpenId,
|
||||
AppId: cfg.AppID,
|
||||
AccessToken: "test-user-access-token",
|
||||
RefreshToken: "test-refresh-token",
|
||||
ExpiresAt: time.Now().Add(1 * time.Hour).UnixMilli(),
|
||||
RefreshExpiresAt: time.Now().Add(24 * time.Hour).UnixMilli(),
|
||||
Scope: "calendar:calendar:read",
|
||||
GrantedAt: time.Now().Add(-1 * time.Hour).UnixMilli(),
|
||||
}
|
||||
if err := auth.SetStoredToken(token); err != nil {
|
||||
t.Fatalf("SetStoredToken() error = %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = auth.RemoveStoredToken(cfg.AppID, cfg.UserOpenId) })
|
||||
|
||||
f, _, _, _ := cmdutil.TestFactory(t, cfg)
|
||||
// TestFactory's default token resolver returns empty Scopes, which skips
|
||||
// identity-aware preflight. Inject a resolver that returns an under-scoped
|
||||
// user token so the MissingScopes path is exercised.
|
||||
f.Credential = credential.NewCredentialProvider(nil, nil, &recordingScopedTokenResolver{
|
||||
scopes: "calendar:calendar:read",
|
||||
}, nil)
|
||||
|
||||
err := mountAndRun(t, VCRecording, []string{"+recording", "--meeting-ids", "m001", "--as", "user"}, f, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected missing_scope error, got nil")
|
||||
@@ -189,6 +175,16 @@ func TestRecording_Validate_MissingScope(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// recordingScopedTokenResolver returns a token with caller-controlled scopes
|
||||
// so tests can deterministically exercise the identity-aware scope preflight.
|
||||
type recordingScopedTokenResolver struct {
|
||||
scopes string
|
||||
}
|
||||
|
||||
func (r *recordingScopedTokenResolver) ResolveToken(_ context.Context, _ credential.TokenSpec) (*credential.TokenResult, error) {
|
||||
return &credential.TokenResult{Token: "test-token", Scopes: r.scopes}, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DryRun tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -20,7 +20,7 @@ metadata:
|
||||
|
||||
## 身份
|
||||
|
||||
所有 minutes 命令默认使用 `--as user`。
|
||||
所有 minutes 命令默认使用 `--as user`。`+detail` 和 `+download` 也支持 `--as bot`(bot 只能访问 bot 有权限的妙记)。
|
||||
|
||||
## Shortcuts
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ metadata:
|
||||
|
||||
## 身份
|
||||
|
||||
所有 vc 命令默认使用 `--as user`。`+search` 和 `meeting get` 也支持 `--as bot`。
|
||||
所有 vc 命令默认使用 `--as user`。`meeting get`、`+detail`、`+recording`、`+notes` 也支持 `--as bot`(bot 只能访问 bot 有权限的会议、录制和纪要)。`+search` 仅支持 user。
|
||||
|
||||
```bash
|
||||
# BAD — 查昨天的会议用 calendar,会漏掉即时会议
|
||||
|
||||
@@ -40,9 +40,9 @@ lark-cli vc +recording --meeting-ids 69xxxxxxxxxxxxx28 --dry-run
|
||||
|
||||
每次只能指定一种输入方式。同时传入会报错。
|
||||
|
||||
### 2. 仅支持 user 身份
|
||||
### 2. 身份支持
|
||||
|
||||
该命令仅支持 `user` 身份,使用前需完成 `lark-cli auth login`。user token 只能查自己有权限的录制。
|
||||
`--meeting-ids` 和 `--calendar-event-ids` 两种模式都支持 `--as user` 和 `--as bot`。user token 只能查自己有权限的录制;bot 使用 tenant_access_token,只能查 bot 有权限的录制。
|
||||
|
||||
### 3. 批量上限
|
||||
|
||||
|
||||
@@ -37,6 +37,29 @@ func TestMinutesApplyPermission_DryRun(t *testing.T) {
|
||||
assert.True(t, strings.Contains(output, `"perm": "view"`) || strings.Contains(output, `"perm":"view"`), "dry-run should contain perm body, got: %s", output)
|
||||
}
|
||||
|
||||
func TestMinutesApplyPermission_DryRun_BotIdentity(t *testing.T) {
|
||||
setDryRunConfigEnv(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"minutes", "+apply-permission",
|
||||
"--minute-token", "obcnexampleminute",
|
||||
"--perm", "view",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
output := result.Stdout
|
||||
assert.True(t, strings.Contains(output, "POST"), "dry-run should contain POST method, got: %s", output)
|
||||
assert.True(t, strings.Contains(output, "/open-apis/minutes/v1/minutes/obcnexampleminute/permissions/apply"), "dry-run should contain API path, got: %s", output)
|
||||
assert.True(t, strings.Contains(output, `"perm": "view"`) || strings.Contains(output, `"perm":"view"`), "dry-run should contain perm body, got: %s", output)
|
||||
}
|
||||
|
||||
func TestMinutesApplyPermission_InvalidPerm(t *testing.T) {
|
||||
setDryRunConfigEnv(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
|
||||
50
tests/cli_e2e/minutes/minutes_detail_dryrun_test.go
Normal file
50
tests/cli_e2e/minutes/minutes_detail_dryrun_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package minutes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestMinutesDetailDryRun_BotIdentity pins that `minutes +detail` accepts
|
||||
// --as bot for both the metadata (GetMinuteArtifacts) and transcript
|
||||
// (GetMinuteTranscript) paths, which accept a tenant access token.
|
||||
func TestMinutesDetailDryRun_BotIdentity(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
setDryRunConfigEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"minutes", "+detail",
|
||||
"--minute-tokens", "obcn1234567890",
|
||||
"--transcript",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
require.Contains(t, result.Args, "--as")
|
||||
require.Contains(t, result.Args, "bot")
|
||||
|
||||
out := result.Stdout
|
||||
require.Equal(t, "GET", clie2e.DryRunGet(out, "api.0.method").String(), "stdout:\n%s", out)
|
||||
require.Equal(t, "/open-apis/minutes/v1/minutes/{minute_token}", clie2e.DryRunGet(out, "api.0.url").String(), "stdout:\n%s", out)
|
||||
require.Equal(t, "/open-apis/minutes/v1/minutes/{minute_token}/artifacts", clie2e.DryRunGet(out, "api.1.url").String(), "stdout:\n%s", out)
|
||||
|
||||
helpResult, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"minutes", "+detail", "--help"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
helpResult.AssertExitCode(t, 0)
|
||||
require.Contains(t, helpResult.Stdout, "identity type: user | bot")
|
||||
}
|
||||
50
tests/cli_e2e/vc/vc_detail_dryrun_test.go
Normal file
50
tests/cli_e2e/vc/vc_detail_dryrun_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package vc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestVCDetailDryRun_BotIdentity pins that `vc +detail` accepts --as bot and
|
||||
// previews the meeting.get + recording API round-trip (GetMeetingByID /
|
||||
// GetRecordingByMeetingID both accept a tenant access token).
|
||||
func TestVCDetailDryRun_BotIdentity(t *testing.T) {
|
||||
setVCDryRunEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"vc", "+detail",
|
||||
"--meeting-ids", "7628568141510692381",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
require.Contains(t, result.Args, "--as")
|
||||
require.Contains(t, result.Args, "bot")
|
||||
|
||||
out := result.Stdout
|
||||
require.Equal(t, int64(2), clie2e.DryRunGet(out, "api.#").Int(), "stdout:\n%s", out)
|
||||
require.Equal(t, "GET", clie2e.DryRunGet(out, "api.0.method").String(), "stdout:\n%s", out)
|
||||
require.Equal(t, "/open-apis/vc/v1/meetings/{meeting_id}", clie2e.DryRunGet(out, "api.0.url").String(), "stdout:\n%s", out)
|
||||
require.Equal(t, "/open-apis/vc/v1/meetings/{meeting_id}/recording", clie2e.DryRunGet(out, "api.1.url").String(), "stdout:\n%s", out)
|
||||
require.Equal(t, "7628568141510692381", clie2e.DryRunGet(out, "meeting_ids.0").String(), "stdout:\n%s", out)
|
||||
|
||||
helpResult, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"vc", "+detail", "--help"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
helpResult.AssertExitCode(t, 0)
|
||||
require.Contains(t, helpResult.Stdout, "identity type: user | bot")
|
||||
}
|
||||
70
tests/cli_e2e/vc/vc_recording_dryrun_test.go
Normal file
70
tests/cli_e2e/vc/vc_recording_dryrun_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package vc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestVCRecordingDryRun_BotIdentity pins that `vc +recording --meeting-ids`
|
||||
// accepts --as bot (GetRecordingByMeetingID accepts a tenant access token).
|
||||
func TestVCRecordingDryRun_BotIdentity(t *testing.T) {
|
||||
setVCDryRunEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"vc", "+recording",
|
||||
"--meeting-ids", "7628568141510692381",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
out := result.Stdout
|
||||
require.Equal(t, "GET", clie2e.DryRunGet(out, "api.0.method").String(), "stdout:\n%s", out)
|
||||
require.Equal(t, "/open-apis/vc/v1/meetings/{meeting_id}/recording", clie2e.DryRunGet(out, "api.0.url").String(), "stdout:\n%s", out)
|
||||
|
||||
helpResult, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"vc", "+recording", "--help"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
helpResult.AssertExitCode(t, 0)
|
||||
require.Contains(t, helpResult.Stdout, "identity type: user | bot")
|
||||
}
|
||||
|
||||
// TestVCRecordingDryRun_BotIdentity_CalendarEventIDs pins that the
|
||||
// calendar-event-ids path also flows under --as bot: a bot has a primary
|
||||
// calendar, so the primary -> mget_instance_relation_info -> recording chain
|
||||
// is previewed without a validation error.
|
||||
func TestVCRecordingDryRun_BotIdentity_CalendarEventIDs(t *testing.T) {
|
||||
setVCDryRunEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"vc", "+recording",
|
||||
"--calendar-event-ids", "evt_001",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
out := result.Stdout
|
||||
require.Contains(t, out, "mget_instance_relation_info", "stdout:\n%s", out)
|
||||
require.Contains(t, out, "recording", "stdout:\n%s", out)
|
||||
}
|
||||
Reference in New Issue
Block a user