package service import ( "context" "testing" "time" ) // ============================================================================= // SMS Provider Tests // ============================================================================= // mockCacheForSMS implements cacheInterface for testing type mockCacheForSMS struct{} func (m *mockCacheForSMS) Get(ctx context.Context, key string) (interface{}, bool) { return nil, false } func (m *mockCacheForSMS) Set(ctx context.Context, key string, value interface{}, l1TTL, l2TTL time.Duration) error { return nil } func (m *mockCacheForSMS) Delete(ctx context.Context, key string) error { return nil } func TestNewAliyunSMSProvider(t *testing.T) { t.Run("incomplete config returns error", func(t *testing.T) { cfg := AliyunSMSConfig{} _, err := NewAliyunSMSProvider(cfg) if err == nil { t.Error("Expected error for incomplete config") } }) t.Run("missing access key", func(t *testing.T) { cfg := AliyunSMSConfig{ AccessKeySecret: "secret", SignName: "sign", TemplateCode: "template", } _, err := NewAliyunSMSProvider(cfg) if err == nil { t.Error("Expected error for missing access key") } }) t.Run("missing sign name", func(t *testing.T) { cfg := AliyunSMSConfig{ AccessKeyID: "key", AccessKeySecret: "secret", TemplateCode: "template", } _, err := NewAliyunSMSProvider(cfg) if err == nil { t.Error("Expected error for missing sign name") } }) } func TestNewTencentSMSProvider(t *testing.T) { t.Run("incomplete config returns error", func(t *testing.T) { cfg := TencentSMSConfig{} _, err := NewTencentSMSProvider(cfg) if err == nil { t.Error("Expected error for incomplete config") } }) t.Run("missing secret ID", func(t *testing.T) { cfg := TencentSMSConfig{ SecretKey: "key", AppID: "app", SignName: "sign", TemplateID: "template", } _, err := NewTencentSMSProvider(cfg) if err == nil { t.Error("Expected error for missing secret ID") } }) t.Run("missing app ID", func(t *testing.T) { cfg := TencentSMSConfig{ SecretID: "id", SecretKey: "key", SignName: "sign", TemplateID: "template", } _, err := NewTencentSMSProvider(cfg) if err == nil { t.Error("Expected error for missing app ID") } }) } // ============================================================================= // SMS Code Service Tests // ============================================================================= type mockSMSProvider struct { sendErr error } func (m *mockSMSProvider) SendVerificationCode(ctx context.Context, phone, code string) error { return m.sendErr } func TestNewSMSCodeService(t *testing.T) { provider := &mockSMSProvider{} cache := &mockCacheForSMS{} t.Run("with default config", func(t *testing.T) { cfg := SMSCodeConfig{} svc := NewSMSCodeService(provider, cache, cfg) if svc == nil { t.Error("Expected non-nil service") } if svc.cfg.CodeTTL <= 0 { t.Error("Expected default CodeTTL to be set") } }) t.Run("with custom config", func(t *testing.T) { cfg := SMSCodeConfig{ CodeTTL: 10 * time.Minute, ResendCooldown: 2 * time.Minute, MaxDailyLimit: 20, } svc := NewSMSCodeService(provider, cache, cfg) if svc == nil { t.Error("Expected non-nil service") } }) } func TestSMSCodeService_DefaultConfig(t *testing.T) { cfg := DefaultSMSCodeConfig() if cfg.CodeTTL <= 0 { t.Error("Expected positive CodeTTL") } if cfg.ResendCooldown <= 0 { t.Error("Expected positive ResendCooldown") } if cfg.MaxDailyLimit <= 0 { t.Error("Expected positive MaxDailyLimit") } } // mockCacheWithGet implements cacheInterface with controllable Get behavior type mockCacheWithGet struct { getResult interface{} getFound bool setErr error deleteErr error lastSetKey string } func (m *mockCacheWithGet) Get(ctx context.Context, key string) (interface{}, bool) { return m.getResult, m.getFound } func (m *mockCacheWithGet) Set(ctx context.Context, key string, value interface{}, l1TTL, l2TTL time.Duration) error { m.lastSetKey = key return m.setErr } func (m *mockCacheWithGet) Delete(ctx context.Context, key string) error { return m.deleteErr } func TestSMSCodeService_SendCode(t *testing.T) { provider := &mockSMSProvider{} cache := &mockCacheWithGet{} svc := NewSMSCodeService(provider, cache, SMSCodeConfig{ CodeTTL: 5 * time.Minute, ResendCooldown: time.Minute, MaxDailyLimit: 10, }) ctx := context.Background() t.Run("nil service returns error", func(t *testing.T) { var nilSvc *SMSCodeService _, err := nilSvc.SendCode(ctx, &SendCodeRequest{Phone: "13800138000"}) if err == nil { t.Error("Expected error for nil service") } }) t.Run("nil request returns error", func(t *testing.T) { _, err := svc.SendCode(ctx, nil) if err == nil { t.Error("Expected error for nil request") } }) t.Run("invalid phone returns error", func(t *testing.T) { _, err := svc.SendCode(ctx, &SendCodeRequest{Phone: "invalid"}) if err == nil { t.Error("Expected error for invalid phone") } }) t.Run("cooldown active returns error", func(t *testing.T) { cache.getResult = true cache.getFound = true _, err := svc.SendCode(ctx, &SendCodeRequest{Phone: "13800138000"}) if err == nil { t.Error("Expected error when cooldown is active") } cache.getFound = false }) t.Run("successful send", func(t *testing.T) { cache.getResult = nil cache.getFound = false resp, err := svc.SendCode(ctx, &SendCodeRequest{Phone: "13800138000", Purpose: "login"}) if err != nil { t.Fatalf("SendCode failed: %v", err) } if resp == nil { t.Error("Expected non-nil response") } }) } func TestSMSCodeService_VerifyCode(t *testing.T) { provider := &mockSMSProvider{} cache := &mockCacheWithGet{} svc := NewSMSCodeService(provider, cache, SMSCodeConfig{ CodeTTL: 5 * time.Minute, ResendCooldown: time.Minute, MaxDailyLimit: 10, }) ctx := context.Background() t.Run("nil service returns error", func(t *testing.T) { var nilSvc *SMSCodeService err := nilSvc.VerifyCode(ctx, "13800138000", "login", "123456") if err == nil { t.Error("Expected error for nil service") } }) t.Run("empty code returns error", func(t *testing.T) { err := svc.VerifyCode(ctx, "13800138000", "login", "") if err == nil { t.Error("Expected error for empty code") } }) t.Run("code not found returns error", func(t *testing.T) { cache.getResult = nil cache.getFound = false err := svc.VerifyCode(ctx, "13800138000", "login", "123456") if err == nil { t.Error("Expected error when code not found") } }) t.Run("wrong code returns error", func(t *testing.T) { cache.getResult = "654321" cache.getFound = true err := svc.VerifyCode(ctx, "13800138000", "login", "123456") if err == nil { t.Error("Expected error for wrong code") } }) t.Run("correct code succeeds", func(t *testing.T) { cache.getResult = "123456" cache.getFound = true err := svc.VerifyCode(ctx, "13800138000", "login", "123456") if err != nil { t.Fatalf("VerifyCode failed: %v", err) } }) } func TestIsValidPhone(t *testing.T) { tests := []struct { phone string expected bool }{ {"13800138000", true}, {"15912345678", true}, {"18612345678", true}, {"12345678901", false}, {"1380013800", false}, {"", false}, {"invalid", false}, } for _, tt := range tests { result := isValidPhone(tt.phone) if result != tt.expected { t.Errorf("isValidPhone(%q) = %v, want %v", tt.phone, result, tt.expected) } } }