349 lines
16 KiB
Go
349 lines
16 KiB
Go
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()
|
||
}
|