Files
larksuite__cli/events/im/message_receive_test.go

275 lines
7.3 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package im
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/larksuite/cli/internal/event"
"github.com/larksuite/cli/internal/event/processing"
)
func TestIMKeys_ProcessedReceiveRegistered(t *testing.T) {
def, ok := lookupCompiledDef(t, "im.message.receive_v1")
if !ok {
t.Fatal("im.message.receive_v1 should be registered via Keys()")
}
if def.Schema.Custom == nil {
t.Error("Processed key must set Schema.Custom")
}
if def.Schema.Native != nil {
t.Error("Processed key must not set Schema.Native")
}
if def.Process == nil {
t.Error("Process must not be nil for Processed key")
}
if len(def.Scopes) == 0 {
t.Error("Scopes must not be empty — preflightScopes would bypass validation")
}
}
func TestIMKeys_NativeEventsRegistered(t *testing.T) {
want := []string{
"im.message.message_read_v1",
"im.message.reaction.created_v1",
"im.message.reaction.deleted_v1",
"im.chat.member.bot.added_v1",
"im.chat.member.bot.deleted_v1",
"im.chat.member.user.added_v1",
"im.chat.member.user.withdrawn_v1",
"im.chat.member.user.deleted_v1",
"im.chat.updated_v1",
"im.chat.disbanded_v1",
}
for _, k := range want {
def, ok := lookupCompiledDef(t, k)
if !ok {
t.Errorf("%s should be registered via Keys()", k)
continue
}
if def.Schema.Native == nil {
t.Errorf("%s: Schema.Native must be set for native key", k)
}
if def.Schema.Custom != nil {
t.Errorf("%s: Native key must not set Schema.Custom", k)
}
if def.Process != nil {
t.Errorf("%s: Native key must not set Process", k)
}
if def.Schema.Native != nil && def.Schema.Native.Type == nil {
t.Errorf("%s: Schema.Native.Type must reference an SDK type", k)
}
}
}
func TestProcessImMessageReceive_Text(t *testing.T) {
payload := `{
"schema": "2.0",
"header": {
"event_id": "ev_test_text",
"event_type": "im.message.receive_v1",
"create_time": "1776409469273",
"app_id": "cli_test"
},
"event": {
"sender": {
"sender_type": "user",
"sender_id": {"open_id": "ou_sender"}
},
"message": {
"message_id": "om_text_001",
"root_id": "om_root_001",
"parent_id": "om_parent_001",
"thread_id": "omt_thread_001",
"chat_id": "oc_chat",
"chat_type": "p2p",
"message_type": "text",
"create_time": "1776409468987",
"update_time": "1776409469999",
"content": "{\"text\":\"hello @_user_1\"}",
"mentions": [
{
"key": "@_user_1",
"id": {"open_id": "ou_mentioned"},
"name": "Alice"
}
]
}
}
}`
out := runReceive(t, payload)
outMap := runReceiveMap(t, payload)
if out.Type != "im.message.receive_v1" {
t.Errorf("Type = %q", out.Type)
}
if out.MessageID != "om_text_001" || out.ID != "om_text_001" {
t.Errorf("MessageID/ID = %q/%q", out.MessageID, out.ID)
}
if out.ChatType != "p2p" || out.ChatID != "oc_chat" {
t.Errorf("chat_id/chat_type = %q/%q", out.ChatID, out.ChatType)
}
if out.SenderID != "ou_sender" {
t.Errorf("SenderID = %q", out.SenderID)
}
if out.Content != "hello @Alice" {
t.Errorf("Content = %q, want \"hello @Alice\"", out.Content)
}
if out.Timestamp != "1776409469273" {
t.Errorf("Timestamp = %q", out.Timestamp)
}
for field, want := range map[string]string{
"sender_type": "user",
"root_id": "om_root_001",
"thread_id": "omt_thread_001",
"reply_to": "om_parent_001",
"update_time": "1776409469999",
} {
if got, _ := outMap[field].(string); got != want {
t.Errorf("%s = %q, want %q", field, got, want)
}
}
mentions, _ := outMap["mentions"].([]interface{})
if len(mentions) != 1 {
t.Fatalf("mentions length = %d, want 1: %#v", len(mentions), outMap["mentions"])
}
mention, _ := mentions[0].(map[string]interface{})
for field, want := range map[string]string{
"key": "@_user_1",
"id": "ou_mentioned",
"name": "Alice",
} {
if got, _ := mention[field].(string); got != want {
t.Errorf("mentions[0].%s = %q, want %q", field, got, want)
}
}
}
func TestProcessImMessageReceive_OmitsUnchangedUpdateTime(t *testing.T) {
payload := `{
"schema": "2.0",
"header": {
"event_id": "ev_test_text",
"event_type": "im.message.receive_v1",
"create_time": "1776409469273",
"app_id": "cli_test"
},
"event": {
"sender": {
"sender_type": "user",
"sender_id": {"open_id": "ou_sender"}
},
"message": {
"message_id": "om_text_001",
"chat_id": "oc_chat",
"chat_type": "p2p",
"message_type": "text",
"create_time": "1776409468987",
"update_time": "1776409468987",
"content": "{\"text\":\"hello there\"}"
}
}
}`
outMap := runReceiveMap(t, payload)
if _, ok := outMap["update_time"]; ok {
t.Errorf("update_time should be omitted when it equals create_time: %#v", outMap)
}
}
func TestProcessImMessageReceive_Interactive(t *testing.T) {
payload := `{
"schema": "2.0",
"header": {
"event_id": "ev_test_card",
"event_type": "im.message.receive_v1",
"create_time": "1776409469274",
"app_id": "cli_test"
},
"event": {
"sender": {
"sender_id": {"open_id": "ou_sender"}
},
"message": {
"message_id": "om_card_001",
"chat_id": "oc_chat",
"chat_type": "group",
"message_type": "interactive",
"create_time": "1776409468987",
"content": "{\"header\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"A card\"}}}"
}
}
}`
out := runReceive(t, payload)
if out.Type != "im.message.receive_v1" {
t.Errorf("Type = %q", out.Type)
}
if out.MessageType != "interactive" {
t.Errorf("MessageType = %q", out.MessageType)
}
if out.ChatType != "group" {
t.Errorf("ChatType = %q", out.ChatType)
}
}
func TestProcessImMessageReceive_MalformedPayload(t *testing.T) {
raw := &event.RawEvent{
EventID: "ev_bad",
EventType: "im.message.receive_v1",
Payload: json.RawMessage(`not json`),
Timestamp: time.Now(),
}
got, err := processImMessageReceive(context.Background(), nil, raw, nil)
if !processing.IsDropMalformed(err) {
t.Fatalf("malformed payload must be dropped with a malformed marker, got err=%v", err)
}
if got != nil {
t.Errorf("malformed payload must be dropped without output, got %q", string(got))
}
}
func runReceive(t *testing.T, payload string) ImMessageReceiveOutput {
t.Helper()
raw := &event.RawEvent{
EventID: "ev_test",
EventType: "im.message.receive_v1",
Payload: json.RawMessage(payload),
Timestamp: time.Now(),
}
fillCanonicalFromHeader(t, raw)
got, err := processImMessageReceive(context.Background(), nil, raw, nil)
if err != nil {
t.Fatalf("Process error: %v", err)
}
var out ImMessageReceiveOutput
if err := json.Unmarshal(got, &out); err != nil {
t.Fatalf("Process output is not valid ImMessageReceiveOutput JSON: %v\nraw=%s", err, string(got))
}
return out
}
func runReceiveMap(t *testing.T, payload string) map[string]interface{} {
t.Helper()
raw := &event.RawEvent{
EventID: "ev_test",
EventType: "im.message.receive_v1",
Payload: json.RawMessage(payload),
Timestamp: time.Now(),
}
fillCanonicalFromHeader(t, raw)
got, err := processImMessageReceive(context.Background(), nil, raw, nil)
if err != nil {
t.Fatalf("Process error: %v", err)
}
var out map[string]interface{}
if err := json.Unmarshal(got, &out); err != nil {
t.Fatalf("Process output is not valid JSON: %v\nraw=%s", err, string(got))
}
return out
}