package user import ( "context" "errors" "testing" "time" roomv1 "hyapp.local/api/proto/room/v1" "hyapp/pkg/appcode" userdomain "hyapp/services/user-service/internal/domain/user" ) func TestSetUserStatusBanRevokesSessionsAndKicksRealtimeSurfaces(t *testing.T) { repository := &fakeModerationRepository{ user: userdomain.User{ AppCode: "lalu", UserID: 10001, Status: userdomain.StatusBanned, }, sessionIDs: []string{"sess_a", "sess_b"}, } denylist := &fakeSessionDenylist{} im := &fakeIMLoginKicker{} room := &fakeRoomEvictor{resp: &roomv1.SystemEvictUserResponse{ HadCurrentRoom: true, Result: &roomv1.CommandResult{Applied: true, RoomVersion: 7}, RoomId: "room_1001", RtcKicked: true, }} svc := New(repository, WithModerationRepository(repository), WithSessionDenylist(denylist, time.Minute), WithIMLoginKicker(im), WithRoomEvictor(room), WithClock(func() time.Time { return time.UnixMilli(1_700_000_000_000).UTC() }), ) result, err := svc.SetUserStatus(appcode.WithContext(context.Background(), "lalu"), UserStatusCommand{ AppCode: "lalu", TargetUserID: 10001, Status: userdomain.StatusBanned, OperatorType: OperatorTypeAdmin, OperatorUserID: 9001, RequestID: "req-ban", }) if err != nil { t.Fatalf("SetUserStatus failed: %v", err) } if repository.lastCommand.Status != userdomain.StatusBanned || repository.lastCommand.OperatorType != OperatorTypeAdmin || repository.lastCommand.NowMs != 1_700_000_000_000 { t.Fatalf("unexpected repository command: %+v", repository.lastCommand) } if len(denylist.sessions) != 2 || denylist.sessions[0] != "sess_a" || denylist.sessions[1] != "sess_b" { t.Fatalf("denylist sessions mismatch: %+v", denylist.sessions) } if im.userID != 10001 { t.Fatalf("im kick target mismatch: %d", im.userID) } if room.req.GetTargetUserId() != 10001 || !room.req.GetBanFromRoom() || room.req.GetMeta().GetAppCode() != "lalu" { t.Fatalf("room eviction request mismatch: %+v", room.req) } if !result.AccessTokenRevoked || result.AccessTokenError != "" { t.Fatalf("access token revoke result mismatch: %+v", result) } if !result.IMKicked || !result.RoomEvicted || !result.RTCKicked || result.RoomID != "room_1001" { t.Fatalf("unexpected moderation result: %+v", result) } } func TestSetUserStatusSurfacesAccessDenylistError(t *testing.T) { repository := &fakeModerationRepository{ user: userdomain.User{ AppCode: "lalu", UserID: 10001, Status: userdomain.StatusBanned, }, sessionIDs: []string{"sess_a"}, } denylist := &fakeSessionDenylist{err: errors.New("redis unavailable")} svc := New(repository, WithModerationRepository(repository), WithSessionDenylist(denylist, time.Minute), ) result, err := svc.SetUserStatus(appcode.WithContext(context.Background(), "lalu"), UserStatusCommand{ AppCode: "lalu", TargetUserID: 10001, Status: userdomain.StatusBanned, RequestID: "req-ban", }) if err != nil { t.Fatalf("SetUserStatus failed: %v", err) } if result.AccessTokenRevoked || result.AccessTokenError != "redis unavailable" { t.Fatalf("denylist error must be surfaced for retry: %+v", result) } if result.RevokedSessionCount != 1 { t.Fatalf("database revocation fact mismatch: %+v", result) } } type fakeModerationRepository struct { user userdomain.User sessionIDs []string countrySessionIDs []string lastCommand UserStatusCommand lastCountryCommand userdomain.CountryChangeCommand countryChangeCalls int lastReport userdomain.Report } func (r *fakeModerationRepository) SetUserStatus(_ context.Context, command UserStatusCommand) (UserStatusPersistenceResult, error) { r.lastCommand = command r.user.Status = command.Status return UserStatusPersistenceResult{User: r.user, RevokedSessionIDs: append([]string{}, r.sessionIDs...)}, nil } func (r *fakeModerationRepository) GetUser(context.Context, int64) (userdomain.User, error) { return r.user, nil } func (r *fakeModerationRepository) GetMyProfileStats(context.Context, int64) (userdomain.ProfileStats, error) { return userdomain.ProfileStats{}, nil } func (r *fakeModerationRepository) RecordProfileVisit(context.Context, int64, int64, int64) (bool, userdomain.ProfileStats, error) { return false, userdomain.ProfileStats{}, nil } func (r *fakeModerationRepository) ListProfileVisitors(context.Context, int64, int32, int32) ([]userdomain.ProfileVisitRecord, int64, error) { return nil, 0, nil } func (r *fakeModerationRepository) FollowUser(context.Context, int64, int64, int64) (userdomain.ProfileStats, error) { return userdomain.ProfileStats{}, nil } func (r *fakeModerationRepository) UnfollowUser(context.Context, int64, int64, int64) (userdomain.ProfileStats, error) { return userdomain.ProfileStats{}, nil } func (r *fakeModerationRepository) ListFollowing(context.Context, int64, int32, int32, int64, int64) ([]userdomain.FollowRecord, int64, error) { return nil, 0, nil } func (r *fakeModerationRepository) ApplyFriend(context.Context, int64, int64, int64) (userdomain.FriendApplication, bool, error) { return userdomain.FriendApplication{}, false, nil } func (r *fakeModerationRepository) AcceptFriendApplication(context.Context, int64, int64, int64) (userdomain.FriendRecord, error) { return userdomain.FriendRecord{}, nil } func (r *fakeModerationRepository) DeleteFriend(context.Context, int64, int64, int64) (bool, error) { return false, nil } func (r *fakeModerationRepository) ListFriends(context.Context, int64, int32, int32, int64, int64) ([]userdomain.FriendRecord, int64, error) { return nil, 0, nil } func (r *fakeModerationRepository) ListFriendApplications(context.Context, int64, string, int32, int32) ([]userdomain.FriendApplication, int64, error) { return nil, 0, nil } func (r *fakeModerationRepository) SubmitReport(_ context.Context, report userdomain.Report) (userdomain.Report, error) { r.lastReport = report return report, nil } func (r *fakeModerationRepository) BusinessUserLookup(context.Context, string, bool, int32) ([]userdomain.BusinessUserLookupItem, error) { return nil, nil } func (r *fakeModerationRepository) BatchGetUsers(context.Context, []int64) (map[int64]userdomain.User, error) { return nil, nil } func (r *fakeModerationRepository) ListUserIDs(context.Context, userdomain.UserIDPageFilter) ([]int64, error) { return nil, nil } func (r *fakeModerationRepository) CreateUserWithIdentity(context.Context, userdomain.User, userdomain.Identity) error { return nil } func (r *fakeModerationRepository) UpdateUserProfile(context.Context, userdomain.ProfileUpdateCommand) (userdomain.User, error) { return r.user, nil } func (r *fakeModerationRepository) CompleteOnboarding(context.Context, userdomain.CompleteOnboardingCommand) (userdomain.User, error) { return r.user, nil } func (r *fakeModerationRepository) ChangeUserCountry(_ context.Context, command userdomain.CountryChangeCommand) (userdomain.User, int64, []string, error) { r.lastCountryCommand = command r.countryChangeCalls++ r.user.Country = command.NewCountry r.user.RegionID = command.NewRegionID r.user.UpdatedAtMs = command.ChangedAtMs return r.user, 0, append([]string{}, r.countrySessionIDs...), nil } type fakeSessionDenylist struct { sessions []string reasons []string err error } func (d *fakeSessionDenylist) SetRevokedSession(_ context.Context, _ string, sessionID string, reason string, _ time.Duration) error { d.sessions = append(d.sessions, sessionID) d.reasons = append(d.reasons, reason) return d.err } type fakeIMLoginKicker struct { userID int64 } func (k *fakeIMLoginKicker) KickUser(_ context.Context, userID int64) error { k.userID = userID return nil } type fakeRoomEvictor struct { req *roomv1.SystemEvictUserRequest resp *roomv1.SystemEvictUserResponse } func (e *fakeRoomEvictor) SystemEvictUser(_ context.Context, req *roomv1.SystemEvictUserRequest) (*roomv1.SystemEvictUserResponse, error) { e.req = req return e.resp, nil }