130 lines
4.6 KiB
Go
130 lines
4.6 KiB
Go
package main
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"io"
|
||
|
|
"math"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"macker/datapoint"
|
||
|
|
)
|
||
|
|
|
||
|
|
type transportFunc func(*http.Request) (*http.Response, error)
|
||
|
|
|
||
|
|
func (f transportFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
|
||
|
|
|
||
|
|
func testGeocoder(t *testing.T, response string, status int) (*geocoder, *int) {
|
||
|
|
t.Helper()
|
||
|
|
g, err := newGeocoder("https://example.com/reverse", "secret")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
calls := 0
|
||
|
|
g.client = &http.Client{Transport: transportFunc(func(r *http.Request) (*http.Response, error) {
|
||
|
|
calls++
|
||
|
|
if r.Header.Get("Authorization") != "Bearer secret" || r.URL.Query().Get("api_key") != "" {
|
||
|
|
t.Error("key must stay in server-side Authorization header")
|
||
|
|
}
|
||
|
|
if r.URL.Query().Get("format") != "jsonv2" || r.URL.Query().Get("lat") == "" || r.URL.Query().Get("lon") == "" {
|
||
|
|
t.Error("missing reverse geocoding parameters")
|
||
|
|
}
|
||
|
|
return &http.Response{StatusCode: status, Body: io.NopCloser(strings.NewReader(response)), Header: make(http.Header)}, nil
|
||
|
|
})}
|
||
|
|
return g, &calls
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGeocodeCacheDistanceAndRateLimit(t *testing.T) {
|
||
|
|
g, calls := testGeocoder(t, `{"lat":"41.8001","lon":"-87.6","display_name":"123 Main Street"}`, 200)
|
||
|
|
now := time.Now().UTC()
|
||
|
|
g.update(context.Background(), 41.8, -87.6, now)
|
||
|
|
v := g.view(41.8, -87.6, now)
|
||
|
|
if v.Status != "ready" || v.Address == nil || v.Address.Label != "123 Main Street" || math.Abs(v.Address.DistanceMeters-11.12) > 0.1 {
|
||
|
|
t.Fatalf("bad address: %+v", v)
|
||
|
|
}
|
||
|
|
// A nearby fix reuses the cached address but computes its own distance.
|
||
|
|
g.update(context.Background(), 41.80005, -87.6, now.Add(time.Minute))
|
||
|
|
moved := g.view(41.80005, -87.6, now.Add(time.Minute))
|
||
|
|
if *calls != 1 || moved.Address.DistanceMeters >= v.Address.DistanceMeters {
|
||
|
|
t.Fatal("cache not reused or distance not updated")
|
||
|
|
}
|
||
|
|
// A distant fix must not display an address from the previous area.
|
||
|
|
if g.view(42, -87.6, now).Status != "pending" {
|
||
|
|
t.Fatal("old address shown for distant fix")
|
||
|
|
}
|
||
|
|
g.update(context.Background(), 42, -87.6, now.Add(time.Second))
|
||
|
|
if *calls != 1 {
|
||
|
|
t.Fatal("rate limit bypassed")
|
||
|
|
}
|
||
|
|
g.update(context.Background(), 42, -87.6, now.Add(31*time.Second))
|
||
|
|
if *calls != 2 {
|
||
|
|
t.Fatal("moved fix did not trigger new lookup")
|
||
|
|
}
|
||
|
|
if g.view(41.8, -87.6, now.Add(6*time.Minute)).Address != nil {
|
||
|
|
t.Fatal("expired cache used")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGeocodeFailureAndRetry(t *testing.T) {
|
||
|
|
for name, response := range map[string]string{"no match": `{"error":"Unable to geocode"}`, "invalid coords": `{"lat":"NaN","lon":"0","display_name":"bad"}`, "malformed": "not JSON", "oversized": strings.Repeat("x", (1<<20)+1)} {
|
||
|
|
t.Run(name, func(t *testing.T) {
|
||
|
|
g, calls := testGeocoder(t, response, 200)
|
||
|
|
now := time.Now()
|
||
|
|
g.update(context.Background(), 1, 2, now)
|
||
|
|
if g.view(1, 2, now).Status != "unavailable" {
|
||
|
|
t.Fatal("bad response accepted")
|
||
|
|
}
|
||
|
|
g.update(context.Background(), 1, 2, now.Add(time.Second))
|
||
|
|
if *calls != 1 {
|
||
|
|
t.Fatal("failure retried too soon")
|
||
|
|
}
|
||
|
|
g.update(context.Background(), 1, 2, now.Add(31*time.Second))
|
||
|
|
if *calls != 2 {
|
||
|
|
t.Fatal("failure not retried")
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
g, _ := testGeocoder(t, `{}`, 429)
|
||
|
|
g.update(context.Background(), 1, 2, time.Now())
|
||
|
|
if g.view(1, 2, time.Now()).Status != "unavailable" {
|
||
|
|
t.Fatal("HTTP error accepted")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGeocoderConfigurationAndAPI(t *testing.T) {
|
||
|
|
for _, endpoint := range []string{"file:///tmp/address", "https://nominatim.openstreetmap.org/reverse", "https://user:pass@example.com"} {
|
||
|
|
if _, err := newGeocoder(endpoint, ""); err == nil {
|
||
|
|
t.Fatalf("accepted endpoint %q", endpoint)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
disabled, err := newGeocoder("", "")
|
||
|
|
if err != nil || disabled != nil || disabled.view(0, 0, time.Now()).Status != "disabled" {
|
||
|
|
t.Fatal("bad disabled behavior")
|
||
|
|
}
|
||
|
|
g, _ := testGeocoder(t, `{"lat":"41.8","lon":"-87.6","display_name":"123 Main Street"}`, 200)
|
||
|
|
now := time.Now()
|
||
|
|
g.update(context.Background(), 41.8, -87.6, now)
|
||
|
|
tr := &tracker{geocoder: g}
|
||
|
|
tr.accept([]datapoint.DataPoint{{Lat: 41.8, Long: -87.6}}, now)
|
||
|
|
rr := httptest.NewRecorder()
|
||
|
|
tr.handler().ServeHTTP(rr, httptest.NewRequest("GET", "/api/location", nil))
|
||
|
|
if !strings.Contains(rr.Body.String(), `"label":"123 Main Street"`) || strings.Contains(rr.Body.String(), "secret") {
|
||
|
|
t.Fatalf("bad API response: %s", rr.Body.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDistanceMeters(t *testing.T) {
|
||
|
|
if distanceMeters(0, 0, 0, 0) != 0 {
|
||
|
|
t.Fatal("identical coordinates have a distance")
|
||
|
|
}
|
||
|
|
if got := distanceMeters(0, 0, 0, 1); math.Abs(got-111195) > 1 {
|
||
|
|
t.Fatalf("unexpected one degree distance: %f", got)
|
||
|
|
}
|
||
|
|
if got := distanceMeters(0, 179.999, 0, -179.999); math.Abs(got-222.39) > 1 {
|
||
|
|
t.Fatalf("bad antimeridian distance: %f", got)
|
||
|
|
}
|
||
|
|
}
|