feat: geoip
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "pkg/geoip/test-data"]
|
||||||
|
path = pkg/geoip/test-data
|
||||||
|
url = https://github.com/maxmind/MaxMind-DB.git
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# TODOs
|
||||||
|
|
||||||
|
## 1. Suggest User's Preferred Language when registering a new User
|
||||||
|
|
||||||
|
Upon user's device/session registration flow, `preferred_language` value
|
||||||
|
must be obtained via existing [geoip](../pkg/geoip) package by returned Country.
|
||||||
|
When geoip feils to return country by ip, fallback is `en` language.
|
||||||
@@ -597,6 +597,7 @@ Role in the system:
|
|||||||
## Geo-IP Source
|
## Geo-IP Source
|
||||||
|
|
||||||
The service uses a locally stored free Geo-IP country database.
|
The service uses a locally stored free Geo-IP country database.
|
||||||
|
The Geo-IP acessible via [geoip](../pkg/geoip/) package.
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
go 1.26.0
|
go 1.26.1
|
||||||
|
|
||||||
use (
|
use (
|
||||||
./authsession
|
./authsession
|
||||||
@@ -8,6 +8,7 @@ use (
|
|||||||
./pkg/calc
|
./pkg/calc
|
||||||
./pkg/connector
|
./pkg/connector
|
||||||
./pkg/error
|
./pkg/error
|
||||||
|
./pkg/geoip
|
||||||
./pkg/model
|
./pkg/model
|
||||||
./pkg/schema
|
./pkg/schema
|
||||||
./pkg/storage
|
./pkg/storage
|
||||||
@@ -19,6 +20,7 @@ replace (
|
|||||||
galaxy/calc v0.0.0 => ./pkg/calc
|
galaxy/calc v0.0.0 => ./pkg/calc
|
||||||
galaxy/connector v0.0.0 => ./pkg/connector
|
galaxy/connector v0.0.0 => ./pkg/connector
|
||||||
galaxy/error v0.0.0 => ./pkg/error
|
galaxy/error v0.0.0 => ./pkg/error
|
||||||
|
galaxy/geoip v0.0.0 => ./pkg/geoip
|
||||||
galaxy/model v0.0.0 => ./pkg/model
|
galaxy/model v0.0.0 => ./pkg/model
|
||||||
galaxy/schema v0.0.0 => ./pkg/schema
|
galaxy/schema v0.0.0 => ./pkg/schema
|
||||||
galaxy/storage v0.0.0 => ./pkg/storage
|
galaxy/storage v0.0.0 => ./pkg/storage
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# `galaxy/geoip`
|
||||||
|
|
||||||
|
`galaxy/geoip` is a small in-process library that resolves a country from an IP
|
||||||
|
address by reading a local MaxMind `.mmdb` database.
|
||||||
|
|
||||||
|
The package is intended for trusted internal services such as `Edge Gateway`
|
||||||
|
and `Geo Profile Service`. It is not a standalone network service.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- A local country-capable MaxMind database in `.mmdb` format.
|
||||||
|
- `GeoLite2 Country` is the expected default database for product deployments.
|
||||||
|
- One long-lived `Resolver` instance should be reused across goroutines and
|
||||||
|
closed during process shutdown.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
See [example](example/) directory.
|
||||||
|
|
||||||
|
## Error Semantics
|
||||||
|
|
||||||
|
- `ErrInvalidAddress`: the caller supplied an invalid IP string or `netip.Addr`.
|
||||||
|
- `ErrCountryNotFound`: the IP is valid, but the database has no country record
|
||||||
|
for it. This is the expected fail-open branch for private, local, or
|
||||||
|
otherwise unmapped addresses.
|
||||||
|
- `ErrClosed`: the resolver has already been closed.
|
||||||
|
|
||||||
|
Returned country codes are normalized to uppercase `ISO 3166-1 alpha-2`
|
||||||
|
because that is the country format exposed by MaxMind Country databases.
|
||||||
|
|
||||||
|
## Test Data
|
||||||
|
|
||||||
|
Tests use the MaxMind reference fixtures via a git submodule under
|
||||||
|
`pkg/geoip/test-data`.
|
||||||
|
|
||||||
|
Initialize submodules before running package tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git submodule update --init --recursive
|
||||||
|
```
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"galaxy/geoip"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
resolver, err := geoip.Open("/srv/geoip/GeoLite2-Country.mmdb")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := resolver.Close(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
country, err := resolver.Country(netip.MustParseAddr("81.2.69.160"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(country)
|
||||||
|
|
||||||
|
fromString, err := resolver.CountryString("203.0.113.10")
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
fmt.Println(fromString)
|
||||||
|
case errors.Is(err, geoip.ErrCountryNotFound):
|
||||||
|
// Fail open: keep request processing and treat the country as unknown.
|
||||||
|
default:
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package geoip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/oschwald/geoip2-golang/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidAddress reports that a caller supplied an invalid IP address.
|
||||||
|
ErrInvalidAddress = errors.New("invalid IP address")
|
||||||
|
|
||||||
|
// ErrCountryNotFound reports that the GeoIP database contains no country
|
||||||
|
// record for a valid IP address.
|
||||||
|
ErrCountryNotFound = errors.New("country not found")
|
||||||
|
|
||||||
|
// ErrClosed reports that the resolver has already released its database
|
||||||
|
// resources and can no longer serve lookups.
|
||||||
|
ErrClosed = errors.New("geoip resolver is closed")
|
||||||
|
)
|
||||||
|
|
||||||
|
type countryReader interface {
|
||||||
|
Country(netip.Addr) (*geoip2.Country, error)
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolver resolves ISO 3166-1 alpha-2 country codes from a local MaxMind
|
||||||
|
// country-capable database.
|
||||||
|
//
|
||||||
|
// Resolver is safe for concurrent lookups. Call Close when the process no
|
||||||
|
// longer needs the memory-mapped database file.
|
||||||
|
type Resolver struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
reader countryReader
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open constructs a Resolver backed by the MaxMind database file at
|
||||||
|
// databasePath.
|
||||||
|
//
|
||||||
|
// databasePath must point to a local country-capable .mmdb file such as
|
||||||
|
// GeoLite2 Country. Open trims surrounding whitespace from databasePath before
|
||||||
|
// opening the file.
|
||||||
|
func Open(databasePath string) (*Resolver, error) {
|
||||||
|
path := strings.TrimSpace(databasePath)
|
||||||
|
if path == "" {
|
||||||
|
return nil, errors.New("open geoip database: database path must not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := geoip2.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open geoip database %q: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newResolver(reader), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGeoIP is a legacy alias for Open.
|
||||||
|
//
|
||||||
|
// Deprecated: use Open for new code.
|
||||||
|
func NewGeoIP(databasePath string) (*Resolver, error) {
|
||||||
|
return Open(databasePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newResolver(reader countryReader) *Resolver {
|
||||||
|
return &Resolver{reader: reader}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Country resolves addr to an uppercase ISO 3166-1 alpha-2 country code.
|
||||||
|
//
|
||||||
|
// Country returns ErrInvalidAddress when addr is not valid, ErrCountryNotFound
|
||||||
|
// when the database contains no country for addr, and ErrClosed after Close
|
||||||
|
// has been called successfully.
|
||||||
|
func (r *Resolver) Country(addr netip.Addr) (string, error) {
|
||||||
|
if !addr.IsValid() {
|
||||||
|
return "", fmt.Errorf("lookup country: %w", ErrInvalidAddress)
|
||||||
|
}
|
||||||
|
if r == nil {
|
||||||
|
return "", fmt.Errorf("lookup country for %s: %w", addr, ErrClosed)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
|
if r.reader == nil {
|
||||||
|
return "", fmt.Errorf("lookup country for %s: %w", addr, ErrClosed)
|
||||||
|
}
|
||||||
|
|
||||||
|
record, err := r.reader.Country(addr)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("lookup country for %s: %w", addr, err)
|
||||||
|
}
|
||||||
|
if record == nil {
|
||||||
|
return "", fmt.Errorf("lookup country for %s: nil country record", addr)
|
||||||
|
}
|
||||||
|
if !record.HasData() || strings.TrimSpace(record.Country.ISOCode) == "" {
|
||||||
|
return "", fmt.Errorf("lookup country for %s: %w", addr, ErrCountryNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
code, err := normalizeCountryCode(record.Country.ISOCode)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("lookup country for %s: %w", addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return code, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountryString resolves raw to an uppercase ISO 3166-1 alpha-2 country code.
|
||||||
|
//
|
||||||
|
// CountryString trims surrounding whitespace from raw before parsing it as an
|
||||||
|
// IP address and then delegates to Country.
|
||||||
|
func (r *Resolver) CountryString(raw string) (string, error) {
|
||||||
|
trimmed := strings.TrimSpace(raw)
|
||||||
|
|
||||||
|
addr, err := netip.ParseAddr(trimmed)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parse IP address %q: %w", raw, errors.Join(ErrInvalidAddress, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Country(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close releases the underlying database resources.
|
||||||
|
//
|
||||||
|
// Close is idempotent and nil-safe.
|
||||||
|
func (r *Resolver) Close() error {
|
||||||
|
if r == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
if r.reader == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := r.reader
|
||||||
|
r.reader = nil
|
||||||
|
|
||||||
|
if err := reader.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close geoip resolver: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeCountryCode(raw string) (string, error) {
|
||||||
|
code := strings.ToUpper(strings.TrimSpace(raw))
|
||||||
|
if len(code) != 2 {
|
||||||
|
return "", fmt.Errorf("invalid ISO 3166-1 alpha-2 code %q", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := 0; idx < len(code); idx++ {
|
||||||
|
if code[idx] < 'A' || code[idx] > 'Z' {
|
||||||
|
return "", fmt.Errorf("invalid ISO 3166-1 alpha-2 code %q", raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return code, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
package geoip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/oschwald/geoip2-golang/v2"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const countryFixturePath = "test-data/test-data/GeoIP2-Country-Test.mmdb"
|
||||||
|
|
||||||
|
type fakeReader struct {
|
||||||
|
countryFunc func(netip.Addr) (*geoip2.Country, error)
|
||||||
|
closeFunc func() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeReader) Country(addr netip.Addr) (*geoip2.Country, error) {
|
||||||
|
if f.countryFunc == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.countryFunc(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeReader) Close() error {
|
||||||
|
if f.closeFunc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.closeFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenRejectsEmptyPath(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{name: "empty", path: ""},
|
||||||
|
{name: "whitespace", path: " \t\n"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
resolver, err := Open(tt.path)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, resolver)
|
||||||
|
assert.Contains(t, err.Error(), "database path must not be empty")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenMissingFile(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
resolver, err := Open("test-data/test-data/does-not-exist.mmdb")
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, resolver)
|
||||||
|
assert.Contains(t, err.Error(), `open geoip database "test-data/test-data/does-not-exist.mmdb"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolverFixtureLookups(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
lookup func(*Resolver) (string, error)
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "addr lookup",
|
||||||
|
lookup: func(resolver *Resolver) (string, error) {
|
||||||
|
return resolver.Country(netip.MustParseAddr("81.2.69.160"))
|
||||||
|
},
|
||||||
|
want: "GB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "string lookup",
|
||||||
|
lookup: func(resolver *Resolver) (string, error) {
|
||||||
|
return resolver.CountryString(" 81.2.69.160 ")
|
||||||
|
},
|
||||||
|
want: "GB",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
resolver := openFixtureResolver(t)
|
||||||
|
|
||||||
|
got, err := tt.lookup(resolver)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewGeoIPLegacyAlias(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
requireFixtureDatabase(t)
|
||||||
|
|
||||||
|
resolver, err := NewGeoIP(countryFixturePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, resolver.Close())
|
||||||
|
})
|
||||||
|
|
||||||
|
got, err := resolver.CountryString("81.2.69.160")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "GB", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolverFixtureReturnsNotFoundForPrivateAddress(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
resolver := openFixtureResolver(t)
|
||||||
|
|
||||||
|
_, err := resolver.Country(netip.MustParseAddr("192.168.1.1"))
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, ErrCountryNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolverCloseIsIdempotent(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
resolver := openFixtureResolver(t)
|
||||||
|
|
||||||
|
require.NoError(t, resolver.Close())
|
||||||
|
require.NoError(t, resolver.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolverCloseIsNilSafe(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var resolver *Resolver
|
||||||
|
require.NoError(t, resolver.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolverCountryAfterClose(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
resolver := openFixtureResolver(t)
|
||||||
|
require.NoError(t, resolver.Close())
|
||||||
|
|
||||||
|
_, err := resolver.Country(netip.MustParseAddr("81.2.69.160"))
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, ErrClosed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolverCountryRejectsZeroAddress(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
resolver := newResolver(fakeReader{})
|
||||||
|
|
||||||
|
_, err := resolver.Country(netip.Addr{})
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, ErrInvalidAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolverCountryStringRejectsInvalidAddress(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []string{
|
||||||
|
"",
|
||||||
|
"not-an-ip",
|
||||||
|
"999.0.0.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, raw := range tests {
|
||||||
|
raw := raw
|
||||||
|
t.Run(raw, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
resolver := newResolver(fakeReader{})
|
||||||
|
|
||||||
|
_, err := resolver.CountryString(raw)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, ErrInvalidAddress)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolverCountryWrapsReaderError(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
lookupErr := errors.New("lookup failed")
|
||||||
|
resolver := newResolver(fakeReader{
|
||||||
|
countryFunc: func(netip.Addr) (*geoip2.Country, error) {
|
||||||
|
return nil, lookupErr
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := resolver.Country(netip.MustParseAddr("203.0.113.10"))
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, lookupErr)
|
||||||
|
assert.Contains(t, err.Error(), "lookup country for 203.0.113.10")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolverCountryReturnsNotFoundWhenISOCodeIsEmpty(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
resolver := newResolver(fakeReader{
|
||||||
|
countryFunc: func(netip.Addr) (*geoip2.Country, error) {
|
||||||
|
return &geoip2.Country{
|
||||||
|
Continent: geoip2.Continent{Code: "EU"},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := resolver.Country(netip.MustParseAddr("203.0.113.10"))
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, ErrCountryNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolverCountryNormalizesCountryCode(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
resolver := newResolver(fakeReader{
|
||||||
|
countryFunc: func(netip.Addr) (*geoip2.Country, error) {
|
||||||
|
return &geoip2.Country{
|
||||||
|
Country: geoip2.CountryRecord{ISOCode: "gb"},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
got, err := resolver.Country(netip.MustParseAddr("203.0.113.10"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "GB", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolverCountryRejectsInvalidISOCode(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
code string
|
||||||
|
}{
|
||||||
|
{name: "digit", code: "G1"},
|
||||||
|
{name: "too long", code: "USA"},
|
||||||
|
{name: "non ascii", code: "éé"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
resolver := newResolver(fakeReader{
|
||||||
|
countryFunc: func(netip.Addr) (*geoip2.Country, error) {
|
||||||
|
return &geoip2.Country{
|
||||||
|
Country: geoip2.CountryRecord{ISOCode: tt.code},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := resolver.Country(netip.MustParseAddr("203.0.113.10"))
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.NotErrorIs(t, err, ErrCountryNotFound)
|
||||||
|
assert.Contains(t, err.Error(), "invalid ISO 3166-1 alpha-2 code")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func openFixtureResolver(t *testing.T) *Resolver {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
requireFixtureDatabase(t)
|
||||||
|
|
||||||
|
resolver, err := Open(countryFixturePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, resolver.Close())
|
||||||
|
})
|
||||||
|
|
||||||
|
return resolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireFixtureDatabase(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
_, err := os.Stat(countryFixturePath)
|
||||||
|
require.NoErrorf(
|
||||||
|
t,
|
||||||
|
err,
|
||||||
|
"fixture database %q is unavailable; run `git submodule update --init --recursive` before running tests",
|
||||||
|
countryFixturePath,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
module galaxy/geoip
|
||||||
|
|
||||||
|
go 1.26.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/oschwald/geoip2-golang/v2 v2.1.0
|
||||||
|
github.com/stretchr/testify v1.11.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
|
github.com/oschwald/maxminddb-golang/v2 v2.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/oschwald/geoip2-golang/v2 v2.1.0 h1:DjnLhNJu9WHwTrmoiQFvgmyJoczhdnm7LB23UBI2Amo=
|
||||||
|
github.com/oschwald/geoip2-golang/v2 v2.1.0/go.mod h1:qdVmcPgrTJ4q2eP9tHq/yldMTdp2VMr33uVdFbHBiBc=
|
||||||
|
github.com/oschwald/maxminddb-golang/v2 v2.1.1 h1:lA8FH0oOrM4u7mLvowq8IT6a3Q/qEnqRzLQn9eH5ojc=
|
||||||
|
github.com/oschwald/maxminddb-golang/v2 v2.1.1/go.mod h1:PLdx6PR+siSIoXqqy7C7r3SB3KZnhxWr1Dp6g0Hacl8=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
Submodule
+1
Submodule pkg/geoip/test-data added at 0be8559aca
Reference in New Issue
Block a user