Skip to content

Commit

Permalink
#minor: refactor to use config struct to init client + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
circa10a committed Nov 26, 2021
1 parent 1c1561b commit 3f6040d
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 48 deletions.
46 changes: 31 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
59 changes: 26 additions & 33 deletions geofence.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
}
94 changes: 94 additions & 0 deletions geofence_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package geofence

import (
"errors"
"fmt"
"net/http"
"testing"
"time"

"github.com/jarcoal/httpmock"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -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)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down

0 comments on commit 3f6040d

Please sign in to comment.