Compare commits

...

3 Commits

Author SHA1 Message Date
shike.11
27b38f8bb9 chore: switch PPE lane to ppe_bot_user_id for view_url test
Debug-only: change x-tt-env from ppe_agent_view to ppe_bot_user_id so
requests land in the bot-user-id PPE lane. Not for merge into main.
2026-07-27 21:32:32 +08:00
shike.11
3958bc047e chore: point feishu brand to PPE for view_url integration test
Debug-only: switch the default Open endpoint to open.feishu-pre.cn and
inject x-tt-env: ppe_agent_view so requests land in the PPE lane. Not
for merge into main.
2026-07-27 21:32:22 +08:00
zhangjun.1
215fe8a614 feat: support bot identity 2026-07-27 21:07:54 +08:00
17 changed files with 445 additions and 45 deletions

View File

@@ -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
}

View File

@@ -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",

View 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)
}
}

View File

@@ -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"}},

View File

@@ -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},

View 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
}

View File

@@ -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},

View File

@@ -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{

View File

@@ -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

View File

@@ -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
// ---------------------------------------------------------------------------

View File

@@ -20,7 +20,7 @@ metadata:
## 身份
所有 minutes 命令默认使用 `--as user`
所有 minutes 命令默认使用 `--as user``+detail``+download` 也支持 `--as bot`bot 只能访问 bot 有权限的妙记)。
## Shortcuts

View File

@@ -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会漏掉即时会议

View File

@@ -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. 批量上限

View File

@@ -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)

View 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")
}

View 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")
}

View 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)
}