2026-06-16 17:00:06 +08:00

166 lines
6.3 KiB
Go

package user
import (
"context"
"strings"
"testing"
"time"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
hostdomain "hyapp/services/user-service/internal/domain/host"
userdomain "hyapp/services/user-service/internal/domain/user"
)
type fakeCountryRegionRepository struct {
countries map[string]userdomain.Country
regions map[string]userdomain.Region
}
func (r fakeCountryRegionRepository) ResolveEnabledCountryByCode(_ context.Context, countryCode string) (userdomain.Country, bool, error) {
country, ok := r.countries[countryCode]
return country, ok, nil
}
func (r fakeCountryRegionRepository) ListRegistrationCountries(context.Context) ([]userdomain.Country, error) {
return nil, nil
}
func (r fakeCountryRegionRepository) ResolveActiveRegionByCountry(_ context.Context, countryCode string) (userdomain.Region, bool, error) {
region, ok := r.regions[countryCode]
return region, ok, nil
}
type fakeRoleSummaryRepository struct {
summary hostdomain.UserRoleSummary
}
func (r fakeRoleSummaryRepository) GetUserRoleSummary(context.Context, int64) (hostdomain.UserRoleSummary, error) {
return r.summary, nil
}
func TestChangeUserCountryAllowsNormalUserWithoutCooldown(t *testing.T) {
ctx := appcode.WithContext(context.Background(), "lalu")
repository := &fakeModerationRepository{user: userdomain.User{
AppCode: "lalu",
UserID: 10001,
Country: "US",
RegionID: 1,
ProfileCompleted: true,
}, countrySessionIDs: []string{"country_sess"}}
denylist := &fakeSessionDenylist{}
svc := New(repository,
WithCountryRegionRepository(fakeCountryRegionRepository{
countries: map[string]userdomain.Country{
"ID": {CountryCode: "ID", Enabled: true},
"TH": {CountryCode: "TH", Enabled: true},
},
regions: map[string]userdomain.Region{
"ID": {RegionID: 2, Status: userdomain.RegionStatusActive},
"TH": {RegionID: 3, Status: userdomain.RegionStatusActive},
},
}),
WithRoleSummaryRepository(fakeRoleSummaryRepository{}),
WithSessionDenylist(denylist, time.Minute),
WithClock(func() time.Time { return time.UnixMilli(1_700_000_000_000).UTC() }),
)
if _, nextAllowedAt, err := svc.ChangeUserCountry(ctx, 10001, "ID", "req-id"); err != nil || nextAllowedAt != 0 {
t.Fatalf("first normal country change failed: next=%d err=%v", nextAllowedAt, err)
}
if repository.lastCountryCommand.CooldownMs != 0 || repository.lastCountryCommand.Source != "app" || repository.lastCountryCommand.NewRegionID != 2 {
t.Fatalf("app change command mismatch: %+v", repository.lastCountryCommand)
}
if _, nextAllowedAt, err := svc.ChangeUserCountry(ctx, 10001, "TH", "req-th"); err != nil || nextAllowedAt != 0 {
t.Fatalf("second normal country change must not hit cooldown: next=%d err=%v", nextAllowedAt, err)
}
if repository.countryChangeCalls != 2 {
t.Fatalf("repository must receive both app changes, calls=%d", repository.countryChangeCalls)
}
if len(denylist.sessions) != 2 || denylist.sessions[0] != "country_sess" || denylist.reasons[0] != countryChangeRevokeReason {
t.Fatalf("country change must write revoked sessions to denylist: sessions=%+v reasons=%+v", denylist.sessions, denylist.reasons)
}
}
func TestChangeUserCountryRejectsProtectedRoles(t *testing.T) {
cases := []struct {
name string
summary hostdomain.UserRoleSummary
role string
}{
{name: "host", summary: hostdomain.UserRoleSummary{IsHost: true}, role: "host"},
{name: "agency", summary: hostdomain.UserRoleSummary{IsAgency: true}, role: "agency"},
{name: "bd leader", summary: hostdomain.UserRoleSummary{IsBDLeader: true, IsBD: true}, role: "bd leader"},
{name: "bd", summary: hostdomain.UserRoleSummary{IsBD: true}, role: "bd"},
{name: "manager", summary: hostdomain.UserRoleSummary{IsManager: true}, role: "manager"},
{name: "coin seller", summary: hostdomain.UserRoleSummary{IsCoinSeller: true}, role: "coin seller"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
repository := &fakeModerationRepository{user: userdomain.User{
AppCode: "lalu",
UserID: 10001,
Country: "US",
RegionID: 1,
ProfileCompleted: true,
}}
svc := New(repository,
WithCountryRegionRepository(fakeCountryRegionRepository{
countries: map[string]userdomain.Country{"ID": {CountryCode: "ID", Enabled: true}},
regions: map[string]userdomain.Region{"ID": {RegionID: 2, Status: userdomain.RegionStatusActive}},
}),
WithRoleSummaryRepository(fakeRoleSummaryRepository{summary: tc.summary}),
)
_, _, err := svc.ChangeUserCountry(appcode.WithContext(context.Background(), "lalu"), 10001, "ID", "req-protected")
if xerr.CodeOf(err) != xerr.PermissionDenied {
t.Fatalf("protected role must be permission denied: role=%s err=%v", tc.role, err)
}
want := "you have " + tc.role + " id, not allow modify country"
if !strings.Contains(err.Error(), want) {
t.Fatalf("protected role message mismatch: got=%q want contains=%q", err.Error(), want)
}
if repository.countryChangeCalls != 0 {
t.Fatalf("protected role must not update repository, calls=%d", repository.countryChangeCalls)
}
})
}
}
func TestUpdateUserProfileBackgroundPersistsProfileBgImg(t *testing.T) {
repository := &fakeModerationRepository{user: userdomain.User{
AppCode: "lalu",
UserID: 10001,
ProfileCompleted: true,
}}
svc := New(repository, WithClock(func() time.Time {
return time.UnixMilli(1_700_000_000_000).UTC()
}))
profileBgImg := " https://cdn.example.com/profile-bg.png "
updated, err := svc.UpdateUserProfileBackground(appcode.WithContext(context.Background(), "lalu"), 10001, profileBgImg)
if err != nil {
t.Fatalf("UpdateUserProfileBackground failed: %v", err)
}
if updated.ProfileBgImg != "https://cdn.example.com/profile-bg.png" {
t.Fatalf("updated profile bg mismatch: %+v", updated)
}
}
func TestUpdateUserProfileBackgroundRejectsInvalidProfileBgImg(t *testing.T) {
repository := &fakeModerationRepository{user: userdomain.User{
AppCode: "lalu",
UserID: 10001,
ProfileCompleted: true,
}}
svc := New(repository)
profileBgImg := "profile_card_gold"
_, err := svc.UpdateUserProfileBackground(appcode.WithContext(context.Background(), "lalu"), 10001, profileBgImg)
if xerr.CodeOf(err) != xerr.InvalidArgument {
t.Fatalf("invalid profile_bg_img must be rejected, err=%v", err)
}
}