2026-04-29 12:43:05 +08:00

575 lines
23 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 user_test 验证 user-service 用户主数据、短号和靓号用例。
package user_test
import (
"context"
"strings"
"testing"
"time"
"hyapp/pkg/xerr"
userdomain "hyapp/services/user-service/internal/domain/user"
userservice "hyapp/services/user-service/internal/service/user"
"hyapp/services/user-service/internal/testutil/mysqltest"
)
type sequenceIDGenerator struct {
// values 是测试预设 user_id 序列。
values []int64
// index 指向当前返回值。
index int
}
func (g *sequenceIDGenerator) NewInt64() int64 {
// 到达末尾后保持最后一个值,便于测试重试行为。
value := g.values[g.index]
if g.index < len(g.values)-1 {
g.index++
}
return value
}
type sequenceDisplayUserIDAllocator struct {
// values 是测试预设短号候选。
values []string
}
func (a sequenceDisplayUserIDAllocator) NextDisplayUserID(_ int64, attempt int) string {
if attempt >= len(a.values) {
// 超出候选数量后复用最后一个值。
return a.values[len(a.values)-1]
}
return a.values[attempt]
}
func strptr(value string) *string {
return &value
}
func seedCountry(t *testing.T, repository *mysqltest.Repository, code string) userdomain.Country {
t.Helper()
if country, ok, err := repository.ResolveActiveCountryByCode(context.Background(), code); err != nil {
t.Fatalf("resolve country %s failed: %v", code, err)
} else if ok {
return country
}
svc := userservice.New(repository)
country, err := svc.CreateCountry(context.Background(), code+" Name", code, code+" Display", 0, 1, "seed-country-"+code)
if err != nil {
t.Fatalf("seed country %s failed: %v", code, err)
}
return country
}
func seedRegion(t *testing.T, repository *mysqltest.Repository, code string, countries []string) userdomain.Region {
t.Helper()
svc := userservice.New(repository)
region, err := svc.CreateRegion(context.Background(), code, code+" Region", countries, 0, 1, "seed-region-"+code)
if err != nil {
t.Fatalf("seed region %s failed: %v", code, err)
}
return region
}
// TestGetUserUsesRepository 验证 user-service 查询用例只依赖 repository 接口。
func TestGetUserUsesRepository(t *testing.T) {
repository := mysqltest.NewRepository(t)
// PutUser 是测试辅助入口,直接准备用户主记录。
repository.PutUser(userdomain.User{UserID: 10001, CurrentDisplayUserID: "100001", Status: userdomain.StatusActive})
svc := userservice.New(repository)
user, err := svc.GetUser(context.Background(), 10001)
if err != nil {
t.Fatalf("GetUser failed: %v", err)
}
if user.UserID != 10001 || user.Status != userdomain.StatusActive {
t.Fatalf("unexpected user: %+v", user)
}
}
func TestUpdateUserProfile(t *testing.T) {
repository := mysqltest.NewRepository(t)
repository.PutUser(userdomain.User{
UserID: 10001,
CurrentDisplayUserID: "100001",
Username: "old-name",
Avatar: "old-avatar",
BirthDate: "1999-01-01",
Status: userdomain.StatusActive,
})
now := time.UnixMilli(2000)
svc := userservice.New(repository, userservice.WithClock(func() time.Time { return now }))
user, err := svc.UpdateUserProfile(context.Background(), 10001, strptr(" new-name "), strptr(" https://cdn.example/a.png "), strptr("2000-01-02"))
if err != nil {
t.Fatalf("UpdateUserProfile failed: %v", err)
}
if user.Username != "new-name" || user.Avatar != "https://cdn.example/a.png" || user.BirthDate != "2000-01-02" || user.UpdatedAtMs != 2000 {
t.Fatalf("profile update mismatch: %+v", user)
}
_, err = svc.UpdateUserProfile(context.Background(), 10001, nil, nil, strptr("2000/01/02"))
if !xerr.IsCode(err, xerr.InvalidArgument) {
t.Fatalf("expected invalid birth format, got %v", err)
}
invalidCases := []struct {
name string
username *string
avatar *string
birth *string
}{
{
name: "empty username",
username: strptr(" "),
},
{
name: "too long username",
username: strptr(strings.Repeat("a", userdomain.ProfileUsernameMaxRunes+1)),
},
{
name: "too long avatar",
avatar: strptr("https://cdn.example/" + strings.Repeat("a", userdomain.ProfileAvatarMaxRunes)),
},
{
name: "non http avatar",
avatar: strptr("ftp://cdn.example/a.png"),
},
}
for _, tc := range invalidCases {
t.Run(tc.name, func(t *testing.T) {
_, err := svc.UpdateUserProfile(context.Background(), 10001, tc.username, tc.avatar, tc.birth)
if !xerr.IsCode(err, xerr.InvalidArgument) {
t.Fatalf("expected invalid profile update, got %v", err)
}
})
}
user, err = svc.UpdateUserProfile(context.Background(), 10001, nil, strptr(""), strptr(""))
if err != nil {
t.Fatalf("clearing optional profile fields failed: %v", err)
}
if user.Avatar != "" || user.BirthDate != "" {
t.Fatalf("optional profile fields should clear: %+v", user)
}
}
func TestChangeUserCountryWritesCooldown(t *testing.T) {
repository := mysqltest.NewRepository(t)
seedCountry(t, repository, "CN")
seedCountry(t, repository, "US")
seedCountry(t, repository, "JP")
repository.PutUser(userdomain.User{UserID: 10001, CurrentDisplayUserID: "100001", Country: "CN", Status: userdomain.StatusActive})
current := time.UnixMilli(1000)
svc := userservice.New(repository,
userservice.WithClock(func() time.Time { return current }),
userservice.WithCountryChangeCooldown(time.Hour),
)
user, nextAllowedAt, err := svc.ChangeUserCountry(context.Background(), 10001, " US ", "req-country-1")
if err != nil {
t.Fatalf("ChangeUserCountry failed: %v", err)
}
if user.Country != "US" || nextAllowedAt != current.Add(time.Hour).UnixMilli() {
t.Fatalf("country change mismatch: user=%+v next=%d", user, nextAllowedAt)
}
current = current.Add(10 * time.Minute)
if _, _, err := svc.ChangeUserCountry(context.Background(), 10001, "JP", "req-country-2"); !xerr.IsCode(err, xerr.CountryChangeCooldown) {
t.Fatalf("expected country cooldown, got %v", err)
}
if user, _, err := svc.ChangeUserCountry(context.Background(), 10001, "US", "req-country-same"); err != nil || user.Country != "US" {
// 相同国家重复提交不写新日志,也不被冷却期阻断。
t.Fatalf("same country should be idempotent: user=%+v err=%v", user, err)
}
current = current.Add(2 * time.Hour)
user, _, err = svc.ChangeUserCountry(context.Background(), 10001, "JP", "req-country-3")
if err != nil {
t.Fatalf("ChangeUserCountry after cooldown failed: %v", err)
}
if user.Country != "JP" {
t.Fatalf("country after cooldown mismatch: %+v", user)
}
}
func TestCountryAndRegionAdminValidation(t *testing.T) {
// 国家管理要接受小写归一同时拒绝非法国家码、alias 国家码和重复区域归属。
ctx := context.Background()
repository := mysqltest.NewRepository(t)
svc := userservice.New(repository)
aa, err := svc.CreateCountry(ctx, "Alpha Area", " aa ", "测试国家", 10, 1, "req-aa")
if err != nil {
t.Fatalf("CreateCountry AA failed: %v", err)
}
if aa.CountryCode != "AA" {
t.Fatalf("country code should normalize to AA: %+v", aa)
}
tla, err := svc.CreateCountry(ctx, "Three Letter Land", "tla", "三字码国家", 30, 1, "req-tla")
if err != nil {
t.Fatalf("CreateCountry TLA failed: %v", err)
}
if tla.CountryCode != "TLA" {
t.Fatalf("three-letter canonical country code mismatch: %+v", tla)
}
if _, err := svc.CreateCountry(ctx, "United States", "usa", "美国", 21, 1, "req-usa"); !xerr.IsCode(err, xerr.Conflict) {
t.Fatalf("expected alias country conflict, got %v", err)
}
if _, err := svc.CreateCountry(ctx, "Bad", "C1", "Bad", 0, 1, "req-bad"); !xerr.IsCode(err, xerr.InvalidArgument) {
t.Fatalf("expected invalid country code, got %v", err)
}
sea, err := svc.CreateRegion(ctx, "sea", "Southeast Asia", []string{"AA"}, 0, 1, "req-sea")
if err != nil {
t.Fatalf("CreateRegion failed: %v", err)
}
if sea.RegionCode != "SEA" || len(sea.Countries) != 1 || sea.Countries[0] != "AA" {
t.Fatalf("region normalization mismatch: %+v", sea)
}
if _, err := svc.CreateRegion(ctx, "APAC", "Asia Pacific", []string{"AA"}, 0, 1, "req-apac"); !xerr.IsCode(err, xerr.RegionCountryConflict) {
t.Fatalf("expected region country conflict, got %v", err)
}
}
func TestCountryAliasCanonicalizesUserAndRegionWrites(t *testing.T) {
// alpha-3 alias 只用于输入兼容users.country 和 region_countries 都必须写 canonical alpha-2。
ctx := context.Background()
repository := mysqltest.NewRepository(t)
seedCountry(t, repository, "US")
repository.PutUser(userdomain.User{UserID: 10001, CurrentDisplayUserID: "100001", Status: userdomain.StatusActive})
current := time.UnixMilli(1000)
svc := userservice.New(repository,
userservice.WithClock(func() time.Time { return current }),
userservice.WithCountryChangeCooldown(0),
)
region, err := svc.CreateRegion(ctx, "NA", "North America", []string{"usa"}, 0, 1, "req-na")
if err != nil {
t.Fatalf("CreateRegion with USA alias failed: %v", err)
}
if len(region.Countries) != 1 || region.Countries[0] != "US" {
t.Fatalf("region countries should store canonical code: %+v", region)
}
user, _, err := svc.ChangeUserCountry(ctx, 10001, "USA", "req-country-usa")
if err != nil {
t.Fatalf("ChangeUserCountry with USA alias failed: %v", err)
}
if user.Country != "US" || user.RegionID != region.RegionID || user.RegionCode != "NA" {
t.Fatalf("user country alias was not canonicalized: %+v", user)
}
if _, err := svc.ReplaceRegionCountries(ctx, region.RegionID, []string{"US", "USA"}, 1, "req-dup-alias"); !xerr.IsCode(err, xerr.InvalidArgument) {
t.Fatalf("expected duplicate canonical countries to be rejected, got %v", err)
}
}
func TestChangeUserCountryUpdatesRegionAndSameCountryRepairsStaleRegion(t *testing.T) {
// 改国家同步重算 region_id相同国家不写冷却日志但允许修正历史 stale region_id。
ctx := context.Background()
repository := mysqltest.NewRepository(t)
seedCountry(t, repository, "US")
seedCountry(t, repository, "SG")
region := seedRegion(t, repository, "SEA", []string{"SG"})
repository.PutUser(userdomain.User{UserID: 10001, CurrentDisplayUserID: "100001", Country: "US", Status: userdomain.StatusActive})
current := time.UnixMilli(1000)
svc := userservice.New(repository,
userservice.WithClock(func() time.Time { return current }),
userservice.WithCountryChangeCooldown(time.Hour),
)
user, _, err := svc.ChangeUserCountry(ctx, 10001, "sg", "req-sg")
if err != nil {
t.Fatalf("ChangeUserCountry SG failed: %v", err)
}
if user.Country != "SG" || user.RegionID != region.RegionID || user.RegionCode != "SEA" {
t.Fatalf("region after country change mismatch: %+v", user)
}
stale := user
stale.RegionID = 0
repository.PutUser(stale)
current = current.Add(10 * time.Minute)
user, _, err = svc.ChangeUserCountry(ctx, 10001, "SG", "req-same")
if err != nil {
t.Fatalf("same-country stale repair should not hit cooldown: %v", err)
}
if user.RegionID != region.RegionID {
t.Fatalf("same-country stale region was not repaired: %+v", user)
}
current = current.Add(10 * time.Minute)
if _, _, err := svc.ChangeUserCountry(ctx, 10001, "US", "req-cooldown"); !xerr.IsCode(err, xerr.CountryChangeCooldown) {
t.Fatalf("expected cooldown after real country change, got %v", err)
}
}
func TestChangeUserCountryRejectsUnknownOrDisabledCountry(t *testing.T) {
// 改国家不能把未配置或 disabled 国家码写入 users.country。
ctx := context.Background()
repository := mysqltest.NewRepository(t)
country := seedCountry(t, repository, "MY")
repository.PutUser(userdomain.User{UserID: 10001, CurrentDisplayUserID: "100001", Country: "", Status: userdomain.StatusActive})
svc := userservice.New(repository, userservice.WithCountryChangeCooldown(0))
if _, err := svc.DisableCountry(ctx, country.CountryID, 1, "disable-my"); err != nil {
t.Fatalf("DisableCountry failed: %v", err)
}
for _, country := range []string{"ZZ", "MY"} {
if _, _, err := svc.ChangeUserCountry(ctx, 10001, country, "req-"+country); !xerr.IsCode(err, xerr.InvalidArgument) {
t.Fatalf("expected INVALID_ARGUMENT for country %s, got %v", country, err)
}
}
}
func TestRegionRebuildWorkerUpdatesHistoricalUsers(t *testing.T) {
// 管理端新增区域后worker 必须消费 rebuild task把历史同 country 用户刷到目标 region_id。
ctx := context.Background()
repository := mysqltest.NewRepository(t)
seedCountry(t, repository, "SG")
repository.PutUser(userdomain.User{UserID: 10001, CurrentDisplayUserID: "100001", Country: "SG", Status: userdomain.StatusActive})
repository.PutUser(userdomain.User{UserID: 10002, CurrentDisplayUserID: "100002", Country: "SG", Status: userdomain.StatusActive})
current := time.UnixMilli(1000)
svc := userservice.New(repository, userservice.WithClock(func() time.Time { return current }))
region, err := svc.CreateRegion(ctx, "SEA", "Southeast Asia", []string{"SG"}, 0, 1, "req-sea")
if err != nil {
t.Fatalf("CreateRegion failed: %v", err)
}
current = time.UnixMilli(2000)
result, err := svc.ProcessNextRegionRebuildTask(ctx, userservice.RegionRebuildWorkerOptions{WorkerID: "test", BatchSize: 100})
if err != nil {
t.Fatalf("ProcessNextRegionRebuildTask failed: %v", err)
}
if result.Status != userdomain.RegionRebuildTaskStatusCompleted || result.ProcessedUsers != 2 {
t.Fatalf("unexpected rebuild result: %+v", result)
}
for _, userID := range []int64{10001, 10002} {
user, err := repository.GetUser(ctx, userID)
if err != nil {
t.Fatalf("GetUser %d failed: %v", userID, err)
}
if user.RegionID != region.RegionID || user.RegionCode != "SEA" {
t.Fatalf("user region was not rebuilt: %+v", user)
}
}
}
func TestRegionRebuildWorkerSkipsStaleRevision(t *testing.T) {
// 同一 country 出现更新 revision 后,旧任务必须 skipped避免旧区域覆盖新区域或清空结果。
ctx := context.Background()
repository := mysqltest.NewRepository(t)
seedCountry(t, repository, "SG")
current := time.UnixMilli(1000)
svc := userservice.New(repository, userservice.WithClock(func() time.Time { return current }))
region, err := svc.CreateRegion(ctx, "SEA", "Southeast Asia", []string{"SG"}, 0, 1, "req-sea")
if err != nil {
t.Fatalf("CreateRegion failed: %v", err)
}
repository.PutUser(userdomain.User{UserID: 10001, CurrentDisplayUserID: "100001", Country: "SG", RegionID: region.RegionID, Status: userdomain.StatusActive})
current = time.UnixMilli(2000)
if _, err := svc.DisableRegion(ctx, region.RegionID, 1, "req-disable-sea"); err != nil {
t.Fatalf("DisableRegion failed: %v", err)
}
current = time.UnixMilli(3000)
result, err := svc.ProcessNextRegionRebuildTask(ctx, userservice.RegionRebuildWorkerOptions{WorkerID: "test", BatchSize: 100})
if err != nil {
t.Fatalf("Process stale task failed: %v", err)
}
if result.Status != userdomain.RegionRebuildTaskStatusSkipped {
t.Fatalf("old task should be skipped: %+v", result)
}
current = time.UnixMilli(4000)
result, err = svc.ProcessNextRegionRebuildTask(ctx, userservice.RegionRebuildWorkerOptions{WorkerID: "test", BatchSize: 100})
if err != nil {
t.Fatalf("Process latest task failed: %v", err)
}
if result.Status != userdomain.RegionRebuildTaskStatusCompleted || result.TargetRegionID != 0 {
t.Fatalf("latest task should clear region: %+v", result)
}
user, err := repository.GetUser(ctx, 10001)
if err != nil {
t.Fatalf("GetUser failed: %v", err)
}
if user.RegionID != 0 {
t.Fatalf("stale region should be cleared by latest task: %+v", user)
}
}
// TestGetUserValidatesID 锁定 service 层的领域错误,不让 transport 层承担参数校验。
func TestGetUserValidatesID(t *testing.T) {
svc := userservice.New(mysqltest.NewRepository(t))
_, err := svc.GetUser(context.Background(), 0)
if !xerr.IsCode(err, xerr.InvalidArgument) {
t.Fatalf("expected INVALID_ARGUMENT, got %v", err)
}
}
// TestCreateUserAllocatesDisplayUserID 验证新用户创建时同时生成 user_id 和 active display_user_id。
func TestCreateUserAllocatesDisplayUserID(t *testing.T) {
repository := mysqltest.NewRepository(t)
svc := userservice.New(repository,
userservice.WithIDGenerator(&sequenceIDGenerator{values: []int64{900001}}),
userservice.WithDisplayUserIDAllocator(sequenceDisplayUserIDAllocator{values: []string{"100001"}}),
userservice.WithClock(func() time.Time { return time.UnixMilli(1000) }),
)
user, identity, err := svc.CreateUser(context.Background())
if err != nil {
t.Fatalf("CreateUser failed: %v", err)
}
if user.UserID != 900001 || identity.DisplayUserID != "100001" {
t.Fatalf("unexpected created identity: user=%+v identity=%+v", user, identity)
}
resolved, err := svc.ResolveDisplayUserID(context.Background(), "100001")
if err != nil {
t.Fatalf("ResolveDisplayUserID failed: %v", err)
}
if resolved.UserID != 900001 {
t.Fatalf("resolved user mismatch: %+v", resolved)
}
}
// TestCreateUserRetriesDisplayUserIDConflict 验证默认短号冲突时有限重试。
func TestCreateUserRetriesDisplayUserIDConflict(t *testing.T) {
repository := mysqltest.NewRepository(t)
// 先占用 100001强制 CreateUser 走第二个候选。
repository.PutUser(userdomain.User{UserID: 1, CurrentDisplayUserID: "100001", Status: userdomain.StatusActive})
svc := userservice.New(repository,
userservice.WithIDGenerator(&sequenceIDGenerator{values: []int64{900001, 900002}}),
userservice.WithDisplayUserIDAllocator(sequenceDisplayUserIDAllocator{values: []string{"100001", "100002"}}),
)
_, identity, err := svc.CreateUser(context.Background())
if err != nil {
t.Fatalf("CreateUser should retry display_user_id conflict: %v", err)
}
if identity.DisplayUserID != "100002" {
t.Fatalf("retry display_user_id mismatch: %+v", identity)
}
}
// TestResolveDisplayUserIDNotFound 锁定短号不存在的专用 reason。
func TestResolveDisplayUserIDNotFound(t *testing.T) {
svc := userservice.New(mysqltest.NewRepository(t))
_, err := svc.ResolveDisplayUserID(context.Background(), "100001")
if !xerr.IsCode(err, xerr.DisplayUserIDNotFound) {
t.Fatalf("expected DISPLAY_USER_ID_NOT_FOUND, got %v", err)
}
}
// TestChangeDisplayUserID 验证修改短号后旧短号不再解析。
func TestChangeDisplayUserID(t *testing.T) {
repository := mysqltest.NewRepository(t)
// 当前默认短号为 100001修改后旧短号应释放。
repository.PutUser(userdomain.User{UserID: 10001, CurrentDisplayUserID: "100001", Status: userdomain.StatusActive})
now := time.UnixMilli(2000)
svc := userservice.New(repository,
userservice.WithClock(func() time.Time { return now }),
userservice.WithDisplayUserIDPolicy(8, 30*24*time.Hour),
)
identity, err := svc.ChangeDisplayUserID(context.Background(), 10001, "100002", "user_request", 10001, "req-1")
if err != nil {
t.Fatalf("ChangeDisplayUserID failed: %v", err)
}
if identity.DisplayUserID != "100002" {
t.Fatalf("changed identity mismatch: %+v", identity)
}
_, err = svc.ResolveDisplayUserID(context.Background(), "100001")
if !xerr.IsCode(err, xerr.DisplayUserIDNotFound) {
t.Fatalf("expected old display_user_id to be released, got %v", err)
}
}
// TestChangeDisplayUserIDConflictAndCooldown 锁定占用和冷却期两个失败分支。
func TestChangeDisplayUserIDConflictAndCooldown(t *testing.T) {
repository := mysqltest.NewRepository(t)
// 100002 被另一个用户占用,用来验证短号冲突分支。
repository.PutUser(userdomain.User{UserID: 10001, CurrentDisplayUserID: "100001", Status: userdomain.StatusActive})
repository.PutUser(userdomain.User{UserID: 10002, CurrentDisplayUserID: "100002", Status: userdomain.StatusActive})
current := time.UnixMilli(3000)
svc := userservice.New(repository,
userservice.WithClock(func() time.Time { return current }),
userservice.WithDisplayUserIDPolicy(8, time.Hour),
)
_, err := svc.ChangeDisplayUserID(context.Background(), 10001, "100002", "conflict", 10001, "req-1")
if !xerr.IsCode(err, xerr.DisplayUserIDExists) {
t.Fatalf("expected DISPLAY_USER_ID_EXISTS, got %v", err)
}
if _, err := svc.ChangeDisplayUserID(context.Background(), 10001, "100003", "first", 10001, "req-2"); err != nil {
t.Fatalf("first change failed: %v", err)
}
current = current.Add(10 * time.Minute)
// 冷却期为 1 小时10 分钟后再次修改应被拒绝。
_, err = svc.ChangeDisplayUserID(context.Background(), 10001, "100004", "second", 10001, "req-3")
if !xerr.IsCode(err, xerr.DisplayUserIDCooldown) {
t.Fatalf("expected DISPLAY_USER_ID_COOLDOWN, got %v", err)
}
}
// TestApplyPrettyDisplayUserIDOverridesAndExpires 锁定“临时靓号覆盖当前展示号,过期恢复默认短号”的核心语义。
func TestApplyPrettyDisplayUserIDOverridesAndExpires(t *testing.T) {
// 覆盖靓号申请、默认号 held、靓号 active 解析、过期后默认号恢复。
repository := mysqltest.NewRepository(t)
current := time.UnixMilli(1000)
svc := userservice.New(repository,
userservice.WithIDGenerator(&sequenceIDGenerator{values: []int64{900001}}),
userservice.WithDisplayUserIDAllocator(sequenceDisplayUserIDAllocator{values: []string{"100001"}}),
userservice.WithClock(func() time.Time { return current }),
)
user, _, err := svc.CreateUser(context.Background())
if err != nil {
t.Fatalf("CreateUser failed: %v", err)
}
identity, leaseID, err := svc.ApplyPrettyDisplayUserID(context.Background(), user.UserID, "888888", 1000, "receipt-1", "req-pretty")
if err != nil {
t.Fatalf("ApplyPrettyDisplayUserID failed: %v", err)
}
if leaseID == "" || identity.DisplayUserID != "888888" || identity.DefaultDisplayUserID != "100001" || identity.DisplayUserIDKind != userdomain.DisplayUserIDKindPretty {
t.Fatalf("unexpected pretty identity: identity=%+v lease=%s", identity, leaseID)
}
if _, _, err := svc.ApplyPrettyDisplayUserID(context.Background(), user.UserID, "999999", 1000, "receipt-2", "req-pretty-2"); !xerr.IsCode(err, xerr.DisplayUserIDPrettyActive) {
// active 靓号期间不能再申请第二个靓号。
t.Fatalf("expected DISPLAY_USER_ID_PRETTY_ACTIVE, got %v", err)
}
if resolved, err := svc.ResolveDisplayUserID(context.Background(), "888888"); err != nil || resolved.UserID != user.UserID {
t.Fatalf("pretty display_user_id should resolve while active: identity=%+v err=%v", resolved, err)
}
if _, err := svc.ResolveDisplayUserID(context.Background(), "100001"); !xerr.IsCode(err, xerr.DisplayUserIDNotFound) {
t.Fatalf("default display_user_id should be held while pretty is active, got %v", err)
}
current = time.UnixMilli(2500)
// Resolve 默认号会触发懒过期恢复。
resolved, err := svc.ResolveDisplayUserID(context.Background(), "100001")
if err != nil {
t.Fatalf("default display_user_id should recover after pretty expires: %v", err)
}
if resolved.DisplayUserID != "100001" || resolved.DisplayUserIDKind != userdomain.DisplayUserIDKindDefault {
t.Fatalf("unexpected recovered identity: %+v", resolved)
}
if _, err := svc.ResolveDisplayUserID(context.Background(), "888888"); !xerr.IsCode(err, xerr.DisplayUserIDNotFound) {
t.Fatalf("expired pretty display_user_id should not resolve, got %v", err)
}
}