fix im forwarding and message list regressions

This commit is contained in:
richjohnrambo
2026-09-11 09:00:38 +08:00
parent 75d1aabcb0
commit 926b4bbd81
9 changed files with 331 additions and 20 deletions
+136 -1
View File
@@ -1625,6 +1625,21 @@ func TestServeIMTenantResolveRequiresAPIKey(t *testing.T) {
}
}
func TestWriteIMRelationErrorUsesResourceNotFoundReason(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/v1/im/test", nil)
rec := httptest.NewRecorder()
writeIMRelationError(rec, req, types.ErrNotFound)
if rec.Code != http.StatusNotFound {
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusNotFound, rec.Body.String())
}
resp := decodeIMHTTPResponse(t, rec)
if resp.Reason != imReasonResourceNotFound {
t.Fatalf("reason: got %q, want %q", resp.Reason, imReasonResourceNotFound)
}
}
func TestServeIMTenantResolveRejectsMalformedTenantCode(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/v1/im/tenants/resolve",
bytes.NewBufferString(`{"tenant_code":"`+strings.Repeat("a", 65)+`"}`))
@@ -5288,6 +5303,10 @@ func TestServeIMFavoriteBatchCreateNormalizesP2PUserTopic(t *testing.T) {
if favorites.created == nil || favorites.created.SourceTopic != normalizedTopic {
t.Fatalf("favorite source topic was not normalized: %#v", favorites.created)
}
snapshot := favorites.created.Snapshot.(map[string]any)
if snapshot["from"] != peer.UserId() {
t.Fatalf("favorite snapshot from must use full user_id: %#v", favorites.created.Snapshot)
}
}
func TestServeIMFavoriteForwardSavesMessages(t *testing.T) {
@@ -5679,6 +5698,100 @@ func TestServeIMMessageForwardKafkaAssignsSeqAndBroadcasts(t *testing.T) {
}
}
func TestServeIMMessageForwardKafkaPublishUnknownReturnsPending(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
expectIMBearer(t, ctrl, "token-1", 7, 42)
oldKafkaBus := kafkaBus
oldPublisher := publishKafkaMessagePersist
oldHub := globals.hub
oldSubs := store.Subs
oldTopics := store.Topics
oldMessages := store.Messages
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
topics := mock_store.NewMockTopicsPersistenceInterface(ctrl)
messages := mock_store.NewMockMessagesPersistenceInterface(ctrl)
kafkaBus = &kafkaRuntime{}
globals.hub = &Hub{routeSrv: make(chan *ServerComMessage, 3)}
store.Subs = subs
store.Topics = topics
store.Messages = messages
t.Cleanup(func() {
kafkaBus = oldKafkaBus
publishKafkaMessagePersist = oldPublisher
globals.hub = oldHub
store.Subs = oldSubs
store.Topics = oldTopics
store.Messages = oldMessages
})
targetUser := types.Uid(99)
targetTopic := types.Uid(42).P2PName(targetUser)
subs.EXPECT().Get(types.TenantID(7), targetTopic, types.Uid(42), false).Return(&types.Subscription{
TenantID: 7,
User: types.Uid(42).String(),
Topic: targetTopic,
ModeWant: types.ModeCPublic,
ModeGiven: types.ModeCPublic,
}, nil)
subs.EXPECT().Get(types.TenantID(7), "grpSource", types.Uid(42), false).Return(&types.Subscription{
TenantID: 7,
User: types.Uid(42).String(),
Topic: "grpSource",
ModeWant: types.ModeCPublic,
ModeGiven: types.ModeCPublic,
}, nil)
topics.EXPECT().Get(types.TenantID(7), targetTopic).Return(&types.Topic{
TenantID: 7,
SeqId: 40,
}, nil)
messages.EXPECT().GetOne(types.TenantID(7), "grpSource", int64(14)).Return(&types.Message{
TenantID: 7,
Topic: "grpSource",
SeqId: 14,
From: targetUser.String(),
Head: types.KVMap{"x-im-type": "text"},
Content: map[string]any{"text": "hello"},
ObjHeader: types.ObjHeader{
CreatedAt: time.Date(2026, 8, 13, 9, 0, 0, 0, time.UTC),
},
}, nil)
publishKafkaMessagePersist = func(_ context.Context, _, _ string, _ kafkaEnvelope) error {
return context.DeadlineExceeded
}
req := httptest.NewRequest(http.MethodPost, "/v1/im/messages/forward", bytes.NewBufferString(`{"mode":"separate","sources":[{"topic":"grpSource","seq":14}],"target_topics":["`+targetUser.UserId()+`"]}`))
req.Header.Set("Authorization", "Bearer token-1")
req.Header.Set("Idempotency-Key", "idem-forward-1")
rec := httptest.NewRecorder()
serveIMMessageForward(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("forward status: got %d body=%s", rec.Code, rec.Body.String())
}
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
items := data["items"].([]any)
if len(items) != 1 {
t.Fatalf("unexpected forward response: %#v", data)
}
got := items[0].(map[string]any)
if got["status"] != "pending" || got["reason"] != "MESSAGE_PERSIST_PENDING" || got["seq"].(float64) != 41 {
t.Fatalf("unexpected pending item: %#v", got)
}
if got["client_mid"] != "fwd_"+stableIMEventID("forward", "idem-forward-1", "separate", targetTopic, "grpSource", int64(14)) {
t.Fatalf("unexpected stable client mid: %#v", got)
}
if failed := data["failed"].([]any); len(failed) != 0 {
t.Fatalf("unexpected failed items: %#v", failed)
}
select {
case delivered := <-globals.hub.routeSrv:
t.Fatalf("pending forward should not broadcast immediately: %#v", delivered)
default:
}
}
func TestServeIMMessageForwardBundleSavesForwardBundle(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
@@ -6024,6 +6137,15 @@ func TestServeIMMessageSearchContextAndMedia(t *testing.T) {
if searchRec.Code != http.StatusOK {
t.Fatalf("search status: got %d body=%s", searchRec.Code, searchRec.Body.String())
}
searchData := decodeIMHTTPResponse(t, searchRec).Data.(map[string]any)
searchItems := searchData["items"].([]any)
if len(searchItems) != 1 || searchItems[0].(map[string]any)["from"] != types.Uid(42).UserId() {
t.Fatalf("search from must use full user_id: %#v", searchData)
}
searchSnapshot := searchItems[0].(map[string]any)["snapshot"].(map[string]any)
if searchSnapshot["from"] != types.Uid(42).UserId() {
t.Fatalf("search snapshot from must use full user_id: %#v", searchSnapshot)
}
contextReq := httptest.NewRequest(http.MethodGet, "/v1/im/topics/grpAlpha/messages/3/context", nil)
contextReq.Header.Set("Authorization", "Bearer token-1")
@@ -6032,6 +6154,11 @@ func TestServeIMMessageSearchContextAndMedia(t *testing.T) {
if contextRec.Code != http.StatusOK {
t.Fatalf("context status: got %d body=%s", contextRec.Code, contextRec.Body.String())
}
contextData := decodeIMHTTPResponse(t, contextRec).Data.(map[string]any)
contextItems := contextData["items"].([]any)
if len(contextItems) != 1 || contextItems[0].(map[string]any)["from"] != types.Uid(42).UserId() {
t.Fatalf("context from must use full user_id: %#v", contextData)
}
mediaReq := httptest.NewRequest(http.MethodGet, "/v1/im/topics/grpAlpha/media?type=image", nil)
mediaReq.Header.Set("Authorization", "Bearer token-1")
@@ -6040,6 +6167,11 @@ func TestServeIMMessageSearchContextAndMedia(t *testing.T) {
if mediaRec.Code != http.StatusOK {
t.Fatalf("media status: got %d body=%s", mediaRec.Code, mediaRec.Body.String())
}
mediaData := decodeIMHTTPResponse(t, mediaRec).Data.(map[string]any)
mediaItems := mediaData["items"].([]any)
if len(mediaItems) != 1 || mediaItems[0].(map[string]any)["from"] != types.Uid(42).UserId() {
t.Fatalf("media from must use full user_id: %#v", mediaData)
}
}
func TestServeIMTopicReadActionsNormalizeP2PUserTopic(t *testing.T) {
@@ -7318,7 +7450,7 @@ func TestServeIMUserProfile(t *testing.T) {
users.EXPECT().Get(types.TenantID(7), peer).Return(&types.User{
ObjHeader: types.ObjHeader{Id: peer.String()},
TenantID: 7,
Public: map[string]any{"display_name": "Alice", "avatar_file_id": "file-1", "signature": "hello"},
Public: map[string]any{"display_name": "Alice", "nickname": "Alice", "avatar_file_id": "file-1", "signature": "hello"},
}, nil)
store.Users = users
store.Relations = &testRelationsStore{context: &types.RelationshipContext{
@@ -7353,6 +7485,9 @@ func TestServeIMUserProfile(t *testing.T) {
if data["display_name"] != "老同事" || data["remark"] != "老同事" {
t.Fatalf("profile display name should use friend remark: %#v", data)
}
if data["nickname"] != "Alice" {
t.Fatalf("profile should include original nickname: %#v", data)
}
}
func TestServeIMUserQRCodes(t *testing.T) {