package api import ( "fmt" "testing" ) // ============================================================ // IsSSLErrorCode 测试 // ============================================================ func TestIsSSLErrorCode(t *testing.T) { tests := []struct { code string want bool }{ // 标准 OpenSSL 错误码 {"UNABLE_TO_VERIFY_LEAF_SIGNATURE", true}, {"CERT_HAS_EXPIRED", true}, {"DEPTH_ZERO_SELF_SIGNED_CERT", true}, {"SELF_SIGNED_CERT_IN_CHAIN", true}, {"HOSTNAME_MISMATCH", true}, {"ERR_TLS_HANDSHAKE_TIMEOUT", true}, // Go crypto/tls 错误 {"tls: failed to verify certificate", true}, {"x509: certificate signed by unknown authority", true}, // 非 SSL 错误 {"ECONNREFUSED", false}, {"ETIMEDOUT", false}, {"some random error", false}, {"", false}, } for _, tt := range tests { t.Run(tt.code, func(t *testing.T) { if got := IsSSLErrorCode(tt.code); got != tt.want { t.Errorf("IsSSLErrorCode(%q) = %v, want %v", tt.code, got, tt.want) } }) } } // ============================================================ // DefaultHinter 测试 // ============================================================ func TestDefaultHinter_SSLHints(t *testing.T) { h := &DefaultHinter{} tests := []struct { name string cause string wantHas string // hint 应包含的关键词 }{ { "leaf signature", "UNABLE_TO_VERIFY_LEAF_SIGNATURE", "SSL 证书验证失败", }, { "expired cert", "CERT_HAS_EXPIRED", "已过期", }, { "self signed", "DEPTH_ZERO_SELF_SIGNED_CERT", "自签名", }, { "hostname mismatch", "HOSTNAME_MISMATCH", "主机名不匹配", }, { "Go x509 error", "x509: certificate signed by unknown authority", "SSL/TLS 证书错误", }, { "Go tls error", "tls: handshake failure", "SSL/TLS 证书错误", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := &APIError{ ErrCategory: ErrSSL, Cause: fmt.Errorf("%s", tt.cause), } hint := h.Hint(err) if hint == "" { t.Fatal("expected non-empty hint") } if !containsSubstring(hint, tt.wantHas) { t.Errorf("hint = %q, want to contain %q", hint, tt.wantHas) } }) } } func TestDefaultHinter_ConnectionHints(t *testing.T) { h := &DefaultHinter{} tests := []struct { name string cause string wantHas string }{ {"DNS failure", "dial tcp: lookup foo.bar: no such host", "DNS"}, {"connection refused", "dial tcp 127.0.0.1:443: connection refused", "被拒绝"}, {"connection reset", "read tcp: connection reset by peer", "被重置"}, {"no route", "dial tcp: no route to host", "无法到达"}, {"generic connection", "some unknown connection error", "网络连接"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := &APIError{ ErrCategory: ErrConnection, Cause: fmt.Errorf("%s", tt.cause), } hint := h.Hint(err) if hint == "" { t.Fatal("expected non-empty hint") } if !containsSubstring(hint, tt.wantHas) { t.Errorf("hint = %q, want to contain %q", hint, tt.wantHas) } }) } } func TestDefaultHinter_TimeoutHint(t *testing.T) { h := &DefaultHinter{} err := &APIError{ErrCategory: ErrTimeout} hint := h.Hint(err) if hint == "" { t.Fatal("timeout should have a hint") } if !containsSubstring(hint, "超时") { t.Errorf("hint = %q, want to contain '超时'", hint) } } func TestDefaultHinter_NilError(t *testing.T) { h := &DefaultHinter{} if hint := h.Hint(nil); hint != "" { t.Errorf("nil error should return empty hint, got %q", hint) } } func TestDefaultHinter_IrrelevantCategory(t *testing.T) { h := &DefaultHinter{} err := &APIError{ErrCategory: ErrRateLimit} if hint := h.Hint(err); hint != "" { t.Errorf("rate limit should have no diagnostic hint, got %q", hint) } } // ============================================================ // CompositeHinter 测试 // ============================================================ func TestCompositeHinter(t *testing.T) { // 第一个返回空,第二个返回提示 h1 := &stubHinter{hint: ""} h2 := &stubHinter{hint: "check proxy"} comp := NewCompositeHinter(h1, h2) err := &APIError{ErrCategory: ErrConnection} if hint := comp.Hint(err); hint != "check proxy" { t.Errorf("hint = %q, want 'check proxy'", hint) } } func TestCompositeHinter_FirstWins(t *testing.T) { h1 := &stubHinter{hint: "first wins"} h2 := &stubHinter{hint: "second"} comp := NewCompositeHinter(h1, h2) err := &APIError{ErrCategory: ErrConnection} if hint := comp.Hint(err); hint != "first wins" { t.Errorf("hint = %q, want 'first wins'", hint) } } func TestCompositeHinter_Add(t *testing.T) { comp := NewCompositeHinter() comp.Add(&stubHinter{hint: "added"}) err := &APIError{ErrCategory: ErrConnection} if hint := comp.Hint(err); hint != "added" { t.Errorf("hint = %q, want 'added'", hint) } } // ============================================================ // classifyConnectionError 测试 // ============================================================ func TestClassifyConnectionError_SSL(t *testing.T) { err := classifyConnectionError(fmt.Errorf("tls: failed to verify certificate: x509: certificate signed by unknown authority")) if err.ErrCategory != ErrSSL { t.Errorf("Category = %v, want ErrSSL", err.ErrCategory) } if !err.IsRetryable() { t.Error("SSL errors should be retryable by default (cert might be renewed)") } } func TestClassifyConnectionError_Timeout(t *testing.T) { err := classifyConnectionError(&mockTimeoutError{msg: "dial tcp: i/o timeout"}) if err.ErrCategory != ErrTimeout { t.Errorf("Category = %v, want ErrTimeout", err.ErrCategory) } } func TestClassifyConnectionError_Generic(t *testing.T) { err := classifyConnectionError(fmt.Errorf("dial tcp: connection refused")) if err.ErrCategory != ErrConnection { t.Errorf("Category = %v, want ErrConnection", err.ErrCategory) } } // ============================================================ // isSSLError 测试 // ============================================================ func TestIsSSLError(t *testing.T) { tests := []struct { msg string want bool }{ {"tls: handshake failure", true}, {"x509: certificate signed by unknown authority", true}, {"UNABLE_TO_VERIFY_LEAF_SIGNATURE", true}, {"dial tcp: connection refused", false}, {"some random error", false}, } for _, tt := range tests { if got := isSSLError(fmt.Errorf("%s", tt.msg)); got != tt.want { t.Errorf("isSSLError(%q) = %v, want %v", tt.msg, got, tt.want) } } } // ============================================================ // isTimeoutError 测试 // ============================================================ func TestIsTimeoutError(t *testing.T) { // net.Error 接口 if !isTimeoutError(&mockTimeoutError{msg: "timeout"}) { t.Error("mockTimeoutError should be detected as timeout") } // 字符串回退 if !isTimeoutError(fmt.Errorf("context deadline exceeded")) { t.Error("deadline exceeded should be detected as timeout") } // 非超时 if isTimeoutError(fmt.Errorf("connection refused")) { t.Error("connection refused should NOT be timeout") } } // ============================================================ // 测试辅助 // ============================================================ type stubHinter struct { hint string } func (s *stubHinter) Hint(err *APIError) string { return s.hint } func containsSubstring(s, substr string) bool { return len(s) >= len(substr) && (substr == "" || findSubstring(s, substr)) } func findSubstring(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false }