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
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:
@@ -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-all’s 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")
|
||||
}
|
||||
Reference in New Issue
Block a user