From 3f6040d4bb3bebbbbe58dd78f69e08f6444c56f6 Mon Sep 17 00:00:00 2001 From: circa10a Date: Fri, 26 Nov 2021 17:20:03 -0600 Subject: [PATCH] #minor: refactor to use config struct to init client + tests --- README.md | 46 ++++++++++++++++-------- geofence.go | 59 ++++++++++++++---------------- geofence_test.go | 94 ++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 ++ 5 files changed, 154 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 45d42b2..54a868c 100644 --- a/README.md +++ b/README.md @@ -25,21 +25,24 @@ import ( ) func main() { - // Empty string to geofence your current public IP address, or you can monitor a remote address by supplying it as the first parameter - // freegeoip.app API token - // Sensitivity - // 0 - 111 km - // 1 - 11.1 km - // 2 - 1.11 km - // 3 111 meters - // 4 11.1 meters - // 5 1.11 meters - geofence, err := geofence.New("", "YOUR_FREEGEOIP_API_TOKEN", 3) + geofence, err := geofence.New(&geofence.Config{ + // Empty string to geofence your current public IP address, or you can monitor a remote address by supplying it as the first parameter + IPAddress: "", + // freegeoip.app API token + Token: "YOUR_FREEGEOIP_API_TOKEN", + // Sensitivity + // 0 - 111 km + // 1 - 11.1 km + // 2 - 1.11 km + // 3 111 meters + // 4 11.1 meters + // 5 1.11 meters + Sensitivity: 3, // 3 is recommended + CacheTTL: 7 * (24 * time.Hour), // 1 week + }) if err != nil { log.Fatal(err) } - // Create cache that holds status in memory until application is restarted - geofence.CreateCache(-1) isAddressNearby, err := geofence.IsIPAddressNear("8.8.8.8") if err != nil { log.Fatal(err) @@ -67,12 +70,25 @@ import ( func main() { // Provide an IP Address to test with ipAddress := "192.168.1.100" - geofence, err := geofence.New("", "YOUR_FREEGEOIP_API_TOKEN", 3) + geofence, err := geofence.New(&geofence.Config{ + // Empty string to geofence your current public IP address, or you can monitor a remote address by supplying it as the first parameter + IPAddress: ipAddress, + // freegeoip.app API token + Token: "YOUR_FREEGEOIP_API_TOKEN", + // Sensitivity + // 0 - 111 km + // 1 - 11.1 km + // 2 - 1.11 km + // 3 111 meters + // 4 11.1 meters + // 5 1.11 meters + Sensitivity: 3, // 3 is recommended + CacheTTL: 7 * (24 * time.Hour), // 1 week + }) if err != nil { log.Fatal(err) } - // Create cache that holds status in memory until application is restarted - geofence.CreateCache(-1) + // Skip Private IP analysis as it will always be false if !strings.HasPrefix(ipAddress, "192.") && !strings.HasPrefix(ipAddress, "172.") && !strings.HasPrefix(ipAddress, "10.") { isAddressNearby, err := geofence.IsIPAddressNear(ipAddress) if err != nil { diff --git a/geofence.go b/geofence.go index 7fd4f0e..c63c958 100644 --- a/geofence.go +++ b/geofence.go @@ -10,19 +10,27 @@ import ( ) const ( - freeGeoIPBaseURL = "https://api.freegeoip.app/json" - invalidSensitivityErrString = "invalid sensitivity. value must be between 0 - 5" - invalidIPAddressString = "invalid IPv4 address provided" + freeGeoIPBaseURL = "https://api.freegeoip.app/json" + invalidSensitivityErrString = "invalid sensitivity. value must be between 0 - 5" + invalidIPAddressString = "invalid IPv4 address provided" + deleteExpiredCacheItemsInternal = 10 * time.Minute ) -// Geofence holds a Geofenced IP config +// Config holds the user configuration to setup a new geofence +type Config struct { + IPAddress string + Token string + Sensitivity int + CacheTTL time.Duration +} + +// Geofence holds a freegeoip.app client, cache and user supplied config type Geofence struct { Cache *cache.Cache FreeGeoIPClient *resty.Client - token string - Sensitivity int - Latitude float64 - Longitude float64 + Config + Latitude float64 + Longitude float64 } // FreeGeoIPResponse is the json response from freegeoip.app @@ -92,7 +100,7 @@ func validateIPAddress(ipAddress string) error { func (g *Geofence) getIPGeoData(ipAddress string) (*FreeGeoIPResponse, error) { resp, err := g.FreeGeoIPClient.R(). SetHeader("Accept", "application/json"). - SetQueryParam("apikey", g.token). + SetQueryParam("apikey", g.Token). SetResult(&FreeGeoIPResponse{}). SetError(&FreeGeoIPError{}). Get(ipAddress) @@ -118,31 +126,29 @@ func (g *Geofence) getIPGeoData(ipAddress string) (*FreeGeoIPResponse, error) { // 3 111 meters // 4 11.1 meters // 5 1.11 meters -func New(ipAddress, freeGeoIPAPIToken string, sensitivity int) (*Geofence, error) { +func New(c *Config) (*Geofence, error) { // Create new client for freegeoip.app freeGeoIPClient := resty.New().SetBaseURL(freeGeoIPBaseURL) // Ensure sensitivity is between 1 - 5 - err := validateSensitivity(sensitivity) + err := validateSensitivity(c.Sensitivity) if err != nil { return nil, err } // New Geofence object geofence := &Geofence{ + Config: *c, FreeGeoIPClient: freeGeoIPClient, - Sensitivity: sensitivity, + Cache: cache.New(c.CacheTTL, deleteExpiredCacheItemsInternal), } - // Hold token - geofence.token = freeGeoIPAPIToken - // Get current location of specified IP address // If empty string, use public IP of device running this // Or use location of the specified IP - ipAddressLookupDetails, err := geofence.getIPGeoData(ipAddress) + ipAddressLookupDetails, err := geofence.getIPGeoData(c.IPAddress) if err != nil { - return nil, err + return geofence, err } // Set the location of our geofence to compare against looked up IP's @@ -152,15 +158,6 @@ func New(ipAddress, freeGeoIPAPIToken string, sensitivity int) (*Geofence, error return geofence, nil } -// CreateCache creates a new cache for IP address lookups to reduce calls/improve performance -// Accepts a duration to keep items in cache. Use -1 to keep items in memory indefinitely -func (g *Geofence) CreateCache(duration time.Duration) { - // Only create if not created yet - if g.Cache == nil { - g.Cache = cache.New(duration, duration) - } -} - // formatCoordinates converts decimal points to size of sensitivity and givens back a string for comparison func formatCoordinates(sensitivity int, location float64) string { return fmt.Sprintf("%*.*f", 0, sensitivity, location) @@ -175,10 +172,8 @@ func (g *Geofence) IsIPAddressNear(ipAddress string) (bool, error) { } // Check if ipaddress has been looked up before and is in cache - if g.Cache != nil { - if isIPAddressNear, found := g.Cache.Get(ipAddress); found { - return isIPAddressNear.(bool), nil - } + if isIPAddressNear, found := g.Cache.Get(ipAddress); found { + return isIPAddressNear.(bool), nil } // If not in cache, lookup IP and compare @@ -197,9 +192,7 @@ func (g *Geofence) IsIPAddressNear(ipAddress string) (bool, error) { isNear := currentLat == clientLat && currentLong == clientLong // Insert ip address and it's status into the cache if user instantiated a cache - if g.Cache != nil { - g.Cache.Set(ipAddress, isNear, cache.DefaultExpiration) - } + g.Cache.Set(ipAddress, isNear, cache.DefaultExpiration) return isNear, nil } diff --git a/geofence_test.go b/geofence_test.go index d3bdb37..a72ad63 100644 --- a/geofence_test.go +++ b/geofence_test.go @@ -2,8 +2,12 @@ package geofence import ( "errors" + "fmt" + "net/http" "testing" + "time" + "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" ) @@ -135,3 +139,93 @@ func TestValidateIPAddress(t *testing.T) { } } } + +func TestGeofenceNear(t *testing.T) { + fakeIPAddress := "8.8.8.8" + fakeApiToken := "fakeApiToken" + fakeLatitude := 37.751 + fakeLongitude := -97.822 + fakeEndpoint := fmt.Sprintf("%s/%s?apikey=%s", freeGeoIPBaseURL, fakeIPAddress, fakeApiToken) + + // new geofence + geofence, _ := New(&Config{ + IPAddress: fakeIPAddress, + Token: fakeApiToken, + Sensitivity: 3, // 3 is recommended + CacheTTL: 7 * (24 * time.Hour), // 1 week + }) + geofence.Latitude = fakeLatitude + geofence.Longitude = fakeLongitude + + httpmock.ActivateNonDefault(geofence.FreeGeoIPClient.GetClient()) + defer httpmock.DeactivateAndReset() + + // mock json rsponse + response := &FreeGeoIPResponse{ + IP: fakeIPAddress, + CountryCode: "US", + CountryName: "United States", + TimeZone: "America/Chicago", + Latitude: fakeLatitude, + Longitude: fakeLongitude, + } + + // mock freegeoip.app response + httpmock.RegisterResponder("GET", fakeEndpoint, + func(req *http.Request) (*http.Response, error) { + resp, err := httpmock.NewJsonResponse(200, response) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }) + + isAddressNearby, err := geofence.IsIPAddressNear(fakeIPAddress) + assert.NoError(t, err) + assert.True(t, isAddressNearby) +} + +func TestGeofenceNotNear(t *testing.T) { + fakeIPAddress := "8.8.8.8" + fakeApiToken := "fakeApiToken" + fakeLatitude := 37.751 + fakeLongitude := -98.822 + fakeEndpoint := fmt.Sprintf("%s/%s?apikey=%s", freeGeoIPBaseURL, fakeIPAddress, fakeApiToken) + + // new geofence + geofence, _ := New(&Config{ + IPAddress: fakeIPAddress, + Token: fakeApiToken, + Sensitivity: 3, // 3 is recommended + CacheTTL: 7 * (24 * time.Hour), // 1 week + }) + geofence.Latitude = fakeLatitude + 1 + geofence.Longitude = fakeLongitude + 1 + + httpmock.ActivateNonDefault(geofence.FreeGeoIPClient.GetClient()) + defer httpmock.DeactivateAndReset() + + // mock json rsponse + response := &FreeGeoIPResponse{ + IP: fakeIPAddress, + CountryCode: "US", + CountryName: "United States", + TimeZone: "America/Chicago", + Latitude: fakeLatitude, + Longitude: fakeLongitude, + } + + // mock freegeoip.app response + httpmock.RegisterResponder("GET", fakeEndpoint, + func(req *http.Request) (*http.Response, error) { + resp, err := httpmock.NewJsonResponse(200, response) + if err != nil { + return httpmock.NewStringResponse(500, ""), nil + } + return resp, nil + }) + + isAddressNearby, err := geofence.IsIPAddressNear(fakeIPAddress) + assert.NoError(t, err) + assert.False(t, isAddressNearby) +} diff --git a/go.mod b/go.mod index 2a5054b..071dabf 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.0 // indirect + github.com/jarcoal/httpmock v1.0.8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.7.0 // indirect golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9 // indirect diff --git a/go.sum b/go.sum index 56aebf4..751e4f5 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= +github.com/jarcoal/httpmock v1.0.8 h1:8kI16SoO6LQKgPE7PvQuV+YuD/inwHd7fOOe2zMbo4k= +github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=