2026-07-10 12:54:45 +00:00
|
|
|
|
package usecase_test
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"fmt"
|
2026-07-13 01:15:30 +00:00
|
|
|
|
"strings"
|
2026-07-10 12:54:45 +00:00
|
|
|
|
"sync"
|
|
|
|
|
|
"testing"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"apps/backend/internal/module/member/domain"
|
|
|
|
|
|
"apps/backend/internal/module/member/usecase"
|
|
|
|
|
|
tokenUC "apps/backend/internal/module/token/usecase"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// fakeRepo is test-only (not a production memory store).
|
|
|
|
|
|
type fakeRepo struct {
|
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
|
byUID map[int64]*domain.Member
|
|
|
|
|
|
byEmail map[string]int64
|
|
|
|
|
|
byPhone map[string]int64
|
|
|
|
|
|
identities map[string]*domain.Identity // platform:login_id
|
|
|
|
|
|
byUIDIds map[int64][]string
|
|
|
|
|
|
seq int64
|
|
|
|
|
|
codes map[string]string
|
|
|
|
|
|
refresh map[string]int64
|
|
|
|
|
|
settings map[int64]*domain.UserSettings
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func newFake() *fakeRepo {
|
|
|
|
|
|
return &fakeRepo{
|
|
|
|
|
|
byUID: map[int64]*domain.Member{}, byEmail: map[string]int64{}, byPhone: map[string]int64{},
|
|
|
|
|
|
identities: map[string]*domain.Identity{}, byUIDIds: map[int64][]string{},
|
|
|
|
|
|
seq: domain.MinMemberUID, codes: map[string]string{}, refresh: map[string]int64{},
|
|
|
|
|
|
settings: map[int64]*domain.UserSettings{},
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func idKey(loginID, platform string) string { return platform + ":" + loginID }
|
|
|
|
|
|
|
|
|
|
|
|
func (f *fakeRepo) NextUID(ctx context.Context) (int64, error) {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
f.seq++
|
|
|
|
|
|
if f.seq < domain.MinMemberUID {
|
|
|
|
|
|
f.seq = domain.MinMemberUID + 1
|
|
|
|
|
|
}
|
|
|
|
|
|
return f.seq, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) CreateMember(ctx context.Context, m *domain.Member) error {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
if m.Email != "" {
|
|
|
|
|
|
if _, ok := f.byEmail[m.Email]; ok {
|
|
|
|
|
|
return domain.ErrEmailTaken
|
|
|
|
|
|
}
|
|
|
|
|
|
f.byEmail[m.Email] = m.UID
|
|
|
|
|
|
}
|
|
|
|
|
|
if m.Phone != "" {
|
|
|
|
|
|
f.byPhone[m.Phone] = m.UID
|
|
|
|
|
|
}
|
|
|
|
|
|
cp := *m
|
|
|
|
|
|
f.byUID[m.UID] = &cp
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) FindByUID(ctx context.Context, uid int64) (*domain.Member, error) {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
m, ok := f.byUID[uid]
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return nil, domain.ErrNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
cp := *m
|
|
|
|
|
|
return &cp, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) FindByEmail(ctx context.Context, email string) (*domain.Member, error) {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
uid, ok := f.byEmail[email]
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return nil, domain.ErrNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
cp := *f.byUID[uid]
|
|
|
|
|
|
return &cp, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) FindByPhone(ctx context.Context, phone string) (*domain.Member, error) {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
uid, ok := f.byPhone[phone]
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return nil, domain.ErrNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
cp := *f.byUID[uid]
|
|
|
|
|
|
return &cp, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) UpdateMember(ctx context.Context, m *domain.Member) error {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
if _, ok := f.byUID[m.UID]; !ok {
|
|
|
|
|
|
return domain.ErrNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
cp := *m
|
|
|
|
|
|
f.byUID[m.UID] = &cp
|
|
|
|
|
|
if m.Email != "" {
|
|
|
|
|
|
f.byEmail[m.Email] = m.UID
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) ListPage(ctx context.Context, page, pageSize int, query, status string) ([]*domain.Member, int64, error) {
|
2026-07-13 01:15:30 +00:00
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
if page < 1 {
|
|
|
|
|
|
page = 1
|
|
|
|
|
|
}
|
|
|
|
|
|
if pageSize < 1 {
|
|
|
|
|
|
pageSize = 20
|
|
|
|
|
|
}
|
|
|
|
|
|
query = strings.ToLower(strings.TrimSpace(query))
|
|
|
|
|
|
var all []*domain.Member
|
|
|
|
|
|
for _, m := range f.byUID {
|
|
|
|
|
|
if status != "" && m.Status != status {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if query != "" {
|
|
|
|
|
|
uidStr := fmt.Sprintf("%d", m.UID)
|
|
|
|
|
|
if !strings.Contains(strings.ToLower(m.Email), query) &&
|
|
|
|
|
|
!strings.Contains(strings.ToLower(m.DisplayName), query) &&
|
|
|
|
|
|
!strings.Contains(uidStr, query) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
cp := *m
|
|
|
|
|
|
all = append(all, &cp)
|
|
|
|
|
|
}
|
|
|
|
|
|
total := int64(len(all))
|
|
|
|
|
|
start := (page - 1) * pageSize
|
|
|
|
|
|
if start >= len(all) {
|
|
|
|
|
|
return []*domain.Member{}, total, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
end := start + pageSize
|
|
|
|
|
|
if end > len(all) {
|
|
|
|
|
|
end = len(all)
|
|
|
|
|
|
}
|
|
|
|
|
|
return all[start:end], total, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) CountActiveAdmins(ctx context.Context) (int64, error) {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
var n int64
|
|
|
|
|
|
for _, m := range f.byUID {
|
|
|
|
|
|
if m.IsAdmin() && m.Status != domain.StatusSuspended {
|
|
|
|
|
|
n++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return n, nil
|
2026-07-10 12:54:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 03:18:08 +00:00
|
|
|
|
func (f *fakeRepo) FindByInviteCode(ctx context.Context, code string) (*domain.Member, error) {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
code = strings.ToUpper(strings.TrimSpace(code))
|
|
|
|
|
|
for _, m := range f.byUID {
|
|
|
|
|
|
if strings.ToUpper(strings.TrimSpace(m.InviteCode)) == code {
|
|
|
|
|
|
cp := *m
|
|
|
|
|
|
return &cp, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil, domain.ErrNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (f *fakeRepo) ListAllMembers(ctx context.Context) ([]*domain.Member, error) {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
out := make([]*domain.Member, 0, len(f.byUID))
|
|
|
|
|
|
for _, m := range f.byUID {
|
|
|
|
|
|
cp := *m
|
|
|
|
|
|
out = append(out, &cp)
|
|
|
|
|
|
}
|
|
|
|
|
|
return out, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 12:54:45 +00:00
|
|
|
|
func (f *fakeRepo) CreateIdentity(ctx context.Context, id *domain.Identity) error {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
k := idKey(id.LoginID, id.Platform)
|
|
|
|
|
|
if _, ok := f.identities[k]; ok {
|
|
|
|
|
|
return domain.ErrIdentityTaken
|
|
|
|
|
|
}
|
|
|
|
|
|
cp := *id
|
|
|
|
|
|
f.identities[k] = &cp
|
|
|
|
|
|
f.byUIDIds[id.UID] = append(f.byUIDIds[id.UID], k)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) FindIdentity(ctx context.Context, loginID, platform string) (*domain.Identity, error) {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
id, ok := f.identities[idKey(loginID, platform)]
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return nil, domain.ErrIdentityNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
cp := *id
|
|
|
|
|
|
return &cp, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) ListIdentitiesByUID(ctx context.Context, uid int64) ([]*domain.Identity, error) {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
var out []*domain.Identity
|
|
|
|
|
|
for _, k := range f.byUIDIds[uid] {
|
|
|
|
|
|
if id, ok := f.identities[k]; ok {
|
|
|
|
|
|
cp := *id
|
|
|
|
|
|
out = append(out, &cp)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return out, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) DeleteIdentity(ctx context.Context, loginID, platform string) error {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
k := idKey(loginID, platform)
|
|
|
|
|
|
id, ok := f.identities[k]
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return domain.ErrIdentityNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
delete(f.identities, k)
|
|
|
|
|
|
var rest []string
|
|
|
|
|
|
for _, x := range f.byUIDIds[id.UID] {
|
|
|
|
|
|
if x != k {
|
|
|
|
|
|
rest = append(rest, x)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
f.byUIDIds[id.UID] = rest
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) UpdateIdentityPassword(ctx context.Context, loginID, platform, passwordHash string) error {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
id, ok := f.identities[idKey(loginID, platform)]
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return domain.ErrIdentityNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
id.PasswordHash = passwordHash
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (f *fakeRepo) SetCode(kind, key, code string, ttl time.Duration) {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
f.codes[kind+":"+key] = code
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) CheckCode(kind, key, code string) bool {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
k := kind + ":" + key
|
|
|
|
|
|
v, ok := f.codes[k]
|
|
|
|
|
|
if !ok || v != code {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
delete(f.codes, k)
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) PeekCode(kind, key, code string) bool {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
return f.codes[kind+":"+key] == code
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) SaveRefresh(jti string, uid int64, exp time.Time) {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
f.refresh[jti] = uid
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) ConsumeRefresh(jti string) (int64, bool) {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
uid, ok := f.refresh[jti]
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return 0, false
|
|
|
|
|
|
}
|
|
|
|
|
|
delete(f.refresh, jti)
|
|
|
|
|
|
return uid, true
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) RevokeRefresh(jti string) {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
delete(f.refresh, jti)
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) RevokeAllRefreshByUID(ctx context.Context, uid int64) error {
|
|
|
|
|
|
f.mu.Lock()
|
|
|
|
|
|
defer f.mu.Unlock()
|
|
|
|
|
|
for j, u := range f.refresh {
|
|
|
|
|
|
if u == uid {
|
|
|
|
|
|
delete(f.refresh, j)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) GetSettings(ctx context.Context, uid int64) (*domain.UserSettings, error) {
|
|
|
|
|
|
return domain.DefaultSettings(uid), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
func (f *fakeRepo) SaveSettings(ctx context.Context, uid int64, s *domain.UserSettings) error {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestAU_01_LoginOk(t *testing.T) {
|
|
|
|
|
|
store := newFake()
|
|
|
|
|
|
svc := usecase.NewAuthService(store, tokenUC.NewJWTIssuer("a", "b", 3600, 86400), 10)
|
|
|
|
|
|
const strong = "Admin123!@#x"
|
|
|
|
|
|
m, tokens, err := svc.Register(context.Background(), "admin@haixun.local", strong, "Admin")
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
m.Roles = []string{"admin", "member"}
|
|
|
|
|
|
m.Status = domain.StatusActive
|
|
|
|
|
|
m.EmailVerified = true
|
|
|
|
|
|
require.NoError(t, store.UpdateMember(context.Background(), m))
|
|
|
|
|
|
|
|
|
|
|
|
m2, tokens2, err := svc.Login(context.Background(), "admin@haixun.local", strong)
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
require.Equal(t, m.UID, m2.UID)
|
|
|
|
|
|
require.True(t, m.UID >= domain.MinMemberUID)
|
|
|
|
|
|
require.NotEmpty(t, tokens.AccessToken)
|
|
|
|
|
|
require.NotEmpty(t, tokens2.AccessToken)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestAU_02_LoginBadPassword(t *testing.T) {
|
|
|
|
|
|
store := newFake()
|
|
|
|
|
|
svc := usecase.NewAuthService(store, tokenUC.NewJWTIssuer("a", "b", 3600, 86400), 10)
|
|
|
|
|
|
const strong = "Admin123!@#x"
|
|
|
|
|
|
_, _, err := svc.Register(context.Background(), "admin@haixun.local", strong, "Admin")
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
_, _, err = svc.Login(context.Background(), "admin@haixun.local", "wrong")
|
|
|
|
|
|
require.ErrorIs(t, err, domain.ErrBadPassword)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestAR_02_RegisterOk(t *testing.T) {
|
|
|
|
|
|
store := newFake()
|
|
|
|
|
|
svc := usecase.NewAuthService(store, tokenUC.NewJWTIssuer("a", "b", 3600, 86400), 10)
|
|
|
|
|
|
m, tokens, err := svc.Register(context.Background(), "new@example.com", "Secret12!@#a", "New")
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
require.True(t, m.UID >= domain.MinMemberUID)
|
|
|
|
|
|
require.NotEmpty(t, tokens.AccessToken)
|
|
|
|
|
|
ids, err := svc.ListIdentities(context.Background(), m.UID)
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
require.Len(t, ids, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestAU_07_Refresh(t *testing.T) {
|
|
|
|
|
|
store := newFake()
|
|
|
|
|
|
svc := usecase.NewAuthService(store, tokenUC.NewJWTIssuer("sec-a", "sec-r", 3600, 86400), 10)
|
|
|
|
|
|
_, tokens, err := svc.Register(context.Background(), "r@example.com", "Secret12!@#a", "R")
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
next, err := svc.Refresh(context.Background(), tokens.RefreshToken)
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
require.NotEmpty(t, next.AccessToken)
|
|
|
|
|
|
_, err = svc.Refresh(context.Background(), tokens.RefreshToken)
|
|
|
|
|
|
require.Error(t, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestBind_Unbind(t *testing.T) {
|
|
|
|
|
|
store := newFake()
|
|
|
|
|
|
svc := usecase.NewAuthService(store, tokenUC.NewJWTIssuer("a", "b", 3600, 86400), 10)
|
|
|
|
|
|
m, _, err := svc.Register(context.Background(), "b@example.com", "Secret12!@#a", "B")
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
id, err := svc.BindAccount(context.Background(), m.UID, "g-sub-1", domain.PlatformGoogle, domain.AccountTypePlatform, "")
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
require.Equal(t, "g-sub-1", id.LoginID)
|
|
|
|
|
|
ids, _ := svc.ListIdentities(context.Background(), m.UID)
|
|
|
|
|
|
require.Len(t, ids, 2)
|
|
|
|
|
|
require.NoError(t, svc.UnbindAccount(context.Background(), m.UID, "g-sub-1", domain.PlatformGoogle))
|
|
|
|
|
|
err = svc.UnbindAccount(context.Background(), m.UID, "b@example.com", domain.PlatformPassword)
|
|
|
|
|
|
require.Error(t, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 08:59:13 +00:00
|
|
|
|
func TestLoginOAuth_TrustedProviderIdentity(t *testing.T) {
|
2026-07-10 12:54:45 +00:00
|
|
|
|
store := newFake()
|
|
|
|
|
|
svc := usecase.NewAuthService(store, tokenUC.NewJWTIssuer("a", "b", 3600, 86400), 10)
|
2026-07-13 08:59:13 +00:00
|
|
|
|
m, pair, err := svc.LoginOAuth(context.Background(), domain.PlatformGoogle, "g123", "u@g.com", "G123")
|
2026-07-10 12:54:45 +00:00
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
require.NotEmpty(t, pair.AccessToken)
|
|
|
|
|
|
require.True(t, m.UID >= domain.MinMemberUID)
|
2026-07-13 08:59:13 +00:00
|
|
|
|
m2, _, err := svc.LoginOAuth(context.Background(), domain.PlatformGoogle, "g123", "u@g.com", "G123")
|
2026-07-10 12:54:45 +00:00
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
require.Equal(t, m.UID, m2.UID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestMember_UidIs8DigitsFrom1M(t *testing.T) {
|
|
|
|
|
|
require.Equal(t, int64(1_000_000), domain.MinMemberUID)
|
|
|
|
|
|
// 1_000_000 是 7 位;顯示補零成 8 位:01000000
|
|
|
|
|
|
require.Equal(t, "01000000", fmt.Sprintf("%08d", domain.MinMemberUID))
|
|
|
|
|
|
require.Equal(t, "01000001", fmt.Sprintf("%08d", domain.MinMemberUID+1))
|
|
|
|
|
|
}
|