package main import ( "bytes" "compress/gzip" "encoding/json" "fmt" "io" "log/slog" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" ) // A tile in central Berlin. var testTile = mapTile{Z: 14, X: 8802, Y: 5373} // fakeOverpass answers every query with a primary road (with a gap in its geometry), a // residential street with a needless middle point, and a footway the car does not draw. func fakeOverpass(t *testing.T, calls *atomic.Int32, status int) *httptest.Server { t.Helper() s, w, n, e := tileBounds(testTile) lat := func(f float64) float64 { return s + (n-s)*f } lon := func(f float64) float64 { return w + (e-w)*f } answer := fmt.Sprintf(`{"elements":[ {"type":"way","tags":{"highway":"primary"},"geometry":[ {"lat":%f,"lon":%f},{"lat":%f,"lon":%f},null,{"lat":%f,"lon":%f},{"lat":%f,"lon":%f}]}, {"type":"way","tags":{"highway":"residential"},"geometry":[ {"lat":%f,"lon":%f},{"lat":%f,"lon":%f},{"lat":%f,"lon":%f}]}, {"type":"way","tags":{"highway":"footway"},"geometry":[ {"lat":%f,"lon":%f},{"lat":%f,"lon":%f}]}]}`, lat(0.1), lon(0.1), lat(0.2), lon(0.2), lat(0.6), lon(0.6), lat(0.7), lon(0.7), lat(0.5), lon(0.1), lat(0.5), lon(0.5), lat(0.5), lon(0.9), lat(0.3), lon(0.3), lat(0.4), lon(0.4)) return httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { calls.Add(1) if r.Method != http.MethodPost || !strings.Contains(r.UserAgent(), "nils-mg4-server") { t.Errorf("request %s %q", r.Method, r.UserAgent()) } _ = r.ParseForm() if q := r.PostForm.Get("data"); !strings.Contains(q, "out geom(") || !strings.Contains(q, "residential") { t.Errorf("query %q", q) } if status != http.StatusOK { http.Error(rw, "busy", status) return } _, _ = rw.Write([]byte(answer)) })) } func mapsAPI(overpass *httptest.Server) http.Handler { api, _ := newTestAPI() api.Maps = NewMaps(overpass.URL, mapUserAgent) api.Maps.Client = overpass.Client() api.Maps.gap = 0 return api.Routes() } func tileBody(t mapTile) string { return fmt.Sprintf(`{"z":%d,"x":%d,"y":%d}`, t.Z, t.X, t.Y) } func TestMapTileRoadsAreClassifiedSplitAndSimplified(t *testing.T) { var calls atomic.Int32 overpass := fakeOverpass(t, &calls, http.StatusOK) defer overpass.Close() h := mapsAPI(overpass) if rec := do(t, h, "POST", "/v1/map/roads", "", tileBody(testTile)); rec.Code != 401 { t.Fatalf("without the car's token: %d", rec.Code) } rec := do(t, h, "POST", "/v1/map/roads", testToken, tileBody(testTile)) if rec.Code != 200 { t.Fatalf("%d %s", rec.Code, rec.Body) } var got roadTile if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { t.Fatal(err) } if got.Extent != 4096 || got.X != testTile.X || got.Y != testTile.Y { t.Fatalf("tile %+v", got) } // The primary road in two pieces, the street straightened to its ends, no footway. if len(got.Ways) != 3 || got.Ways[0].C != 1 || got.Ways[1].C != 1 || got.Ways[2].C != 3 { t.Fatalf("ways %+v", got.Ways) } if len(got.Ways[2].P) != 4 { t.Fatalf("the straight street keeps only its ends: %v", got.Ways[2].P) } // Tile-local: 0.1 of the way in is about 410 units from the west edge. if x := got.Ways[0].P[0]; x < 400 || x > 420 { t.Fatalf("x %d", x) } // Cached: the second car asking costs Overpass nothing. if rec := do(t, h, "POST", "/v1/map/roads", testToken, tileBody(testTile)); rec.Code != 200 { t.Fatalf("%d", rec.Code) } if calls.Load() != 1 { t.Fatalf("overpass asked %d times", calls.Load()) } } func TestMapTilesAreGzippedWhenTheCarAcceptsIt(t *testing.T) { var calls atomic.Int32 overpass := fakeOverpass(t, &calls, http.StatusOK) defer overpass.Close() req := httptest.NewRequest("POST", "/v1/map/roads", strings.NewReader(tileBody(testTile))) req.Header.Set("Authorization", "Bearer "+testToken) req.Header.Set("Accept-Encoding", "gzip") rec := httptest.NewRecorder() mapsAPI(overpass).ServeHTTP(rec, req) if rec.Code != 200 || rec.Header().Get("Content-Encoding") != "gzip" { t.Fatalf("%d %v", rec.Code, rec.Header()) } zr, err := gzip.NewReader(rec.Body) if err != nil { t.Fatal(err) } plain, _ := io.ReadAll(zr) if !bytes.Contains(plain, []byte(`"extent":4096`)) { t.Fatalf("%s", plain) } } func TestMapRequestsAreChecked(t *testing.T) { var calls atomic.Int32 overpass := fakeOverpass(t, &calls, http.StatusOK) defer overpass.Close() h := mapsAPI(overpass) for _, body := range []string{`{}`, `{"z":15,"x":1,"y":1}`, `{"z":14,"x":-1,"y":1}`, `{"z":14,"x":1,"y":16384}`, `not json`} { if rec := do(t, h, "POST", "/v1/map/roads", testToken, body); rec.Code != 400 { t.Errorf("%s: %d", body, rec.Code) } } if calls.Load() != 0 { t.Fatal("a bad request reached Overpass") } } func TestABusyOverpassIsATemporaryFailureAndNotCached(t *testing.T) { var logged bytes.Buffer previous := slog.Default() slog.SetDefault(slog.New(slog.NewTextHandler(&logged, nil))) defer slog.SetDefault(previous) var calls atomic.Int32 overpass := fakeOverpass(t, &calls, http.StatusTooManyRequests) defer overpass.Close() h := mapsAPI(overpass) for i := 0; i < 2; i++ { if rec := do(t, h, "POST", "/v1/map/roads", testToken, tileBody(testTile)); rec.Code != 503 { t.Fatalf("%d", rec.Code) } } if calls.Load() != 2 { t.Fatalf("a failure was cached: %d calls", calls.Load()) } // Where the car is stays out of the log. out := logged.String() if !strings.Contains(out, "overpass answered 429") || strings.Contains(out, "8802") || strings.Contains(out, "5373") { t.Fatalf("log:\n%s", out) } } func TestMapsAreOffWithoutOverpass(t *testing.T) { api, _ := newTestAPI() if rec := do(t, api.Routes(), "POST", "/v1/map/roads", testToken, tileBody(testTile)); rec.Code != 404 { t.Fatalf("%d", rec.Code) } } func TestOverpassURLConfiguration(t *testing.T) { base := map[string]string{"INFLUX_URL": "http://influx", "INFLUX_TOKEN": "t", "INFLUX_ORG": "o", "INGEST_TOKEN": "0123456789abcdef01234567"} load := func(overpass string) (config, error) { return loadConfig(func(k string) string { if k == "OVERPASS_URL" { return overpass } return base[k] }) } if c, err := load(""); err != nil || c.overpassURL != DefaultOverpassURL { t.Fatalf("default: %q %v", c.overpassURL, err) } if c, err := load("off"); err != nil || c.overpassURL != "" { t.Fatalf("off: %q %v", c.overpassURL, err) } if _, err := load("http://overpass.example.com/api/interpreter"); err == nil { t.Fatal("plain http accepted") } } func TestTilePointsAndSimplification(t *testing.T) { s, w, n, e := tileBounds(testTile) if x, y := tilePoint(testTile, n, w); x != 0 || y != 0 { t.Fatalf("north-west corner %d,%d", x, y) } if x, y := tilePoint(testTile, s, e); x != 4096 || y != 4096 { t.Fatalf("south-east corner %d,%d", x, y) } line := [][2]int{{0, 0}, {10, 1}, {20, 0}, {30, 40}} got := simplify(line, 4) if len(got) != 3 || got[1] != [2]int{20, 0} { t.Fatalf("%v", got) } }