diff --git a/server/im_http.go b/server/im_http.go index 28a74ce..c3e04ab 100644 --- a/server/im_http.go +++ b/server/im_http.go @@ -4723,6 +4723,12 @@ func serveIMGroupMyProfile(wrt http.ResponseWriter, req *http.Request, topic str return } if _, err := store.GroupBusiness.UpdateMember(principal.TenantID, topic, principal.Uid, types.GroupMemberPatch{Nickname: &nickname, Version: body.Version}); err != nil { + if errors.Is(err, types.ErrNotFound) { + logs.Warn.Printf("group[%s]: member profile saved to subscription only; business member row missing for user %s", topic, principal.Uid.UserId()) + dispatchIMGroupEvents(principal.TenantID) + writeIMSuccess(wrt, req, map[string]any{"nickname": nickname, "version": currentVersion + 1, "updated_at": updatedAt.Format(time.RFC3339Nano)}) + return + } if rollbackErr := store.Subs.Update(principal.TenantID, topic, principal.Uid, map[string]any{"Private": selfSub.Private}); rollbackErr != nil { logs.Warn.Printf("group[%s]: failed to roll back member profile: %v", topic, rollbackErr) } diff --git a/server/im_http_test.go b/server/im_http_test.go index 95e77eb..ccb55c8 100644 --- a/server/im_http_test.go +++ b/server/im_http_test.go @@ -334,6 +334,7 @@ type testGroupBusinessStore struct { updatedSettings types.GroupSettingsPatch updatedMembers []types.GroupMemberPatch updatedRoleMode types.AccessMode + updateMemberErr error } func (s *testGroupBusinessStore) CreateGroup(_ types.TenantID, _ *types.Topic, _ types.Uid, _ any, @@ -388,6 +389,9 @@ func (s *testGroupBusinessStore) ListMembers(_ types.TenantID, _ string, _ types } func (s *testGroupBusinessStore) UpdateMember(_ types.TenantID, topic string, uid types.Uid, patch types.GroupMemberPatch) (*types.GroupMember, error) { + if s.updateMemberErr != nil { + return nil, s.updateMemberErr + } s.updatedMembers = append(s.updatedMembers, patch) return &types.GroupMember{Topic: topic, UserID: uid, Role: defaultStringPtr(patch.Role, types.GroupMemberRoleMember), MemberStatus: types.GroupMemberStatusActive, Version: 2}, nil } @@ -3780,6 +3784,57 @@ func TestServeIMGroupDetailPatchSettingsProfileAndLeave(t *testing.T) { } } +func TestServeIMGroupMyProfileKeepsSubscriptionNicknameWhenBusinessMemberMissing(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + expectIMBearer(t, ctrl, "token-1", 7, 42) + oldSubs := store.Subs + oldGroupBusiness := store.GroupBusiness + subs := mock_store.NewMockSubsPersistenceInterface(ctrl) + store.Subs = subs + store.GroupBusiness = &testGroupBusinessStore{updateMemberErr: types.ErrNotFound} + defer func() { + store.Subs = oldSubs + store.GroupBusiness = oldGroupBusiness + }() + + selfSub := &types.Subscription{ + TenantID: 7, + User: types.Uid(42).String(), + Topic: "grpAlpha", + ModeWant: types.ModeCFull, + ModeGiven: types.ModeCFull, + Private: map[string]any{"comment": "", "profile_version": 1}, + } + var savedPrivate map[string]any + subs.EXPECT().Get(types.TenantID(7), "grpAlpha", types.Uid(42), false).Return(selfSub, nil) + subs.EXPECT().Update(types.TenantID(7), "grpAlpha", types.Uid(42), gomock.Any()).DoAndReturn( + func(_ types.TenantID, _ string, _ types.Uid, update map[string]any) error { + private, ok := update["Private"].(map[string]any) + if !ok { + t.Fatalf("Private update missing: %#v", update) + } + savedPrivate = private + return nil + }, + ) + + req := httptest.NewRequest(http.MethodPut, "/v1/im/groups/grpAlpha/members/me/profile", bytes.NewBufferString(`{"nickname":"Coffee 备注","version":1}`)) + req.Header.Set("Authorization", "Bearer token-1") + rec := httptest.NewRecorder() + serveIMGroupResource(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("profile status: got %d body=%s", rec.Code, rec.Body.String()) + } + if savedPrivate["nickname"] != "Coffee 备注" || savedPrivate["profile_version"] != 2 { + t.Fatalf("subscription private not saved: %#v", savedPrivate) + } + if data := decodeIMHTTPResponse(t, rec).Data.(map[string]any); data["nickname"] != "Coffee 备注" || data["version"].(float64) != 2 { + t.Fatalf("unexpected profile response: %#v", data) + } +} + func TestServeIMGroupLeave(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish()