706 lines
32 KiB
Go
706 lines
32 KiB
Go
// 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)
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
// 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
|
||
}
|