mirror of
https://github.com/larksuite/cli.git
synced 2026-07-09 10:29:48 +08:00
feat(im): add message_app_link to IM message outputs (#668)
- Assemble applinks via net/url to ensure proper encoding - Normalize message position values across more numeric types - Avoid leaking null message_app_link; assemble when missing - Update unit tests to assert URL semantics and cover edge cases Change-Id: Ic473cb563c8a648c4f6677c32b25b9f371a0f84e
This commit is contained in:
@@ -27,6 +27,7 @@ type Endpoints struct {
|
||||
Open string // e.g. "https://open.feishu.cn"
|
||||
Accounts string // e.g. "https://accounts.feishu.cn"
|
||||
MCP string // e.g. "https://mcp.feishu.cn"
|
||||
AppLink string // e.g. "https://applink.feishu.cn"
|
||||
}
|
||||
|
||||
// ResolveEndpoints resolves endpoint URLs based on brand.
|
||||
@@ -37,12 +38,14 @@ func ResolveEndpoints(brand LarkBrand) Endpoints {
|
||||
Open: "https://open.larksuite.com",
|
||||
Accounts: "https://accounts.larksuite.com",
|
||||
MCP: "https://mcp.larksuite.com",
|
||||
AppLink: "https://applink.larksuite.com",
|
||||
}
|
||||
default:
|
||||
return Endpoints{
|
||||
Open: "https://open.feishu.cn",
|
||||
Accounts: "https://accounts.feishu.cn",
|
||||
MCP: "https://mcp.feishu.cn",
|
||||
AppLink: "https://applink.feishu.cn",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ func TestResolveEndpoints_Feishu(t *testing.T) {
|
||||
if ep.MCP != "https://mcp.feishu.cn" {
|
||||
t.Errorf("MCP = %q, want feishu.cn", ep.MCP)
|
||||
}
|
||||
if ep.AppLink != "https://applink.feishu.cn" {
|
||||
t.Errorf("AppLink = %q, want feishu.cn", ep.AppLink)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveEndpoints_Lark(t *testing.T) {
|
||||
@@ -29,6 +32,9 @@ func TestResolveEndpoints_Lark(t *testing.T) {
|
||||
if ep.MCP != "https://mcp.larksuite.com" {
|
||||
t.Errorf("MCP = %q, want larksuite.com", ep.MCP)
|
||||
}
|
||||
if ep.AppLink != "https://applink.larksuite.com" {
|
||||
t.Errorf("AppLink = %q, want larksuite.com", ep.AppLink)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveEndpoints_EmptyDefaultsToFeishu(t *testing.T) {
|
||||
|
||||
@@ -4,9 +4,15 @@
|
||||
package convertlib
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
@@ -148,6 +154,27 @@ func FormatMessageItem(m map[string]interface{}, runtime *common.RuntimeContext,
|
||||
msg["reply_to"] = pid
|
||||
}
|
||||
|
||||
// Preserve API-provided fields (even if this formatter doesn't otherwise use them).
|
||||
if v, ok := m["chat_id"]; ok {
|
||||
msg["chat_id"] = v
|
||||
}
|
||||
if v, ok := m["message_position"]; ok {
|
||||
msg["message_position"] = v
|
||||
}
|
||||
if v, ok := m["thread_message_position"]; ok {
|
||||
msg["thread_message_position"] = v
|
||||
}
|
||||
|
||||
// Prefer API-provided message_app_link when it's a non-empty string; otherwise assemble deterministically.
|
||||
appLink, _ := m["message_app_link"].(string)
|
||||
appLink = strings.TrimSpace(appLink)
|
||||
if appLink == "" && runtime != nil && runtime.Config != nil {
|
||||
appLink = assembleMessageAppLink(m, runtime.Config.Brand)
|
||||
}
|
||||
if appLink != "" {
|
||||
msg["message_app_link"] = appLink
|
||||
}
|
||||
|
||||
if len(mentions) > 0 {
|
||||
simplified := make([]map[string]interface{}, 0, len(mentions))
|
||||
for _, raw := range mentions {
|
||||
@@ -166,6 +193,150 @@ func FormatMessageItem(m map[string]interface{}, runtime *common.RuntimeContext,
|
||||
return msg
|
||||
}
|
||||
|
||||
func assembleMessageAppLink(m map[string]interface{}, brand core.LarkBrand) string {
|
||||
domain := resolveAppLinkDomain(brand)
|
||||
if domain == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
chatID, _ := m["chat_id"].(string)
|
||||
threadID, _ := m["thread_id"].(string)
|
||||
msgPos, okMsgPos := normalizeMessagePosition(m["message_position"])
|
||||
threadPos, okThreadPos := normalizeMessagePosition(m["thread_message_position"])
|
||||
|
||||
// Thread app link requires both thread_id and chat_id.
|
||||
// Emit both underscore-less (openthreadid/openchatid) and snake_case (open_thread_id/open_chat_id)
|
||||
// query keys so PC and mobile clients can both resolve the link.
|
||||
if threadID != "" && chatID != "" && okThreadPos {
|
||||
u := &url.URL{Scheme: "https", Host: domain, Path: "/client/thread/open"}
|
||||
q := url.Values{}
|
||||
q.Set("openthreadid", threadID)
|
||||
q.Set("openchatid", chatID)
|
||||
q.Set("open_thread_id", threadID)
|
||||
q.Set("open_chat_id", chatID)
|
||||
q.Set("thread_position", threadPos)
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String()
|
||||
}
|
||||
if chatID != "" && okMsgPos {
|
||||
u := &url.URL{Scheme: "https", Host: domain, Path: "/client/chat/open"}
|
||||
q := url.Values{}
|
||||
q.Set("openChatId", chatID)
|
||||
q.Set("position", msgPos)
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeMessagePosition(v interface{}) (string, bool) {
|
||||
if v == nil {
|
||||
return "", false
|
||||
}
|
||||
switch vv := v.(type) {
|
||||
case float32:
|
||||
f := float64(vv)
|
||||
if math.IsNaN(f) || math.IsInf(f, 0) {
|
||||
return "", false
|
||||
}
|
||||
if math.Trunc(f) == f {
|
||||
return strconv.FormatInt(int64(f), 10), true
|
||||
}
|
||||
return strconv.FormatFloat(f, 'f', -1, 64), true
|
||||
case float64:
|
||||
if math.IsNaN(vv) || math.IsInf(vv, 0) {
|
||||
return "", false
|
||||
}
|
||||
if math.Trunc(vv) == vv {
|
||||
return strconv.FormatInt(int64(vv), 10), true
|
||||
}
|
||||
return strconv.FormatFloat(vv, 'f', -1, 64), true
|
||||
case int:
|
||||
return strconv.Itoa(vv), true
|
||||
case int8:
|
||||
return strconv.FormatInt(int64(vv), 10), true
|
||||
case int16:
|
||||
return strconv.FormatInt(int64(vv), 10), true
|
||||
case int32:
|
||||
return strconv.FormatInt(int64(vv), 10), true
|
||||
case int64:
|
||||
return strconv.FormatInt(vv, 10), true
|
||||
case uint:
|
||||
return strconv.FormatUint(uint64(vv), 10), true
|
||||
case uint8:
|
||||
return strconv.FormatUint(uint64(vv), 10), true
|
||||
case uint16:
|
||||
return strconv.FormatUint(uint64(vv), 10), true
|
||||
case uint32:
|
||||
return strconv.FormatUint(uint64(vv), 10), true
|
||||
case uint64:
|
||||
return strconv.FormatUint(vv, 10), true
|
||||
case uintptr:
|
||||
return strconv.FormatUint(uint64(vv), 10), true
|
||||
case json.Number:
|
||||
s := strings.TrimSpace(vv.String())
|
||||
if s == "" {
|
||||
return "", false
|
||||
}
|
||||
f, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil || math.IsNaN(f) || math.IsInf(f, 0) {
|
||||
return "", false
|
||||
}
|
||||
if math.Trunc(f) == f {
|
||||
return strconv.FormatInt(int64(f), 10), true
|
||||
}
|
||||
return strconv.FormatFloat(f, 'f', -1, 64), true
|
||||
case string:
|
||||
s := strings.TrimSpace(vv)
|
||||
if s == "" {
|
||||
return "", false
|
||||
}
|
||||
f, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil || math.IsNaN(f) || math.IsInf(f, 0) {
|
||||
return "", false
|
||||
}
|
||||
if math.Trunc(f) == f {
|
||||
return strconv.FormatInt(int64(f), 10), true
|
||||
}
|
||||
return strconv.FormatFloat(f, 'f', -1, 64), true
|
||||
default:
|
||||
// Fallback for typed numeric values (e.g. int32/uint64 via struct -> interface{}), pointers, etc.
|
||||
rv := reflect.ValueOf(v)
|
||||
for rv.Kind() == reflect.Ptr {
|
||||
if rv.IsNil() {
|
||||
return "", false
|
||||
}
|
||||
rv = rv.Elem()
|
||||
}
|
||||
switch rv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return strconv.FormatInt(rv.Int(), 10), true
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return strconv.FormatUint(rv.Uint(), 10), true
|
||||
case reflect.Float32, reflect.Float64:
|
||||
f := rv.Float()
|
||||
if math.IsNaN(f) || math.IsInf(f, 0) {
|
||||
return "", false
|
||||
}
|
||||
if math.Trunc(f) == f {
|
||||
return strconv.FormatInt(int64(f), 10), true
|
||||
}
|
||||
return strconv.FormatFloat(f, 'f', -1, 64), true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveAppLinkDomain(brand core.LarkBrand) string {
|
||||
appLink := core.ResolveEndpoints(brand).AppLink
|
||||
u, err := url.Parse(appLink)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return u.Host
|
||||
}
|
||||
|
||||
// extractMentionOpenId extracts open_id from mention id (string or {"open_id":...} object).
|
||||
func extractMentionOpenId(id interface{}) string {
|
||||
if s, ok := id.(string); ok {
|
||||
|
||||
@@ -4,11 +4,44 @@
|
||||
package convertlib
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func mustParseURL(t *testing.T, raw string) *url.URL {
|
||||
t.Helper()
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("url.Parse(%q) error: %v", raw, err)
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func assertURLHasQuery(t *testing.T, raw, host, path string, want map[string]string) {
|
||||
t.Helper()
|
||||
u := mustParseURL(t, raw)
|
||||
if u.Scheme != "https" {
|
||||
t.Fatalf("url scheme = %q, want https (%q)", u.Scheme, raw)
|
||||
}
|
||||
if u.Host != host {
|
||||
t.Fatalf("url host = %q, want %q (%q)", u.Host, host, raw)
|
||||
}
|
||||
if u.Path != path {
|
||||
t.Fatalf("url path = %q, want %q (%q)", u.Path, path, raw)
|
||||
}
|
||||
q := u.Query()
|
||||
for k, v := range want {
|
||||
if got := q.Get(k); got != v {
|
||||
t.Fatalf("query[%q] = %q, want %q (%q)", k, got, v, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertBodyContent(t *testing.T) {
|
||||
ctx := &ConvertContext{RawContent: `{"text":"hello"}`}
|
||||
|
||||
@@ -62,6 +95,300 @@ func TestFormatMessageItem(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveAppLinkDomain(t *testing.T) {
|
||||
if got := resolveAppLinkDomain(core.BrandFeishu); got != "applink.feishu.cn" {
|
||||
t.Fatalf("resolveAppLinkDomain(feishu) = %q", got)
|
||||
}
|
||||
if got := resolveAppLinkDomain(core.BrandLark); got != "applink.larksuite.com" {
|
||||
t.Fatalf("resolveAppLinkDomain(lark) = %q", got)
|
||||
}
|
||||
if got := resolveAppLinkDomain(core.LarkBrand("other")); got != "applink.feishu.cn" {
|
||||
t.Fatalf("resolveAppLinkDomain(other) = %q, want feishu", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatMessageItem_MessageAppLink_PassThrough(t *testing.T) {
|
||||
runtime := &common.RuntimeContext{Config: &core.CliConfig{Brand: core.BrandFeishu}}
|
||||
raw := map[string]interface{}{
|
||||
"msg_type": "text",
|
||||
"message_id": "om_123",
|
||||
"create_time": "1710500000",
|
||||
"chat_id": "oc_1",
|
||||
"message_position": 12,
|
||||
"message_app_link": "https://applink.feishu.cn/client/chat/open?openChatId=oc_1&position=12",
|
||||
"body": map[string]interface{}{"content": `{"text":"hi"}`},
|
||||
}
|
||||
|
||||
got := FormatMessageItem(raw, runtime)
|
||||
if got["message_app_link"] != raw["message_app_link"] {
|
||||
t.Fatalf("FormatMessageItem() message_app_link = %#v, want pass-through", got["message_app_link"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatMessageItem_MessageAppLink_AssembleChat(t *testing.T) {
|
||||
runtime := &common.RuntimeContext{Config: &core.CliConfig{Brand: core.BrandFeishu}}
|
||||
raw := map[string]interface{}{
|
||||
"msg_type": "text",
|
||||
"message_id": "om_123",
|
||||
"create_time": "1710500000",
|
||||
"chat_id": "oc_1",
|
||||
"message_position": float64(12),
|
||||
"body": map[string]interface{}{"content": `{"text":"hi"}`},
|
||||
}
|
||||
|
||||
got := FormatMessageItem(raw, runtime)
|
||||
assertURLHasQuery(t, got["message_app_link"].(string), "applink.feishu.cn", "/client/chat/open", map[string]string{
|
||||
"openChatId": "oc_1",
|
||||
"position": "12",
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatMessageItem_MessageAppLink_AssembleThread(t *testing.T) {
|
||||
runtime := &common.RuntimeContext{Config: &core.CliConfig{Brand: core.BrandLark}}
|
||||
raw := map[string]interface{}{
|
||||
"msg_type": "text",
|
||||
"message_id": "om_123",
|
||||
"create_time": "1710500000",
|
||||
"chat_id": "oc_1",
|
||||
"thread_id": "omt_1",
|
||||
"thread_message_position": "9",
|
||||
"message_position": 12,
|
||||
"body": map[string]interface{}{"content": `{"text":"hi"}`},
|
||||
}
|
||||
|
||||
got := FormatMessageItem(raw, runtime)
|
||||
assertURLHasQuery(t, got["message_app_link"].(string), "applink.larksuite.com", "/client/thread/open", map[string]string{
|
||||
"openthreadid": "omt_1",
|
||||
"openchatid": "oc_1",
|
||||
"open_thread_id": "omt_1",
|
||||
"open_chat_id": "oc_1",
|
||||
"thread_position": "9",
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatMessageItem_MessageAppLink_FallbackToChatWhenThreadPositionInvalid(t *testing.T) {
|
||||
runtime := &common.RuntimeContext{Config: &core.CliConfig{Brand: core.BrandFeishu}}
|
||||
raw := map[string]interface{}{
|
||||
"msg_type": "text",
|
||||
"message_id": "om_123",
|
||||
"create_time": "1710500000",
|
||||
"chat_id": "oc_1",
|
||||
"thread_id": "omt_1",
|
||||
"thread_message_position": "bad",
|
||||
"message_position": "12",
|
||||
"body": map[string]interface{}{"content": `{"text":"hi"}`},
|
||||
}
|
||||
|
||||
got := FormatMessageItem(raw, runtime)
|
||||
assertURLHasQuery(t, got["message_app_link"].(string), "applink.feishu.cn", "/client/chat/open", map[string]string{
|
||||
"openChatId": "oc_1",
|
||||
"position": "12",
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatMessageItem_MessageAppLink_BrandUnknownDefaultsToFeishu(t *testing.T) {
|
||||
runtime := &common.RuntimeContext{Config: &core.CliConfig{Brand: core.LarkBrand("other")}}
|
||||
raw := map[string]interface{}{
|
||||
"msg_type": "text",
|
||||
"message_id": "om_123",
|
||||
"create_time": "1710500000",
|
||||
"chat_id": "oc_1",
|
||||
"message_position": 12,
|
||||
"body": map[string]interface{}{"content": `{"text":"hi"}`},
|
||||
}
|
||||
|
||||
got := FormatMessageItem(raw, runtime)
|
||||
assertURLHasQuery(t, got["message_app_link"].(string), "applink.feishu.cn", "/client/chat/open", map[string]string{
|
||||
"openChatId": "oc_1",
|
||||
"position": "12",
|
||||
})
|
||||
}
|
||||
|
||||
func TestNormalizeMessagePosition_TypedIntsAndUints(t *testing.T) {
|
||||
if got, ok := normalizeMessagePosition(int32(-3)); !ok || got != "-3" {
|
||||
t.Fatalf("normalizeMessagePosition(int32(-3)) = (%q,%v)", got, ok)
|
||||
}
|
||||
if got, ok := normalizeMessagePosition(uint64(9)); !ok || got != "9" {
|
||||
t.Fatalf("normalizeMessagePosition(uint64(9)) = (%q,%v)", got, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeMessagePosition_CoversMoreNumericTypesAndInvalidInputs(t *testing.T) {
|
||||
// ints
|
||||
if got, ok := normalizeMessagePosition(int8(-1)); !ok || got != "-1" {
|
||||
t.Fatalf("normalizeMessagePosition(int8(-1)) = (%q,%v)", got, ok)
|
||||
}
|
||||
if got, ok := normalizeMessagePosition(int16(2)); !ok || got != "2" {
|
||||
t.Fatalf("normalizeMessagePosition(int16(2)) = (%q,%v)", got, ok)
|
||||
}
|
||||
|
||||
// uints
|
||||
if got, ok := normalizeMessagePosition(uint(3)); !ok || got != "3" {
|
||||
t.Fatalf("normalizeMessagePosition(uint(3)) = (%q,%v)", got, ok)
|
||||
}
|
||||
if got, ok := normalizeMessagePosition(uintptr(4)); !ok || got != "4" {
|
||||
t.Fatalf("normalizeMessagePosition(uintptr(4)) = (%q,%v)", got, ok)
|
||||
}
|
||||
|
||||
// float32
|
||||
if got, ok := normalizeMessagePosition(float32(1)); !ok || got != "1" {
|
||||
t.Fatalf("normalizeMessagePosition(float32(1)) = (%q,%v)", got, ok)
|
||||
}
|
||||
if got, ok := normalizeMessagePosition(float64(1.5)); !ok || got != "1.5" {
|
||||
t.Fatalf("normalizeMessagePosition(float64(1.5)) = (%q,%v)", got, ok)
|
||||
}
|
||||
if got, ok := normalizeMessagePosition(float64(-1.5)); !ok || got != "-1.5" {
|
||||
t.Fatalf("normalizeMessagePosition(float64(-1.5)) = (%q,%v)", got, ok)
|
||||
}
|
||||
if got, ok := normalizeMessagePosition(float32(math.NaN())); ok || got != "" {
|
||||
t.Fatalf("normalizeMessagePosition(float32(NaN)) = (%q,%v), want ('',false)", got, ok)
|
||||
}
|
||||
if got, ok := normalizeMessagePosition(float32(math.Inf(1))); ok || got != "" {
|
||||
t.Fatalf("normalizeMessagePosition(float32(+Inf)) = (%q,%v), want ('',false)", got, ok)
|
||||
}
|
||||
|
||||
// json.Number invalid
|
||||
if got, ok := normalizeMessagePosition(json.Number("1.5")); !ok || got != "1.5" {
|
||||
t.Fatalf("normalizeMessagePosition(json.Number(1.5)) = (%q,%v)", got, ok)
|
||||
}
|
||||
if got, ok := normalizeMessagePosition(json.Number("bad")); ok || got != "" {
|
||||
t.Fatalf("normalizeMessagePosition(json.Number(bad)) = (%q,%v), want ('',false)", got, ok)
|
||||
}
|
||||
if got, ok := normalizeMessagePosition(json.Number("1e309")); ok || got != "" {
|
||||
t.Fatalf("normalizeMessagePosition(json.Number(1e309)) = (%q,%v), want ('',false)", got, ok)
|
||||
}
|
||||
|
||||
// string invalid
|
||||
if got, ok := normalizeMessagePosition(" 1.5 "); !ok || got != "1.5" {
|
||||
t.Fatalf("normalizeMessagePosition(\" 1.5 \") = (%q,%v)", got, ok)
|
||||
}
|
||||
if got, ok := normalizeMessagePosition(" "); ok || got != "" {
|
||||
t.Fatalf("normalizeMessagePosition(blank) = (%q,%v), want ('',false)", got, ok)
|
||||
}
|
||||
if got, ok := normalizeMessagePosition("not-a-number"); ok || got != "" {
|
||||
t.Fatalf("normalizeMessagePosition(not-a-number) = (%q,%v), want ('',false)", got, ok)
|
||||
}
|
||||
|
||||
// reflect fallback: pointers
|
||||
i := int32(7)
|
||||
if got, ok := normalizeMessagePosition(&i); !ok || got != "7" {
|
||||
t.Fatalf("normalizeMessagePosition(*int32(7)) = (%q,%v)", got, ok)
|
||||
}
|
||||
u := uint64(8)
|
||||
if got, ok := normalizeMessagePosition(&u); !ok || got != "8" {
|
||||
t.Fatalf("normalizeMessagePosition(*uint64(8)) = (%q,%v)", got, ok)
|
||||
}
|
||||
f := float64(2.25)
|
||||
if got, ok := normalizeMessagePosition(&f); !ok || got != "2.25" {
|
||||
t.Fatalf("normalizeMessagePosition(*float64(2.25)) = (%q,%v)", got, ok)
|
||||
}
|
||||
fNaN := float64(math.NaN())
|
||||
if got, ok := normalizeMessagePosition(&fNaN); ok || got != "" {
|
||||
t.Fatalf("normalizeMessagePosition(*float64(NaN)) = (%q,%v), want ('',false)", got, ok)
|
||||
}
|
||||
var nilPtr *int
|
||||
if got, ok := normalizeMessagePosition(nilPtr); ok || got != "" {
|
||||
t.Fatalf("normalizeMessagePosition(nil ptr) = (%q,%v), want ('',false)", got, ok)
|
||||
}
|
||||
if got, ok := normalizeMessagePosition(struct{}{}); ok || got != "" {
|
||||
t.Fatalf("normalizeMessagePosition(struct{}) = (%q,%v), want ('',false)", got, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssembleMessageAppLink_EncodesQueryValues(t *testing.T) {
|
||||
// chat link encoding
|
||||
chat := map[string]interface{}{
|
||||
"chat_id": "oc_1+2/3",
|
||||
"message_position": 12,
|
||||
}
|
||||
gotChat := assembleMessageAppLink(chat, core.BrandFeishu)
|
||||
assertURLHasQuery(t, gotChat, "applink.feishu.cn", "/client/chat/open", map[string]string{
|
||||
"openChatId": "oc_1+2/3",
|
||||
"position": "12",
|
||||
})
|
||||
|
||||
// thread link encoding
|
||||
thread := map[string]interface{}{
|
||||
"chat_id": "oc_1+2/3",
|
||||
"thread_id": "omt_1+2/3",
|
||||
"thread_message_position": -1,
|
||||
}
|
||||
gotThread := assembleMessageAppLink(thread, core.BrandFeishu)
|
||||
assertURLHasQuery(t, gotThread, "applink.feishu.cn", "/client/thread/open", map[string]string{
|
||||
"open_thread_id": "omt_1+2/3",
|
||||
"open_chat_id": "oc_1+2/3",
|
||||
"openthreadid": "omt_1+2/3",
|
||||
"openchatid": "oc_1+2/3",
|
||||
"thread_position": "-1",
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatMessageItem_MessageAppLink_NonStringDoesNotLeakNull(t *testing.T) {
|
||||
runtime := &common.RuntimeContext{Config: &core.CliConfig{Brand: core.BrandFeishu}}
|
||||
raw := map[string]interface{}{
|
||||
"msg_type": "text",
|
||||
"message_id": "om_123",
|
||||
"create_time": "1710500000",
|
||||
"chat_id": "oc_1",
|
||||
"message_position": 12,
|
||||
"message_app_link": nil,
|
||||
"body": map[string]interface{}{"content": `{"text":"hi"}`},
|
||||
}
|
||||
|
||||
got := FormatMessageItem(raw, runtime)
|
||||
// Should assemble instead of emitting JSON null.
|
||||
assertURLHasQuery(t, got["message_app_link"].(string), "applink.feishu.cn", "/client/chat/open", map[string]string{
|
||||
"openChatId": "oc_1",
|
||||
"position": "12",
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatMessageItem_MessageAppLink_RuntimeNilNoAssemble(t *testing.T) {
|
||||
raw := map[string]interface{}{
|
||||
"msg_type": "text",
|
||||
"message_id": "om_123",
|
||||
"create_time": "1710500000",
|
||||
"chat_id": "oc_1",
|
||||
"message_position": 12,
|
||||
"body": map[string]interface{}{"content": `{"text":"hi"}`},
|
||||
}
|
||||
|
||||
got := FormatMessageItem(raw, nil)
|
||||
if _, ok := got["message_app_link"]; ok {
|
||||
t.Fatalf("FormatMessageItem() should not assemble without runtime, got %#v", got["message_app_link"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatMessageItem_MessageAppLink_MissingFieldsNoPanic(t *testing.T) {
|
||||
runtime := &common.RuntimeContext{Config: &core.CliConfig{Brand: core.BrandFeishu}}
|
||||
raw := map[string]interface{}{
|
||||
"msg_type": "text",
|
||||
"message_id": "om_123",
|
||||
"create_time": "1710500000",
|
||||
"body": map[string]interface{}{"content": `{"text":"hi"}`},
|
||||
}
|
||||
|
||||
got := FormatMessageItem(raw, runtime)
|
||||
if _, ok := got["message_app_link"]; ok {
|
||||
t.Fatalf("FormatMessageItem() message_app_link should be absent when fields are missing, got %#v", got["message_app_link"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeMessagePosition_AllowsZeroAndNegative(t *testing.T) {
|
||||
if got, ok := normalizeMessagePosition("0"); !ok || got != "0" {
|
||||
t.Fatalf("normalizeMessagePosition(\"0\") = (%q,%v)", got, ok)
|
||||
}
|
||||
if got, ok := normalizeMessagePosition("-3"); !ok || got != "-3" {
|
||||
t.Fatalf("normalizeMessagePosition(\"-3\") = (%q,%v)", got, ok)
|
||||
}
|
||||
if got, ok := normalizeMessagePosition(float64(0)); !ok || got != "0" {
|
||||
t.Fatalf("normalizeMessagePosition(0.0) = (%q,%v)", got, ok)
|
||||
}
|
||||
if got, ok := normalizeMessagePosition(float64(-1)); !ok || got != "-1" {
|
||||
t.Fatalf("normalizeMessagePosition(-1.0) = (%q,%v)", got, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractMentionOpenIdAndTruncateContent(t *testing.T) {
|
||||
if got := extractMentionOpenId("ou_1"); got != "ou_1" {
|
||||
t.Fatalf("extractMentionOpenId(string) = %q", got)
|
||||
|
||||
Reference in New Issue
Block a user