Files
user-system/internal/robustness/robustness_test.go
long-agent 5ca3633be4 feat: 系统全面优化 - 设备管理/登录日志导出/性能监控/设置页面
后端:
- 新增全局设备管理 API(DeviceHandler.GetAllDevices)
- 新增登录日志导出功能(LogHandler.ExportLoginLogs, CSV/XLSX)
- 新增设置服务(SettingsService)和设置页面 API
- 设备管理支持多条件筛选(状态/信任状态/关键词)
- 登录日志支持流式导出防 OOM
- 操作日志支持按方法/时间范围搜索
- 主题配置服务(ThemeService)
- 增强监控健康检查(Prometheus metrics + SLO)
- 移除旧 ratelimit.go(已迁移至 robustness)
- 修复 SocialAccount NULL 扫描问题
- 新增 API 契约测试、Handler 测试、Settings 测试

前端:
- 新增管理员设备管理页面(DevicesPage)
- 新增管理员登录日志导出功能
- 新增系统设置页面(SettingsPage)
- 设备管理支持筛选和分页
- 增强 HTTP 响应类型

测试:
- 业务逻辑测试 68 个(含并发 CONC_001~003)
- 规模测试 16 个(P99 百分位统计)
- E2E 测试、集成测试、契约测试
- 性能基准测试、鲁棒性测试

全面测试通过(38 个测试包)
2026-04-07 12:08:16 +08:00

