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,78 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestIM_ChatMessageWorkflowAsUser(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
parentT := t
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
chatName := "im-chat-" + suffix
|
||||
messageText := "im-chat-msg-" + suffix
|
||||
var chatID string
|
||||
var messageID string
|
||||
|
||||
t.Run("create chat as user", func(t *testing.T) {
|
||||
chatID = createChatAs(t, parentT, ctx, chatName, "user")
|
||||
})
|
||||
|
||||
t.Run("send message as user", func(t *testing.T) {
|
||||
messageID = sendMessageAs(t, ctx, chatID, messageText, "user")
|
||||
})
|
||||
|
||||
t.Run("list chat messages as user", func(t *testing.T) {
|
||||
startTime := time.Now().UTC().Add(-10 * time.Minute).Format(time.RFC3339)
|
||||
endTime := time.Now().UTC().Add(10 * time.Minute).Format(time.RFC3339)
|
||||
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+chat-messages-list",
|
||||
"--chat-id", chatID,
|
||||
"--start", startTime,
|
||||
"--end", endTime,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
}, clie2e.RetryOptions{
|
||||
ShouldRetry: func(result *clie2e.Result) bool {
|
||||
if result == nil || result.ExitCode != 0 {
|
||||
return true
|
||||
}
|
||||
for _, item := range gjson.Get(result.Stdout, "data.messages").Array() {
|
||||
if item.Get("message_id").String() == messageID && strings.Contains(item.Get("content").String(), messageText) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
var found bool
|
||||
for _, item := range gjson.Get(result.Stdout, "data.messages").Array() {
|
||||
if item.Get("message_id").String() != messageID {
|
||||
continue
|
||||
}
|
||||
require.True(t, strings.Contains(item.Get("content").String(), messageText), "stdout:\n%s", result.Stdout)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
require.True(t, found, "expected message %s in stdout:\n%s", messageID, result.Stdout)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// TestIM_ChatUpdateWorkflow tests the +chat-update shortcut.
|
||||
func TestIM_ChatUpdateWorkflow(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
originalName := "lark-cli-e2e-im-update-" + suffix
|
||||
updatedName := originalName + "-updated"
|
||||
updatedDescription := "Updated description for e2e test"
|
||||
|
||||
chatID := createChat(t, parentT, ctx, originalName)
|
||||
|
||||
t.Run("update chat name as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+chat-update",
|
||||
"--chat-id", chatID,
|
||||
"--name", updatedName,
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("update chat description as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+chat-update",
|
||||
"--chat-id", chatID,
|
||||
"--description", updatedDescription,
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("get updated chat as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "chats", "get"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"chat_id": chatID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
assert.Equal(t, updatedName, gjson.Get(result.Stdout, "data.name").String())
|
||||
assert.Equal(t, updatedDescription, gjson.Get(result.Stdout, "data.description").String())
|
||||
})
|
||||
}
|
||||
|
||||
// TestIM_ChatsGetWorkflow tests the im chats get command.
|
||||
func TestIM_ChatsGetWorkflow(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
chatName := "lark-cli-e2e-chats-get-" + suffix
|
||||
|
||||
chatID := createChat(t, parentT, ctx, chatName)
|
||||
|
||||
t.Run("get chat info as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "chats", "get"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"chat_id": chatID},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Logf("chats get result: %s", result.Stdout)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
dataExists := gjson.Get(result.Stdout, "data").Exists()
|
||||
require.True(t, dataExists, "data object should exist")
|
||||
|
||||
chatNameGot := gjson.Get(result.Stdout, "data.name").String()
|
||||
require.Equal(t, chatName, chatNameGot)
|
||||
})
|
||||
}
|
||||
|
||||
// TestIM_ChatsLinkWorkflow tests the im chats link command.
|
||||
func TestIM_ChatsLinkWorkflow(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
chatName := "lark-cli-e2e-chats-link-" + suffix
|
||||
|
||||
chatID := createChat(t, parentT, ctx, chatName)
|
||||
|
||||
t.Run("get chat share link as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "chats", "link"},
|
||||
DefaultAs: "bot",
|
||||
Params: map[string]any{"chat_id": chatID},
|
||||
Data: map[string]any{
|
||||
"validity_period": "week",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
shareLink := gjson.Get(result.Stdout, "data.share_link").String()
|
||||
require.NotEmpty(t, shareLink, "share_link should not be empty")
|
||||
t.Logf("Generated share link: %s", shareLink)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
# IM CLI E2E Coverage
|
||||
|
||||
## Metrics
|
||||
- Denominator: 30 leaf commands
|
||||
- Covered: 11
|
||||
- Coverage: 36.7%
|
||||
|
||||
## Summary
|
||||
- TestIM_ChatUpdateWorkflow: proves `im +chat-create`, `im +chat-update`, and `im chats get`; key `t.Run(...)` proof points are `update chat name as bot`, `update chat description as bot`, and `get updated chat as bot`.
|
||||
- TestIM_ChatsGetWorkflow: proves `im chats get` on a fresh chat fixture via `get chat info as bot`.
|
||||
- TestIM_ChatsLinkWorkflow: proves `im chats link` via `get chat share link as bot`.
|
||||
- TestIM_ChatMessageWorkflowAsUser: proves the user chat message flow through `create chat as user`, `send message as user`, and `list chat messages as user` with the created message ID and content asserted from read-after-write output.
|
||||
- TestIM_MessageGetWorkflowAsUser: proves user message readback through `batch get message as user` after creating a fresh chat and sending a unique message.
|
||||
- TestIM_MessageReplyWorkflowAsBot: proves threaded reply flow through `reply to message in thread as bot` and `list thread replies as bot`, reading back the reply from `im +threads-messages-list`.
|
||||
- TestIM_MessagesSendAudioDryRunRejectsNonOpus: proves the `im +messages-send --audio` dry-run validation rejects non-Opus local audio before upload, with typed validation metadata and recovery guidance.
|
||||
- TestIM_MessageForwardWorkflowAsUser: proves UAT-backed API forwarding through `im messages forward` and `im threads forward` using a fresh message/thread fixture; skips the forward assertions when the current test app/UAT lacks IM forward permission.
|
||||
- Blocked area: `im +chat-search` did not reliably return freshly created private chats in UAT, and `im +messages-search` did not reliably index freshly sent messages in time for a deterministic read-after-write assertion, so both remain uncovered.
|
||||
|
||||
## Command Table
|
||||
|
||||
| Status | Cmd | Type | Testcase | Key parameter shapes | Notes / uncovered reason |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| ✓ | im +chat-create | shortcut | im/chat_message_workflow_test.go::TestIM_ChatMessageWorkflowAsUser/create chat as user; im/chat_workflow_test.go::TestIM_ChatUpdateWorkflow; im/chat_workflow_test.go::TestIM_ChatsGetWorkflow; im/chat_workflow_test.go::TestIM_ChatsLinkWorkflow; im/message_get_workflow_test.go::TestIM_MessageGetWorkflowAsUser; im/message_reply_workflow_test.go::TestIM_MessageReplyWorkflowAsBot | `--name`; `--type private` | covered via workflow setup with created chat IDs asserted |
|
||||
| ✓ | im +chat-messages-list | shortcut | im/chat_message_workflow_test.go::TestIM_ChatMessageWorkflowAsUser/list chat messages as user; im/message_reply_workflow_test.go::TestIM_MessageReplyWorkflowAsBot/list thread replies as bot | `--chat-id`; `--start`; `--end` | reads back created message and discovers thread ID |
|
||||
| ✕ | im +chat-search | shortcut | | none | UAT did not reliably return freshly created private chats, so it is left uncovered |
|
||||
| ✓ | im +chat-update | shortcut | im/chat_workflow_test.go::TestIM_ChatUpdateWorkflow/update chat name as bot; im/chat_workflow_test.go::TestIM_ChatUpdateWorkflow/update chat description as bot | `--chat-id`; `--name`; `--description` | |
|
||||
| ✓ | im +messages-mget | shortcut | im/message_get_workflow_test.go::TestIM_MessageGetWorkflowAsUser/batch get message as user | `--message-ids` | verifies sent message content by ID |
|
||||
| ✓ | im +messages-reply | shortcut | im/message_reply_workflow_test.go::TestIM_MessageReplyWorkflowAsBot/reply to message in thread as bot | `--message-id`; `--text`; `--reply-in-thread` | reply is read back via thread list |
|
||||
| ✕ | im +messages-resources-download | shortcut | | none | needs a stable image/file message fixture plus file_key proof; left uncovered |
|
||||
| ✕ | im +messages-search | shortcut | | none | freshly sent messages were not indexed deterministically in UAT time for a stable read-after-write proof |
|
||||
| ✓ | im +messages-send | shortcut | im/chat_message_workflow_test.go::TestIM_ChatMessageWorkflowAsUser/send message as user; im/message_get_workflow_test.go::TestIM_MessageGetWorkflowAsUser; im/message_reply_workflow_test.go::TestIM_MessageReplyWorkflowAsBot; im/message_audio_dryrun_test.go::TestIM_MessagesSendAudioDryRunRejectsNonOpus | `--chat-id`; `--text`; `--audio ./voice.mp3 --dry-run` | live text sends feed follow-up reads; dry-run pins non-Opus audio validation before upload |
|
||||
| ✓ | im +threads-messages-list | shortcut | im/message_reply_workflow_test.go::TestIM_MessageReplyWorkflowAsBot/list thread replies as bot | `--thread` | proves threaded reply is persisted |
|
||||
| ✕ | im chat.members create | api | | none | no member mutation workflow yet |
|
||||
| ✕ | im chat.members get | api | | none | no member get workflow yet |
|
||||
| ✕ | im chats create | api | | none | only covered indirectly through `+chat-create` |
|
||||
| ✓ | im chats get | api | im/chat_workflow_test.go::TestIM_ChatUpdateWorkflow/get updated chat as bot; im/chat_workflow_test.go::TestIM_ChatsGetWorkflow/get chat info as bot | `chat_id` in `--params` | |
|
||||
| ✓ | im chats link | api | im/chat_workflow_test.go::TestIM_ChatsLinkWorkflow/get chat share link as bot | `chat_id` in `--params`; `validity_period` in `--data` | |
|
||||
| ✕ | im chats list | api | | none | no chats list workflow yet |
|
||||
| ✕ | im chats update | api | | none | only covered indirectly through `+chat-update` |
|
||||
| ✕ | im images create | api | | none | no image upload workflow yet |
|
||||
| ✕ | im messages delete | api | | none | no recall workflow yet |
|
||||
| ✓ | im messages forward | api | im/message_forward_workflow_test.go::TestIM_MessageForwardWorkflowAsUser/forward message with api command as user | `message_id`; `receive_id_type`; `uuid`; `receive_id` | forwards a fresh message back into the test chat using UAT |
|
||||
| ✕ | im messages merge_forward | api | | none | no merge-forward workflow yet |
|
||||
| ✕ | im messages read_users | api | | none | no read-user workflow yet |
|
||||
| ✓ | im threads forward | api | im/message_forward_workflow_test.go::TestIM_MessageForwardWorkflowAsUser/forward thread with api command as user | `thread_id`; `receive_id_type`; `uuid`; `receive_id` | forwards a fresh thread back into the test chat using UAT |
|
||||
| ✕ | im pins create | api | | none | pin workflows not covered |
|
||||
| ✕ | im pins delete | api | | none | pin workflows not covered |
|
||||
| ✕ | im pins list | api | | none | pin workflows not covered |
|
||||
| ✕ | im reactions batch_query | api | | none | reaction workflows not covered |
|
||||
| ✕ | im reactions create | api | | none | reaction workflows not covered |
|
||||
| ✕ | im reactions delete | api | | none | reaction workflows not covered |
|
||||
| ✕ | im reactions list | api | | none | reaction workflows not covered |
|
||||
@@ -0,0 +1,447 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// TestIM_FeedShortcutWorkflowAsUser walks the full create → list → remove
|
||||
// loop for a single CHAT-type feed shortcut, mirroring the +flag-* workflow
|
||||
// test. The feed_shortcuts API is user-identity only and version-locked, so
|
||||
// the assertion strategy uses RunCmdWithRetry against the list endpoint
|
||||
// rather than assuming the index is updated synchronously.
|
||||
func TestIM_FeedShortcutWorkflowAsUser(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
parentT := t
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
chatName := "im-feed-shortcut-" + suffix
|
||||
var chatID string
|
||||
t.Cleanup(func() {
|
||||
cleanupFeedShortcuts(parentT, "user", chatID)
|
||||
})
|
||||
|
||||
t.Run("create chat as user", func(t *testing.T) {
|
||||
chatID = createChatAs(t, parentT, ctx, chatName, "user")
|
||||
})
|
||||
|
||||
t.Run("pin chat to feed as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+feed-shortcut-create",
|
||||
"--chat-id", chatID,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
// failed_shortcuts may be absent (server returns {} on full success)
|
||||
// or present-and-empty — either way it must not contain our chat.
|
||||
for _, item := range gjson.Get(result.Stdout, "data.failed_shortcuts").Array() {
|
||||
require.NotEqual(t, chatID, item.Get("shortcut.feed_card_id").String(),
|
||||
"create should not report our chat as failed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("list feed shortcuts as user with detail enrichment", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+feed-shortcut-list",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
}, clie2e.RetryOptions{
|
||||
ShouldRetry: func(result *clie2e.Result) bool {
|
||||
if result == nil || result.ExitCode != 0 {
|
||||
return true
|
||||
}
|
||||
for _, item := range gjson.Get(result.Stdout, "data.shortcuts").Array() {
|
||||
if item.Get("feed_card_id").String() == chatID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
var found bool
|
||||
for _, item := range gjson.Get(result.Stdout, "data.shortcuts").Array() {
|
||||
if item.Get("feed_card_id").String() != chatID {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
require.Equal(t, int64(1), item.Get("type").Int(), "type should be 1 (CHAT)")
|
||||
// detail enrichment is on by default — the chat we just created
|
||||
// must come back with the chat info object attached.
|
||||
require.True(t, item.Get("detail").Exists(),
|
||||
"detail field should be attached when enrichment is enabled")
|
||||
require.Equal(t, chatID, item.Get("detail.chat_id").String(),
|
||||
"detail.chat_id should echo feed_card_id")
|
||||
require.Equal(t, chatName, item.Get("detail.name").String(),
|
||||
"detail.name should carry the chat's group name")
|
||||
break
|
||||
}
|
||||
require.True(t, found, "expected chat %s in feed shortcut list", chatID)
|
||||
})
|
||||
|
||||
t.Run("list feed shortcuts with --no-detail skips lookup", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+feed-shortcut-list",
|
||||
"--no-detail",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
var foundEntry gjson.Result
|
||||
for _, item := range gjson.Get(result.Stdout, "data.shortcuts").Array() {
|
||||
if item.Get("feed_card_id").String() == chatID {
|
||||
foundEntry = item
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, foundEntry.Exists(), "expected our chat in the bare list")
|
||||
require.False(t, foundEntry.Get("detail").Exists(),
|
||||
"detail field should NOT be present with --no-detail")
|
||||
})
|
||||
|
||||
t.Run("unpin chat from feed as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+feed-shortcut-remove",
|
||||
"--chat-id", chatID,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
for _, item := range gjson.Get(result.Stdout, "data.failed_shortcuts").Array() {
|
||||
require.NotEqual(t, chatID, item.Get("shortcut.feed_card_id").String(),
|
||||
"remove should not report our chat as failed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify chat removed from feed", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+feed-shortcut-list",
|
||||
"--no-detail",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
}, clie2e.RetryOptions{
|
||||
ShouldRetry: func(result *clie2e.Result) bool {
|
||||
if result == nil || result.ExitCode != 0 {
|
||||
return true
|
||||
}
|
||||
for _, item := range gjson.Get(result.Stdout, "data.shortcuts").Array() {
|
||||
if item.Get("feed_card_id").String() == chatID {
|
||||
return true // still there, retry
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
for _, item := range gjson.Get(result.Stdout, "data.shortcuts").Array() {
|
||||
require.NotEqual(t, chatID, item.Get("feed_card_id").String(),
|
||||
"chat should not be in feed list after remove")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestIM_FeedShortcutBatchAsUser exercises batch create / remove with two
|
||||
// chats in a single API call. The per-item failure path (failed_shortcuts)
|
||||
// is checked by mixing a real chat with an obviously invalid id.
|
||||
func TestIM_FeedShortcutBatchAsUser(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
parentT := t
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
var chatA, chatB string
|
||||
t.Cleanup(func() {
|
||||
cleanupFeedShortcuts(parentT, "user", chatA, chatB)
|
||||
})
|
||||
|
||||
t.Run("create two chats as user", func(t *testing.T) {
|
||||
chatA = createChatAs(t, parentT, ctx, "im-feed-batch-a-"+suffix, "user")
|
||||
chatB = createChatAs(t, parentT, ctx, "im-feed-batch-b-"+suffix, "user")
|
||||
})
|
||||
|
||||
t.Run("batch pin both with --tail", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+feed-shortcut-create",
|
||||
"--chat-id", chatA + "," + chatB,
|
||||
"--tail",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("batch pin with one invalid id reports per-item failure", func(t *testing.T) {
|
||||
// Re-pinning chatA (already pinned) should still succeed without
|
||||
// noting it as failure; the synthetic invalid id should land in
|
||||
// failed_shortcuts with reason_label=invalid_item.
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+feed-shortcut-create",
|
||||
"--chat-id", chatA,
|
||||
"--chat-id", "oc_definitely_not_a_real_chat_id_" + suffix,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// Partial failure exits with the generic API failure code while the
|
||||
// full ledger stays machine-readable on stdout.
|
||||
result.AssertExitCode(t, 1)
|
||||
result.AssertStdoutStatus(t, false)
|
||||
require.Equal(t, int64(2), gjson.Get(result.Stdout, "data.total").Int())
|
||||
require.Equal(t, int64(1), gjson.Get(result.Stdout, "data.success_count").Int())
|
||||
require.Equal(t, int64(1), gjson.Get(result.Stdout, "data.failure_count").Int())
|
||||
|
||||
var sawInvalid bool
|
||||
for _, item := range gjson.Get(result.Stdout, "data.failed_shortcuts").Array() {
|
||||
require.NotEqual(t, chatA, item.Get("shortcut.feed_card_id").String(),
|
||||
"real chat should not appear in failed_shortcuts")
|
||||
if item.Get("shortcut.feed_card_id").String() == "oc_definitely_not_a_real_chat_id_"+suffix {
|
||||
sawInvalid = true
|
||||
require.Equal(t, "invalid_item", item.Get("reason_label").String(),
|
||||
"invalid id should be labeled invalid_item")
|
||||
}
|
||||
}
|
||||
require.True(t, sawInvalid, "expected the bogus chat id in failed_shortcuts")
|
||||
var sawSucceeded bool
|
||||
for _, item := range gjson.Get(result.Stdout, "data.succeeded_shortcuts").Array() {
|
||||
if item.Get("feed_card_id").String() == chatA {
|
||||
sawSucceeded = true
|
||||
require.Equal(t, int64(1), item.Get("type").Int(), "succeeded shortcut type should be CHAT")
|
||||
}
|
||||
}
|
||||
require.True(t, sawSucceeded, "expected the real chat id in succeeded_shortcuts")
|
||||
})
|
||||
|
||||
t.Run("batch remove both", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+feed-shortcut-remove",
|
||||
"--chat-id", chatA,
|
||||
"--chat-id", chatB,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
}
|
||||
|
||||
func cleanupFeedShortcuts(parentT *testing.T, defaultAs string, chatIDs ...string) {
|
||||
parentT.Helper()
|
||||
var ids []string
|
||||
for _, id := range chatIDs {
|
||||
if id != "" {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
cleanupCtx, cancel := clie2e.CleanupContext()
|
||||
defer cancel()
|
||||
listResult, listErr := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: []string{"im", "+feed-shortcut-list", "--no-detail"},
|
||||
DefaultAs: defaultAs,
|
||||
})
|
||||
clie2e.ReportCleanupFailure(parentT, "cleanup feed shortcuts list", listResult, listErr)
|
||||
if listErr != nil || listResult == nil || listResult.ExitCode != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
present := make(map[string]bool, len(ids))
|
||||
for _, id := range ids {
|
||||
present[id] = false
|
||||
}
|
||||
for _, item := range gjson.Get(listResult.Stdout, "data.shortcuts").Array() {
|
||||
id := item.Get("feed_card_id").String()
|
||||
if _, ok := present[id]; ok {
|
||||
present[id] = true
|
||||
}
|
||||
}
|
||||
|
||||
args := []string{"im", "+feed-shortcut-remove"}
|
||||
for _, id := range ids {
|
||||
if present[id] {
|
||||
args = append(args, "--chat-id", id)
|
||||
}
|
||||
}
|
||||
if len(args) == 2 {
|
||||
return
|
||||
}
|
||||
result, err := clie2e.RunCmd(cleanupCtx, clie2e.Request{
|
||||
Args: args,
|
||||
DefaultAs: defaultAs,
|
||||
})
|
||||
clie2e.ReportCleanupFailure(parentT, "cleanup feed shortcuts", result, err)
|
||||
}
|
||||
|
||||
// TestIM_FeedShortcutDryRun covers all three shortcuts in --dry-run mode
|
||||
// using env-only identity hints. strict_mode/default_as lock the command to
|
||||
// user identity without injecting a fake user token that would trigger
|
||||
// user_info verification during startup.
|
||||
func TestIM_FeedShortcutDryRun(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")
|
||||
t.Setenv("LARKSUITE_CLI_STRICT_MODE", "user")
|
||||
t.Setenv("LARKSUITE_CLI_DEFAULT_AS", "user")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
t.Run("create dry-run", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+feed-shortcut-create",
|
||||
"--chat-id", "oc_test_dry_run",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
require.Contains(t, result.Stdout, "POST")
|
||||
require.Contains(t, result.Stdout, "/open-apis/im/v2/feed_shortcuts")
|
||||
require.Contains(t, result.Stdout, "oc_test_dry_run")
|
||||
// --head is the default so the body should be is_header=true
|
||||
require.Contains(t, result.Stdout, `"is_header"`)
|
||||
})
|
||||
|
||||
t.Run("create dry-run with --tail flips is_header to false", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+feed-shortcut-create",
|
||||
"--chat-id", "oc_test_dry_run",
|
||||
"--tail",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
require.Contains(t, result.Stdout, `"is_header": false`)
|
||||
})
|
||||
|
||||
t.Run("create dry-run rejects --head + --tail", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+feed-shortcut-create",
|
||||
"--chat-id", "oc_test_dry_run",
|
||||
"--head",
|
||||
"--tail",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// Validation errors exit 2 with the structured error envelope;
|
||||
// assert against combined output to stay channel-agnostic.
|
||||
result.AssertExitCode(t, 2)
|
||||
combined := result.Stdout + "\n" + result.Stderr
|
||||
require.Contains(t, combined, "mutually exclusive")
|
||||
})
|
||||
|
||||
t.Run("create dry-run rejects non-oc chat ids", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+feed-shortcut-create",
|
||||
"--chat-id", "om_not_a_chat",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 2)
|
||||
combined := result.Stdout + "\n" + result.Stderr
|
||||
require.Contains(t, combined, "must be an open_chat_id")
|
||||
})
|
||||
|
||||
t.Run("remove dry-run hits /remove endpoint", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+feed-shortcut-remove",
|
||||
"--chat-id", "oc_a,oc_b",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
require.Contains(t, result.Stdout, "POST")
|
||||
require.Contains(t, result.Stdout, "/open-apis/im/v2/feed_shortcuts/remove")
|
||||
require.Contains(t, result.Stdout, "oc_a")
|
||||
require.Contains(t, result.Stdout, "oc_b")
|
||||
require.NotContains(t, result.Stdout, "is_header", "remove must not send is_header")
|
||||
})
|
||||
|
||||
t.Run("list dry-run mentions detail enrichment path", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+feed-shortcut-list",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
require.Contains(t, result.Stdout, "GET")
|
||||
require.Contains(t, result.Stdout, "/open-apis/im/v2/feed_shortcuts")
|
||||
// Enrichment is on by default → DryRun adds a desc about the extra
|
||||
// chats.batch_query call and the conditional scope.
|
||||
require.Contains(t, result.Stdout, "im:chat:read")
|
||||
require.Contains(t, result.Stdout, "batch_query")
|
||||
})
|
||||
|
||||
t.Run("list dry-run with --no-detail omits the extra-scope note", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+feed-shortcut-list",
|
||||
"--no-detail",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
require.NotContains(t, result.Stdout, "im:chat:read",
|
||||
"with --no-detail, dry-run must not mention im:chat:read")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestIM_FlagWorkflowAsUser(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
parentT := t
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
chatName := "im-flag-" + suffix
|
||||
messageText := "flag-test-msg-" + suffix
|
||||
var chatID string
|
||||
var messageID string
|
||||
|
||||
t.Run("create chat as user", func(t *testing.T) {
|
||||
chatID = createChatAs(t, parentT, ctx, chatName, "user")
|
||||
})
|
||||
|
||||
t.Run("send message as user", func(t *testing.T) {
|
||||
messageID = sendMessageAs(t, ctx, chatID, messageText, "user")
|
||||
})
|
||||
|
||||
t.Run("create flag as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+flag-create",
|
||||
"--message-id", messageID,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("list flags as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+flag-list",
|
||||
"--page-size", "10",
|
||||
"--page-all",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
}, clie2e.RetryOptions{
|
||||
ShouldRetry: func(result *clie2e.Result) bool {
|
||||
if result == nil || result.ExitCode != 0 {
|
||||
return true
|
||||
}
|
||||
// Check if our message is in the list
|
||||
for _, item := range gjson.Get(result.Stdout, "data.flag_items").Array() {
|
||||
if item.Get("item_id").String() == messageID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
// Verify our flagged message is in the list
|
||||
var found bool
|
||||
for _, item := range gjson.Get(result.Stdout, "data.flag_items").Array() {
|
||||
if item.Get("item_id").String() == messageID {
|
||||
found = true
|
||||
// Verify it's a message-type flag (flag_type=2)
|
||||
require.Equal(t, "2", item.Get("flag_type").String(), "expected flag_type=2 (message)")
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, found, "expected message %s in flag list", messageID)
|
||||
})
|
||||
|
||||
t.Run("cancel flag as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+flag-cancel",
|
||||
"--message-id", messageID,
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("verify flag removed", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+flag-list",
|
||||
"--page-size", "10",
|
||||
"--page-all",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
}, clie2e.RetryOptions{
|
||||
ShouldRetry: func(result *clie2e.Result) bool {
|
||||
if result == nil || result.ExitCode != 0 {
|
||||
return true
|
||||
}
|
||||
// Check if our message is still in the list
|
||||
for _, item := range gjson.Get(result.Stdout, "data.flag_items").Array() {
|
||||
if item.Get("item_id").String() == messageID {
|
||||
return true // Still there, retry
|
||||
}
|
||||
}
|
||||
return false // Not found, success
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
// Verify our message is NOT in the list
|
||||
for _, item := range gjson.Get(result.Stdout, "data.flag_items").Array() {
|
||||
require.NotEqual(t, messageID, item.Get("item_id").String(), "message should not be in flag list after cancel")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestIM_FlagCreateWithExplicitTypeAsUser(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
parentT := t
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
chatName := "im-flag-explicit-" + suffix
|
||||
messageText := "flag-explicit-msg-" + suffix
|
||||
var chatID string
|
||||
var messageID string
|
||||
|
||||
t.Run("create chat as user", func(t *testing.T) {
|
||||
chatID = createChatAs(t, parentT, ctx, chatName, "user")
|
||||
})
|
||||
|
||||
t.Run("send message as user", func(t *testing.T) {
|
||||
messageID = sendMessageAs(t, ctx, chatID, messageText, "user")
|
||||
})
|
||||
|
||||
t.Run("create flag with explicit types as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+flag-create",
|
||||
"--message-id", messageID,
|
||||
"--item-type", "default",
|
||||
"--flag-type", "message",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
|
||||
t.Run("list flags to verify explicit types as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+flag-list",
|
||||
"--page-size", "10",
|
||||
"--page-all",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
}, clie2e.RetryOptions{
|
||||
ShouldRetry: func(result *clie2e.Result) bool {
|
||||
if result == nil || result.ExitCode != 0 {
|
||||
return true
|
||||
}
|
||||
for _, item := range gjson.Get(result.Stdout, "data.flag_items").Array() {
|
||||
if item.Get("item_id").String() == messageID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
// Verify explicit types were applied
|
||||
var found bool
|
||||
for _, item := range gjson.Get(result.Stdout, "data.flag_items").Array() {
|
||||
if item.Get("item_id").String() == messageID {
|
||||
found = true
|
||||
require.Equal(t, "0", item.Get("item_type").String(), "expected item_type=0 (default)")
|
||||
require.Equal(t, "2", item.Get("flag_type").String(), "expected flag_type=2 (message)")
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, found, "expected message %s in flag list", messageID)
|
||||
})
|
||||
|
||||
t.Run("cancel flag with explicit types as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+flag-cancel",
|
||||
"--message-id", messageID,
|
||||
"--flag-type", "message",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIM_FlagListPaginationAsUser(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
t.Run("list flags with page-all as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+flag-list",
|
||||
"--page-size", "5",
|
||||
"--page-all",
|
||||
"--page-limit", "3",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIM_FlagDryRun(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")
|
||||
t.Setenv("LARKSUITE_CLI_STRICT_MODE", "user")
|
||||
t.Setenv("LARKSUITE_CLI_DEFAULT_AS", "user")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
t.Run("create flag dry-run", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+flag-create",
|
||||
"--message-id", "om_test_dry_run",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
require.Contains(t, result.Stdout, "POST")
|
||||
require.Contains(t, result.Stdout, "/open-apis/im/v1/flags")
|
||||
require.Contains(t, result.Stdout, "flag_items")
|
||||
require.Contains(t, result.Stdout, "om_test_dry_run")
|
||||
})
|
||||
|
||||
t.Run("cancel flag dry-run with om", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+flag-cancel",
|
||||
"--message-id", "om_test_dry_run",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
require.Contains(t, result.Stdout, "POST")
|
||||
require.Contains(t, result.Stdout, "/open-apis/im/v1/flags/cancel")
|
||||
require.Contains(t, result.Stdout, "flag_items")
|
||||
require.Contains(t, result.Stdout, "om_test_dry_run")
|
||||
})
|
||||
|
||||
t.Run("list flag dry-run", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+flag-list",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
require.Contains(t, result.Stdout, "GET")
|
||||
require.Contains(t, result.Stdout, "/open-apis/im/v1/flags")
|
||||
require.Contains(t, result.Stdout, "page_size")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// createChat creates a private chat with the given name and returns the chatID.
|
||||
// The chat will be automatically cleaned up via parentT.Cleanup().
|
||||
// Note: Chat deletion is not available via lark-cli im command.
|
||||
func createChat(t *testing.T, parentT *testing.T, ctx context.Context, name string) string {
|
||||
t.Helper()
|
||||
return createChatAs(t, parentT, ctx, name, "bot")
|
||||
}
|
||||
|
||||
func createChatAs(t *testing.T, parentT *testing.T, ctx context.Context, name string, defaultAs string) string {
|
||||
t.Helper()
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+chat-create",
|
||||
"--name", name,
|
||||
"--type", "private",
|
||||
},
|
||||
DefaultAs: defaultAs,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
chatID := gjson.Get(result.Stdout, "data.chat_id").String()
|
||||
require.NotEmpty(t, chatID, "chat_id should not be empty")
|
||||
|
||||
parentT.Cleanup(func() {
|
||||
// No IM chat delete command is currently available in lark-cli,
|
||||
// so created chats are intentionally left in the test account.
|
||||
})
|
||||
|
||||
return chatID
|
||||
}
|
||||
|
||||
// sendMessage sends a text message to the specified chat and returns the messageID.
|
||||
func sendMessage(t *testing.T, ctx context.Context, chatID string, text string) string {
|
||||
t.Helper()
|
||||
return sendMessageAs(t, ctx, chatID, text, "bot")
|
||||
}
|
||||
|
||||
func sendMessageAs(t *testing.T, ctx context.Context, chatID string, text string, defaultAs string) string {
|
||||
t.Helper()
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+messages-send",
|
||||
"--chat-id", chatID,
|
||||
"--text", text,
|
||||
},
|
||||
DefaultAs: defaultAs,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
messageID := gjson.Get(result.Stdout, "data.message_id").String()
|
||||
require.NotEmpty(t, messageID, "message_id should not be empty")
|
||||
|
||||
return messageID
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// TestIM_DownloadResourcesDryRun verifies the --download-resources flag is wired
|
||||
// into +chat-messages-list without breaking the existing request structure: the
|
||||
// underlying GET /open-apis/im/v1/messages call is identical with or without the
|
||||
// flag (AC4), and the flag only adds a resource-download declaration to dry-run.
|
||||
func TestIM_DownloadResourcesDryRun(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")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
run := func(t *testing.T, extraArgs ...string) string {
|
||||
t.Helper()
|
||||
args := append([]string{
|
||||
"im", "+chat-messages-list",
|
||||
"--chat-id", "oc_dryrun",
|
||||
"--no-reactions",
|
||||
}, extraArgs...)
|
||||
args = append(args, "--dry-run")
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{Args: args, DefaultAs: "bot"})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
return result.Stdout
|
||||
}
|
||||
|
||||
t.Run("default off: no resources declaration, request unchanged", func(t *testing.T) {
|
||||
out := run(t)
|
||||
require.Equal(t, "GET", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out)
|
||||
require.Equal(t, "/open-apis/im/v1/messages", gjson.Get(out, "api.0.url").String(), "stdout:\n%s", out)
|
||||
require.Equal(t, "oc_dryrun", gjson.Get(out, "api.0.params.container_id").String(), "stdout:\n%s", out)
|
||||
require.NotContains(t, strings.ToLower(out), "lark-im-resources", "default must not declare resource download:\n%s", out)
|
||||
})
|
||||
|
||||
t.Run("with --download-resources: request unchanged, declares download", func(t *testing.T) {
|
||||
out := run(t, "--download-resources")
|
||||
require.Equal(t, "GET", gjson.Get(out, "api.0.method").String(), "stdout:\n%s", out)
|
||||
require.Equal(t, "/open-apis/im/v1/messages", gjson.Get(out, "api.0.url").String(), "stdout:\n%s", out)
|
||||
require.Equal(t, "oc_dryrun", gjson.Get(out, "api.0.params.container_id").String(), "stdout:\n%s", out)
|
||||
require.Contains(t, strings.ToLower(out), "lark-im-resources", "flag must declare resource download:\n%s", out)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestIM_MessagesSendAudioDryRunRejectsNonOpus(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
t.Setenv("LARKSUITE_CLI_APP_ID", "im_audio_dryrun_test")
|
||||
t.Setenv("LARKSUITE_CLI_APP_SECRET", "im_audio_dryrun_secret")
|
||||
t.Setenv("LARKSUITE_CLI_BRAND", "feishu")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
workDir := t.TempDir()
|
||||
audioPath := filepath.Join(workDir, "voice.mp3")
|
||||
require.NoError(t, os.WriteFile(audioPath, []byte("not real mp3; validation checks extension before upload"), 0o600))
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+messages-send",
|
||||
"--chat-id", "oc_123",
|
||||
"--audio", "./voice.mp3",
|
||||
"--dry-run",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
WorkDir: workDir,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 2)
|
||||
|
||||
if got := gjson.Get(result.Stderr, "error.type").String(); got != "validation" {
|
||||
t.Fatalf("error.type = %q, want validation\nstderr:\n%s", got, result.Stderr)
|
||||
}
|
||||
if got := gjson.Get(result.Stderr, "error.subtype").String(); got != "invalid_argument" {
|
||||
t.Fatalf("error.subtype = %q, want invalid_argument\nstderr:\n%s", got, result.Stderr)
|
||||
}
|
||||
if got := gjson.Get(result.Stderr, "error.param").String(); got != "--audio" {
|
||||
t.Fatalf("error.param = %q, want --audio\nstderr:\n%s", got, result.Stderr)
|
||||
}
|
||||
message := gjson.Get(result.Stderr, "error.message").String()
|
||||
if !strings.Contains(message, "--audio supports only Opus audio files") {
|
||||
t.Fatalf("error.message = %q, want Opus guidance\nstderr:\n%s", message, result.Stderr)
|
||||
}
|
||||
hint := gjson.Get(result.Stderr, "error.hint").String()
|
||||
if !strings.Contains(hint, "--file") || !strings.Contains(hint, "ffmpeg") {
|
||||
t.Fatalf("error.hint = %q, want --file and ffmpeg guidance\nstderr:\n%s", hint, result.Stderr)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestIM_MessageForwardWorkflowAsUser(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
messageText := "im-forward-msg-" + suffix
|
||||
replyText := "im-forward-reply-" + suffix
|
||||
|
||||
selfOpenID := getSelfOpenID(t, ctx)
|
||||
chatID, messageID := sendDirectMessageToUser(t, ctx, selfOpenID, messageText, "bot")
|
||||
|
||||
t.Run("forward message with api command as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "messages", "forward"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{
|
||||
"message_id": messageID,
|
||||
"receive_id_type": "chat_id",
|
||||
"uuid": "msg-forward-" + suffix,
|
||||
},
|
||||
Data: map[string]any{
|
||||
"receive_id": chatID,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
skipIfMissingIMForwardPermission(t, result)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
forwardedID := gjson.Get(result.Stdout, "data.message_id").String()
|
||||
require.NotEmpty(t, forwardedID, "stdout:\n%s", result.Stdout)
|
||||
require.NotEqual(t, messageID, forwardedID, "stdout:\n%s", result.Stdout)
|
||||
require.Equal(t, chatID, gjson.Get(result.Stdout, "data.chat_id").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
var threadID string
|
||||
t.Run("create thread fixture as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+messages-reply",
|
||||
"--message-id", messageID,
|
||||
"--text", replyText,
|
||||
"--reply-in-thread",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
threadID = findThreadIDForMessage(t, ctx, chatID, messageID, "bot")
|
||||
})
|
||||
|
||||
t.Run("forward thread with api command as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "threads", "forward"},
|
||||
DefaultAs: "user",
|
||||
Params: map[string]any{
|
||||
"thread_id": threadID,
|
||||
"receive_id_type": "chat_id",
|
||||
"uuid": "thread-forward-" + suffix,
|
||||
},
|
||||
Data: map[string]any{
|
||||
"receive_id": chatID,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
skipIfMissingIMForwardPermission(t, result)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
forwardedID := gjson.Get(result.Stdout, "data.message_id").String()
|
||||
require.NotEmpty(t, forwardedID, "stdout:\n%s", result.Stdout)
|
||||
require.Equal(t, chatID, gjson.Get(result.Stdout, "data.chat_id").String(), "stdout:\n%s", result.Stdout)
|
||||
require.Equal(t, "merge_forward", gjson.Get(result.Stdout, "data.msg_type").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
}
|
||||
|
||||
func findThreadIDForMessage(t *testing.T, ctx context.Context, chatID string, messageID string, defaultAs string) string {
|
||||
t.Helper()
|
||||
|
||||
listResult, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+chat-messages-list",
|
||||
"--chat-id", chatID,
|
||||
"--start", time.Now().UTC().Add(-10 * time.Minute).Format(time.RFC3339),
|
||||
"--end", time.Now().UTC().Add(10 * time.Minute).Format(time.RFC3339),
|
||||
},
|
||||
DefaultAs: defaultAs,
|
||||
}, clie2e.RetryOptions{
|
||||
ShouldRetry: func(result *clie2e.Result) bool {
|
||||
if result == nil || result.ExitCode != 0 {
|
||||
return true
|
||||
}
|
||||
for _, item := range gjson.Get(result.Stdout, "data.messages").Array() {
|
||||
if item.Get("message_id").String() == messageID && item.Get("thread_id").String() != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
listResult.AssertExitCode(t, 0)
|
||||
listResult.AssertStdoutStatus(t, true)
|
||||
|
||||
for _, item := range gjson.Get(listResult.Stdout, "data.messages").Array() {
|
||||
if item.Get("message_id").String() == messageID {
|
||||
threadID := item.Get("thread_id").String()
|
||||
require.NotEmpty(t, threadID, "expected thread_id for message %s in stdout:\n%s", messageID, listResult.Stdout)
|
||||
return threadID
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("expected message %s in stdout:\n%s", messageID, listResult.Stdout)
|
||||
return ""
|
||||
}
|
||||
|
||||
func getSelfOpenID(t *testing.T, ctx context.Context) string {
|
||||
t.Helper()
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"contact", "+get-user"},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
openID := gjson.Get(result.Stdout, "data.user.open_id").String()
|
||||
require.NotEmpty(t, openID, "stdout:\n%s", result.Stdout)
|
||||
return openID
|
||||
}
|
||||
|
||||
func sendDirectMessageToUser(t *testing.T, ctx context.Context, userOpenID string, text string, defaultAs string) (string, string) {
|
||||
t.Helper()
|
||||
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+messages-send",
|
||||
"--user-id", userOpenID,
|
||||
"--text", text,
|
||||
},
|
||||
DefaultAs: defaultAs,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
chatID := gjson.Get(result.Stdout, "data.chat_id").String()
|
||||
messageID := gjson.Get(result.Stdout, "data.message_id").String()
|
||||
require.NotEmpty(t, chatID, "stdout:\n%s", result.Stdout)
|
||||
require.NotEmpty(t, messageID, "stdout:\n%s", result.Stdout)
|
||||
return chatID, messageID
|
||||
}
|
||||
|
||||
func skipIfMissingIMForwardPermission(t *testing.T, result *clie2e.Result) {
|
||||
t.Helper()
|
||||
if result == nil || result.ExitCode == 0 {
|
||||
return
|
||||
}
|
||||
stderrLower := strings.ToLower(result.Stderr)
|
||||
if strings.Contains(stderrLower, "permission denied") ||
|
||||
strings.Contains(stderrLower, "230027") ||
|
||||
strings.Contains(stderrLower, "missing_scope") {
|
||||
t.Skipf("skip UAT forward workflow due to missing IM forward permissions: %s", result.Stderr)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestIM_MessageGetWorkflowAsUser(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
clie2e.SkipWithoutUserToken(t)
|
||||
|
||||
parentT := t
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
chatName := "im-lookup-" + suffix
|
||||
messageText := "im-msg-" + suffix
|
||||
|
||||
chatID := createChatAs(t, parentT, ctx, chatName, "user")
|
||||
messageID := sendMessageAs(t, ctx, chatID, messageText, "user")
|
||||
|
||||
t.Run("batch get message as user", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+messages-mget", "--message-ids", messageID},
|
||||
DefaultAs: "user",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
|
||||
messages := gjson.Get(result.Stdout, "data.messages").Array()
|
||||
require.Len(t, messages, 1, "stdout:\n%s", result.Stdout)
|
||||
require.Equal(t, messageID, messages[0].Get("message_id").String(), "stdout:\n%s", result.Stdout)
|
||||
require.True(t, strings.Contains(messages[0].Get("content").String(), messageText), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"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 TestIM_MessageReplyWorkflowAsBot(t *testing.T) {
|
||||
parentT := t
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
suffix := clie2e.GenerateSuffix()
|
||||
chatName := "lark-cli-e2e-im-reply-" + suffix
|
||||
originalMessage := "lark-cli-e2e-original-message-" + suffix
|
||||
replyText := "lark-cli-e2e-reply-text-" + suffix
|
||||
|
||||
chatID := createChat(t, parentT, ctx, chatName)
|
||||
messageID := sendMessage(t, ctx, chatID, originalMessage)
|
||||
|
||||
t.Run("reply to message in thread as bot", func(t *testing.T) {
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+messages-reply",
|
||||
"--message-id", messageID,
|
||||
"--text", replyText,
|
||||
"--reply-in-thread",
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
result.AssertStdoutStatus(t, true)
|
||||
assert.NotEmpty(t, gjson.Get(result.Stdout, "data.message_id").String(), "stdout:\n%s", result.Stdout)
|
||||
assert.Equal(t, chatID, gjson.Get(result.Stdout, "data.chat_id").String(), "stdout:\n%s", result.Stdout)
|
||||
})
|
||||
|
||||
t.Run("list thread replies as bot", func(t *testing.T) {
|
||||
listResult, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"im", "+chat-messages-list",
|
||||
"--chat-id", chatID,
|
||||
"--start", time.Now().UTC().Add(-10 * time.Minute).Format(time.RFC3339),
|
||||
"--end", time.Now().UTC().Add(10 * time.Minute).Format(time.RFC3339),
|
||||
},
|
||||
DefaultAs: "bot",
|
||||
}, clie2e.RetryOptions{
|
||||
ShouldRetry: func(result *clie2e.Result) bool {
|
||||
if result == nil || result.ExitCode != 0 {
|
||||
return true
|
||||
}
|
||||
for _, item := range gjson.Get(result.Stdout, "data.messages").Array() {
|
||||
if item.Get("message_id").String() == messageID && item.Get("thread_id").String() != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
listResult.AssertExitCode(t, 0)
|
||||
listResult.AssertStdoutStatus(t, true)
|
||||
|
||||
var threadID string
|
||||
for _, item := range gjson.Get(listResult.Stdout, "data.messages").Array() {
|
||||
if item.Get("message_id").String() == messageID {
|
||||
threadID = item.Get("thread_id").String()
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotEmpty(t, threadID, "expected thread_id for message %s in stdout:\n%s", messageID, listResult.Stdout)
|
||||
|
||||
threadResult, err := clie2e.RunCmdWithRetry(ctx, clie2e.Request{
|
||||
Args: []string{"im", "+threads-messages-list", "--thread", threadID},
|
||||
DefaultAs: "bot",
|
||||
}, clie2e.RetryOptions{
|
||||
ShouldRetry: func(result *clie2e.Result) bool {
|
||||
if result == nil || result.ExitCode != 0 {
|
||||
return true
|
||||
}
|
||||
for _, item := range gjson.Get(result.Stdout, "data.messages").Array() {
|
||||
if strings.Contains(item.Get("content").String(), replyText) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
threadResult.AssertExitCode(t, 0)
|
||||
threadResult.AssertStdoutStatus(t, true)
|
||||
|
||||
var found bool
|
||||
for _, item := range gjson.Get(threadResult.Stdout, "data.messages").Array() {
|
||||
if strings.Contains(item.Get("content").String(), replyText) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.True(t, found, "expected reply content in stdout:\n%s", threadResult.Stdout)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user