From b1fba8e76c046426c00b2838dc1f780640710c22 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 26 Nov 2025 14:14:34 +0000 Subject: [PATCH] test: add unit tests for pkg/tlsutil --- pkg/tlsutil/tlsutil_test.go | 318 ++++++++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 pkg/tlsutil/tlsutil_test.go diff --git a/pkg/tlsutil/tlsutil_test.go b/pkg/tlsutil/tlsutil_test.go new file mode 100644 index 0000000..6128a21 --- /dev/null +++ b/pkg/tlsutil/tlsutil_test.go @@ -0,0 +1,318 @@ +package tlsutil + +import ( + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "encoding/hex" + "net/http" + "strings" + "testing" + "time" +) + +func TestFingerprintVerifier_NormalizesFingerprint(t *testing.T) { + // Fingerprint with colons + fp1 := "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99" + // Same fingerprint without colons, lowercase + fp2 := "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899" + + config1 := FingerprintVerifier(fp1) + config2 := FingerprintVerifier(fp2) + + // Both should have InsecureSkipVerify set (we do our own verification) + if !config1.InsecureSkipVerify { + t.Error("FingerprintVerifier should set InsecureSkipVerify to true") + } + if !config2.InsecureSkipVerify { + t.Error("FingerprintVerifier should set InsecureSkipVerify to true") + } + + // Both should have VerifyPeerCertificate function set + if config1.VerifyPeerCertificate == nil { + t.Error("FingerprintVerifier should set VerifyPeerCertificate") + } + if config2.VerifyPeerCertificate == nil { + t.Error("FingerprintVerifier should set VerifyPeerCertificate") + } +} + +func TestFingerprintVerifier_NoCertificates(t *testing.T) { + config := FingerprintVerifier("aabbccdd") + + // Should fail when no certificates presented + err := config.VerifyPeerCertificate([][]byte{}, nil) + if err == nil { + t.Error("Should fail when no certificates presented") + } + if !strings.Contains(err.Error(), "no certificates") { + t.Errorf("Error message should mention no certificates, got: %v", err) + } +} + +func TestFingerprintVerifier_MatchingFingerprint(t *testing.T) { + // Create a mock certificate (just random bytes for testing) + mockCert := []byte("mock certificate data for testing purposes") + + // Calculate its fingerprint + fingerprint := sha256.Sum256(mockCert) + expectedFP := hex.EncodeToString(fingerprint[:]) + + config := FingerprintVerifier(expectedFP) + + // Should succeed with matching fingerprint + err := config.VerifyPeerCertificate([][]byte{mockCert}, nil) + if err != nil { + t.Errorf("Should succeed with matching fingerprint, got: %v", err) + } +} + +func TestFingerprintVerifier_MismatchedFingerprint(t *testing.T) { + mockCert := []byte("mock certificate data") + + // Use a different fingerprint + wrongFP := "0000000000000000000000000000000000000000000000000000000000000000" + + config := FingerprintVerifier(wrongFP) + + // Should fail with mismatched fingerprint + err := config.VerifyPeerCertificate([][]byte{mockCert}, nil) + if err == nil { + t.Error("Should fail with mismatched fingerprint") + } + if !strings.Contains(err.Error(), "mismatch") { + t.Errorf("Error message should mention mismatch, got: %v", err) + } +} + +func TestCreateHTTPClient_InsecureMode(t *testing.T) { + client := CreateHTTPClient(false, "") + + if client == nil { + t.Fatal("CreateHTTPClient returned nil") + } + + transport, ok := client.Transport.(*http.Transport) + if !ok { + t.Fatal("Transport is not *http.Transport") + } + + if transport.TLSClientConfig == nil { + t.Fatal("TLSClientConfig should be set") + } + + if !transport.TLSClientConfig.InsecureSkipVerify { + t.Error("InsecureSkipVerify should be true in insecure mode") + } +} + +func TestCreateHTTPClient_FingerprintMode(t *testing.T) { + fingerprint := "aabbccdd" + client := CreateHTTPClient(false, fingerprint) + + if client == nil { + t.Fatal("CreateHTTPClient returned nil") + } + + transport, ok := client.Transport.(*http.Transport) + if !ok { + t.Fatal("Transport is not *http.Transport") + } + + if transport.TLSClientConfig == nil { + t.Fatal("TLSClientConfig should be set") + } + + // Should have custom verification function + if transport.TLSClientConfig.VerifyPeerCertificate == nil { + t.Error("VerifyPeerCertificate should be set in fingerprint mode") + } +} + +func TestCreateHTTPClient_SecureMode(t *testing.T) { + client := CreateHTTPClient(true, "") + + if client == nil { + t.Fatal("CreateHTTPClient returned nil") + } + + transport, ok := client.Transport.(*http.Transport) + if !ok { + t.Fatal("Transport is not *http.Transport") + } + + // In secure mode, TLSClientConfig should either be nil (use defaults) + // or not have InsecureSkipVerify set + if transport.TLSClientConfig != nil && transport.TLSClientConfig.InsecureSkipVerify { + t.Error("InsecureSkipVerify should not be true in secure mode") + } +} + +func TestCreateHTTPClientWithTimeout_DefaultTimeout(t *testing.T) { + client := CreateHTTPClientWithTimeout(true, "", 0) + + if client == nil { + t.Fatal("CreateHTTPClientWithTimeout returned nil") + } + + // Should use default timeout when 0 is passed + if client.Timeout != 60*time.Second { + t.Errorf("Timeout = %v, want %v", client.Timeout, 60*time.Second) + } +} + +func TestCreateHTTPClientWithTimeout_CustomTimeout(t *testing.T) { + customTimeout := 30 * time.Second + client := CreateHTTPClientWithTimeout(true, "", customTimeout) + + if client == nil { + t.Fatal("CreateHTTPClientWithTimeout returned nil") + } + + if client.Timeout != customTimeout { + t.Errorf("Timeout = %v, want %v", client.Timeout, customTimeout) + } +} + +func TestCreateHTTPClientWithTimeout_NegativeTimeout(t *testing.T) { + client := CreateHTTPClientWithTimeout(true, "", -10*time.Second) + + if client == nil { + t.Fatal("CreateHTTPClientWithTimeout returned nil") + } + + // Should use default timeout when negative is passed + if client.Timeout != 60*time.Second { + t.Errorf("Timeout = %v, want %v (default)", client.Timeout, 60*time.Second) + } +} + +func TestCreateHTTPClient_TransportSettings(t *testing.T) { + client := CreateHTTPClient(true, "") + + transport, ok := client.Transport.(*http.Transport) + if !ok { + t.Fatal("Transport is not *http.Transport") + } + + // Verify transport settings + if transport.MaxIdleConns != 100 { + t.Errorf("MaxIdleConns = %v, want 100", transport.MaxIdleConns) + } + if transport.MaxIdleConnsPerHost != 20 { + t.Errorf("MaxIdleConnsPerHost = %v, want 20", transport.MaxIdleConnsPerHost) + } + if transport.MaxConnsPerHost != 20 { + t.Errorf("MaxConnsPerHost = %v, want 20", transport.MaxConnsPerHost) + } + if transport.IdleConnTimeout != 90*time.Second { + t.Errorf("IdleConnTimeout = %v, want 90s", transport.IdleConnTimeout) + } + if !transport.DisableCompression { + t.Error("DisableCompression should be true") + } + if transport.TLSHandshakeTimeout != 10*time.Second { + t.Errorf("TLSHandshakeTimeout = %v, want 10s", transport.TLSHandshakeTimeout) + } +} + +func TestFingerprintVerifier_IgnoresVerifiedChains(t *testing.T) { + mockCert := []byte("test certificate") + fingerprint := sha256.Sum256(mockCert) + expectedFP := hex.EncodeToString(fingerprint[:]) + + config := FingerprintVerifier(expectedFP) + + // verifiedChains parameter should be ignored + mockChains := [][]*x509.Certificate{{&x509.Certificate{}}} + err := config.VerifyPeerCertificate([][]byte{mockCert}, mockChains) + if err != nil { + t.Errorf("Should ignore verifiedChains, got error: %v", err) + } +} + +func TestGetDNSResolver(t *testing.T) { + resolver := GetDNSResolver() + + if resolver == nil { + t.Fatal("GetDNSResolver returned nil") + } + + // Call again - should return same instance (singleton) + resolver2 := GetDNSResolver() + if resolver != resolver2 { + t.Error("GetDNSResolver should return same instance") + } +} + +func TestSetDNSCacheTTL_DefaultValue(t *testing.T) { + // Setting TTL to 0 should use default + SetDNSCacheTTL(0) + // No panic means success - we can't easily verify the internal state +} + +func TestSetDNSCacheTTL_NegativeValue(t *testing.T) { + // Setting TTL to negative should use default + SetDNSCacheTTL(-5 * time.Minute) + // No panic means success +} + +func TestSetDNSCacheTTL_CustomValue(t *testing.T) { + // Setting custom TTL + SetDNSCacheTTL(10 * time.Minute) + // No panic means success +} + +func TestFingerprintVerifier_ColonSeparatedFingerprint(t *testing.T) { + mockCert := []byte("test cert with colons") + fingerprint := sha256.Sum256(mockCert) + + // Format with colons (common format from openssl) + fpBytes := fingerprint[:] + var parts []string + for _, b := range fpBytes { + parts = append(parts, hex.EncodeToString([]byte{b})) + } + colonSeparated := strings.ToUpper(strings.Join(parts, ":")) + + config := FingerprintVerifier(colonSeparated) + + err := config.VerifyPeerCertificate([][]byte{mockCert}, nil) + if err != nil { + t.Errorf("Should handle colon-separated fingerprint, got: %v", err) + } +} + +func TestCreateHTTPClient_FingerprintTakesPrecedence(t *testing.T) { + // Even if verifySSL is true, fingerprint mode should be used if fingerprint is provided + fingerprint := "aabbccdd" + client := CreateHTTPClient(true, fingerprint) + + transport, ok := client.Transport.(*http.Transport) + if !ok { + t.Fatal("Transport is not *http.Transport") + } + + // Should use fingerprint verification + if transport.TLSClientConfig == nil { + t.Fatal("TLSClientConfig should be set") + } + if transport.TLSClientConfig.VerifyPeerCertificate == nil { + t.Error("Should use fingerprint verification when fingerprint is provided") + } +} + +func TestFingerprintVerifier_TLSVersion(t *testing.T) { + config := FingerprintVerifier("aabbccdd") + + // Check that config is a valid TLS config + if config == nil { + t.Fatal("FingerprintVerifier returned nil config") + } + + // MinVersion should not be set to an insecure version + // (default is fine, or TLS 1.2+) + if config.MinVersion != 0 && config.MinVersion < tls.VersionTLS12 { + t.Errorf("MinVersion should be 0 (default) or >= TLS 1.2, got %v", config.MinVersion) + } +}