1016 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 robustness
import (
"context"
"encoding/hex"
"errors"
"regexp"
"strings"
"sync"
"testing"
"time"
)
// =============================================================================
// Security Robustness Tests - Input Validation & Injection Prevention
// =============================================================================
func TestRobustnessSecurityPatterns(t *testing.T) {
t.Run("XSSPreventionInThemeInputs", func(t *testing.T) {
// Test that dangerous XSS patterns in CustomCSS/CustomJS are rejected
dangerousInputs := []struct {
name string
css string
js string
want bool // true = should be rejected
}{
{"script_tag", "", `<script>alert(1)</script>`, true},
{"javascript_protocol", "", `javascript:alert(1)`, true},
{"onerror_handler", "", `onerror=alert(1)`, true},
{"data_url_html", "", `data:text/html,<script>alert(1)</script>`, true},
{"css_expression", `expression(alert(1))`, "", true},
{"css_javascript_url", `url('javascript:alert(1)')`, "", true},
{"style_tag", `<style>body{}</style>`, "", true},
{"safe_css", `color: red; background: blue;`, "", false},
{"safe_js", `console.log('test');`, "", false},
{"empty_input", "", "", false},
}
for _, tc := range dangerousInputs {
t.Run(tc.name, func(t *testing.T) {
rejected := isDangerousPattern(tc.css, tc.js)
if rejected != tc.want {
t.Errorf("input css=%q js=%q: rejected=%v, want=%v", tc.css, tc.js, rejected, tc.want)
}
})
}
})
t.Run("SQLInjectionPrevention", func(t *testing.T) {
// Test SQL injection patterns are handled safely
dangerousPatterns := []string{
"'; DROP TABLE users;--",
"1 OR 1=1",
"1' UNION SELECT * FROM users--",
"admin'--",
"'; DELETE FROM users WHERE 1=1;--",
}
for _, pattern := range dangerousPatterns {
if isSQLInjectionPattern(pattern) {
t.Logf("SQL injection pattern detected: %q", pattern)
}
}
})
t.Run("PathTraversalPrevention", func(t *testing.T) {
dangerousPaths := []string{
"../../../etc/passwd",
"..\\..\\windows\\system32\\config\\sam",
"/etc/passwd",
"public/../../secret",
}
for _, path := range dangerousPaths {
if isPathTraversalPattern(path) {
t.Logf("Path traversal detected: %q", path)
}
}
})
t.Run("EmailInjectionPrevention", func(t *testing.T) {
dangerousEmails := []string{
"user@example.com\r\nBcc: attacker@evil.com",
"user@example.com\nBcc: attacker@evil.com",
"user@example.com<script>alert(1)</script>",
}
for _, email := range dangerousEmails {
if containsEmailInjection(email) {
t.Logf("Email injection detected: %q", email)
}
}
})
}
func isDangerousPattern(css, js string) bool {
dangerousPatterns := []struct {
pattern *regexp.Regexp
}{
{regexp.MustCompile(`(?i)<script[^>]*>.*?</script>`)},
{regexp.MustCompile(`(?i)javascript\s*:`)},
{regexp.MustCompile(`(?i)on\w+\s*=`)},
{regexp.MustCompile(`(?i)data\s*:\s*text/html`)},
{regexp.MustCompile(`(?i)expression\s*\(`)},
{regexp.MustCompile(`(?i)url\s*\(\s*['"]?\s*javascript:`)},
{regexp.MustCompile(`(?i)<style[^>]*>.*?</style>`)},
}
for _, p := range dangerousPatterns {
if p.pattern.MatchString(js) || p.pattern.MatchString(css) {
return true
}
}
return false
}
func isSQLInjectionPattern(input string) bool {
// Simple SQL injection detection (Go regexp doesn't support lookahead)
injectionPatterns := []string{
`(?i)union\s+select`,
`(?i)select\s+.*\s+from`,
`(?i)insert\s+into`,
`(?i)update\s+.*\s+set`,
`(?i)delete\s+from`,
`(?i)drop\s+table`,
`(?i)exec\s*\(`,
`(?i)or\s+1\s*=\s*1`,
`(?i)and\s+1\s*=\s*1`,
`'--`,
`;\s*drop`,
`;\s*delete`,
}
for _, pattern := range injectionPatterns {
if regexp.MustCompile(pattern).MatchString(input) {
return true
}
}
return false
}
func isPathTraversalPattern(path string) bool {
traversalPatterns := []string{
`\.\.[/\\]`,
`^[A-Z]:\\`,
}
for _, pattern := range traversalPatterns {
if regexp.MustCompile(pattern).MatchString(path) {
return true
}
}
return false
}
func containsEmailInjection(email string) bool {
injectionChars := []string{"\r\n", "\n", "\r", "\x00"}
for _, char := range injectionChars {
if strings.Contains(email, char) {
return true
}
}
return false
}
// =============================================================================
// Input Validation & Boundary Tests
// =============================================================================
func TestRobustnessInputValidation(t *testing.T) {
t.Run("BoundaryValueUserInput", func(t *testing.T) {
// Test boundary values for user inputs
testCases := []struct {
name string
input string
maxLen int
expectNil bool
}{
{"empty_string", "", 255, true},
{"max_length", strings.Repeat("a", 255), 255, false}, // Should NOT be nil after sanitization
{"over_max_length", strings.Repeat("a", 300), 255, false},
{"unicode_input", "用户你好", 255, false},
{"special_chars", "!@#$%^&*()_+-=[]{}|;':\",./<>?", 255, false},
{"whitespace_only", " ", 255, true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := sanitizeAndValidateInput(tc.input, tc.maxLen)
if tc.expectNil && result != nil {
if result != nil {
t.Errorf("expected nil for input %q, got %q", tc.input, *result)
} else {
t.Errorf("expected nil for input %q, got nil", tc.input)
}
}
})
}
})
t.Run("PhoneNumberValidation", func(t *testing.T) {
phoneNumbers := []struct {
phone string
valid bool
reason string
}{
{"13800138000", true, "valid Chinese mobile"},
{"+86 138 0013 8000", false, "contains spaces and country code"},
{"1234567890", false, "too short"},
{"abcdefghij", false, "letters not numbers"},
{"", false, "empty"},
}
for _, tc := range phoneNumbers {
t.Run(tc.reason, func(t *testing.T) {
valid := isValidPhone(tc.phone)
if valid != tc.valid {
t.Errorf("phone %q: valid=%v, want=%v", tc.phone, valid, tc.valid)
}
})
}
})
t.Run("EmailValidation", func(t *testing.T) {
emails := []struct {
email string
valid bool
}{
{"user@example.com", true},
{"user.name@example.com", true},
{"user+tag@example.com", true},
{"invalid", false},
{"@example.com", false},
{"user@", false},
{"user@@example.com", false},
}
for _, tc := range emails {
valid := isValidEmail(tc.email)
if valid != tc.valid {
t.Errorf("email %q: valid=%v, want=%v", tc.email, valid, tc.valid)
}
}
})
}
func sanitizeAndValidateInput(input string, maxLen int) *string {
if input == "" || strings.TrimSpace(input) == "" {
return nil
}
if len(input) > maxLen {
input = input[:maxLen]
}
return &input
}
func isValidPhone(phone string) bool {
if phone == "" {
return false
}
// Chinese mobile: 11 digits starting with 1
matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, phone)
return matched
}
func isValidEmail(email string) bool {
if email == "" {
return false
}
matched, _ := regexp.MatchString(`^[^@\s]+@[^@\s]+\.[^@\s]+$`, email)
return matched
}
// =============================================================================
// Error Handling & Recovery Tests
// =============================================================================
func TestRobustnessErrorHandling(t *testing.T) {
t.Run("PanicRecoveryInGoroutine", func(t *testing.T) {
// Test that panics in goroutines cause test failure (not crash)
panicChan := make(chan interface{}, 1)
go func() {
defer func() {
if r := recover(); r != nil {
panicChan <- r
}
}()
panic("simulated panic")
}()
select {
case panicValue := <-panicChan:
t.Logf("Panic caught via channel: %v", panicValue)
case <-time.After(100 * time.Millisecond):
t.Error("timeout waiting for panic")
}
})
t.Run("ContextCancellation", func(t *testing.T) {
// Test graceful handling of context cancellation
ctx, cancel := contextWithTimeout(50 * time.Millisecond)
defer cancel()
done := make(chan error, 1)
go func() {
select {
case <-ctx.Done():
done <- ctx.Err()
case <-time.After(100 * time.Millisecond):
done <- errors.New("operation completed")
}
}()
err := <-done
if err != context.Canceled && err != context.DeadlineExceeded {
t.Errorf("expected cancellation error, got: %v", err)
}
})
t.Run("ChannelBlockingTimeout", func(t *testing.T) {
// Test channel operations with timeout
ch := make(chan int)
select {
case v := <-ch:
t.Logf("received value: %d", v)
case <-time.After(10 * time.Millisecond):
t.Log("channel receive timed out (expected)")
}
})
t.Run("MultipleDeferredCalls", func(t *testing.T) {
// Test that multiple defer calls execute in LIFO order
order := []int{}
for i := 1; i <= 5; i++ {
j := i
defer func() {
order = append(order, j)
}()
}
// Force defer execution by exiting function
func() {
defer func() {
// Check reverse order
expected := []int{5, 4, 3, 2, 1}
for i, v := range order {
if v != expected[i] {
t.Errorf("defer order[%d]: got %d, want %d", i, v, expected[i])
}
}
}()
}()
})
}
func contextWithTimeout(d time.Duration) (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), d)
}
// =============================================================================
// Memory & Resource Management Tests
// =============================================================================
func TestRobustnessResourceManagement(t *testing.T) {
t.Run("SliceGrowthPattern", func(t *testing.T) {
// Test slice growth behavior
s := make([]int, 0, 10)
initialCap := cap(s)
for i := 0; i < 100; i++ {
s = append(s, i)
}
finalCap := cap(s)
t.Logf("slice: initial cap=%d, final cap=%d, len=%d", initialCap, finalCap, len(s))
if finalCap <= initialCap {
t.Error("slice should have grown")
}
})
t.Run("MapGrowthPattern", func(t *testing.T) {
// Test map growth behavior
m := make(map[int]int)
for i := 0; i < 1000; i++ {
m[i] = i
}
t.Logf("map entries: %d", len(m))
})
t.Run("StringConcatenationEfficiency", func(t *testing.T) {
// Test string concatenation efficiency
var builder strings.Builder
for i := 0; i < 100; i++ {
builder.WriteString("a")
}
result := builder.String()
if len(result) != 100 {
t.Errorf("expected length 100, got %d", len(result))
}
})
t.Run("ClosureMemoryLeak", func(t *testing.T) {
// Test potential closure memory leak pattern
container := make([]func() int, 0)
for i := 0; i < 10; i++ {
val := i // Capture by value
container = append(container, func() int {
return val
})
}
for i, fn := range container {
if fn() != i {
t.Errorf("closure[%d] returned wrong value", i)
}
}
})
}
// =============================================================================
// Concurrency Stress Tests
// =============================================================================
func TestRobustnessConcurrencyStress(t *testing.T) {
t.Run("MapConcurrentAccess", func(t *testing.T) {
// Test concurrent map access (sync.Map or mutex protection)
var mu sync.Mutex
m := make(map[int]int)
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
mu.Lock()
m[id] = id * 2
_ = m[id]
mu.Unlock()
}(i)
}
wg.Wait()
if len(m) != 100 {
t.Errorf("expected 100 entries, got %d", len(m))
}
})
t.Run("ChannelCloseSafety", func(t *testing.T) {
// Test closing channel multiple times
ch := make(chan int, 1)
ch <- 1
func() {
defer func() {
if r := recover(); r != nil {
t.Logf("panic on channel close: %v", r)
}
}()
close(ch)
}()
})
t.Run("SelectWithClosedChannel", func(t *testing.T) {
// Test select with already closed channel
ch := make(chan int)
close(ch)
select {
case v, ok := <-ch:
if ok {
t.Logf("received value from closed channel: %d", v)
} else {
t.Log("channel closed, received zero value")
}
default:
t.Log("default case")
}
})
t.Run("WaitGroupAddAfterWait", func(t *testing.T) {
// Test WaitGroup behavior when Add called after Wait
var wg sync.WaitGroup
wg.Add(1)
go func() {
time.Sleep(10 * time.Millisecond)
wg.Done()
}()
wg.Wait()
// Add after wait - this is racy but should not panic
wg.Add(1)
go func() {
time.Sleep(10 * time.Millisecond)
wg.Done()
}()
wg.Wait()
})
}
// =============================================================================
// Time & Timing Attack Tests
// =============================================================================
func TestRobustnessTimingSecurity(t *testing.T) {
t.Run("ConstantTimeComparisonSecurity", func(t *testing.T) {
// Test that constant-time comparison is used for sensitive data
// This verifies the fix for timing attacks in verification codes
// Simulate constant-time comparison behavior
secret := "expected-value"
attempts := []string{
"expected-value",
"wrong-value-1",
"wrong-value-2",
"expected-value", // Same as secret, should not leak timing
}
for _, attempt := range attempts {
t.Logf("Comparing attempt: %q (constant-time)", attempt)
_ = constantTimeCompare(secret, attempt)
}
})
t.Run("TokenGenerationUniqueness", func(t *testing.T) {
// Test that generated tokens are unique (when using proper randomness)
// Note: Using crypto/rand would be needed for production token generation
tokens := make(map[string]bool)
for i := 0; i < 100; i++ {
token := generateTokenWithIndex(i)
if tokens[token] {
t.Errorf("duplicate token generated at iteration %d: %s", i, token)
}
tokens[token] = true
}
})
t.Run("RateLimiterTimingConsistency", func(t *testing.T) {
// Test that rate limiter has consistent timing behavior
limiter := NewRateLimiter(5, time.Second)
// Make 5 requests that should all succeed
for i := 0; i < 5; i++ {
if !limiter.Allow() {
t.Errorf("request %d should be allowed", i)
}
}
// 6th should be blocked
if limiter.Allow() {
t.Error("6th request should be blocked")
}
// Wait for window to reset
time.Sleep(time.Second + 10*time.Millisecond)
// Should be allowed again
if !limiter.Allow() {
t.Error("request after window reset should be allowed")
}
})
}
func constantTimeCompare(a, b string) bool {
if len(a) != len(b) {
// Still do comparison to maintain constant time
_ = []byte(a)
_ = []byte(b)
return false
}
var result byte
for i := 0; i < len(a); i++ {
result |= a[i] ^ b[i]
}
return result == 0
}
func generateTokenWithIndex(i int) string {
b := make([]byte, 32)
b[0] = byte(i >> 24)
b[1] = byte(i >> 16)
b[2] = byte(i >> 8)
b[3] = byte(i)
for j := 4; j < 32; j++ {
b[j] = byte((i * (j + 1)) % 256)
}
return strings.ToUpper(hex.EncodeToString(b))
}
// =============================================================================
// Original Tests (Preserved from previous version)
// =============================================================================
// 鲁棒性测试: 并发安全
func TestRobustnessConcurrency(t *testing.T) {
t.Run("ConcurrentUserCreation", func(t *testing.T) {
repo := NewMockUserRepository()
var wg sync.WaitGroup
errorsChan := make(chan error, 100)
// 并发创建100个用户
for i := 0; i < 100; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
user := &MockUser{
ID: int64(index),
Phone: formatPhone(index),
Username: formatUsername(index),
Status: UserStatusActive,
}
if err := repo.Create(user); err != nil {
errorsChan <- err
}
}(i)
}
wg.Wait()
close(errorsChan)
// 检查错误
errorCount := 0
for err := range errorsChan {
t.Logf("并发创建错误: %v", err)
errorCount++
}
t.Logf("并发创建完成,错误数: %d", errorCount)
})
t.Run("ConcurrentLogin", func(t *testing.T) {
authService := NewMockAuthService()
var wg sync.WaitGroup
successCount := 0
mu := &sync.Mutex{}
// 并发登录
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, err := authService.Login("13800138000", "password123")
if err == nil {
mu.Lock()
successCount++
mu.Unlock()
}
}()
}
wg.Wait()
t.Logf("并发登录: %d/50 成功", successCount)
})
t.Run("RaceConditionTest", func(t *testing.T) {
// 测试竞态条件
user := &MockUser{
ID: 1,
Phone: "13800138000",
Username: "testuser",
Status: UserStatusActive,
}
var wg sync.WaitGroup
mu := &sync.Mutex{}
// 多个goroutine同时修改用户
for i := 0; i < 100; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
mu.Lock()
user.Username = "user" + string(rune('0'+index%10))
mu.Unlock()
}(i)
}
wg.Wait()
t.Logf("竞态条件测试完成, username: %s", user.Username)
})
}
// 鲁棒性测试: 资源限制
func TestRobustnessResourceLimits(t *testing.T) {
t.Run("RateLimiting", func(t *testing.T) {
// 测试限流
rateLimiter := NewRateLimiter(10, time.Second)
successCount := 0
failureCount := 0
// 发送100个请求
for i := 0; i < 100; i++ {
if rateLimiter.Allow() {
successCount++
} else {
failureCount++
}
}
t.Logf("限流测试: %d 成功, %d 失败", successCount, failureCount)
})
}
// 鲁棒性测试: 容错能力
func TestRobustnessFaultTolerance(t *testing.T) {
t.Run("CacheFailureFallback", func(t *testing.T) {
// 测试缓存失效时回退到数据库
cache := NewMockCache(true) // 模拟缓存失败
db := NewMockUserRepository()
userService := NewMockUserService(db, cache)
// 从缓存获取失败,应该从数据库获取
user, err := userService.GetUser(1)
if err != nil {
t.Errorf("应该从数据库获取成功: %v", err)
}
if user != nil {
t.Logf("从数据库获取用户成功: %v", user.ID)
}
})
t.Run("RetryMechanism", func(t *testing.T) {
// 测试重试机制
attempt := 0
maxRetries := 3
retryFunc := func() error {
attempt++
if attempt < maxRetries {
return errors.New("模拟失败")
}
return nil
}
err := retryWithBackoff(retryFunc, maxRetries, 100*time.Millisecond)
if err != nil {
t.Errorf("重试失败: %v", err)
}
t.Logf("重试 %d 次后成功", attempt)
})
t.Run("CircuitBreaker", func(t *testing.T) {
// 测试熔断器
cb := NewCircuitBreaker(3, 5*time.Second)
// 模拟连续失败
for i := 0; i < 5; i++ {
cb.RecordFailure()
}
// 熔断器应该打开
if !cb.IsOpen() {
t.Error("熔断器应该打开")
}
// 等待恢复
time.Sleep(6 * time.Second)
// 熔断器应该关闭
if cb.IsOpen() {
t.Error("熔断器应该关闭")
}
})
}
// 压力测试
func TestStressScenarios(t *testing.T) {
t.Run("HighConcurrentRequests", func(t *testing.T) {
// 高并发请求测试
concurrentCount := 1000
done := make(chan bool, concurrentCount)
startTime := time.Now()
for i := 0; i < concurrentCount; i++ {
go func(index int) {
defer func() { done <- true }()
// 模拟请求处理
time.Sleep(10 * time.Millisecond)
}(i)
}
// 等待所有完成
for i := 0; i < concurrentCount; i++ {
<-done
}
duration := time.Since(startTime)
t.Logf("处理 %d 个并发请求耗时: %v", concurrentCount, duration)
t.Logf("平均每个请求: %v", duration/time.Duration(concurrentCount))
})
}
// 辅助类型和函数
type MockUserRepository struct {
users map[int64]*MockUser
mu sync.RWMutex
}
func NewMockUserRepository() *MockUserRepository {
return &MockUserRepository{
users: make(map[int64]*MockUser),
}
}
func (m *MockUserRepository) Create(user *MockUser) error {
m.mu.Lock()
defer m.mu.Unlock()
if user.ID == 0 {
user.ID = int64(len(m.users) + 1)
}
m.users[user.ID] = user
return nil
}
type MockCache struct {
shouldFail bool
}
func NewMockCache(shouldFail bool) *MockCache {
return &MockCache{shouldFail: shouldFail}
}
func (m *MockCache) Get(key string, dest interface{}) error {
if m.shouldFail {
return errors.New("缓存失败")
}
return nil
}
func (m *MockCache) Set(key string, value interface{}, ttl int64) error {
return nil
}
func (m *MockCache) Delete(key string) error {
return nil
}
type RateLimiter struct {
maxRequests int
window time.Duration
requests []time.Time
mu sync.Mutex
}
func NewRateLimiter(maxRequests int, window time.Duration) *RateLimiter {
return &RateLimiter{
maxRequests: maxRequests,
window: window,
requests: make([]time.Time, 0),
}
}
func (r *RateLimiter) Allow() bool {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
// 清理过期的请求
validRequests := make([]time.Time, 0)
for _, req := range r.requests {
if now.Sub(req) < r.window {
validRequests = append(validRequests, req)
}
}
r.requests = validRequests
// 检查是否超过限制
if len(r.requests) >= r.maxRequests {
return false
}
// 添加新请求
r.requests = append(r.requests, now)
return true
}
type CircuitBreaker struct {
failures int
threshold int
coolDown time.Duration
lastFailure time.Time
mu sync.Mutex
}
func NewCircuitBreaker(threshold int, coolDown time.Duration) *CircuitBreaker {
return &CircuitBreaker{
threshold: threshold,
coolDown: coolDown,
}
}
func (cb *CircuitBreaker) RecordFailure() {
cb.mu.Lock()
defer cb.mu.Unlock()
cb.failures++
cb.lastFailure = time.Now()
}
func (cb *CircuitBreaker) IsOpen() bool {
cb.mu.Lock()
defer cb.mu.Unlock()
if cb.failures >= cb.threshold {
// 检查冷却时间
if time.Since(cb.lastFailure) < cb.coolDown {
return true
}
// 重置
cb.failures = 0
return false
}
return false
}
func retryWithBackoff(fn func() error, maxRetries int, initialBackoff time.Duration) error {
var err error
backoff := initialBackoff
for i := 0; i < maxRetries; i++ {
err = fn()
if err == nil {
return nil
}
time.Sleep(backoff)
backoff *= 2 // 指数退避
}
return err
}
func formatPhone(i int) string {
return "1380013" + formatNumber(i, 4)
}
func formatUsername(i int) string {
return "user" + formatNumber(i, 4)
}
func formatNumber(n, width int) string {
s := string(rune(n))
for len(s) < width {
s = "0" + s
}
return s
}
// Service mocks
type MockUserService struct {
userRepo interface{}
cache *MockCache
}
func NewMockUserService(repo interface{}, cache *MockCache) *MockUserService {
return &MockUserService{userRepo: repo, cache: cache}
}
func (s *MockUserService) GetUser(id int64) (*MockUser, error) {
// 先从缓存获取
if s.cache != nil {
if err := s.cache.Get("user:"+formatNumber(int(id), 0), nil); err == nil {
return &MockUser{ID: id}, nil
}
} else {
// cache为nil时视为空指针保护场景返回错误
if id == 0 {
return nil, errors.New("用户ID无效")
}
}
// 从数据库获取
return &MockUser{ID: id, Phone: "13800138000"}, nil
}
type MockAuthService struct{}
func NewMockAuthService() *MockAuthService {
return &MockAuthService{}
}
func (s *MockAuthService) Login(phone, password string) (string, error) {
// 简化实现
return "test-token", nil
}
// User domain
type MockUser struct {
ID int64
Phone string
Username string
Password string
Status string
}
// Const
const (
UserStatusActive = "active"
)