Files
creator-hub/internal/creator/private_messages_test.go
T
rogee d94f83cde9
douyin-release-gate / verify (push) Failing after 18m44s
fix(messages): stabilize conversation ordering by real message time
2026-10-07 13:13:06 +08:00

349 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package creator
import (
"context"
"errors"
"fmt"
"reflect"
"sort"
"strings"
"testing"
"time"
)
func TestPrivateMessageInputValidation(t *testing.T) {
valid := PrivateMessageSendInput{RequestID: "one", AccountID: "account", PeerUID: "123", Text: "你好"}
if err := valid.Validate(); err != nil {
t.Fatal(err)
}
for _, edit := range []func(*PrivateMessageSendInput){
func(i *PrivateMessageSendInput) { i.RequestID = "" }, func(i *PrivateMessageSendInput) { i.AccountID = "" },
func(i *PrivateMessageSendInput) { i.PeerUID = "abc" }, func(i *PrivateMessageSendInput) { i.PeerUID = "0" },
func(i *PrivateMessageSendInput) { i.Text = " " }, func(i *PrivateMessageSendInput) { i.Text = strings.Repeat("你", 1001) },
} {
i := valid
edit(&i)
if !errors.Is(i.Validate(), ErrInvalid) {
t.Fatalf("accepted %+v", i)
}
}
valid.Text = strings.Repeat("你", 1000)
if err := valid.Validate(); err != nil {
t.Fatal(err)
}
}
func TestDecodePrivateInbox(t *testing.T) {
raw := []byte(`{"status":"succeeded","account_uid":"123","peers":[{"peer_uid":"456","peer_name":"真实昵称","error":""}],"messages":[{"peer_uid":"456","peer_name":"UID占位","server_id":"1001","sender_uid":"456","message_type":"7","content":"{\"aweType\":700,\"type\":0,\"text\":\"你好\"}","created_at":"1700000000123"},{"peer_uid":"456","server_id":"1002","sender_uid":"123","message_type":"7","content":"{\"aweType\":701,\"type\":1,\"resource_url\":\"https://example.invalid\"}","created_at":null}]}`)
inbox, err := DecodePrivateInbox(raw, "123")
if err != nil {
t.Fatal(err)
}
items := inbox.Messages
if len(inbox.Peers) != 1 || items[0].PeerName != "真实昵称" || items[1].PeerName != "真实昵称" {
t.Fatalf("nickname source=%+v", inbox)
}
if len(items) != 2 || items[0].Direction != "inbound" || items[0].Text != "你好" || items[0].MessageAt == nil || items[0].MessageAt.UnixMilli() != 1700000000123 || items[1].Direction != "outbound" || items[1].MessageType != "image" || items[1].MessageAt != nil {
t.Fatalf("items=%+v", items)
}
for _, raw := range []string{`{}`, `{"status":"failed","code":"IM_SDK_NOT_READY"}`, `{"status":"succeeded","account_uid":"999","messages":[]}`, `{"status":"succeeded","account_uid":"123"}`, `{"status":"succeeded","account_uid":"123","peers":[{"peer_uid":"456","peer_name":"昵称"}],"messages":[{"peer_uid":"456","server_id":"0","sender_uid":"456","message_type":"7","content":"{}"}]}`, `{"status":"succeeded","account_uid":"123","peers":[{"peer_uid":"456","peer_name":"昵称"}],"messages":[{"peer_uid":"456","server_id":"1","sender_uid":"777","message_type":"7","content":"{}"}]}`, `{"status":"succeeded","account_uid":"123","peers":[],"messages":[{"peer_uid":"456","server_id":"1","sender_uid":"456","message_type":"7","content":"{}"}]}`} {
if _, err := DecodePrivateInbox([]byte(raw), "123"); err == nil {
t.Fatalf("accepted %s", raw)
}
}
}
func TestDecodePrivateInboxMissingFieldsIdentifiesGatewayMismatch(t *testing.T) {
for _, test := range []struct{ raw, field string }{
{`{"status":"succeeded","account_uid":"123","messages":[]}`, "peers"},
{`{"status":"succeeded","account_uid":"123","peers":[]}`, "messages"},
} {
_, err := DecodePrivateInbox([]byte(test.raw), "123")
if err == nil || !strings.Contains(err.Error(), "missing "+test.field) || !strings.Contains(err.Error(), "browser gateway") {
t.Fatalf("missing-field diagnostic: %v", err)
}
}
}
func TestDecodePrivateInboxPeersWithoutNewMessages(t *testing.T) {
inbox, err := DecodePrivateInbox([]byte(`{"status":"succeeded","account_uid":"123","messages":[],"peers":[{"peer_uid":"456","peer_name":" 更新后的昵称 ","error":""}]}`), "123")
if err != nil || len(inbox.Messages) != 0 || inbox.Peers[0].PeerName != "更新后的昵称" {
t.Fatalf("inbox=%+v err=%v", inbox, err)
}
for _, peers := range []string{`null`, `[{"peer_uid":"123","peer_name":"自己"}]`, `[{"peer_uid":"x","peer_name":"错误"}]`, `[{"peer_uid":"456","peer_name":""}]`, `[{"peer_uid":"456","peer_name":"假名","error":"失败"}]`, `[{"peer_uid":"456","peer_name":"昵称"},{"peer_uid":"456","peer_name":"重复"}]`} {
raw := fmt.Sprintf(`{"status":"succeeded","account_uid":"123","messages":[],"peers":%s}`, peers)
if _, err := DecodePrivateInbox([]byte(raw), "123"); err == nil {
t.Fatalf("accepted %s", raw)
}
}
inbox, err = DecodePrivateInbox([]byte(`{"status":"succeeded","account_uid":"123","messages":[],"peers":[{"peer_uid":"456","peer_name":"","error":"昵称读取失败"}]}`), "123")
if err != nil || inbox.Peers[0].Error != "昵称读取失败" {
t.Fatalf("lookup error=%+v %v", inbox, err)
}
}
func TestPrivateInboxNicknameRefreshIsAccountScoped(t *testing.T) {
store, accounts, ctx := openCreatorIntegrationStore(t)
var ids, generations []string
for _, key := range []string{"nickname-owner", "nickname-other"} {
id := createIntegrationAccount(t, ctx, accounts, key)
profile, err := store.GetAccountProfile(ctx, id)
if err != nil {
t.Fatal(err)
}
if _, err := store.RecordVerifiedLoginResult(ctx, id, profile.PlatformAccountKey); err != nil {
t.Fatal(err)
}
listener, err := store.SetListenerEnabled(ctx, id, true)
if err != nil {
t.Fatal(err)
}
inbox := &PrivateInbox{Messages: []PrivateInboxMessage{{PeerUID: "456", PeerName: key, ServerID: "101", Direction: "inbound", MessageType: "text", Text: "已有消息"}}}
if err := store.SavePrivateInbox(ctx, id, listener.Generation, inbox); err != nil {
t.Fatal(err)
}
ids = append(ids, id)
generations = append(generations, listener.Generation)
}
inbox := &PrivateInbox{Peers: []PrivateInboxPeer{{PeerUID: "456", PeerName: "更新后的真实昵称"}}}
if err := store.SavePrivateInbox(ctx, ids[0], generations[0], inbox); err != nil {
t.Fatal(err)
}
for i, expected := range []string{"更新后的真实昵称", "nickname-other"} {
page, err := store.ListPrivateMessages(ctx, ids[i], "456", 1, 25)
if err != nil || page.Total != 1 || page.Data[0].PeerName != expected || page.Data[0].Text != "已有消息" {
t.Fatalf("account=%s messages=%+v error=%v", ids[i], page, err)
}
}
if _, err := store.SetListenerEnabled(ctx, ids[0], false); err != nil {
t.Fatal(err)
}
if err := store.SavePrivateInbox(ctx, ids[0], generations[0], inbox); !errors.Is(err, ErrConflict) {
t.Fatalf("stale profile refresh accepted: %v", err)
}
}
func TestPrivateConversationOrderUsesRealInboundAndOutboundTimes(t *testing.T) {
store, accounts, ctx := openCreatorIntegrationStore(t)
var ids, generations []string
for _, name := range []string{"conversation-order-a", "conversation-order-b"} {
id := createIntegrationAccount(t, ctx, accounts, name)
profile, err := store.GetAccountProfile(ctx, id)
if err != nil {
t.Fatal(err)
}
if _, err := store.RecordVerifiedLoginResult(ctx, id, profile.PlatformAccountKey); err != nil {
t.Fatal(err)
}
listener, err := store.SetListenerEnabled(ctx, id, true)
if err != nil {
t.Fatal(err)
}
ids = append(ids, id)
generations = append(generations, listener.Generation)
}
older := time.Date(2026, 1, 1, 10, 0, 0, 0, time.UTC)
newer := older.Add(time.Hour)
batch := &PrivateInbox{Messages: []PrivateInboxMessage{
{PeerUID: "111", PeerName: "收到较早消息", ServerID: "101", Direction: "inbound", MessageType: "text", Text: "真实收到的消息", MessageAt: &older},
{PeerUID: "222", PeerName: "发出最新消息", ServerID: "102", Direction: "outbound", MessageType: "text", Text: "真实发出的消息", MessageAt: &newer},
{PeerUID: "333", PeerName: "收到最新消息", ServerID: "103", Direction: "inbound", MessageType: "text", Text: "真实收到的最新消息", MessageAt: &newer},
}}
if err := store.SavePrivateInbox(ctx, ids[0], generations[0], batch); err != nil {
t.Fatal(err)
}
batch.Messages = []PrivateInboxMessage{{PeerUID: "222", ServerID: "104", Direction: "inbound", MessageType: "text", MessageAt: &newer}}
if err := store.SavePrivateInbox(ctx, ids[1], generations[1], batch); err != nil {
t.Fatal(err)
}
for i, peer := range []string{"111", "444"} {
if _, err := store.BeginPrivateMessage(ctx, PrivateMessageSendInput{RequestID: fmt.Sprintf("ordering-pending-%d", i), AccountID: ids[0], PeerUID: peer, Text: "尚无真实发送时间"}); err != nil {
t.Fatal(err)
}
}
first := []string{ids[0] + ":222", ids[0] + ":333", ids[1] + ":222"}
sort.Strings(first)
expected := append(first, ids[0]+":111", ids[0]+":444")
var actual []string
for page := 1; page <= 3; page++ {
result, err := store.ListPrivateConversations(ctx, "", page, 2)
if err != nil || result.Total != 5 {
t.Fatalf("page=%d result=%+v error=%v", page, result, err)
}
for _, c := range result.Data {
actual = append(actual, c.AccountID+":"+c.PeerUID)
if c.PeerUID == "111" && (c.LastMessageAt == nil || !c.LastMessageAt.Equal(older) || c.LastText != "真实收到的消息") {
t.Fatalf("pending attempt changed real conversation time: %+v", c)
}
if c.PeerUID == "444" && c.LastMessageAt != nil {
t.Fatalf("storage time presented as real time: %+v", c)
}
}
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("order=%v want=%v", actual, expected)
}
}
func TestPrivateListenerStateDoesNotDependOnNoticeProgress(t *testing.T) {
store, accounts, ctx := openCreatorIntegrationStore(t)
id := createIntegrationAccount(t, ctx, accounts, "independent-listener")
profile, err := store.GetAccountProfile(ctx, id)
if err != nil {
t.Fatal(err)
}
if _, err := store.RecordVerifiedLoginResult(ctx, id, profile.PlatformAccountKey); err != nil {
t.Fatal(err)
}
state, err := store.SetListenerEnabled(ctx, id, true)
if err != nil {
t.Fatal(err)
}
if _, err := store.db.ExecContext(ctx, `ALTER TABLE creator_account_listener DROP COLUMN IF EXISTS notice_checkpoints`); err != nil {
t.Fatal(err)
}
states, err := store.ListPrivateListeners(ctx)
if err != nil || len(states) != 1 || states[0].AccountID != id || !states[0].Enabled || states[0].Generation != state.Generation {
t.Fatalf("states=%+v %v", states, err)
}
}
func TestPreviouslyMisclassifiedNetworkFailureIsUnconfirmed(t *testing.T) {
store, accounts, ctx := openCreatorIntegrationStore(t)
id := createIntegrationAccount(t, ctx, accounts, "network-reclassification")
profile, err := store.GetAccountProfile(ctx, id)
if err != nil {
t.Fatal(err)
}
if _, err := store.RecordVerifiedLoginResult(ctx, id, profile.PlatformAccountKey); err != nil {
t.Fatal(err)
}
if _, err := store.SetListenerEnabled(ctx, id, true); err != nil {
t.Fatal(err)
}
reservation, err := store.BeginPrivateMessage(ctx, PrivateMessageSendInput{RequestID: "old-network", AccountID: id, PeerUID: "456", Text: "你好"})
if err != nil {
t.Fatal(err)
}
if _, err := store.FinishPrivateMessage(ctx, reservation.Message.ID, "failed", "", "发送失败:MESSAGE_REJECTED;status_code=1008", nil); err != nil {
t.Fatal(err)
}
if err := store.RecoverInterruptedPrivateMessages(ctx); err != nil {
t.Fatal(err)
}
page, err := store.ListPrivateMessages(ctx, id, "456", 1, 25)
if err != nil || page.Data[0].State != "unknown" {
t.Fatalf("network=%+v %v", page, err)
}
}
func TestPrivateMessagesLifecycle(t *testing.T) {
store, accounts, ctx := openCreatorIntegrationStore(t)
uid := fmt.Sprint(time.Now().UnixNano())
id := createIntegrationAccount(t, ctx, accounts, uid)
input := PrivateMessageSendInput{RequestID: "request-1", AccountID: id, PeerUID: "456", Text: "你好"}
if _, err := store.BeginPrivateMessage(ctx, input); !errors.Is(err, ErrConflict) {
t.Fatalf("unlistened send=%v", err)
}
profile, err := store.GetAccountProfile(ctx, id)
if err != nil {
t.Fatal(err)
}
if _, err := store.RecordVerifiedLoginResult(ctx, id, profile.PlatformAccountKey); err != nil {
t.Fatal(err)
}
listener, err := store.SetListenerEnabled(ctx, id, true)
if err != nil {
t.Fatal(err)
}
first, err := store.BeginPrivateMessage(ctx, input)
if err != nil || !first.New || first.Message.State != "sending" {
t.Fatalf("begin=%+v %v", first, err)
}
duplicate, err := store.BeginPrivateMessage(ctx, input)
if err != nil || duplicate.New || duplicate.Message.ID != first.Message.ID {
t.Fatalf("duplicate=%+v %v", duplicate, err)
}
changed := input
changed.Text = "不同内容"
if _, err := store.BeginPrivateMessage(ctx, changed); !errors.Is(err, ErrConflict) {
t.Fatalf("changed request=%v", err)
}
self := input
self.RequestID = "self"
self.PeerUID = profile.PlatformAccountKey
if _, err := store.BeginPrivateMessage(ctx, self); !errors.Is(err, ErrInvalid) {
t.Fatalf("self=%v", err)
}
at := time.Now().UTC()
batch := &PrivateInbox{Messages: []PrivateInboxMessage{{PeerUID: "456", PeerName: "好友", ServerID: "100", Direction: "outbound", MessageType: "text", Text: "你好", MessageAt: &at}, {PeerUID: "456", PeerName: "好友", ServerID: "101", Direction: "inbound", MessageType: "text", Text: "你好呀", MessageAt: &at}}, Peers: []PrivateInboxPeer{{PeerUID: "456", PeerName: "好友"}}}
if err := store.SavePrivateInbox(ctx, id, listener.Generation, batch); err != nil {
t.Fatal(err)
}
if err := store.SavePrivateInbox(ctx, id, listener.Generation, batch); err != nil {
t.Fatal(err)
}
finished, err := store.FinishPrivateMessage(ctx, first.Message.ID, "succeeded", "100", "", &at)
if err != nil || finished.State != "succeeded" {
t.Fatalf("finish=%+v %v", finished, err)
}
page, err := store.ListPrivateMessages(ctx, id, "456", 1, 25)
if err != nil || page.Total != 2 || len(page.Data) != 2 {
t.Fatalf("messages=%+v %v", page, err)
}
conv, err := store.ListPrivateConversations(ctx, id, 1, 25)
if err != nil || conv.Total != 1 || conv.Data[0].MessageCount != 2 {
t.Fatalf("conversations=%+v %v", conv, err)
}
refresh := &PrivateInbox{Peers: []PrivateInboxPeer{{PeerUID: "456", PeerName: "正式昵称"}}}
if err := store.SavePrivateInbox(ctx, id, listener.Generation, refresh); err != nil {
t.Fatal(err)
}
conv, err = store.ListPrivateConversations(ctx, id, 1, 25)
if err != nil || conv.Data[0].PeerName != "正式昵称" || conv.Data[0].MessageCount != 2 {
t.Fatalf("nickname-only refresh=%+v %v", conv, err)
}
page, err = store.ListPrivateMessages(ctx, id, "456", 1, 25)
if err != nil || page.Total != 2 || page.Data[0].PeerName != "正式昵称" || page.Data[1].PeerName != "正式昵称" || page.Data[0].MessageAt == nil || !page.Data[0].MessageAt.Equal(at.Truncate(time.Microsecond)) {
t.Fatalf("preserved messages=%+v %v", page, err)
}
refresh.Peers[0] = PrivateInboxPeer{PeerUID: "456", Error: "昵称读取失败"}
if err := store.SavePrivateInbox(ctx, id, listener.Generation, refresh); err != nil {
t.Fatal(err)
}
conv, err = store.ListPrivateConversations(ctx, id, 1, 25)
statuses, statusErr := store.ListPrivateSyncStatus(ctx)
if err != nil || statusErr != nil || conv.Data[0].PeerName != "" || conv.Data[0].MessageCount != 2 || statuses[0].Error != "昵称读取失败" {
t.Fatalf("nickname error=%+v %+v %v %v", conv, statuses, err, statusErr)
}
if err := store.UpdatePrivateSyncStatus(ctx, id, listener.Generation, "network failed"); err != nil {
t.Fatal(err)
}
statuses, err = store.ListPrivateSyncStatus(ctx)
if err != nil || len(statuses) != 1 || statuses[0].Error != "network failed" {
t.Fatalf("status=%+v %v", statuses, err)
}
if _, err := store.SetListenerEnabled(ctx, id, false); err != nil {
t.Fatal(err)
}
if err := store.SavePrivateInbox(ctx, id, listener.Generation, batch); !errors.Is(err, ErrConflict) {
t.Fatalf("late sync=%v", err)
}
page, err = store.ListPrivateMessages(ctx, id, "456", 1, 25)
if err != nil || page.Total != 0 || page.Data == nil {
t.Fatalf("disabled=%+v %v", page, err)
}
conv, err = store.ListPrivateConversations(ctx, "", 1, 25)
if err != nil || conv.Total != 0 || conv.Data == nil {
t.Fatalf("disabled conversations=%+v %v", conv, err)
}
if _, err := store.SetListenerEnabled(ctx, id, true); err != nil {
t.Fatal(err)
}
page, err = store.ListPrivateMessages(ctx, id, "456", 1, 25)
if err != nil || page.Total != 2 {
t.Fatalf("reenabled=%+v %v", page, err)
}
_ = context.Background()
}