Files
Charon/backend/internal/services/geoip_service_test.go
2026-03-04 18:34:49 +00:00

198 lines
5.5 KiB
Go

package services
import (
"errors"
"net/netip"
"os"
"path/filepath"
"testing"
"github.com/oschwald/geoip2-golang/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type fakeGeoIPReader struct {
isoCode string
err error
}
func (f *fakeGeoIPReader) Country(_ netip.Addr) (*geoip2.Country, error) {
if f.err != nil {
return nil, f.err
}
rec := &geoip2.Country{}
rec.Country.ISOCode = f.isoCode
return rec, nil
}
func (f *fakeGeoIPReader) Close() error { return nil }
// TestNewGeoIPService_InvalidPath tests creation with an invalid database path.
func TestNewGeoIPService_InvalidPath(t *testing.T) {
_, err := NewGeoIPService("/nonexistent/path/to/GeoLite2-Country.mmdb")
assert.Error(t, err)
}
// TestGeoIPService_NotLoaded tests lookup behavior when database is not loaded.
func TestGeoIPService_NotLoaded(t *testing.T) {
svc := &GeoIPService{dbPath: "/invalid/path.mmdb"}
// Should return error when database not loaded
_, err := svc.LookupCountry("8.8.8.8")
assert.ErrorIs(t, err, ErrGeoIPDatabaseNotLoaded)
// IsLoaded should return false
assert.False(t, svc.IsLoaded())
}
// TestGeoIPService_InvalidIP tests lookup with invalid IP addresses.
func TestGeoIPService_InvalidIP(t *testing.T) {
svc := &GeoIPService{dbPath: "/test/path.mmdb", db: &fakeGeoIPReader{isoCode: "US"}}
_, err := svc.LookupCountry("not-an-ip")
assert.ErrorIs(t, err, ErrInvalidGeoIP)
}
func TestGeoIPService_LookupCountry_CountryNotFound(t *testing.T) {
svc := &GeoIPService{dbPath: "/test/path.mmdb", db: &fakeGeoIPReader{isoCode: ""}}
_, err := svc.LookupCountry("8.8.8.8")
assert.ErrorIs(t, err, ErrCountryNotFound)
}
func TestGeoIPService_LookupCountry_Success(t *testing.T) {
svc := &GeoIPService{dbPath: "/test/path.mmdb", db: &fakeGeoIPReader{isoCode: "US"}}
cc, err := svc.LookupCountry("8.8.8.8")
assert.NoError(t, err)
assert.Equal(t, "US", cc)
}
func TestGeoIPService_LookupCountry_ReaderError(t *testing.T) {
svc := &GeoIPService{dbPath: "/test/path.mmdb", db: &fakeGeoIPReader{err: errors.New("boom")}}
_, err := svc.LookupCountry("8.8.8.8")
assert.Error(t, err)
}
// TestGeoIPService_Close tests closing behavior.
func TestGeoIPService_Close(t *testing.T) {
svc := &GeoIPService{dbPath: "/test/path.mmdb"}
// Close on nil db should not error
err := svc.Close()
assert.NoError(t, err)
assert.False(t, svc.IsLoaded())
}
// TestGeoIPService_GetDatabasePath tests the path getter.
func TestGeoIPService_GetDatabasePath(t *testing.T) {
expectedPath := "/app/data/geoip/GeoLite2-Country.mmdb"
svc := &GeoIPService{dbPath: expectedPath}
assert.Equal(t, expectedPath, svc.GetDatabasePath())
}
// TestGeoIPService_ConcurrentAccess tests thread-safety of lookups.
func TestGeoIPService_ConcurrentAccess(t *testing.T) {
svc := &GeoIPService{dbPath: "/test/path.mmdb"}
// Launch multiple goroutines to access the service concurrently
done := make(chan bool)
for i := 0; i < 10; i++ {
go func() {
_ = svc.IsLoaded()
_, _ = svc.LookupCountry("8.8.8.8")
_ = svc.GetDatabasePath()
done <- true
}()
}
// Wait for all goroutines
for i := 0; i < 10; i++ {
<-done
}
}
// Integration test - only runs if a real GeoIP database is available
// To run: go test -tags=integration -run TestGeoIPService_Integration
func TestGeoIPService_Integration(t *testing.T) {
// Check common paths for GeoIP database
homePath := filepath.Join(os.Getenv("HOME"), ".local", "share", "GeoIP", "GeoLite2-Country.mmdb")
possiblePaths := []string{
"/app/data/geoip/GeoLite2-Country.mmdb",
"../../../data/geoip/GeoLite2-Country.mmdb",
homePath,
}
var dbPath string
for _, p := range possiblePaths {
if _, err := os.Stat(p); err == nil {
dbPath = p
break
}
}
if dbPath == "" {
t.Skip("GeoIP database not found, skipping integration test")
}
svc, err := NewGeoIPService(dbPath)
require.NoError(t, err)
defer func() { _ = svc.Close() }()
t.Run("IsLoaded", func(t *testing.T) {
assert.True(t, svc.IsLoaded())
})
t.Run("LookupKnownIP", func(t *testing.T) {
// Google's DNS is in the US
country, err := svc.LookupCountry("8.8.8.8")
assert.NoError(t, err)
assert.Equal(t, "US", country)
})
t.Run("LookupPrivateIP", func(t *testing.T) {
// Private IPs typically return no country
_, err := svc.LookupCountry("192.168.1.1")
// This might return ErrCountryNotFound or a valid country depending on database
// Either is acceptable behavior
if err != nil {
assert.ErrorIs(t, err, ErrCountryNotFound)
}
})
t.Run("LookupInvalidIP", func(t *testing.T) {
_, err := svc.LookupCountry("invalid-ip")
assert.ErrorIs(t, err, ErrInvalidGeoIP)
})
t.Run("LookupIPv6", func(t *testing.T) {
// Google's IPv6 DNS
country, err := svc.LookupCountry("2001:4860:4860::8888")
assert.NoError(t, err)
assert.Equal(t, "US", country)
})
t.Run("Reload", func(t *testing.T) {
// Test hot-reload capability
err := svc.Load()
assert.NoError(t, err)
assert.True(t, svc.IsLoaded())
// Verify lookup still works after reload
country, err := svc.LookupCountry("8.8.8.8")
assert.NoError(t, err)
assert.Equal(t, "US", country)
})
}
// TestGeoIPService_ErrorTypes verifies error constants are properly defined.
func TestGeoIPService_ErrorTypes(t *testing.T) {
assert.NotNil(t, ErrGeoIPDatabaseNotLoaded)
assert.NotNil(t, ErrInvalidGeoIP)
assert.NotNil(t, ErrCountryNotFound)
// Verify error messages
assert.Contains(t, ErrGeoIPDatabaseNotLoaded.Error(), "database")
assert.Contains(t, ErrInvalidGeoIP.Error(), "invalid")
assert.Contains(t, ErrCountryNotFound.Error(), "country")
}