Files
geoipapi/client/geoipapi/geoipclient_test.go
Ask Bjørn Hansen c991335da7
All checks were successful
continuous-integration/drone/push Build is passing
Add comprehensive test suite and documentation
- 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
2025-07-02 01:02:28 -07:00

408 lines
11 KiB
Go

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)
})
}