chore: import upstream snapshot with attribution
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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:54 +08:00
commit bf9395e022
2349 changed files with 588574 additions and 0 deletions
+83
View File
@@ -0,0 +1,83 @@
# Mail CLI E2E Coverage
## Metrics
- Denominator: 65 leaf commands
- Covered: 16
- Coverage: 24.6%
## Summary
- TestMail_DraftLifecycleWorkflowAsUser: proves a self-contained user draft workflow across `mail user_mailboxes profile`, `mail +draft-create`, `mail user_mailbox.drafts list`, `mail user_mailbox.drafts get`, `mail +draft-edit`, and `mail user_mailbox.drafts delete`; key `t.Run(...)` proof points are `get mailbox profile as user`, `create draft with shortcut as user`, `list draft as user`, `get created draft as user`, `inspect created draft as user`, `update draft subject with shortcut as user`, `inspect updated draft as user`, `delete draft as user`, and `verify draft removed from list as user`.
- TestMail_DraftSendWorkflowAsUser: proves a self-contained user draft-send workflow across `mail user_mailboxes profile`, `mail +draft-create`, `mail +draft-send`, and `mail +triage`; key `t.Run(...)` proof points are `get mailbox profile as user`, `create self-addressed draft as user`, `send draft with shortcut as user`, and `find self-received message for cleanup`.
- TestMail_SendWorkflowAsUser: proves a self-contained self-mail workflow across `mail +send`, `mail +triage`, `mail +message`, `mail +messages`, `mail +thread`, `mail +reply`, and `mail +forward`; key `t.Run(...)` proof points are `send mail to self with shortcut as user`, `find self sent mail in triage as user`, `get sent message as user`, `get received message as user`, `get both self sent messages as user`, `get self send thread as user`, `reply to received message with shortcut as user`, `inspect reply draft as user`, `forward received message with shortcut as user`, and `inspect forward draft as user`.
- Blocked area: `mail +reply-all` is still uncovered because the self-send workflow produces only self-recipient traffic and reply-alls recipient expansion becomes degenerate after self-address exclusion; `+signature`, `+watch`, event commands, and many raw message/thread mutation APIs still need dedicated tenant-aware workflows.
## Command Table
| Status | Cmd | Type | Testcase | Key parameter shapes | Notes / uncovered reason |
| --- | --- | --- | --- | --- | --- |
| ✓ | mail +draft-create | shortcut | mail_draft_lifecycle_workflow_test.go::TestMail_DraftLifecycleWorkflowAsUser/create draft with shortcut as user | `--subject`; `--body`; `--plain-text` | creates a new self-owned draft without relying on external recipients |
| ✓ | mail +draft-edit | shortcut | mail_draft_lifecycle_workflow_test.go::TestMail_DraftLifecycleWorkflowAsUser/inspect created draft as user; mail_draft_lifecycle_workflow_test.go::TestMail_DraftLifecycleWorkflowAsUser/update draft subject with shortcut as user; mail_draft_lifecycle_workflow_test.go::TestMail_DraftLifecycleWorkflowAsUser/inspect updated draft as user | `--draft-id`; `--mailbox me`; `--inspect`; `--set-subject` | shortcut proves readback projection and subject update |
| ✓ | mail +draft-send | shortcut | mail_draft_send_workflow_test.go::TestMail_DraftSendWorkflowAsUser/send draft with shortcut as user; mail_draft_send_dryrun_test.go::TestMail_DraftSendDryRun | `--draft-id`; `--mailbox me`; `--yes`; dry-run repeated/comma-separated `--draft-id` | sends a self-addressed draft through the batch shortcut and locks dry-run request shape |
| ✓ | mail +forward | shortcut | mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/forward received message with shortcut as user; mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/inspect forward draft as user | `--message-id`; `--to`; `--body`; `--plain-text` | uses self-generated inbox message as source and inspects forwarded draft projection |
| ✓ | mail +message | shortcut | mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/get sent message as user; mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/get received message as user | `--mailbox me`; `--message-id` | verifies both SENT and INBOX copies after self-send |
| ✓ | mail +message-modify | shortcut | shortcuts/mail/mail_message_manage_test.go::TestMessageModify_DryRunShowsPlanWithoutValidationGET; shortcuts/mail/mail_message_manage_test.go::TestMessageModify_BatchesAndAggregatesPartialFailure | `--message-ids`; `--add-label-ids`; `--remove-label-ids`; `--add-folder`; `--dry-run` | unit/dry-run coverage locks validation, batching, request shape, and partial failure aggregation; live E2E needs controlled disposable messages/labels/folders |
| ✓ | mail +message-trash | shortcut | shortcuts/mail/mail_message_manage_test.go::TestMessageTrash_RequiresYesAndBatches | `--message-ids`; `--yes`; `--dry-run` | unit coverage locks high-risk confirmation and batch_trash request shape; live E2E needs controlled disposable messages |
| ✓ | mail +messages | shortcut | mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/get both self sent messages as user | `--mailbox me`; `--message-ids` | batch reads both sent and received message copies |
| ✓ | mail +reply | shortcut | mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/reply to received message with shortcut as user; mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/inspect reply draft as user | `--message-id`; `--body`; `--plain-text` | creates reply draft from self-generated inbox message and inspects quoted content |
| ✕ | mail +reply-all | shortcut | | none | self-send traffic leaves no stable non-self recipient set for deterministic reply-all assertions |
| ✓ | mail +send | shortcut | mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/send mail to self with shortcut as user | `--to`; `--subject`; `--body`; `--plain-text`; `--confirm-send` | self-send creates both sent and inbox copies for follow-up assertions |
| ✕ | mail +signature | shortcut | | none | signature availability is mailbox-configuration dependent |
| ✓ | mail +thread | shortcut | mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/get self send thread as user | `--mailbox me`; `--thread-id` | verifies readback of the sent-message thread created by self-send |
| ✓ | mail +triage | shortcut | mail_send_workflow_test.go::TestMail_SendWorkflowAsUser/find self sent mail in triage as user | `--mailbox me`; `--query`; `--max`; `--format data` | polls until self-sent subject becomes searchable and captures sent/inbox message ids |
| ✕ | mail +watch | shortcut | | none | requires websocket event subscription setup and external mail delivery |
| ✕ | mail multi_entity search | api | | none | requires deterministic searchable contact entities |
| ✕ | mail user_mailbox.drafts create | api | | none | only covered indirectly through `mail +draft-create` |
| ✓ | mail user_mailbox.drafts delete | api | mail_draft_lifecycle_workflow_test.go::TestMail_DraftLifecycleWorkflowAsUser/delete draft as user | `user_mailbox_id`; `draft_id` in `--params` | explicit lifecycle delete plus read-after-delete list check |
| ✓ | mail user_mailbox.drafts get | api | mail_draft_lifecycle_workflow_test.go::TestMail_DraftLifecycleWorkflowAsUser/get created draft as user | `user_mailbox_id`; `draft_id` in `--params` | asserts persisted draft id, subject, and draft state |
| ✓ | mail user_mailbox.drafts list | api | mail_draft_lifecycle_workflow_test.go::TestMail_DraftLifecycleWorkflowAsUser/list draft as user; mail_draft_lifecycle_workflow_test.go::TestMail_DraftLifecycleWorkflowAsUser/verify draft removed from list as user | `user_mailbox_id`; `page_size` in `--params` | proves create visibility and delete removal |
| ✕ | mail user_mailbox.drafts send | api | | none | draft send needs recipient-side or send-status assertions to be deterministic |
| ✕ | mail user_mailbox.drafts update | api | | none | only covered indirectly through `mail +draft-edit` |
| ✕ | mail user_mailbox.drafts cancel_scheduled_send | api | | none | requires a scheduled-send draft lifecycle |
| ✕ | mail user_mailbox.event subscribe | api | | none | requires event subscription setup |
| ✕ | mail user_mailbox.event subscription | api | | none | requires event subscription setup |
| ✕ | mail user_mailbox.event unsubscribe | api | | none | requires event subscription setup |
| ✕ | mail user_mailbox.folders create | api | | none | folder lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.folders delete | api | | none | folder lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.folders get | api | | none | folder lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.folders list | api | | none | folder lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.folders patch | api | | none | folder lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.labels create | api | | none | label lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.labels delete | api | | none | label lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.labels get | api | | none | label lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.labels list | api | | none | label lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.labels patch | api | | none | label lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.mail_contacts create | api | | none | contact lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.mail_contacts delete | api | | none | contact lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.mail_contacts list | api | | none | contact lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.mail_contacts patch | api | | none | contact lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.message.attachments download_url | api | | none | requires an existing message attachment |
| ✕ | mail user_mailbox.messages batch_get | api | | none | requires existing message ids |
| ✕ | mail user_mailbox.messages batch_modify | api | | none | requires existing messages and mailbox folders/labels |
| ✕ | mail user_mailbox.messages batch_trash | api | | none | requires existing messages |
| ✕ | mail user_mailbox.messages get | api | | none | requires an existing message id |
| ✕ | mail user_mailbox.messages list | api | | none | requires deterministic existing folder or label message inventory |
| ✕ | mail user_mailbox.messages modify | api | | none | requires existing messages and mailbox folders/labels |
| ✕ | mail user_mailbox.messages send_status | api | | none | requires a sent message id |
| ✕ | mail user_mailbox.messages trash | api | | none | requires an existing message id |
| ✕ | mail user_mailbox.rules create | api | | none | rule lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.rules delete | api | | none | rule lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.rules list | api | | none | rule lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.rules reorder | api | | none | rule lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.rules update | api | | none | rule lifecycle left for a dedicated workflow |
| ✕ | mail user_mailbox.sent_messages get_recall_detail | api | | none | requires a recallable sent message |
| ✕ | mail user_mailbox.sent_messages recall | api | | none | requires a delivered sent message within recall window |
| ✕ | mail user_mailbox.settings send_as | api | | none | mailbox alias availability is tenant-configuration dependent |
| ✕ | mail user_mailbox.threads batch_modify | api | | none | requires existing threads and mailbox folders/labels |
| ✕ | mail user_mailbox.threads batch_trash | api | | none | requires existing thread ids |
| ✕ | mail user_mailbox.threads get | api | | none | requires an existing thread id |
| ✕ | mail user_mailbox.threads list | api | | none | requires deterministic existing folder or label thread inventory |
| ✕ | mail user_mailbox.threads modify | api | | none | requires existing threads and mailbox folders/labels |
| ✕ | mail user_mailbox.threads trash | api | | none | requires an existing thread id |
| ✕ | mail user_mailboxes accessible_mailboxes | api | | none | mailbox visibility differs by tenant and shared-mailbox configuration |
| ✓ | mail user_mailboxes profile | api | mail_draft_lifecycle_workflow_test.go::TestMail_DraftLifecycleWorkflowAsUser/get mailbox profile as user | `user_mailbox_id=me` in `--params` | proves current mailbox identity before draft lifecycle |
| ✕ | mail user_mailboxes search | api | | none | requires deterministic searchable mailbox content |
@@ -0,0 +1,213 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package mail
import (
"context"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func TestMail_DraftLifecycleWorkflowAsUser(t *testing.T) {
parentT := t
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
t.Cleanup(cancel)
clie2e.SkipWithoutUserToken(t)
suffix := clie2e.GenerateSuffix()
originalSubject := "lark-cli-e2e-mail-draft-" + suffix
updatedSubject := originalSubject + "-updated"
originalBody := "draft lifecycle body " + suffix
const mailboxID = "me"
var draftID string
var draftDeleted bool
parentT.Cleanup(func() {
if draftID == "" || draftDeleted {
return
}
cleanupCtx, cancel := clie2e.CleanupContext()
defer cancel()
result, err := clie2e.RunCmd(cleanupCtx, clie2e.Request{
Args: []string{"mail", "user_mailbox.drafts", "delete"},
DefaultAs: "user",
Params: map[string]any{
"user_mailbox_id": mailboxID,
"draft_id": draftID,
},
Yes: true,
})
clie2e.ReportCleanupFailure(parentT, "delete draft "+draftID, result, err)
})
t.Run("get mailbox profile as user", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"mail", "user_mailboxes", "profile"},
DefaultAs: "user",
Params: map[string]any{"user_mailbox_id": mailboxID},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
require.NotEmpty(t, gjson.Get(result.Stdout, "data.primary_email_address").String(), "stdout:\n%s", result.Stdout)
})
t.Run("create draft with shortcut as user", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+draft-create",
"--subject", originalSubject,
"--body", originalBody,
"--plain-text",
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
draftID = gjson.Get(result.Stdout, "data.draft_id").String()
require.NotEmpty(t, draftID, "stdout:\n%s", result.Stdout)
})
t.Run("list draft as user", func(t *testing.T) {
require.NotEmpty(t, draftID, "draft should be created before listing drafts")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"mail", "user_mailbox.drafts", "list"},
DefaultAs: "user",
Params: map[string]any{
"user_mailbox_id": mailboxID,
"page_size": 100,
},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
require.True(t, gjson.Get(result.Stdout, `data.items.#(id=="`+draftID+`")`).Exists(), "stdout:\n%s", result.Stdout)
})
t.Run("get created draft as user", func(t *testing.T) {
require.NotEmpty(t, draftID, "draft should be created before get")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"mail", "user_mailbox.drafts", "get"},
DefaultAs: "user",
Params: map[string]any{
"user_mailbox_id": mailboxID,
"draft_id": draftID,
},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, draftID, gjson.Get(result.Stdout, "data.draft.id").String())
assert.Equal(t, originalSubject, gjson.Get(result.Stdout, "data.draft.message.subject").String())
assert.Equal(t, int64(3), gjson.Get(result.Stdout, "data.draft.message.message_state").Int())
})
t.Run("inspect created draft as user", func(t *testing.T) {
require.NotEmpty(t, draftID, "draft should be created before inspect")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+draft-edit",
"--draft-id", draftID,
"--mailbox", mailboxID,
"--inspect",
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, draftID, gjson.Get(result.Stdout, "data.draft_id").String())
assert.Equal(t, originalSubject, gjson.Get(result.Stdout, "data.projection.subject").String())
assert.Equal(t, originalBody, gjson.Get(result.Stdout, "data.projection.body_text").String())
})
t.Run("update draft subject with shortcut as user", func(t *testing.T) {
require.NotEmpty(t, draftID, "draft should be created before update")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+draft-edit",
"--draft-id", draftID,
"--mailbox", mailboxID,
"--set-subject", updatedSubject,
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, draftID, gjson.Get(result.Stdout, "data.draft_id").String())
assert.Equal(t, updatedSubject, gjson.Get(result.Stdout, "data.projection.subject").String())
assert.Equal(t, originalBody, gjson.Get(result.Stdout, "data.projection.body_text").String())
})
t.Run("inspect updated draft as user", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+draft-edit",
"--draft-id", draftID,
"--mailbox", mailboxID,
"--inspect",
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, updatedSubject, gjson.Get(result.Stdout, "data.projection.subject").String())
assert.Equal(t, originalBody, gjson.Get(result.Stdout, "data.projection.body_text").String())
})
t.Run("delete draft as user", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"mail", "user_mailbox.drafts", "delete"},
DefaultAs: "user",
Params: map[string]any{
"user_mailbox_id": mailboxID,
"draft_id": draftID,
},
Yes: true,
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
draftDeleted = true
})
t.Run("verify draft removed from list as user", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"mail", "user_mailbox.drafts", "get"},
DefaultAs: "user",
Params: map[string]any{
"user_mailbox_id": mailboxID,
"draft_id": draftID,
},
})
require.NoError(t, err)
assert.NotEqual(t, 0, result.ExitCode, "stdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr)
assert.Equal(t, "not_found", gjson.Get(result.Stderr, "error.detail.type").String(), "stdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr)
})
}
@@ -0,0 +1,124 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package mail
import (
"context"
"strconv"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func TestMail_DraftSendDryRun(t *testing.T) {
setMailDraftSendDryRunEnv(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+draft-send",
"--mailbox", "alias@example.com",
"--draft-id", " draft_001, draft_002 ",
"--draft-id", " draft_003 ",
"--dry-run",
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
wantURLs := []string{
"/open-apis/mail/v1/user_mailboxes/alias@example.com/drafts/draft_001/send",
"/open-apis/mail/v1/user_mailboxes/alias@example.com/drafts/draft_002/send",
"/open-apis/mail/v1/user_mailboxes/alias@example.com/drafts/draft_003/send",
}
assert.Equal(t, int64(len(wantURLs)), gjson.Get(result.Stdout, "api.#").Int(), "stdout:\n%s", result.Stdout)
for i, wantURL := range wantURLs {
idx := strconv.Itoa(i)
assert.Equal(t, "POST", gjson.Get(result.Stdout, "api."+idx+".method").String(), "stdout:\n%s", result.Stdout)
assert.Equal(t, wantURL, gjson.Get(result.Stdout, "api."+idx+".url").String(), "stdout:\n%s", result.Stdout)
assert.False(t, gjson.Get(result.Stdout, "api."+idx+".body").Exists(), "stdout:\n%s", result.Stdout)
}
}
func TestMail_DraftSendDryRunValidation(t *testing.T) {
setMailDraftSendDryRunEnv(t)
tests := []struct {
name string
args []string
wantMsg string
}{
{
name: "reject whitespace draft id",
args: []string{
"mail", "+draft-send",
"--draft-id", " ",
"--dry-run",
},
wantMsg: "--draft-id contains empty value",
},
{
name: "reject too many draft ids",
args: []string{
"mail", "+draft-send",
"--draft-id", manyDraftIDsForE2E(51),
"--dry-run",
},
wantMsg: "too many drafts",
},
{
name: "reject duplicate draft id",
args: []string{
"mail", "+draft-send",
"--draft-id", "draft_001,draft_002,draft_001",
"--dry-run",
},
wantMsg: "--draft-id contains duplicate value: draft_001",
},
}
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, 2)
output := result.Stdout + result.Stderr
assert.Contains(t, output, tt.wantMsg, "stdout:\n%s\nstderr:\n%s", result.Stdout, result.Stderr)
})
}
}
func setMailDraftSendDryRunEnv(t *testing.T) {
t.Helper()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
t.Setenv("LARKSUITE_CLI_APP_ID", "mail_draft_send_dryrun_test")
t.Setenv("LARKSUITE_CLI_APP_SECRET", "mail_draft_send_dryrun_secret")
t.Setenv("LARKSUITE_CLI_BRAND", "feishu")
}
func manyDraftIDsForE2E(n int) string {
ids := make([]byte, 0, n*4)
for i := 0; i < n; i++ {
if i > 0 {
ids = append(ids, ',')
}
ids = append(ids, 'd')
ids = strconv.AppendInt(ids, int64(i), 10)
}
return string(ids)
}
@@ -0,0 +1,166 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package mail
import (
"context"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func TestMail_DraftSendWorkflowAsUser(t *testing.T) {
parentT := t
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
t.Cleanup(cancel)
clie2e.SkipWithoutUserToken(t)
const mailboxID = "me"
suffix := clie2e.GenerateSuffix()
subject := "lark-cli-e2e-mail-draft-send-" + suffix
body := "draft-send workflow body " + suffix
var primaryEmail string
var draftID string
var draftSent bool
var sentMessageID string
var inboxMessageID string
parentT.Cleanup(func() {
if draftID != "" && !draftSent {
cleanupCtx, cancel := clie2e.CleanupContext()
defer cancel()
result, err := clie2e.RunCmd(cleanupCtx, clie2e.Request{
Args: []string{"mail", "user_mailbox.drafts", "delete"},
DefaultAs: "user",
Params: map[string]any{
"user_mailbox_id": mailboxID,
"draft_id": draftID,
},
Yes: true,
})
clie2e.ReportCleanupFailure(parentT, "delete draft "+draftID, result, err)
}
var messageIDs []string
if sentMessageID != "" {
messageIDs = append(messageIDs, sentMessageID)
}
if inboxMessageID != "" && inboxMessageID != sentMessageID {
messageIDs = append(messageIDs, inboxMessageID)
}
if len(messageIDs) == 0 {
return
}
cleanupCtx, cancel := clie2e.CleanupContext()
defer cancel()
result, err := clie2e.RunCmd(cleanupCtx, clie2e.Request{
Args: []string{"mail", "user_mailbox.messages", "batch_trash"},
DefaultAs: "user",
Params: map[string]any{"user_mailbox_id": mailboxID},
Data: map[string]any{"message_ids": messageIDs},
})
clie2e.ReportCleanupFailure(parentT, "trash draft-send messages", result, err)
})
t.Run("get mailbox profile as user", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"mail", "user_mailboxes", "profile"},
DefaultAs: "user",
Params: map[string]any{"user_mailbox_id": mailboxID},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
primaryEmail = gjson.Get(result.Stdout, "data.primary_email_address").String()
require.NotEmpty(t, primaryEmail, "stdout:\n%s", result.Stdout)
})
t.Run("create self-addressed draft as user", func(t *testing.T) {
require.NotEmpty(t, primaryEmail, "mailbox profile should be loaded before draft create")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+draft-create",
"--to", primaryEmail,
"--subject", subject,
"--body", body,
"--plain-text",
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
draftID = gjson.Get(result.Stdout, "data.draft_id").String()
require.NotEmpty(t, draftID, "stdout:\n%s", result.Stdout)
})
t.Run("send draft with shortcut as user", func(t *testing.T) {
require.NotEmpty(t, draftID, "draft should be created before +draft-send")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+draft-send",
"--mailbox", mailboxID,
"--draft-id", draftID,
},
DefaultAs: "user",
Yes: true,
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, int64(1), gjson.Get(result.Stdout, "data.total").Int(), "stdout:\n%s", result.Stdout)
assert.Equal(t, int64(1), gjson.Get(result.Stdout, "data.success_count").Int(), "stdout:\n%s", result.Stdout)
assert.Equal(t, int64(0), gjson.Get(result.Stdout, "data.failure_count").Int(), "stdout:\n%s", result.Stdout)
assert.Equal(t, draftID, gjson.Get(result.Stdout, "data.sent.0.draft_id").String(), "stdout:\n%s", result.Stdout)
sentMessageID = gjson.Get(result.Stdout, "data.sent.0.message_id").String()
require.NotEmpty(t, sentMessageID, "stdout:\n%s", result.Stdout)
draftSent = true
})
t.Run("find self-received message for cleanup", func(t *testing.T) {
require.NotEmpty(t, sentMessageID, "draft should be sent before triage lookup")
for attempt := 0; attempt < 12; attempt++ {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+triage",
"--mailbox", mailboxID,
"--query", subject,
"--max", "10",
"--format", "data",
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
for _, item := range gjson.Get(result.Stdout, "messages").Array() {
if item.Get("subject").String() != subject {
continue
}
messageID := item.Get("message_id").String()
if messageID != "" && messageID != sentMessageID {
inboxMessageID = messageID
return
}
}
time.Sleep(2 * time.Second)
}
})
}
@@ -0,0 +1,342 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package mail
import (
"context"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func TestMail_SendWorkflowAsUser(t *testing.T) {
parentT := t
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Minute)
t.Cleanup(cancel)
clie2e.SkipWithoutUserToken(t)
const mailboxID = "me"
suffix := clie2e.GenerateSuffix()
subject := "mail-self-" + suffix
body := "self send body " + suffix
replyBody := "self send reply body " + suffix
forwardBody := "self send forward body " + suffix
var primaryEmail string
var sentMessageID string
var threadID string
var inboxMessageID string
var replyDraftID string
var forwardDraftID string
parentT.Cleanup(func() {
if replyDraftID != "" {
cleanupCtx, cancel := clie2e.CleanupContext()
defer cancel()
result, err := clie2e.RunCmd(cleanupCtx, clie2e.Request{
Args: []string{"mail", "user_mailbox.drafts", "delete"},
DefaultAs: "user",
Params: map[string]any{
"user_mailbox_id": mailboxID,
"draft_id": replyDraftID,
},
Yes: true,
})
clie2e.ReportCleanupFailure(parentT, "delete reply draft "+replyDraftID, result, err)
}
if forwardDraftID != "" {
cleanupCtx, cancel := clie2e.CleanupContext()
defer cancel()
result, err := clie2e.RunCmd(cleanupCtx, clie2e.Request{
Args: []string{"mail", "user_mailbox.drafts", "delete"},
DefaultAs: "user",
Params: map[string]any{
"user_mailbox_id": mailboxID,
"draft_id": forwardDraftID,
},
Yes: true,
})
clie2e.ReportCleanupFailure(parentT, "delete forward draft "+forwardDraftID, result, err)
}
var messageIDs []string
if sentMessageID != "" {
messageIDs = append(messageIDs, sentMessageID)
}
if inboxMessageID != "" && inboxMessageID != sentMessageID {
messageIDs = append(messageIDs, inboxMessageID)
}
if len(messageIDs) == 0 {
return
}
cleanupCtx, cancel := clie2e.CleanupContext()
defer cancel()
result, err := clie2e.RunCmd(cleanupCtx, clie2e.Request{
Args: []string{"mail", "user_mailbox.messages", "batch_trash"},
DefaultAs: "user",
Params: map[string]any{"user_mailbox_id": mailboxID},
Data: map[string]any{"message_ids": messageIDs},
})
clie2e.ReportCleanupFailure(parentT, "trash self-send messages", result, err)
})
t.Run("get mailbox profile as user", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{"mail", "user_mailboxes", "profile"},
DefaultAs: "user",
Params: map[string]any{"user_mailbox_id": mailboxID},
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
primaryEmail = gjson.Get(result.Stdout, "data.primary_email_address").String()
require.NotEmpty(t, primaryEmail, "stdout:\n%s", result.Stdout)
})
t.Run("send mail to self with shortcut as user", func(t *testing.T) {
require.NotEmpty(t, primaryEmail, "mailbox profile should be loaded before self-send")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+send",
"--to", primaryEmail,
"--subject", subject,
"--body", body,
"--plain-text",
"--confirm-send",
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
sentMessageID = gjson.Get(result.Stdout, "data.message_id").String()
threadID = gjson.Get(result.Stdout, "data.thread_id").String()
require.NotEmpty(t, sentMessageID, "stdout:\n%s", result.Stdout)
require.NotEmpty(t, threadID, "stdout:\n%s", result.Stdout)
})
t.Run("find self sent mail in triage as user", func(t *testing.T) {
require.NotEmpty(t, subject, "subject should be set before triage")
var stdout string
for attempt := 0; attempt < 12; attempt++ {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+triage",
"--mailbox", mailboxID,
"--query", subject,
"--max", "10",
"--format", "data",
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
stdout = result.Stdout
messages := gjson.Get(stdout, "messages").Array()
for _, item := range messages {
if item.Get("subject").String() != subject {
continue
}
messageID := item.Get("message_id").String()
if messageID == "" {
continue
}
if messageID != sentMessageID {
inboxMessageID = messageID
}
}
if inboxMessageID != "" {
require.GreaterOrEqual(t, int(gjson.Get(stdout, "count").Int()), 2, "stdout:\n%s", stdout)
require.True(t, gjson.Get(stdout, `messages.#(message_id=="`+sentMessageID+`")`).Exists(), "stdout:\n%s", stdout)
require.True(t, gjson.Get(stdout, `messages.#(message_id=="`+inboxMessageID+`")`).Exists(), "stdout:\n%s", stdout)
return
}
time.Sleep(2 * time.Second)
}
t.Fatalf("failed to observe inbox copy for self-sent message in triage:\n%s", stdout)
})
t.Run("get sent message as user", func(t *testing.T) {
require.NotEmpty(t, sentMessageID, "sent message id should be available before message read")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+message",
"--mailbox", mailboxID,
"--message-id", sentMessageID,
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, sentMessageID, gjson.Get(result.Stdout, "data.message_id").String())
assert.Equal(t, threadID, gjson.Get(result.Stdout, "data.thread_id").String())
assert.Equal(t, subject, gjson.Get(result.Stdout, "data.subject").String())
assert.Equal(t, body, gjson.Get(result.Stdout, "data.body_plain_text").String())
assert.Equal(t, "SENT", gjson.Get(result.Stdout, "data.folder_id").String())
assert.Equal(t, "sent", gjson.Get(result.Stdout, "data.message_state_text").String())
assert.Equal(t, primaryEmail, gjson.Get(result.Stdout, "data.to.0.mail_address").String())
})
t.Run("get received message as user", func(t *testing.T) {
require.NotEmpty(t, inboxMessageID, "inbox message id should be available before message read")
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+message",
"--mailbox", mailboxID,
"--message-id", inboxMessageID,
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, inboxMessageID, gjson.Get(result.Stdout, "data.message_id").String())
assert.Equal(t, threadID, gjson.Get(result.Stdout, "data.thread_id").String())
assert.Equal(t, subject, gjson.Get(result.Stdout, "data.subject").String())
assert.Equal(t, body, gjson.Get(result.Stdout, "data.body_plain_text").String())
assert.Equal(t, "INBOX", gjson.Get(result.Stdout, "data.folder_id").String())
assert.Equal(t, "received", gjson.Get(result.Stdout, "data.message_state_text").String())
assert.Equal(t, primaryEmail, gjson.Get(result.Stdout, "data.to.0.mail_address").String())
})
t.Run("get both self sent messages as user", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+messages",
"--mailbox", mailboxID,
"--message-ids", sentMessageID + "," + inboxMessageID,
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, int64(2), gjson.Get(result.Stdout, "data.total").Int())
assert.Len(t, gjson.Get(result.Stdout, "data.unavailable_message_ids").Array(), 0, "stdout:\n%s", result.Stdout)
assert.True(t, gjson.Get(result.Stdout, `data.messages.#(message_id=="`+sentMessageID+`")`).Exists(), "stdout:\n%s", result.Stdout)
assert.True(t, gjson.Get(result.Stdout, `data.messages.#(message_id=="`+inboxMessageID+`")`).Exists(), "stdout:\n%s", result.Stdout)
})
t.Run("get self send thread as user", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+thread",
"--mailbox", mailboxID,
"--thread-id", threadID,
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, threadID, gjson.Get(result.Stdout, "data.thread_id").String())
assert.Equal(t, int64(1), gjson.Get(result.Stdout, "data.message_count").Int(), "stdout:\n%s", result.Stdout)
assert.True(t, gjson.Get(result.Stdout, `data.messages.#(message_id=="`+sentMessageID+`")`).Exists(), "stdout:\n%s", result.Stdout)
})
t.Run("reply to received message with shortcut as user", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+reply",
"--message-id", inboxMessageID,
"--body", replyBody,
"--plain-text",
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
replyDraftID = gjson.Get(result.Stdout, "data.draft_id").String()
require.NotEmpty(t, replyDraftID, "stdout:\n%s", result.Stdout)
})
t.Run("inspect reply draft as user", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+draft-edit",
"--draft-id", replyDraftID,
"--mailbox", mailboxID,
"--inspect",
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, "Re: "+subject, gjson.Get(result.Stdout, "data.projection.subject").String())
assert.Equal(t, primaryEmail, gjson.Get(result.Stdout, "data.projection.to.0.address").String())
assert.Contains(t, gjson.Get(result.Stdout, "data.projection.body_text").String(), replyBody)
assert.Contains(t, gjson.Get(result.Stdout, "data.projection.body_text").String(), body)
assert.NotEmpty(t, gjson.Get(result.Stdout, "data.projection.in_reply_to").String(), "stdout:\n%s", result.Stdout)
})
t.Run("forward received message with shortcut as user", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+forward",
"--message-id", inboxMessageID,
"--to", primaryEmail,
"--body", forwardBody,
"--plain-text",
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
forwardDraftID = gjson.Get(result.Stdout, "data.draft_id").String()
require.NotEmpty(t, forwardDraftID, "stdout:\n%s", result.Stdout)
})
t.Run("inspect forward draft as user", func(t *testing.T) {
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+draft-edit",
"--draft-id", forwardDraftID,
"--mailbox", mailboxID,
"--inspect",
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
result.AssertStdoutStatus(t, true)
assert.Equal(t, "Fwd: "+subject, gjson.Get(result.Stdout, "data.projection.subject").String())
assert.Equal(t, primaryEmail, gjson.Get(result.Stdout, "data.projection.to.0.address").String())
assert.Contains(t, gjson.Get(result.Stdout, "data.projection.body_text").String(), forwardBody)
assert.Contains(t, gjson.Get(result.Stdout, "data.projection.body_text").String(), body)
assert.NotEmpty(t, gjson.Get(result.Stdout, "data.projection.in_reply_to").String(), "stdout:\n%s", result.Stdout)
})
}
@@ -0,0 +1,138 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package mail
import (
"context"
"strconv"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
// TestMail_ShareToChatDryRun validates the request shape emitted by
// +share-to-chat under --dry-run: the full CLI binary is invoked end-to-end
// so flag parsing, validation, and the dry-run renderer all execute.
// Fake credentials are sufficient because --dry-run short-circuits before
// any network call.
func TestMail_ShareToChatDryRun(t *testing.T) {
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
wantURLs []string
wantCreateBody map[string]string
wantSendBody map[string]string
wantSendParams map[string]string
}{
{
name: "message-id with default chat_id",
args: []string{
"mail", "+share-to-chat",
"--message-id", "msg_001",
"--receive-id", "oc_xxx",
"--dry-run",
},
wantURLs: []string{
"/open-apis/mail/v1/user_mailboxes/me/messages/share_token",
"/open-apis/mail/v1/user_mailboxes/me/share_tokens/%3Ccard_id%3E/send",
},
wantCreateBody: map[string]string{"message_id": "msg_001"},
wantSendBody: map[string]string{"receive_id": "oc_xxx"},
wantSendParams: map[string]string{"receive_id_type": "chat_id"},
},
{
name: "thread-id with email type",
args: []string{
"mail", "+share-to-chat",
"--thread-id", "thread_001",
"--receive-id", "user@example.com",
"--receive-id-type", "email",
"--dry-run",
},
wantURLs: []string{
"/open-apis/mail/v1/user_mailboxes/me/messages/share_token",
"/open-apis/mail/v1/user_mailboxes/me/share_tokens/%3Ccard_id%3E/send",
},
wantCreateBody: map[string]string{"thread_id": "thread_001"},
wantSendBody: map[string]string{"receive_id": "user@example.com"},
wantSendParams: map[string]string{"receive_id_type": "email"},
},
{
name: "custom mailbox",
args: []string{
"mail", "+share-to-chat",
"--message-id", "msg_002",
"--receive-id", "oc_xxx",
"--mailbox", "alias@example.com",
"--dry-run",
},
wantURLs: []string{
"/open-apis/mail/v1/user_mailboxes/alias@example.com/messages/share_token",
"/open-apis/mail/v1/user_mailboxes/alias@example.com/share_tokens/%3Ccard_id%3E/send",
},
wantCreateBody: map[string]string{"message_id": "msg_002"},
wantSendBody: map[string]string{"receive_id": "oc_xxx"},
wantSendParams: map[string]string{"receive_id_type": "chat_id"},
},
}
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
gotCount := int(gjson.Get(out, "api.#").Int())
if gotCount != len(tt.wantURLs) {
t.Fatalf("expected %d API calls, got %d\nstdout:\n%s", len(tt.wantURLs), gotCount, out)
}
for i, wantURL := range tt.wantURLs {
idx := strconv.Itoa(i)
gotMethod := gjson.Get(out, "api."+idx+".method").String()
gotURL := gjson.Get(out, "api."+idx+".url").String()
if gotMethod != "POST" {
t.Fatalf("api[%d].method = %q, want POST\nstdout:\n%s", i, gotMethod, out)
}
if gotURL != wantURL {
t.Fatalf("api[%d].url = %q, want %q\nstdout:\n%s", i, gotURL, wantURL, out)
}
}
for k, v := range tt.wantCreateBody {
got := gjson.Get(out, "api.0.body."+k).String()
if got != v {
t.Fatalf("api[0].body.%s = %q, want %q\nstdout:\n%s", k, got, v, out)
}
}
for k, v := range tt.wantSendBody {
got := gjson.Get(out, "api.1.body."+k).String()
if got != v {
t.Fatalf("api[1].body.%s = %q, want %q\nstdout:\n%s", k, got, v, out)
}
}
for k, v := range tt.wantSendParams {
got := gjson.Get(out, "api.1.params."+k).String()
if got != v {
t.Fatalf("api[1].params.%s = %q, want %q\nstdout:\n%s", k, got, v, out)
}
}
})
}
}
@@ -0,0 +1,52 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package mail
import (
"context"
"testing"
"time"
clie2e "github.com/larksuite/cli/tests/cli_e2e"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
)
func TestMail_TriageDryRunPreservesMailboxInRequestChain(t *testing.T) {
setMailTriageDryRunEnv(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: []string{
"mail", "+triage",
"--mailbox", "alias@example.com",
"--filter", `{"folder_id":"INBOX"}`,
"--max", "3",
"--dry-run",
},
DefaultAs: "user",
})
require.NoError(t, err)
result.AssertExitCode(t, 0)
require.Equal(t, int64(2), gjson.Get(result.Stdout, "api.#").Int(), "stdout:\n%s", result.Stdout)
require.Equal(t, "GET", gjson.Get(result.Stdout, "api.0.method").String(), "stdout:\n%s", result.Stdout)
require.Equal(t, "/open-apis/mail/v1/user_mailboxes/alias@example.com/messages", gjson.Get(result.Stdout, "api.0.url").String(), "stdout:\n%s", result.Stdout)
require.Equal(t, int64(3), gjson.Get(result.Stdout, "api.0.params.page_size").Int(), "stdout:\n%s", result.Stdout)
require.Equal(t, "INBOX", gjson.Get(result.Stdout, "api.0.params.folder_id").String(), "stdout:\n%s", result.Stdout)
require.Equal(t, "POST", gjson.Get(result.Stdout, "api.1.method").String(), "stdout:\n%s", result.Stdout)
require.Equal(t, "/open-apis/mail/v1/user_mailboxes/alias@example.com/messages/batch_get", gjson.Get(result.Stdout, "api.1.url").String(), "stdout:\n%s", result.Stdout)
require.Equal(t, "metadata", gjson.Get(result.Stdout, "api.1.body.format").String(), "stdout:\n%s", result.Stdout)
}
func setMailTriageDryRunEnv(t *testing.T) {
t.Helper()
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
t.Setenv("LARKSUITE_CLI_APP_ID", "mail_triage_dryrun_test")
t.Setenv("LARKSUITE_CLI_APP_SECRET", "mail_triage_dryrun_secret")
t.Setenv("LARKSUITE_CLI_BRAND", "feishu")
}