Add comprehensive test suite and documentation
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
- Complete unit, integration, and E2E test coverage (189 test cases) - Enhanced CI/CD pipeline with race detection and quality checks - Comprehensive godoc documentation for all packages - Updated README with API docs, examples, and deployment guides
This commit is contained in:
407
client/geoipapi/geoipclient_test.go
Normal file
407
client/geoipapi/geoipclient_test.go
Normal file
@@ -0,0 +1,407 @@
|
||||
package geoipapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/oschwald/geoip2-golang"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetCity(t *testing.T) {
|
||||
// Save original environment variable
|
||||
originalGeoIPService := os.Getenv("geoip_service")
|
||||
defer func() {
|
||||
if originalGeoIPService == "" {
|
||||
os.Unsetenv("geoip_service")
|
||||
} else {
|
||||
os.Setenv("geoip_service", originalGeoIPService)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Run("Missing geoip_service environment variable", func(t *testing.T) {
|
||||
os.Unsetenv("geoip_service")
|
||||
|
||||
ctx := context.Background()
|
||||
ip := netip.MustParseAddr("8.8.8.8")
|
||||
|
||||
_, err := GetCity(ctx, ip)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "geoip_service not configured")
|
||||
})
|
||||
|
||||
t.Run("Successful API call", func(t *testing.T) {
|
||||
// Create mock server
|
||||
mockCity := &geoip2.City{}
|
||||
mockCity.Country.GeoNameID = 6252001
|
||||
mockCity.Country.IsoCode = "US"
|
||||
mockCity.Country.Names = map[string]string{"en": "United States"}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify the request
|
||||
assert.Equal(t, "GET", r.Method)
|
||||
assert.Equal(t, "/api/json", r.URL.Path)
|
||||
assert.Equal(t, "8.8.8.8", r.URL.Query().Get("ip"))
|
||||
|
||||
// Return mock response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(mockCity)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Set environment variable to point to mock server
|
||||
serverAddr := server.Listener.Addr().String()
|
||||
os.Setenv("geoip_service", serverAddr)
|
||||
|
||||
ctx := context.Background()
|
||||
ip := netip.MustParseAddr("8.8.8.8")
|
||||
|
||||
city, err := GetCity(ctx, ip)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, city)
|
||||
assert.Equal(t, "US", city.Country.IsoCode)
|
||||
assert.Equal(t, "United States", city.Country.Names["en"])
|
||||
})
|
||||
|
||||
t.Run("Server returns error", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("Internal server error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
serverAddr := server.Listener.Addr().String()
|
||||
os.Setenv("geoip_service", serverAddr)
|
||||
|
||||
ctx := context.Background()
|
||||
ip := netip.MustParseAddr("8.8.8.8")
|
||||
|
||||
_, err := GetCity(ctx, ip)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Invalid JSON response", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("invalid json"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
serverAddr := server.Listener.Addr().String()
|
||||
os.Setenv("geoip_service", serverAddr)
|
||||
|
||||
ctx := context.Background()
|
||||
ip := netip.MustParseAddr("8.8.8.8")
|
||||
|
||||
_, err := GetCity(ctx, ip)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Network timeout", func(t *testing.T) {
|
||||
// Create server that delays response beyond client timeout
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(15 * time.Second) // Longer than client timeout
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
serverAddr := server.Listener.Addr().String()
|
||||
os.Setenv("geoip_service", serverAddr)
|
||||
|
||||
ctx := context.Background()
|
||||
ip := netip.MustParseAddr("8.8.8.8")
|
||||
|
||||
_, err := GetCity(ctx, ip)
|
||||
assert.Error(t, err)
|
||||
// Should be a timeout or context deadline exceeded error
|
||||
})
|
||||
|
||||
t.Run("Context cancellation", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(1 * time.Second)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
serverAddr := server.Listener.Addr().String()
|
||||
os.Setenv("geoip_service", serverAddr)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
ip := netip.MustParseAddr("8.8.8.8")
|
||||
|
||||
_, err := GetCity(ctx, ip)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "context canceled")
|
||||
})
|
||||
|
||||
t.Run("Different IP address formats", func(t *testing.T) {
|
||||
testIPs := []struct {
|
||||
ip string
|
||||
expected string
|
||||
}{
|
||||
{"192.168.1.1", "192.168.1.1"},
|
||||
{"::1", "::1"},
|
||||
{"2001:4860:4860::8888", "2001:4860:4860::8888"},
|
||||
{"127.0.0.1", "127.0.0.1"},
|
||||
}
|
||||
|
||||
for _, tt := range testIPs {
|
||||
t.Run("IP_"+tt.ip, func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify the IP parameter
|
||||
assert.Equal(t, tt.expected, r.URL.Query().Get("ip"))
|
||||
|
||||
mockCity := &geoip2.City{}
|
||||
mockCity.Country.IsoCode = "XX"
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(mockCity)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
serverAddr := server.Listener.Addr().String()
|
||||
os.Setenv("geoip_service", serverAddr)
|
||||
|
||||
ctx := context.Background()
|
||||
ip := netip.MustParseAddr(tt.ip)
|
||||
|
||||
city, err := GetCity(ctx, ip)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "XX", city.Country.IsoCode)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTPClientConfiguration(t *testing.T) {
|
||||
t.Run("Client timeout configuration", func(t *testing.T) {
|
||||
// Test that client has appropriate timeout
|
||||
assert.Equal(t, 10*time.Second, client.Timeout)
|
||||
})
|
||||
|
||||
t.Run("Transport configuration", func(t *testing.T) {
|
||||
// Verify transport is configured
|
||||
transport, ok := client.Transport.(*http.Transport)
|
||||
assert.True(t, ok)
|
||||
assert.NotNil(t, transport)
|
||||
|
||||
// Check timeout settings
|
||||
assert.Equal(t, 5*time.Second, transport.TLSHandshakeTimeout)
|
||||
})
|
||||
}
|
||||
|
||||
func TestURLConstruction(t *testing.T) {
|
||||
t.Run("URL construction with query parameters", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify URL construction
|
||||
assert.Equal(t, "/api/json", r.URL.Path)
|
||||
assert.Equal(t, "8.8.8.8", r.URL.Query().Get("ip"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("{}"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
serverAddr := server.Listener.Addr().String()
|
||||
os.Setenv("geoip_service", serverAddr)
|
||||
|
||||
ctx := context.Background()
|
||||
ip := netip.MustParseAddr("8.8.8.8")
|
||||
|
||||
GetCity(ctx, ip)
|
||||
})
|
||||
|
||||
t.Run("URL encoding of special characters", func(t *testing.T) {
|
||||
// Test IPv6 addresses that might need URL encoding
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ipParam := r.URL.Query().Get("ip")
|
||||
assert.Equal(t, "2001:4860:4860::8888", ipParam)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("{}"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
serverAddr := server.Listener.Addr().String()
|
||||
os.Setenv("geoip_service", serverAddr)
|
||||
|
||||
ctx := context.Background()
|
||||
ip := netip.MustParseAddr("2001:4860:4860::8888")
|
||||
|
||||
GetCity(ctx, ip)
|
||||
})
|
||||
}
|
||||
|
||||
func TestErrorHandling(t *testing.T) {
|
||||
t.Run("Network connection refused", func(t *testing.T) {
|
||||
// Point to a non-existent server
|
||||
os.Setenv("geoip_service", "localhost:99999")
|
||||
|
||||
ctx := context.Background()
|
||||
ip := netip.MustParseAddr("8.8.8.8")
|
||||
|
||||
_, err := GetCity(ctx, ip)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Invalid hostname", func(t *testing.T) {
|
||||
// Point to invalid hostname
|
||||
os.Setenv("geoip_service", "invalid-hostname-that-does-not-exist:8009")
|
||||
|
||||
ctx := context.Background()
|
||||
ip := netip.MustParseAddr("8.8.8.8")
|
||||
|
||||
_, err := GetCity(ctx, ip)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("Empty response body", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// No body
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
serverAddr := server.Listener.Addr().String()
|
||||
os.Setenv("geoip_service", serverAddr)
|
||||
|
||||
ctx := context.Background()
|
||||
ip := netip.MustParseAddr("8.8.8.8")
|
||||
|
||||
_, err := GetCity(ctx, ip)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOpenTelemetryIntegration(t *testing.T) {
|
||||
t.Run("Tracing span creation", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mockCity := &geoip2.City{}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(mockCity)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
serverAddr := server.Listener.Addr().String()
|
||||
os.Setenv("geoip_service", serverAddr)
|
||||
|
||||
ctx := context.Background()
|
||||
ip := netip.MustParseAddr("8.8.8.8")
|
||||
|
||||
// Test that the function runs without panicking
|
||||
// (actual tracing verification would require more complex setup)
|
||||
city, err := GetCity(ctx, ip)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, city)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConcurrentRequests(t *testing.T) {
|
||||
t.Run("Concurrent API calls", func(t *testing.T) {
|
||||
var requestCount int64
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
count := atomic.AddInt64(&requestCount, 1)
|
||||
|
||||
mockCity := &geoip2.City{}
|
||||
mockCity.Country.IsoCode = fmt.Sprintf("T%d", count)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(mockCity)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
serverAddr := server.Listener.Addr().String()
|
||||
os.Setenv("geoip_service", serverAddr)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Launch multiple concurrent requests
|
||||
numRequests := 5
|
||||
results := make(chan error, numRequests)
|
||||
|
||||
for i := 0; i < numRequests; i++ {
|
||||
go func() {
|
||||
ip := netip.MustParseAddr("8.8.8.8")
|
||||
_, err := GetCity(ctx, ip)
|
||||
results <- err
|
||||
}()
|
||||
}
|
||||
|
||||
// Collect results
|
||||
for i := 0; i < numRequests; i++ {
|
||||
err := <-results
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, int64(numRequests), atomic.LoadInt64(&requestCount))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNetIPAddressHandling(t *testing.T) {
|
||||
t.Run("netip.Addr conversions", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
ip string
|
||||
}{
|
||||
{"IPv4", "192.168.1.1"},
|
||||
{"IPv6", "2001:db8::1"},
|
||||
{"IPv4 mapped IPv6", "::ffff:192.168.1.1"},
|
||||
{"Loopback IPv4", "127.0.0.1"},
|
||||
{"Loopback IPv6", "::1"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
addr := netip.MustParseAddr(tc.ip)
|
||||
|
||||
// Test that netip.Addr can be converted to string properly
|
||||
addrStr := addr.String()
|
||||
assert.NotEmpty(t, addrStr)
|
||||
|
||||
// Test that we can parse it back
|
||||
parsed := net.ParseIP(addrStr)
|
||||
assert.NotNil(t, parsed)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTPHeaderHandling(t *testing.T) {
|
||||
t.Run("HTTP headers in requests", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify that standard HTTP headers are present
|
||||
assert.NotEmpty(t, r.Header.Get("User-Agent"))
|
||||
|
||||
mockCity := &geoip2.City{}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(mockCity)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
serverAddr := server.Listener.Addr().String()
|
||||
os.Setenv("geoip_service", serverAddr)
|
||||
|
||||
ctx := context.Background()
|
||||
ip := netip.MustParseAddr("8.8.8.8")
|
||||
|
||||
_, err := GetCity(ctx, ip)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user