720 lines
33 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package mysqltest provides real MySQL repositories for user-service tests.
package mysqltest
import (
"context"
"database/sql"
"strings"
"testing"
"time"
"hyapp/pkg/appcode"
authdomain "hyapp/services/user-service/internal/domain/auth"
hostdomain "hyapp/services/user-service/internal/domain/host"
invitedomain "hyapp/services/user-service/internal/domain/invite"
userdomain "hyapp/services/user-service/internal/domain/user"
mysqlstorage "hyapp/services/user-service/internal/storage/mysql"
"hyapp/services/user-service/internal/testutil/mysqlschema"
)
// Repository wraps the production MySQL repository and adds test-only seed helpers.
type Repository struct {
*mysqlstorage.Repository
t testing.TB
schema *mysqlschema.Schema
}
// NewRepository creates an isolated MySQL schema and initializes the user-service tables.
func NewRepository(t testing.TB) *Repository {
t.Helper()
schema := mysqlschema.New(t)
repository, err := mysqlstorage.Open(context.Background(), schema.DSN)
if err != nil {
t.Fatalf("open storage repository failed: %v", err)
}
wrapped := &Repository{
Repository: repository,
t: t,
schema: schema,
}
t.Cleanup(wrapped.Close)
return wrapped
}
// Close shuts down the storage repository; mysqlschema owns schema cleanup.
func (r *Repository) Close() {
r.t.Helper()
if r.Repository != nil {
_ = r.Repository.Close()
}
}
// RawDB exposes the isolated test schema for assertions that span read-model tables.
func (r *Repository) RawDB() *sql.DB {
if r == nil || r.schema == nil {
return nil
}
return r.schema.DB
}
// GetUser 让测试 wrapper 继续满足业务层用户主数据接口。
func (r *Repository) GetUser(ctx context.Context, userID int64) (userdomain.User, error) {
return r.Repository.UserRepository().GetUser(ctx, userID)
}
// GetMyProfileStats 让测试 wrapper 继续满足我的页统计 read model 接口。
func (r *Repository) GetMyProfileStats(ctx context.Context, userID int64) (userdomain.ProfileStats, error) {
return r.Repository.UserRepository().GetMyProfileStats(ctx, userID)
}
func (r *Repository) RecordProfileVisit(ctx context.Context, visitorUserID int64, targetUserID int64, nowMs int64) (bool, userdomain.ProfileStats, error) {
return r.Repository.UserRepository().RecordProfileVisit(ctx, visitorUserID, targetUserID, nowMs)
}
func (r *Repository) ListProfileVisitors(ctx context.Context, userID int64, page int32, pageSize int32) ([]userdomain.ProfileVisitRecord, int64, error) {
return r.Repository.UserRepository().ListProfileVisitors(ctx, userID, page, pageSize)
}
func (r *Repository) FollowUser(ctx context.Context, followerUserID int64, followeeUserID int64, nowMs int64) (userdomain.ProfileStats, error) {
return r.Repository.UserRepository().FollowUser(ctx, followerUserID, followeeUserID, nowMs)
}
func (r *Repository) UnfollowUser(ctx context.Context, followerUserID int64, followeeUserID int64, nowMs int64) (userdomain.ProfileStats, error) {
return r.Repository.UserRepository().UnfollowUser(ctx, followerUserID, followeeUserID, nowMs)
}
func (r *Repository) ListFollowing(ctx context.Context, userID int64, page int32, pageSize int32, cursorUpdatedAtMS int64, cursorUserID int64) ([]userdomain.FollowRecord, int64, error) {
return r.Repository.UserRepository().ListFollowing(ctx, userID, page, pageSize, cursorUpdatedAtMS, cursorUserID)
}
func (r *Repository) ApplyFriend(ctx context.Context, requesterUserID int64, targetUserID int64, nowMs int64) (userdomain.FriendApplication, bool, error) {
return r.Repository.UserRepository().ApplyFriend(ctx, requesterUserID, targetUserID, nowMs)
}
func (r *Repository) AcceptFriendApplication(ctx context.Context, accepterUserID int64, requesterUserID int64, nowMs int64) (userdomain.FriendRecord, error) {
return r.Repository.UserRepository().AcceptFriendApplication(ctx, accepterUserID, requesterUserID, nowMs)
}
func (r *Repository) DeleteFriend(ctx context.Context, userID int64, friendUserID int64, nowMs int64) (bool, error) {
return r.Repository.UserRepository().DeleteFriend(ctx, userID, friendUserID, nowMs)
}
func (r *Repository) ListFriends(ctx context.Context, userID int64, page int32, pageSize int32, cursorUpdatedAtMS int64, cursorUserID int64) ([]userdomain.FriendRecord, int64, error) {
return r.Repository.UserRepository().ListFriends(ctx, userID, page, pageSize, cursorUpdatedAtMS, cursorUserID)
}
func (r *Repository) ListFriendApplications(ctx context.Context, userID int64, direction string, page int32, pageSize int32) ([]userdomain.FriendApplication, int64, error) {
return r.Repository.UserRepository().ListFriendApplications(ctx, userID, direction, page, pageSize)
}
func (r *Repository) SubmitReport(ctx context.Context, report userdomain.Report) (userdomain.Report, error) {
return r.Repository.UserRepository().SubmitReport(ctx, report)
}
// BusinessUserLookup 让测试 wrapper 继续满足业务场景用户查询接口。
func (r *Repository) BusinessUserLookup(ctx context.Context, keyword string, numericKeyword bool, pageSize int32) ([]userdomain.BusinessUserLookupItem, error) {
return r.Repository.UserRepository().BusinessUserLookup(ctx, keyword, numericKeyword, pageSize)
}
// BatchGetUsers 让测试 wrapper 继续满足业务层用户主数据接口。
func (r *Repository) BatchGetUsers(ctx context.Context, userIDs []int64) (map[int64]userdomain.User, error) {
return r.Repository.UserRepository().BatchGetUsers(ctx, userIDs)
}
// ListUserIDs 让测试 wrapper 继续满足后台 fanout 目标用户查询接口。
func (r *Repository) ListUserIDs(ctx context.Context, filter userdomain.UserIDPageFilter) ([]int64, error) {
return r.Repository.UserRepository().ListUserIDs(ctx, filter)
}
// CreateUserWithIdentity 让测试 wrapper 继续满足业务层用户创建接口。
func (r *Repository) CreateUserWithIdentity(ctx context.Context, user userdomain.User, identity userdomain.Identity) error {
return r.Repository.UserRepository().CreateUserWithIdentity(ctx, user, identity)
}
// UpdateUserProfile 让测试 wrapper 继续满足业务层资料更新接口。
func (r *Repository) UpdateUserProfile(ctx context.Context, command userdomain.ProfileUpdateCommand) (userdomain.User, error) {
return r.Repository.UserRepository().UpdateUserProfile(ctx, command)
}
// CompleteOnboarding 让测试 wrapper 继续满足业务层注册资料接口。
func (r *Repository) CompleteOnboarding(ctx context.Context, command userdomain.CompleteOnboardingCommand) (userdomain.User, error) {
return r.Repository.UserRepository().CompleteOnboarding(ctx, command)
}
// ChangeUserCountry 让测试 wrapper 继续满足业务层国家修改接口。
func (r *Repository) ChangeUserCountry(ctx context.Context, command userdomain.CountryChangeCommand) (userdomain.User, int64, error) {
return r.Repository.UserRepository().ChangeUserCountry(ctx, command)
}
// BindPushToken 让测试 wrapper 继续满足设备 push token repository。
func (r *Repository) BindPushToken(ctx context.Context, command userdomain.PushTokenCommand) error {
return r.Repository.DeviceRepository().BindPushToken(ctx, command)
}
// DeletePushToken 让测试 wrapper 继续满足设备 push token repository。
func (r *Repository) DeletePushToken(ctx context.Context, command userdomain.DeletePushTokenCommand) (bool, error) {
return r.Repository.DeviceRepository().DeletePushToken(ctx, command)
}
// AllocateDisplayUserID 让测试 wrapper 继续满足认证注册接口。
func (r *Repository) AllocateDisplayUserID(ctx context.Context, appCode string) (string, error) {
return r.Repository.AuthRepository().AllocateDisplayUserID(ctx, appCode)
}
// GetUserIdentity 让测试 wrapper 继续满足短号接口。
func (r *Repository) GetUserIdentity(ctx context.Context, userID int64) (userdomain.Identity, error) {
return r.Repository.IdentityRepository().GetUserIdentity(ctx, userID)
}
// ResolveDisplayUserID 让测试 wrapper 继续满足短号接口。
func (r *Repository) ResolveDisplayUserID(ctx context.Context, displayUserID string, nowMs int64) (userdomain.Identity, error) {
return r.Repository.IdentityRepository().ResolveDisplayUserID(ctx, displayUserID, nowMs)
}
// ChangeDisplayUserID 让测试 wrapper 继续满足短号接口。
func (r *Repository) ChangeDisplayUserID(ctx context.Context, command userdomain.DisplayUserIDChangeCommand) (userdomain.Identity, error) {
return r.Repository.IdentityRepository().ChangeDisplayUserID(ctx, command)
}
// ApplyPrettyDisplayUserID 让测试 wrapper 继续满足靓号接口。
func (r *Repository) ApplyPrettyDisplayUserID(ctx context.Context, command userdomain.PrettyDisplayUserIDCommand) (userdomain.Identity, string, error) {
return r.Repository.IdentityRepository().ApplyPrettyDisplayUserID(ctx, command)
}
// ExpirePrettyDisplayUserID 让测试 wrapper 继续满足靓号接口。
func (r *Repository) ExpirePrettyDisplayUserID(ctx context.Context, command userdomain.ExpirePrettyDisplayUserIDCommand) (userdomain.Identity, error) {
return r.Repository.IdentityRepository().ExpirePrettyDisplayUserID(ctx, command)
}
// ResolveEnabledCountryByCode 让测试 wrapper 继续满足国家/区域查询接口。
func (r *Repository) ResolveEnabledCountryByCode(ctx context.Context, countryCode string) (userdomain.Country, bool, error) {
return r.Repository.RegionRepository().ResolveEnabledCountryByCode(ctx, countryCode)
}
// ListRegistrationCountries 让测试 wrapper 继续满足国家/区域查询接口。
func (r *Repository) ListRegistrationCountries(ctx context.Context) ([]userdomain.Country, error) {
return r.Repository.RegionRepository().ListRegistrationCountries(ctx)
}
// ResolveActiveRegionByCountry 让测试 wrapper 继续满足国家/区域查询接口。
func (r *Repository) ResolveActiveRegionByCountry(ctx context.Context, countryCode string) (userdomain.Region, bool, error) {
return r.Repository.RegionRepository().ResolveActiveRegionByCountry(ctx, countryCode)
}
// UpdateCountry 让测试 wrapper 继续满足国家管理接口。
func (r *Repository) UpdateCountry(ctx context.Context, command userdomain.UpdateCountryCommand) (userdomain.Country, error) {
return r.Repository.RegionRepository().UpdateCountry(ctx, command)
}
// ListCountries 让测试 wrapper 继续满足国家管理接口。
func (r *Repository) ListCountries(ctx context.Context, filter userdomain.CountryFilter) ([]userdomain.Country, error) {
return r.Repository.RegionRepository().ListCountries(ctx, filter)
}
// UpdateRegion 让测试 wrapper 继续满足区域管理接口。
func (r *Repository) UpdateRegion(ctx context.Context, command userdomain.UpdateRegionCommand) (userdomain.Region, error) {
return r.Repository.RegionRepository().UpdateRegion(ctx, command)
}
// ListRegions 让测试 wrapper 继续满足区域管理接口。
func (r *Repository) ListRegions(ctx context.Context, filter userdomain.RegionFilter) ([]userdomain.Region, error) {
return r.Repository.RegionRepository().ListRegions(ctx, filter)
}
// GetRegion 让测试 wrapper 继续满足区域管理接口。
func (r *Repository) GetRegion(ctx context.Context, regionID int64) (userdomain.Region, error) {
return r.Repository.RegionRepository().GetRegion(ctx, regionID)
}
// ReplaceRegionCountries 让测试 wrapper 继续满足区域管理接口。
func (r *Repository) ReplaceRegionCountries(ctx context.Context, command userdomain.ReplaceRegionCountriesCommand) (userdomain.Region, error) {
return r.Repository.RegionRepository().ReplaceRegionCountries(ctx, command)
}
// ProcessNextRegionRebuildTask 让测试 wrapper 继续满足历史用户区域重算接口。
func (r *Repository) ProcessNextRegionRebuildTask(ctx context.Context, workerID string, nowMs int64, lockTTL time.Duration, batchSize int) (userdomain.RegionRebuildProcessResult, error) {
return r.Repository.RegionRepository().ProcessNextRegionRebuildTask(ctx, workerID, nowMs, lockTTL, batchSize)
}
// SetPassword 让测试 wrapper 继续满足认证接口。
func (r *Repository) SetPassword(ctx context.Context, account authdomain.PasswordAccount) error {
return r.Repository.AuthRepository().SetPassword(ctx, account)
}
// CreatePasswordUser 让测试 wrapper 继续满足认证接口。
func (r *Repository) CreatePasswordUser(ctx context.Context, user userdomain.User, identity userdomain.Identity, account authdomain.PasswordAccount, session authdomain.Session, invite invitedomain.BindCommand) error {
return r.Repository.AuthRepository().CreatePasswordUser(ctx, user, identity, account, session, invite)
}
// FindPasswordByDisplayUserID 让测试 wrapper 继续满足认证接口。
func (r *Repository) FindPasswordByDisplayUserID(ctx context.Context, displayUserID string, nowMs int64) (authdomain.PasswordAccount, error) {
return r.Repository.AuthRepository().FindPasswordByDisplayUserID(ctx, displayUserID, nowMs)
}
// CreateSession 让测试 wrapper 继续满足认证接口。
func (r *Repository) CreateSession(ctx context.Context, session authdomain.Session) error {
return r.Repository.AuthRepository().CreateSession(ctx, session)
}
// FindActiveSessionByRefreshHash 让测试 wrapper 继续满足认证接口。
func (r *Repository) FindActiveSessionByRefreshHash(ctx context.Context, refreshTokenHash string, nowMs int64) (authdomain.Session, error) {
return r.Repository.AuthRepository().FindActiveSessionByRefreshHash(ctx, refreshTokenHash, nowMs)
}
// FindActiveSessionByID 让测试 wrapper 继续满足认证接口。
func (r *Repository) FindActiveSessionByID(ctx context.Context, sessionID string, nowMs int64) (authdomain.Session, error) {
return r.Repository.AuthRepository().FindActiveSessionByID(ctx, sessionID, nowMs)
}
// ReplaceSession 让测试 wrapper 继续满足认证接口。
func (r *Repository) ReplaceSession(ctx context.Context, oldSessionID string, newSession authdomain.Session, revokedAtMs int64) error {
return r.Repository.AuthRepository().ReplaceSession(ctx, oldSessionID, newSession, revokedAtMs)
}
// RevokeSession 让测试 wrapper 继续满足认证接口。
func (r *Repository) RevokeSession(ctx context.Context, sessionID string, refreshTokenHash string, revokedAtMs int64) (bool, error) {
return r.Repository.AuthRepository().RevokeSession(ctx, sessionID, refreshTokenHash, revokedAtMs)
}
// RevokeSessionWithReason 让测试 wrapper 继续满足认证接口。
func (r *Repository) RevokeSessionWithReason(ctx context.Context, sessionID string, revokedAtMs int64, reason string, requestID string, revokedBy string) (bool, error) {
return r.Repository.AuthRepository().RevokeSessionWithReason(ctx, sessionID, revokedAtMs, reason, requestID, revokedBy)
}
// FindThirdPartyIdentity 让测试 wrapper 继续满足认证接口。
func (r *Repository) FindThirdPartyIdentity(ctx context.Context, provider string, providerSubject string) (authdomain.ThirdPartyIdentity, error) {
return r.Repository.AuthRepository().FindThirdPartyIdentity(ctx, provider, providerSubject)
}
// FindUserIDByRegisterDeviceID 让测试 wrapper 继续满足认证接口。
func (r *Repository) FindUserIDByRegisterDeviceID(ctx context.Context, appCode string, deviceID string) (int64, error) {
return r.Repository.AuthRepository().FindUserIDByRegisterDeviceID(ctx, appCode, deviceID)
}
// CreateThirdPartyUser 让测试 wrapper 继续满足认证接口。
func (r *Repository) CreateThirdPartyUser(ctx context.Context, user userdomain.User, identity userdomain.Identity, thirdParty authdomain.ThirdPartyIdentity, session authdomain.Session, invite invitedomain.BindCommand) error {
return r.Repository.AuthRepository().CreateThirdPartyUser(ctx, user, identity, thirdParty, session, invite)
}
// RecordLoginAudit 让测试 wrapper 继续满足认证接口。
func (r *Repository) RecordLoginAudit(ctx context.Context, audit authdomain.LoginAudit) error {
return r.Repository.AuthRepository().RecordLoginAudit(ctx, audit)
}
// CreateLoginIPRiskJob 让测试 wrapper 继续满足认证接口。
func (r *Repository) CreateLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error {
return r.Repository.AuthRepository().CreateLoginIPRiskJob(ctx, job)
}
// ClaimLoginIPRiskJobs 让测试 wrapper 继续满足认证接口。
func (r *Repository) ClaimLoginIPRiskJobs(ctx context.Context, workerID string, nowMs int64, lockTTL time.Duration, batchSize int) ([]authdomain.LoginIPRiskJob, error) {
return r.Repository.AuthRepository().ClaimLoginIPRiskJobs(ctx, workerID, nowMs, lockTTL, batchSize)
}
// CompleteLoginIPRiskJob 让测试 wrapper 继续满足认证接口。
func (r *Repository) CompleteLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error {
return r.Repository.AuthRepository().CompleteLoginIPRiskJob(ctx, job)
}
// CreateLoginIPRiskDecision 让测试 wrapper 继续满足认证接口。
func (r *Repository) CreateLoginIPRiskDecision(ctx context.Context, decision authdomain.LoginIPRiskDecision) error {
return r.Repository.AuthRepository().CreateLoginIPRiskDecision(ctx, decision)
}
// ListEnabledLoginRiskCountryBlocks 让测试 wrapper 继续满足认证风控接口。
func (r *Repository) ListEnabledLoginRiskCountryBlocks(ctx context.Context) ([]authdomain.LoginRiskCountryBlock, error) {
return r.Repository.AuthRepository().ListEnabledLoginRiskCountryBlocks(ctx)
}
// MarkLoginAuditBlocked 让测试 wrapper 继续满足认证接口。
func (r *Repository) MarkLoginAuditBlocked(ctx context.Context, requestID string, userID int64, countryCode string, blockReason string) error {
return r.Repository.AuthRepository().MarkLoginAuditBlocked(ctx, requestID, userID, countryCode, blockReason)
}
// PutUser seeds or overwrites a user row plus its active default display_user_id.
func (r *Repository) PutUser(user userdomain.User) {
r.t.Helper()
if user.DefaultDisplayUserID == "" {
user.DefaultDisplayUserID = user.CurrentDisplayUserID
}
if user.CurrentDisplayUserID == "" {
user.CurrentDisplayUserID = user.DefaultDisplayUserID
}
user.AppCode = appcode.Normalize(user.AppCode)
if user.CurrentDisplayUserIDKind == "" {
user.CurrentDisplayUserIDKind = userdomain.DisplayUserIDKindDefault
}
if user.Status == "" {
user.Status = userdomain.StatusActive
}
if user.OnboardingStatus == "" {
// 测试种子默认模拟三方刚创建但未完成注册资料的用户。
if user.ProfileCompleted {
user.OnboardingStatus = userdomain.OnboardingStatusCompleted
} else {
user.OnboardingStatus = userdomain.OnboardingStatusProfileRequired
}
}
nowMs := user.UpdatedAtMs
if nowMs == 0 {
nowMs = time.Now().UnixMilli()
}
if user.CreatedAtMs == 0 {
user.CreatedAtMs = nowMs
}
user.UpdatedAtMs = nowMs
_, err := r.schema.DB.ExecContext(context.Background(), `
INSERT INTO users (
app_code, user_id, default_display_user_id, current_display_user_id, current_display_user_id_kind,
current_display_user_id_expires_at_ms, username, gender, country, region_id, invite_code,
register_ip, register_user_agent, country_by_ip, register_device_id, register_device,
os_version, avatar, birth_date, app_version, build_number, source, install_channel,
campaign, platform, language, timezone, profile_completed, profile_completed_at_ms,
onboarding_status, status, created_at_ms, updated_at_ms
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
app_code = VALUES(app_code),
default_display_user_id = VALUES(default_display_user_id),
current_display_user_id = VALUES(current_display_user_id),
current_display_user_id_kind = VALUES(current_display_user_id_kind),
current_display_user_id_expires_at_ms = VALUES(current_display_user_id_expires_at_ms),
username = VALUES(username),
gender = VALUES(gender),
country = VALUES(country),
region_id = VALUES(region_id),
invite_code = VALUES(invite_code),
register_ip = VALUES(register_ip),
register_user_agent = VALUES(register_user_agent),
country_by_ip = VALUES(country_by_ip),
register_device_id = VALUES(register_device_id),
register_device = VALUES(register_device),
os_version = VALUES(os_version),
avatar = VALUES(avatar),
birth_date = VALUES(birth_date),
app_version = VALUES(app_version),
build_number = VALUES(build_number),
source = VALUES(source),
install_channel = VALUES(install_channel),
campaign = VALUES(campaign),
platform = VALUES(platform),
language = VALUES(language),
timezone = VALUES(timezone),
profile_completed = VALUES(profile_completed),
profile_completed_at_ms = VALUES(profile_completed_at_ms),
onboarding_status = VALUES(onboarding_status),
status = VALUES(status),
updated_at_ms = VALUES(updated_at_ms)
`, user.AppCode, user.UserID, user.DefaultDisplayUserID, user.CurrentDisplayUserID, string(user.CurrentDisplayUserIDKind), nullableInt64(user.CurrentDisplayUserIDExpiresAtMs), nullableString(user.Username), nullableString(user.Gender), nullableString(user.Country), nullableInt64(user.RegionID), nullableString(user.InviteCode), nullableString(user.RegisterIP), nullableString(user.RegisterUserAgent), nullableString(user.CountryByIP), nullableString(user.RegisterDeviceID), nullableString(user.RegisterDevice), nullableString(user.RegisterOSVersion), nullableString(user.Avatar), nullableString(user.BirthDate), nullableString(user.RegisterAppVersion), nullableString(user.RegisterBuildNumber), nullableString(user.RegisterSource), nullableString(user.RegisterInstallChannel), nullableString(user.RegisterCampaign), nullableString(user.RegisterPlatform), nullableString(user.Language), nullableString(user.Timezone), user.ProfileCompleted, user.ProfileCompletedAtMs, string(user.OnboardingStatus), string(user.Status), user.CreatedAtMs, user.UpdatedAtMs)
if err != nil {
r.t.Fatalf("seed user %d failed: %v", user.UserID, err)
}
_, err = r.schema.DB.ExecContext(context.Background(), `
INSERT INTO user_display_user_ids (app_code, display_user_id, user_id, display_user_id_kind, status, assigned_at_ms, activated_at_ms, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
user_id = VALUES(user_id),
display_user_id_kind = VALUES(display_user_id_kind),
status = VALUES(status),
activated_at_ms = VALUES(activated_at_ms),
updated_at_ms = VALUES(updated_at_ms)
`, user.AppCode, user.DefaultDisplayUserID, user.UserID, string(userdomain.DisplayUserIDKindDefault), string(userdomain.DisplayUserIDStatusActive), user.CreatedAtMs, user.CreatedAtMs, user.CreatedAtMs, user.UpdatedAtMs)
if err != nil {
r.t.Fatalf("seed display_user_id %s failed: %v", user.DefaultDisplayUserID, err)
}
}
// PutCountry seeds or overwrites a country row for user-service tests.
// 国家创建、启用、禁用属于 admin-server 后台能力user-service 测试只直接准备主数据状态。
func (r *Repository) PutCountry(country userdomain.Country) userdomain.Country {
r.t.Helper()
country.CountryCode = userdomain.NormalizeCountryCode(country.CountryCode)
country.ISOAlpha3 = userdomain.NormalizeISOAlpha3(country.ISOAlpha3)
country.ISONumeric = userdomain.NormalizeISONumeric(country.ISONumeric)
country.PhoneCountryCode = userdomain.NormalizePhoneCountryCode(country.PhoneCountryCode)
country.CountryName = strings.TrimSpace(country.CountryName)
country.CountryDisplayName = strings.TrimSpace(country.CountryDisplayName)
country.Flag = strings.TrimSpace(country.Flag)
if country.CountryName == "" {
country.CountryName = country.CountryCode + " Name"
}
if country.CountryDisplayName == "" {
country.CountryDisplayName = country.CountryCode + " Display"
}
nowMs := country.UpdatedAtMs
if nowMs == 0 {
nowMs = time.Now().UnixMilli()
}
if country.CreatedAtMs == 0 {
country.CreatedAtMs = nowMs
}
country.UpdatedAtMs = nowMs
result, err := r.schema.DB.ExecContext(context.Background(), `
INSERT INTO countries (
country_id, country_name, country_code, iso_alpha3, iso_numeric, country_display_name,
phone_country_code, flag, enabled, sort_order,
created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
country_name = VALUES(country_name),
iso_alpha3 = VALUES(iso_alpha3),
iso_numeric = VALUES(iso_numeric),
country_display_name = VALUES(country_display_name),
phone_country_code = VALUES(phone_country_code),
flag = VALUES(flag),
enabled = VALUES(enabled),
sort_order = VALUES(sort_order),
updated_by_user_id = VALUES(updated_by_user_id),
updated_at_ms = VALUES(updated_at_ms)
`, nullableInt64(country.CountryID), country.CountryName, country.CountryCode, nullableString(country.ISOAlpha3), nullableString(country.ISONumeric), country.CountryDisplayName, nullableString(country.PhoneCountryCode), country.Flag, country.Enabled, country.SortOrder, nullableInt64(country.CreatedByUserID), nullableInt64(country.UpdatedByUserID), country.CreatedAtMs, country.UpdatedAtMs)
if err != nil {
r.t.Fatalf("seed country %s failed: %v", country.CountryCode, err)
}
if country.CountryID == 0 {
country.CountryID, err = result.LastInsertId()
if err != nil {
r.t.Fatalf("read seeded country %s id failed: %v", country.CountryCode, err)
}
}
return country
}
// PutRegion seeds a region, its active country mappings, and rebuild tasks for user-service tests.
// 区域创建属于 admin-server 后台能力user-service 测试只直接准备区域主数据状态。
func (r *Repository) PutRegion(region userdomain.Region) userdomain.Region {
r.t.Helper()
region.RegionCode = userdomain.NormalizeRegionCode(region.RegionCode)
region.AppCode = appcode.Normalize(region.AppCode)
region.Name = strings.TrimSpace(region.Name)
if region.Name == "" {
region.Name = region.RegionCode + " Region"
}
if region.Status == "" {
region.Status = userdomain.RegionStatusActive
}
nowMs := region.UpdatedAtMs
if nowMs == 0 {
nowMs = time.Now().UnixMilli()
}
if region.CreatedAtMs == 0 {
region.CreatedAtMs = nowMs
}
region.UpdatedAtMs = nowMs
tx, err := r.schema.DB.BeginTx(context.Background(), nil)
if err != nil {
r.t.Fatalf("begin seed region %s failed: %v", region.RegionCode, err)
}
defer tx.Rollback()
result, err := tx.ExecContext(context.Background(), `
INSERT INTO regions (app_code, region_id, region_code, name, status, sort_order, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
region_code = VALUES(region_code),
name = VALUES(name),
status = VALUES(status),
sort_order = VALUES(sort_order),
updated_by_user_id = VALUES(updated_by_user_id),
updated_at_ms = VALUES(updated_at_ms)
`, region.AppCode, nullableInt64(region.RegionID), region.RegionCode, region.Name, region.Status, region.SortOrder, nullableInt64(region.CreatedByUserID), nullableInt64(region.UpdatedByUserID), region.CreatedAtMs, region.UpdatedAtMs)
if err != nil {
r.t.Fatalf("seed region %s failed: %v", region.RegionCode, err)
}
if region.RegionID == 0 {
region.RegionID, err = result.LastInsertId()
if err != nil {
r.t.Fatalf("read seeded region %s id failed: %v", region.RegionCode, err)
}
}
countries := make([]string, 0, len(region.Countries))
for _, input := range region.Countries {
country := userdomain.NormalizeCountryCode(input)
countries = append(countries, country)
if _, err := tx.ExecContext(context.Background(), `
UPDATE region_countries
SET status = ?, updated_at_ms = ?
WHERE app_code = ? AND country_code = ? AND status = ?
`, "inactive", region.UpdatedAtMs, region.AppCode, country, userdomain.RegionStatusActive); err != nil {
r.t.Fatalf("deactivate existing region country %s failed: %v", country, err)
}
if _, err := tx.ExecContext(context.Background(), `
INSERT INTO region_countries (app_code, region_id, country_code, status, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, region.AppCode, region.RegionID, country, userdomain.RegionStatusActive, nullableInt64(region.CreatedByUserID), nullableInt64(region.UpdatedByUserID), region.CreatedAtMs, region.UpdatedAtMs); err != nil {
r.t.Fatalf("seed region %s country %s failed: %v", region.RegionCode, country, err)
}
if _, err := tx.ExecContext(context.Background(), `
INSERT INTO user_region_rebuild_tasks (app_code, region_id, country_code, target_region_id, mapping_revision, status, cursor_user_id, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
`, region.AppCode, nullableInt64(region.RegionID), country, nullableInt64(region.RegionID), region.UpdatedAtMs, userdomain.RegionRebuildTaskStatusPending, region.CreatedAtMs, region.UpdatedAtMs); err != nil {
r.t.Fatalf("seed region %s rebuild task for %s failed: %v", region.RegionCode, country, err)
}
}
if err := tx.Commit(); err != nil {
r.t.Fatalf("commit seed region %s failed: %v", region.RegionCode, err)
}
region.Countries = countries
return region
}
// SetCountryEnabled directly prepares country enabled state for user-service tests.
func (r *Repository) SetCountryEnabled(countryID int64, enabled bool) {
r.t.Helper()
result, err := r.schema.DB.ExecContext(context.Background(), `
UPDATE countries
SET enabled = ?, updated_at_ms = ?
WHERE country_id = ?
`, enabled, time.Now().UnixMilli(), countryID)
if err != nil {
r.t.Fatalf("set country %d enabled=%t failed: %v", countryID, enabled, err)
}
if affected, err := result.RowsAffected(); err != nil {
r.t.Fatalf("read country %d affected rows failed: %v", countryID, err)
} else if affected == 0 {
r.t.Fatalf("country %d not found", countryID)
}
}
// PutHostProfile seeds a host profile fact for host-domain tests.
func (r *Repository) PutHostProfile(profile hostdomain.HostProfile) {
r.t.Helper()
if profile.Status == "" {
profile.Status = hostdomain.HostStatusActive
}
if profile.Source == "" {
profile.Source = "test"
}
nowMs := profile.UpdatedAtMs
if nowMs == 0 {
nowMs = time.Now().UnixMilli()
}
if profile.CreatedAtMs == 0 {
profile.CreatedAtMs = nowMs
}
if profile.FirstBecameHostAtMs == 0 {
profile.FirstBecameHostAtMs = profile.CreatedAtMs
}
profile.UpdatedAtMs = nowMs
_, err := r.schema.DB.ExecContext(context.Background(), `
INSERT INTO host_profiles (
user_id, status, region_id, current_agency_id, current_membership_id, source,
first_became_host_at_ms, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
status = VALUES(status),
region_id = VALUES(region_id),
current_agency_id = VALUES(current_agency_id),
current_membership_id = VALUES(current_membership_id),
source = VALUES(source),
updated_at_ms = VALUES(updated_at_ms)
`, profile.UserID, profile.Status, profile.RegionID, nullableInt64(profile.CurrentAgencyID), nullableInt64(profile.CurrentMembershipID), profile.Source, profile.FirstBecameHostAtMs, profile.CreatedAtMs, profile.UpdatedAtMs)
if err != nil {
r.t.Fatalf("seed host profile %d failed: %v", profile.UserID, err)
}
}
// PutBDProfile seeds a BD or BD Leader profile fact for host-domain tests.
func (r *Repository) PutBDProfile(profile hostdomain.BDProfile) {
r.t.Helper()
if profile.Role == "" {
profile.Role = hostdomain.BDRoleBD
}
if profile.Status == "" {
profile.Status = hostdomain.BDStatusActive
}
nowMs := profile.UpdatedAtMs
if nowMs == 0 {
nowMs = time.Now().UnixMilli()
}
if profile.CreatedAtMs == 0 {
profile.CreatedAtMs = nowMs
}
profile.UpdatedAtMs = nowMs
_, err := r.schema.DB.ExecContext(context.Background(), `
INSERT INTO bd_profiles (
user_id, role, region_id, parent_leader_user_id, status,
created_by_user_id, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
role = VALUES(role),
region_id = VALUES(region_id),
parent_leader_user_id = VALUES(parent_leader_user_id),
status = VALUES(status),
updated_at_ms = VALUES(updated_at_ms)
`, profile.UserID, profile.Role, profile.RegionID, nullableInt64(profile.ParentLeaderUserID), profile.Status, profile.CreatedByUserID, profile.CreatedAtMs, profile.UpdatedAtMs)
if err != nil {
r.t.Fatalf("seed bd profile %d failed: %v", profile.UserID, err)
}
}
// PutAgency seeds an active Agency fact for host-domain tests.
func (r *Repository) PutAgency(agency hostdomain.Agency) {
r.t.Helper()
if agency.Status == "" {
agency.Status = hostdomain.AgencyStatusActive
}
if agency.Name == "" {
agency.Name = "Test Agency"
}
nowMs := agency.UpdatedAtMs
if nowMs == 0 {
nowMs = time.Now().UnixMilli()
}
if agency.CreatedAtMs == 0 {
agency.CreatedAtMs = nowMs
}
agency.UpdatedAtMs = nowMs
_, err := r.schema.DB.ExecContext(context.Background(), `
INSERT INTO agencies (
agency_id, owner_user_id, region_id, parent_bd_user_id, name, status,
join_enabled, max_hosts, created_by_user_id, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
owner_user_id = VALUES(owner_user_id),
region_id = VALUES(region_id),
parent_bd_user_id = VALUES(parent_bd_user_id),
name = VALUES(name),
status = VALUES(status),
join_enabled = VALUES(join_enabled),
max_hosts = VALUES(max_hosts),
updated_at_ms = VALUES(updated_at_ms)
`, agency.AgencyID, agency.OwnerUserID, agency.RegionID, agency.ParentBDUserID, agency.Name, agency.Status, agency.JoinEnabled, agency.MaxHosts, agency.CreatedByUserID, agency.CreatedAtMs, agency.UpdatedAtMs)
if err != nil {
r.t.Fatalf("seed agency %d failed: %v", agency.AgencyID, err)
}
}
func nullableString(value string) any {
if strings.TrimSpace(value) == "" {
return nil
}
return value
}
func nullableInt64(value int64) any {
if value <= 0 {
return nil
}
return value
}