feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
2026-04-02 11:19:50 +08:00
|
|
|
|
package robustness
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-04-07 12:08:16 +08:00
|
|
|
|
"context"
|
|
|
|
|
|
"encoding/hex"
|
feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
2026-04-02 11:19:50 +08:00
|
|
|
|
"errors"
|
2026-04-07 12:08:16 +08:00
|
|
|
|
"regexp"
|
|
|
|
|
|
"strings"
|
feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
2026-04-02 11:19:50 +08:00
|
|
|
|
"sync"
|
|
|
|
|
|
"testing"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-04-07 12:08:16 +08:00
|
|
|
|
// =============================================================================
|
|
|
|
|
|
// Security Robustness Tests - Input Validation & Injection Prevention
|
|
|
|
|
|
// =============================================================================
|
feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
2026-04-02 11:19:50 +08:00
|
|
|
|
|
2026-04-07 12:08:16 +08:00
|
|
|
|
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},
|
feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
2026-04-02 11:19:50 +08:00
|
|
|
|
}
|
2026-04-07 12:08:16 +08:00
|
|
|
|
|
|
|
|
|
|
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])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
}()
|
feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
2026-04-02 11:19:50 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 12:08:16 +08:00
|
|
|
|
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)
|
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
|
feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
2026-04-02 11:19:50 +08:00
|
|
|
|
// 鲁棒性测试: 并发安全
|
|
|
|
|
|
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"
|
|
|
|
|
|
)
|