bf9395e022
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
278 lines
9.5 KiB
Go
278 lines
9.5 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package mail
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/internal/output"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
// assertValidationError fails the test unless err carries the validation
|
|
// category with ExitValidation exit code and a message containing wantSubstr.
|
|
// Mail-produced validation errors should be typed; the exit-code fallback keeps
|
|
// shared framework validation gates covered without asserting their shape here.
|
|
func assertValidationError(t *testing.T, err error, wantSubstr string) {
|
|
t.Helper()
|
|
if err == nil {
|
|
t.Fatal("expected a validation error, got nil")
|
|
}
|
|
code := output.ExitCodeOf(err)
|
|
if !errs.IsValidation(err) && code != output.ExitValidation {
|
|
t.Fatalf("expected a validation-category error, got %T: %v", err, err)
|
|
}
|
|
if code != output.ExitValidation {
|
|
t.Errorf("expected exit code %d (ExitValidation), got %d", output.ExitValidation, code)
|
|
}
|
|
if wantSubstr != "" && !strings.Contains(err.Error(), wantSubstr) {
|
|
t.Errorf("expected error message to contain %q, got: %v", wantSubstr, err.Error())
|
|
}
|
|
}
|
|
|
|
// assertValidatePasses fails the test if err is a validation error; other
|
|
// errors (e.g. API call failures from missing tokens) are acceptable because
|
|
// we only care that the Validate callback passed.
|
|
func assertValidatePasses(t *testing.T, err error) {
|
|
t.Helper()
|
|
if err == nil {
|
|
return
|
|
}
|
|
if errs.IsValidation(err) || output.ExitCodeOf(err) == output.ExitValidation {
|
|
t.Fatalf("Validate callback should have passed but returned validation error: %v", err)
|
|
}
|
|
// Non-validation errors (auth/API failures) are expected without HTTP mocks.
|
|
}
|
|
|
|
func TestRequiredBodyRejectsWhitespaceBodyFile(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
shortcut common.Shortcut
|
|
args []string
|
|
}{
|
|
{
|
|
name: "send",
|
|
shortcut: MailSend,
|
|
args: []string{
|
|
"+send", "--as", "user", "--to", "alice@example.com",
|
|
"--subject", "blank body-file", "--body-file", "blank.html",
|
|
},
|
|
},
|
|
{
|
|
name: "draft-create",
|
|
shortcut: MailDraftCreate,
|
|
args: []string{
|
|
"+draft-create", "--as", "user",
|
|
"--subject", "blank body-file", "--body-file", "blank.html",
|
|
},
|
|
},
|
|
{
|
|
name: "reply",
|
|
shortcut: MailReply,
|
|
args: []string{
|
|
"+reply", "--as", "user", "--message-id", "msg_001",
|
|
"--body-file", "blank.html",
|
|
},
|
|
},
|
|
{
|
|
name: "reply-all",
|
|
shortcut: MailReplyAll,
|
|
args: []string{
|
|
"+reply-all", "--as", "user", "--message-id", "msg_001",
|
|
"--body-file", "blank.html",
|
|
},
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
chdirTemp(t)
|
|
if err := os.WriteFile("blank.html", []byte(" \n\t"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, stdout, _, _ := mailShortcutTestFactory(t)
|
|
err := runMountedMailShortcut(t, tc.shortcut, tc.args, f, stdout)
|
|
assertValidationError(t, err, "--body or --body-file is required")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TC-1: +message --as bot --mailbox me → ErrValidation
|
|
func TestMailMessageBotMailboxMeReturnsValidationError(t *testing.T) {
|
|
f, stdout, _, _ := mailShortcutTestFactory(t)
|
|
err := runMountedMailShortcut(t, MailMessage, []string{
|
|
"+message", "--as", "bot", "--mailbox", "me", "--message-id", "msg_xxx",
|
|
}, f, stdout)
|
|
assertValidationError(t, err, "does not support --mailbox me")
|
|
}
|
|
|
|
// TC-2: +message --as bot --mailbox explicit → Validate passes
|
|
func TestMailMessageBotExplicitMailboxPassesValidation(t *testing.T) {
|
|
f, stdout, _, _ := mailShortcutTestFactory(t)
|
|
err := runMountedMailShortcut(t, MailMessage, []string{
|
|
"+message", "--as", "bot", "--mailbox", "alice@example.com", "--message-id", "msg_xxx",
|
|
}, f, stdout)
|
|
assertValidatePasses(t, err)
|
|
}
|
|
|
|
// TC-3: +message --as user --mailbox me → Validate passes
|
|
func TestMailMessageUserMailboxMePassesValidation(t *testing.T) {
|
|
f, stdout, _, _ := mailShortcutTestFactory(t)
|
|
err := runMountedMailShortcut(t, MailMessage, []string{
|
|
"+message", "--as", "user", "--mailbox", "me", "--message-id", "msg_xxx",
|
|
}, f, stdout)
|
|
assertValidatePasses(t, err)
|
|
}
|
|
|
|
// TC-4: +messages --as bot (default mailbox=me) → ErrValidation
|
|
func TestMailMessagesBotDefaultMailboxMeReturnsValidationError(t *testing.T) {
|
|
f, stdout, _, _ := mailShortcutTestFactory(t)
|
|
err := runMountedMailShortcut(t, MailMessages, []string{
|
|
"+messages", "--as", "bot", "--message-ids", validMessageIDForTest("biz-x"),
|
|
}, f, stdout)
|
|
assertValidationError(t, err, "does not support --mailbox me")
|
|
}
|
|
|
|
// TC-5: +messages --as bot --mailbox explicit → Validate passes
|
|
func TestMailMessagesBotExplicitMailboxPassesValidation(t *testing.T) {
|
|
f, stdout, _, _ := mailShortcutTestFactory(t)
|
|
err := runMountedMailShortcut(t, MailMessages, []string{
|
|
"+messages", "--as", "bot", "--mailbox", "alice@example.com", "--message-ids", validMessageIDForTest("biz-x"),
|
|
}, f, stdout)
|
|
assertValidatePasses(t, err)
|
|
}
|
|
|
|
// TC-6: +thread --as bot (default mailbox=me) → ErrValidation
|
|
func TestMailThreadBotDefaultMailboxMeReturnsValidationError(t *testing.T) {
|
|
f, stdout, _, _ := mailShortcutTestFactory(t)
|
|
err := runMountedMailShortcut(t, MailThread, []string{
|
|
"+thread", "--as", "bot", "--thread-id", "thread_xxx",
|
|
}, f, stdout)
|
|
assertValidationError(t, err, "does not support --mailbox me")
|
|
}
|
|
|
|
// TC-7: +thread --as bot --mailbox explicit → Validate passes
|
|
func TestMailThreadBotExplicitMailboxPassesValidation(t *testing.T) {
|
|
f, stdout, _, _ := mailShortcutTestFactory(t)
|
|
err := runMountedMailShortcut(t, MailThread, []string{
|
|
"+thread", "--as", "bot", "--mailbox", "alice@example.com", "--thread-id", "thread_xxx",
|
|
}, f, stdout)
|
|
assertValidatePasses(t, err)
|
|
}
|
|
|
|
// TC-8: +triage --as bot (default mailbox=me) → ErrValidation
|
|
func TestMailTriageBotDefaultMailboxMeReturnsValidationError(t *testing.T) {
|
|
f, stdout, _, _ := mailShortcutTestFactory(t)
|
|
err := runMountedMailShortcut(t, MailTriage, []string{
|
|
"+triage", "--as", "bot",
|
|
}, f, stdout)
|
|
assertValidationError(t, err, "does not support --mailbox me")
|
|
}
|
|
|
|
// TC-9: +triage --as bot --mailbox explicit → Validate passes
|
|
func TestMailTriageBotExplicitMailboxPassesValidation(t *testing.T) {
|
|
f, stdout, _, _ := mailShortcutTestFactory(t)
|
|
err := runMountedMailShortcut(t, MailTriage, []string{
|
|
"+triage", "--as", "bot", "--mailbox", "alice@example.com",
|
|
}, f, stdout)
|
|
assertValidatePasses(t, err)
|
|
}
|
|
|
|
// --- message_ids validation tests (S2) ---
|
|
|
|
func validMessageIDForTest(s string) string {
|
|
return base64.URLEncoding.EncodeToString([]byte(s))
|
|
}
|
|
|
|
func rawMessageIDForTest(s string) string {
|
|
return base64.RawURLEncoding.EncodeToString([]byte(s))
|
|
}
|
|
|
|
func TestValidateMessageIDsAcceptsValidIDs(t *testing.T) {
|
|
_, err := validateMessageIDs(validMessageIDForTest("biz-001") + "," + validMessageIDForTest("biz-002"))
|
|
if err != nil {
|
|
t.Fatalf("expected nil error for valid IDs, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateMessageIDsAcceptsRawBase64URLIDs(t *testing.T) {
|
|
_, err := validateMessageIDs(rawMessageIDForTest("biz-raw-001"))
|
|
if err != nil {
|
|
t.Fatalf("expected nil error for raw base64url ID, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateMessageIDsRejectsEmpty(t *testing.T) {
|
|
_, err := validateMessageIDs("")
|
|
assertValidationError(t, err, "--message-ids is required")
|
|
_, err = validateMessageIDs(" ")
|
|
assertValidationError(t, err, "--message-ids is required")
|
|
}
|
|
|
|
func TestValidateMessageIDsAcceptsMoreThanSingleBackendBatch(t *testing.T) {
|
|
ids := make([]string, 21)
|
|
for i := range ids {
|
|
ids[i] = validMessageIDForTest(string(rune('a' + i)))
|
|
}
|
|
_, err := validateMessageIDs(strings.Join(ids, ","))
|
|
if err != nil {
|
|
t.Fatalf("expected nil error for more than one backend batch, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateMessageIDsRejectsEmptyEntry(t *testing.T) {
|
|
_, err := validateMessageIDs(validMessageIDForTest("biz-1") + ",," + validMessageIDForTest("biz-2"))
|
|
assertValidationError(t, err, "entry 2 is empty")
|
|
}
|
|
|
|
func TestValidateMessageIDsRejectsLeadingOrTrailingWhitespace(t *testing.T) {
|
|
id1 := validMessageIDForTest("biz-1")
|
|
id2 := validMessageIDForTest("biz-2")
|
|
_, err := validateMessageIDs(id1 + ", " + id2)
|
|
assertValidationError(t, err, "must not contain leading or trailing whitespace")
|
|
_, err = validateMessageIDs(" " + id1 + "," + id2)
|
|
assertValidationError(t, err, "must not contain leading or trailing whitespace")
|
|
}
|
|
|
|
func TestValidateMessageIDsRejectsDuplicateIDs(t *testing.T) {
|
|
id := validMessageIDForTest("biz-1")
|
|
_, err := validateMessageIDs(id + "," + id)
|
|
assertValidationError(t, err, "duplicate message ID is not allowed")
|
|
}
|
|
|
|
func TestValidateMessageIDsRejectsJSONLikeInput(t *testing.T) {
|
|
_, err := validateMessageIDs(`["id1","id2"]`)
|
|
assertValidationError(t, err, "expected a base64url")
|
|
}
|
|
|
|
func TestValidateMessageIDsRejectsColonJoinedInput(t *testing.T) {
|
|
_, err := validateMessageIDs("id1:id2")
|
|
assertValidationError(t, err, "expected a base64url")
|
|
}
|
|
|
|
func TestValidateMessageIDsRejectsNumericPrimaryID(t *testing.T) {
|
|
_, err := validateMessageIDs("123456789")
|
|
assertValidationError(t, err, "numeric primary IDs are not supported")
|
|
}
|
|
|
|
func TestValidateMessageIDsAcceptsExactlyTwenty(t *testing.T) {
|
|
ids := make([]string, 20)
|
|
for i := range ids {
|
|
ids[i] = validMessageIDForTest(string(rune('A' + i)))
|
|
}
|
|
_, err := validateMessageIDs(strings.Join(ids, ","))
|
|
if err != nil {
|
|
t.Fatalf("expected nil error for exactly 20 IDs, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateMessageIDRejectsInvalidBase64(t *testing.T) {
|
|
_, err := validateMessageIDs("msg 1")
|
|
assertValidationError(t, err, "expected a base64url")
|
|
_, err = validateMessageIDs("not-base64!")
|
|
assertValidationError(t, err, "expected a base64url")
|
|
}
|