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() }