2026-06-08 19:00:23 +08:00

263 lines
9.0 KiB
Go

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) CreateManagerUserBlock(_ context.Context, command ManagerUserBlockCommand, target userdomain.User) (ManagerUserBlock, error) {
return ManagerUserBlock{
BlockID: "block-1",
ManagerUserID: command.ManagerUserID,
TargetUserID: command.TargetUserID,
TargetDisplayUserID: target.CurrentDisplayUserID,
TargetUsername: target.Username,
TargetAvatar: target.Avatar,
Country: target.Country,
BlockedUntilMS: command.BlockedUntilMS,
Status: ManagerUserBlockStatusActive,
Reason: command.Reason,
}, nil
}
func (r *fakeModerationRepository) ListManagerUserBlocks(context.Context, int64, string, int32) ([]ManagerUserBlock, error) {
return nil, nil
}
func (r *fakeModerationRepository) ReleaseManagerUserBlock(context.Context, ReleaseManagerUserBlockCommand) (ManagerUserBlockRelease, error) {
return ManagerUserBlockRelease{}, nil
}
func (r *fakeModerationRepository) ExpireManagerUserBlocks(context.Context, int64, int32) ([]ManagerUserBlockRelease, error) {
return nil, 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
}