feat: add message mutation sync

This commit is contained in:
richjohnrambo
2026-09-15 15:54:52 +08:00
parent 55afd976c9
commit bc27a5c574
12 changed files with 949 additions and 13 deletions
+115
View File
@@ -548,6 +548,15 @@ func (s *testGroupBusinessStore) ListMutes(_ types.TenantID, _ string, _ string,
return s.mutes, "", nil
}
func (s *testGroupBusinessStore) GetMute(_ types.TenantID, _ string, uid types.Uid) (*types.GroupMute, error) {
for i := range s.mutes {
if s.mutes[i].UserID == uid && s.mutes[i].Status == types.GroupMuteStatusActive {
return &s.mutes[i], nil
}
}
return nil, types.ErrNotFound
}
func (s *testGroupBusinessStore) UpsertMute(_ types.TenantID, mute *types.GroupMute) (*types.GroupMute, error) {
mute.Version = 1
s.mutes = append(s.mutes, *mute)
@@ -5625,6 +5634,56 @@ func TestServeIMGroupAdminsMutesAndOwnerTransfer(t *testing.T) {
}
}
func TestServeIMGroupMutesMemberGetsOwnMuteOnly(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
oldStore := store.Store
oldSubs := store.Subs
oldGroupBusiness := store.GroupBusiness
mockStore := mock_store.NewMockPersistentStorageInterface(ctrl)
tokenAuth := mock_auth.NewMockAuthHandler(ctrl)
mockStore.EXPECT().GetLogicalAuthHandler("token").Return(tokenAuth).Times(2)
tokenAuth.EXPECT().IsInitialized().Return(true).Times(2)
tokenAuth.EXPECT().Authenticate(auth.AuthContext{}, []byte("token-1")).
Return(&auth.Rec{TenantID: 7, Uid: 43, AuthLevel: auth.LevelAuth}, nil, nil).Times(2)
store.Store = mockStore
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
store.Subs = subs
memberSub := &types.Subscription{TenantID: 7, User: types.Uid(43).String(), Topic: "grpAlpha", ModeWant: types.ModeCPublic, ModeGiven: types.ModeCPublic}
subs.EXPECT().Get(types.TenantID(7), "grpAlpha", types.Uid(43), false).Return(memberSub, nil).Times(2)
groupStore := &testGroupBusinessStore{mutes: []types.GroupMute{
{Topic: "grpAlpha", UserID: types.Uid(42), MutedBy: types.Uid(99), MutedAt: time.Now().UTC(), Status: types.GroupMuteStatusActive, Version: 1},
{Topic: "grpAlpha", UserID: types.Uid(43), MutedBy: types.Uid(99), MutedAt: time.Now().UTC(), Reason: "test", Status: types.GroupMuteStatusActive, Version: 2},
}}
store.GroupBusiness = groupStore
defer func() {
store.Store = oldStore
store.Subs = oldSubs
store.GroupBusiness = oldGroupBusiness
}()
getReq := httptest.NewRequest(http.MethodGet, "/v1/im/groups/grpAlpha/mutes", nil)
getReq.Header.Set("Authorization", "Bearer token-1")
getRec := httptest.NewRecorder()
serveIMGroupResource(getRec, getReq)
if getRec.Code != http.StatusOK {
t.Fatalf("member mute get status: got %d body=%s", getRec.Code, getRec.Body.String())
}
data := decodeIMHTTPResponse(t, getRec).Data.(map[string]any)
items := data["items"].([]any)
if len(items) != 1 || items[0].(map[string]any)["user_id"] != types.Uid(43).UserId() {
t.Fatalf("member should only see own mute item: %#v", items)
}
postReq := httptest.NewRequest(http.MethodPost, "/v1/im/groups/grpAlpha/mutes/batch", bytes.NewBufferString(`{"user_ids":["`+types.Uid(42).UserId()+`"]}`))
postReq.Header.Set("Authorization", "Bearer token-1")
postRec := httptest.NewRecorder()
serveIMGroupResource(postRec, postReq)
if postRec.Code != http.StatusForbidden {
t.Fatalf("member mute post status: got %d body=%s", postRec.Code, postRec.Body.String())
}
}
func TestServeIMGroupDissolveConsumesActionToken(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
@@ -7647,6 +7706,62 @@ func TestServeIMMessageRecall(t *testing.T) {
}
}
func TestServeIMMessageMutations(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
actor := types.Uid(42)
expectIMBearer(t, ctrl, "token-1", 7, actor)
oldSubs := store.Subs
oldMessages := store.Messages
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
messages := mock_store.NewMockMessagesPersistenceInterface(ctrl)
store.Subs = subs
store.Messages = messages
defer func() {
store.Subs = oldSubs
store.Messages = oldMessages
}()
subs.EXPECT().Get(types.TenantID(7), "grpRecall", actor, false).Return(&types.Subscription{
TenantID: 7,
User: actor.String(),
Topic: "grpRecall",
ModeWant: types.ModeCPublic,
ModeGiven: types.ModeCPublic,
}, nil)
mutatedAt := time.Date(2026, 9, 15, 10, 0, 0, 0, time.UTC)
messages.EXPECT().Mutations(types.TenantID(7), "grpRecall", actor, int64(1), 20).Return([]types.MessageMutation{{
SeqId: 9,
Version: 2,
Event: "message.recalled",
Operator: actor.UserId(),
MutatedAt: mutatedAt,
MutationVersion: 3,
}}, "3", false, int64(3), nil)
req := httptest.NewRequest(http.MethodGet, "/v1/im/topics/grpRecall/message-mutations?since_version=1&limit=20", nil)
req.Header.Set("Authorization", "Bearer token-1")
rec := httptest.NewRecorder()
serveIMTopicMessageAction(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
if data["mutation_version"].(float64) != 3 || data["next_cursor"] != "3" {
t.Fatalf("unexpected mutation response: %#v", data)
}
items := data["items"].([]any)
if len(items) != 1 {
t.Fatalf("unexpected mutation items: %#v", items)
}
item := items[0].(map[string]any)
if item["seq"].(float64) != 9 || item["version"].(float64) != 2 || item["event"] != "message.recalled" {
t.Fatalf("unexpected mutation item: %#v", item)
}
}
func TestServeIMMessageBatchRecallPartialSuccess(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()