fix: harden route command

This commit is contained in:
Peter Steinberger 2026-01-02 22:38:59 +01:00
parent d157022860
commit 6089c1af9a
9 changed files with 520 additions and 36 deletions

View File

@ -8,6 +8,7 @@ Modern Go client + CLI for the Google Places API (New). Fast for humans, tidy fo
- Autocomplete suggestions for places + queries (session tokens supported).
- Nearby search around a location restriction.
- Place photos in details + photo media URLs.
- Route search along a driving path (Routes API).
- Location bias (lat/lng/radius) and pagination tokens.
- Place details: hours, phone, website, rating, price, types.
- Optional reviews in details (`--reviews` / `IncludeReviews`).
@ -31,6 +32,7 @@ export GOOGLE_PLACES_API_KEY="..."
Optional overrides:
- `GOOGLE_PLACES_BASE_URL` (testing, proxying, or mock servers)
- `GOOGLE_ROUTES_BASE_URL` (testing Routes API or proxying)
### Getting a Google Places API Key
@ -44,18 +46,22 @@ Optional overrides:
- Search for "Places API (New)" — make sure it says **(New)**!
- Click "Enable"
3. **Create an API Key**
3. **Enable the Routes API (for `route`)**
- Search for "Routes API"
- Click "Enable"
4. **Create an API Key**
- Go to [APIs & Services → Credentials](https://console.cloud.google.com/apis/credentials)
- Click "Create Credentials" → "API Key"
- Copy the key
4. **Set the Environment Variable**
5. **Set the Environment Variable**
```bash
export GOOGLE_PLACES_API_KEY="your-api-key-here"
```
Add to your `~/.zshrc` or `~/.bashrc` to persist.
5. **(Recommended) Restrict the Key**
6. **(Recommended) Restrict the Key**
- Click on the key in Credentials
- Under "API restrictions", select "Restrict key" → "Places API (New)"
- Set quota limits in [Quotas](https://console.cloud.google.com/apis/api/places.googleapis.com/quotas)
@ -65,13 +71,14 @@ Optional overrides:
## CLI
```text
goplaces [--api-key=KEY] [--base-url=URL] [--timeout=10s] [--json] [--no-color] [--verbose]
goplaces [--api-key=KEY] [--base-url=URL] [--routes-base-url=URL] [--timeout=10s] [--json] [--no-color] [--verbose]
<command>
Commands:
autocomplete Autocomplete places and queries.
nearby Search nearby places by location.
search Search places by text query.
route Search places along a route.
details Fetch place details by place ID.
photo Fetch a photo URL by photo name.
resolve Resolve a location string to candidate places.
@ -102,6 +109,12 @@ Nearby search:
goplaces nearby --lat 47.6062 --lng -122.3321 --radius-m 1500 --type cafe --limit 5
```
Route search:
```bash
goplaces route "coffee" --from "Seattle, WA" --to "Portland, OR" --max-waypoints 5
```
Details (with reviews):
```bash
@ -181,6 +194,13 @@ photo, err := client.PhotoMedia(ctx, goplaces.PhotoMediaRequest{
Name: "places/PLACE_ID/photos/PHOTO_ID",
MaxWidthPx: 1200,
})
route, err := client.Route(ctx, goplaces.RouteRequest{
Query: "coffee",
From: "Seattle, WA",
To: "Portland, OR",
MaxWaypoints: 5,
})
```
## Notes
@ -189,6 +209,7 @@ photo, err := client.PhotoMedia(ctx, goplaces.PhotoMediaRequest{
- Price levels map to Google enums: `0` (free) → `4` (very expensive).
- Reviews are returned only when `IncludeReviews`/`--reviews` is set.
- Photos are returned only when `IncludePhotos`/`--photos` is set.
- Route search requires the Google Routes API to be enabled.
- Field masks are defined alongside each request (e.g. `search.go`, `details.go`, `autocomplete.go`).
- The Places API is billed and quota-limited; keep an eye on your Cloud Console quotas.

View File

@ -19,17 +19,19 @@ const DefaultBaseURL = "https://places.googleapis.com/v1"
// Client wraps access to the Google Places API.
type Client struct {
apiKey string
baseURL string
httpClient *http.Client
apiKey string
baseURL string
routesBaseURL string
httpClient *http.Client
}
// Options configures the Places client.
type Options struct {
APIKey string
BaseURL string
HTTPClient *http.Client
Timeout time.Duration
APIKey string
BaseURL string
RoutesBaseURL string
HTTPClient *http.Client
Timeout time.Duration
}
// NewClient builds a client with sane defaults.
@ -38,6 +40,10 @@ func NewClient(opts Options) *Client {
if baseURL == "" {
baseURL = DefaultBaseURL
}
routesBaseURL := strings.TrimRight(opts.RoutesBaseURL, "/")
if routesBaseURL == "" {
routesBaseURL = defaultRoutesBaseURL
}
client := opts.HTTPClient
if client == nil {
@ -49,9 +55,10 @@ func NewClient(opts Options) *Client {
}
return &Client{
apiKey: opts.APIKey,
baseURL: baseURL,
httpClient: client,
apiKey: opts.APIKey,
baseURL: baseURL,
routesBaseURL: routesBaseURL,
httpClient: client,
}
}

34
docs/route.md Normal file
View File

@ -0,0 +1,34 @@
# Route Search
Route search samples waypoints along a route and runs a text search at each waypoint.
## CLI
```bash
goplaces route "coffee" --from "Seattle, WA" --to "Portland, OR" --max-waypoints 5
```
Options:
- `--mode` travel mode: DRIVE, WALK, BICYCLE, TWO_WHEELER, TRANSIT.
- `--radius-m` search radius per waypoint.
- `--limit` results per waypoint.
## Library
```go
response, err := client.Route(ctx, goplaces.RouteRequest{
Query: "coffee",
From: "Seattle, WA",
To: "Portland, OR",
Mode: "DRIVE",
RadiusM: 1000,
MaxWaypoints: 5,
Limit: 5,
})
```
## Notes
- Requires the Google Routes API to be enabled.
- Waypoints are sampled evenly along the route polyline.

View File

@ -12,9 +12,11 @@ import (
"github.com/steipete/goplaces"
)
const placesSearchPath = "/places:searchText"
func TestRunSearchJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/places:searchText" {
if r.URL.Path != placesSearchPath {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
_, _ = w.Write([]byte(`{"places": [{"id": "abc"}]}`))
@ -232,6 +234,78 @@ func TestRunNearbyHuman(t *testing.T) {
}
}
func TestRunRouteJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/directions/v2:computeRoutes":
_, _ = w.Write([]byte("{\"routes\":[{\"polyline\":{\"encodedPolyline\":\"_p~iF~ps|U_ulLnnqC_mqNvxq`@\"}}]}"))
case placesSearchPath:
_, _ = w.Write([]byte(`{"places":[{"id":"abc","displayName":{"text":"Cafe"}}]}`))
default:
t.Fatalf("unexpected path: %s", r.URL.Path)
}
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"route",
"coffee",
"--from", "A",
"--to", "B",
"--api-key", "test-key",
"--base-url", server.URL,
"--routes-base-url", server.URL,
"--json",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
if !strings.Contains(stdout.String(), "\"waypoints\"") {
t.Fatalf("unexpected stdout: %s", stdout.String())
}
}
func TestRunRouteHuman(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/directions/v2:computeRoutes":
_, _ = w.Write([]byte("{\"routes\":[{\"polyline\":{\"encodedPolyline\":\"_p~iF~ps|U_ulLnnqC_mqNvxq`@\"}}]}"))
case placesSearchPath:
_, _ = w.Write([]byte(`{"places":[{"id":"abc","displayName":{"text":"Cafe"}}]}`))
default:
t.Fatalf("unexpected path: %s", r.URL.Path)
}
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"route",
"coffee",
"--from", "A",
"--to", "B",
"--api-key", "test-key",
"--base-url", server.URL,
"--routes-base-url", server.URL,
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
if !strings.Contains(stdout.String(), "Route waypoints") {
t.Fatalf("unexpected stdout: %s", stdout.String())
}
if stderr.Len() != 0 {
t.Fatalf("unexpected stderr: %s", stderr.String())
}
}
func TestRunDetailsJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/places/place-1" {
@ -399,7 +473,7 @@ func TestRunPhotoHuman(t *testing.T) {
func TestRunResolveHuman(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/places:searchText" {
if r.URL.Path != placesSearchPath {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
_, _ = w.Write([]byte(`{"places": [{"id": "loc-1", "displayName": {"text": "Downtown"}}]}`))

View File

@ -103,6 +103,34 @@ func TestRenderNearby(t *testing.T) {
}
}
func TestRenderRoute(t *testing.T) {
response := goplaces.RouteResponse{
Waypoints: []goplaces.RouteWaypoint{
{
Location: goplaces.LatLng{Lat: 1, Lng: 2},
Results: []goplaces.PlaceSummary{{PlaceID: "place-1", Name: "Cafe"}},
},
},
}
output := renderRoute(NewColor(false), response)
if !strings.Contains(output, "Route waypoints") {
t.Fatalf("missing route header")
}
if !strings.Contains(output, "Waypoint 1") {
t.Fatalf("missing waypoint label")
}
if !strings.Contains(output, "Cafe") {
t.Fatalf("missing place name")
}
}
func TestRenderRouteEmpty(t *testing.T) {
output := renderRoute(NewColor(false), goplaces.RouteResponse{})
if !strings.Contains(output, "No results") {
t.Fatalf("unexpected output: %s", output)
}
}
func TestFormatTitleFallback(t *testing.T) {
title := formatTitle(NewColor(false), "", "")
if !strings.Contains(title, "(no name)") {

View File

@ -9,22 +9,23 @@ type Root struct {
Global GlobalOptions `embed:""`
Autocomplete AutocompleteCmd `cmd:"" help:"Autocomplete places and queries."`
Nearby NearbyCmd `cmd:"" help:"Search nearby places by location."`
Search SearchCmd `cmd:"" help:"Search places by text query."`
Route RouteCmd `cmd:"" help:"Search places along a route."`
Details DetailsCmd `cmd:"" help:"Fetch place details by place ID."`
Photo PhotoCmd `cmd:"" help:"Fetch a photo URL by photo name."`
Resolve ResolveCmd `cmd:"" help:"Resolve a location string to candidate places."`
Search SearchCmd `cmd:"" help:"Search places by text query."`
Route RouteCmd `cmd:"" help:"Search places along a route."`
Details DetailsCmd `cmd:"" help:"Fetch place details by place ID."`
Photo PhotoCmd `cmd:"" help:"Fetch a photo URL by photo name."`
Resolve ResolveCmd `cmd:"" help:"Resolve a location string to candidate places."`
}
// GlobalOptions are flags shared by all commands.
type GlobalOptions struct {
APIKey string `help:"Google Places API key." env:"GOOGLE_PLACES_API_KEY"`
BaseURL string `help:"Places API base URL." env:"GOOGLE_PLACES_BASE_URL" default:"https://places.googleapis.com/v1"`
Timeout time.Duration `help:"HTTP timeout." default:"10s"`
JSON bool `help:"Output JSON."`
NoColor bool `help:"Disable color output."`
Verbose bool `help:"Verbose logging."`
Version VersionFlag `name:"version" help:"Print version and exit."`
APIKey string `help:"Google Places API key." env:"GOOGLE_PLACES_API_KEY"`
BaseURL string `help:"Places API base URL." env:"GOOGLE_PLACES_BASE_URL" default:"https://places.googleapis.com/v1"`
RoutesBaseURL string `help:"Routes API base URL." env:"GOOGLE_ROUTES_BASE_URL" default:"https://routes.googleapis.com"`
Timeout time.Duration `help:"HTTP timeout." default:"10s"`
JSON bool `help:"Output JSON."`
NoColor bool `help:"Disable color output."`
Verbose bool `help:"Verbose logging."`
Version VersionFlag `name:"version" help:"Print version and exit."`
}
// SearchCmd runs text search queries.

View File

@ -69,9 +69,10 @@ func Run(args []string, stdout io.Writer, stderr io.Writer) int {
}
client := goplaces.NewClient(goplaces.Options{
APIKey: root.Global.APIKey,
BaseURL: root.Global.BaseURL,
Timeout: root.Global.Timeout,
APIKey: root.Global.APIKey,
BaseURL: root.Global.BaseURL,
RoutesBaseURL: root.Global.RoutesBaseURL,
Timeout: root.Global.Timeout,
})
app := &App{

View File

@ -12,8 +12,9 @@ import (
)
const (
routesEndpoint = "https://routes.googleapis.com/directions/v2:computeRoutes"
routesFieldMask = "routes.polyline.encodedPolyline"
defaultRoutesBaseURL = "https://routes.googleapis.com"
routesPath = "/directions/v2:computeRoutes"
routesFieldMask = "routes.polyline.encodedPolyline"
)
const (
@ -165,8 +166,8 @@ func (c *Client) computeRoutePolyline(ctx context.Context, req RouteRequest) (st
"destination": map[string]any{
"address": req.To,
},
"travelMode": req.Mode,
"polylineQuality": "OVERVIEW",
"travelMode": req.Mode,
"polylineQuality": "OVERVIEW",
"polylineEncoding": "ENCODED_POLYLINE",
}
if req.Language != "" {
@ -176,7 +177,8 @@ func (c *Client) computeRoutePolyline(ctx context.Context, req RouteRequest) (st
body["regionCode"] = req.Region
}
payload, err := c.doRequest(ctx, http.MethodPost, routesEndpoint, body, routesFieldMask)
endpoint := c.routesBaseURL + routesPath
payload, err := c.doRequest(ctx, http.MethodPost, endpoint, body, routesFieldMask)
if err != nil {
return "", err
}

316
route_test.go Normal file
View File

@ -0,0 +1,316 @@
package goplaces
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestComputeRoutePolyline(t *testing.T) {
var gotBody map[string]any
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != routesPath {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if r.Header.Get("X-Goog-FieldMask") != routesFieldMask {
t.Fatalf("unexpected field mask: %s", r.Header.Get("X-Goog-FieldMask"))
}
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
t.Fatalf("decode body: %v", err)
}
_, _ = w.Write([]byte("{\"routes\": [{\"polyline\": {\"encodedPolyline\": \"_p~iF~ps|U_ulLnnqC_mqNvxq`@\"}}]}"))
}))
defer server.Close()
client := NewClient(Options{APIKey: "test-key", RoutesBaseURL: server.URL})
polyline, err := client.computeRoutePolyline(context.Background(), RouteRequest{
From: "Seattle",
To: "Portland",
Mode: travelModeDrive,
})
if err != nil {
t.Fatalf("computeRoutePolyline error: %v", err)
}
if polyline == "" {
t.Fatalf("expected polyline")
}
if gotBody["travelMode"] != travelModeDrive {
t.Fatalf("unexpected travelMode: %#v", gotBody["travelMode"])
}
}
func TestDecodePolyline(t *testing.T) {
points, err := decodePolyline("_p~iF~ps|U_ulLnnqC_mqNvxq`@")
if err != nil {
t.Fatalf("decodePolyline error: %v", err)
}
if len(points) != 3 {
t.Fatalf("expected 3 points, got %d", len(points))
}
if points[0].Lat != 38.5 || points[0].Lng != -120.2 {
t.Fatalf("unexpected first point: %#v", points[0])
}
}
func TestDecodePolylineInvalid(t *testing.T) {
_, err := decodePolyline("")
if err == nil {
t.Fatalf("expected decode error")
}
}
func TestDecodePolylineMalformed(t *testing.T) {
_, err := decodePolyline("abc")
if err == nil {
t.Fatalf("expected malformed error")
}
}
func TestSampleWaypoints(t *testing.T) {
points := []LatLng{{Lat: 0, Lng: 0}, {Lat: 0, Lng: 1}, {Lat: 0, Lng: 2}}
waypoints := sampleWaypoints(points, 2)
if len(waypoints) != 2 {
t.Fatalf("expected 2 waypoints, got %d", len(waypoints))
}
if waypoints[0].Lng != 0 || waypoints[1].Lng != 2 {
t.Fatalf("unexpected waypoints: %#v", waypoints)
}
}
func TestSampleWaypointsSingle(t *testing.T) {
points := []LatLng{{Lat: 1, Lng: 1}, {Lat: 2, Lng: 2}}
waypoints := sampleWaypoints(points, 1)
if len(waypoints) != 1 {
t.Fatalf("expected 1 waypoint")
}
}
func TestSampleWaypointsSinglePoint(t *testing.T) {
points := []LatLng{{Lat: 1, Lng: 1}}
waypoints := sampleWaypoints(points, 5)
if len(waypoints) != 1 {
t.Fatalf("expected 1 waypoint")
}
}
func TestPointAtDistanceBounds(t *testing.T) {
points := []LatLng{{Lat: 0, Lng: 0}, {Lat: 0, Lng: 2}}
cumulative := cumulativeDistances(points)
if got := pointAtCumulative(points, cumulative, -1); got != points[0] {
t.Fatalf("expected first point, got %#v", got)
}
if got := pointAtCumulative(points, cumulative, cumulative[len(cumulative)-1]+1); got != points[1] {
t.Fatalf("expected last point, got %#v", got)
}
}
func TestUniqueWaypoints(t *testing.T) {
points := []LatLng{{Lat: 1, Lng: 1}, {Lat: 1, Lng: 1}, {Lat: 2, Lng: 2}}
unique := uniqueWaypoints(points)
if len(unique) != 2 {
t.Fatalf("expected 2 unique points, got %d", len(unique))
}
}
func TestDistanceMeters(t *testing.T) {
distance := distanceMeters(LatLng{Lat: 0, Lng: 0}, LatLng{Lat: 0, Lng: 1})
if distance <= 0 {
t.Fatalf("expected positive distance")
}
}
func TestTotalDistanceEmpty(t *testing.T) {
if totalDistance([]LatLng{{Lat: 1, Lng: 1}}) != 0 {
t.Fatalf("expected zero distance")
}
}
func TestPointAtDistanceEmpty(t *testing.T) {
point := pointAtDistance(nil, 10)
if point != (LatLng{}) {
t.Fatalf("expected empty point")
}
}
func TestValidateRouteRequest(t *testing.T) {
err := validateRouteRequest(RouteRequest{})
if err == nil {
t.Fatalf("expected error")
}
err = validateRouteRequest(RouteRequest{
Query: "coffee",
From: "A",
To: "B",
Mode: "FLY",
Limit: 1,
RadiusM: 1,
MaxWaypoints: 1,
})
if err == nil {
t.Fatalf("expected mode error")
}
}
func TestValidateRouteRequestBounds(t *testing.T) {
err := validateRouteRequest(RouteRequest{
Query: "coffee",
From: "A",
To: "B",
Mode: travelModeDrive,
Limit: 0,
RadiusM: -1,
MaxWaypoints: 999,
})
if err == nil {
t.Fatalf("expected bounds error")
}
}
func TestApplyRouteDefaults(t *testing.T) {
req := applyRouteDefaults(RouteRequest{
Query: " coffee ",
From: " A ",
To: " B ",
Mode: "walk",
})
if req.Mode != travelModeWalk {
t.Fatalf("unexpected mode: %s", req.Mode)
}
if req.Limit != defaultRouteLimit {
t.Fatalf("unexpected limit: %d", req.Limit)
}
}
func TestApplyRouteDefaultsEmpty(t *testing.T) {
req := applyRouteDefaults(RouteRequest{})
if req.Mode != travelModeDrive {
t.Fatalf("expected default mode")
}
if req.Limit != defaultRouteLimit {
t.Fatalf("expected default limit")
}
if req.RadiusM != defaultRouteRadiusM {
t.Fatalf("expected default radius")
}
if req.MaxWaypoints != defaultRouteWaypoints {
t.Fatalf("expected default waypoints")
}
}
func TestComputeRoutePolylineErrors(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"routes":[]}`))
}))
defer server.Close()
client := NewClient(Options{APIKey: "test-key", RoutesBaseURL: server.URL})
_, err := client.computeRoutePolyline(context.Background(), RouteRequest{From: "A", To: "B"})
if err == nil {
t.Fatalf("expected route error")
}
}
func TestComputeRoutePolylineEmpty(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"routes":[{"polyline":{"encodedPolyline":""}}]}`))
}))
defer server.Close()
client := NewClient(Options{APIKey: "test-key", RoutesBaseURL: server.URL})
_, err := client.computeRoutePolyline(context.Background(), RouteRequest{From: "A", To: "B"})
if err == nil {
t.Fatalf("expected empty polyline error")
}
}
func TestComputeRoutePolylineInvalidJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("not-json"))
}))
defer server.Close()
client := NewClient(Options{APIKey: "test-key", RoutesBaseURL: server.URL})
_, err := client.computeRoutePolyline(context.Background(), RouteRequest{From: "A", To: "B"})
if err == nil {
t.Fatalf("expected json error")
}
}
func TestRouteEndToEnd(t *testing.T) {
searchCalls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case routesPath:
_, _ = w.Write([]byte("{\"routes\": [{\"polyline\": {\"encodedPolyline\": \"_p~iF~ps|U_ulLnnqC_mqNvxq`@\"}}]}"))
case "/places:searchText":
searchCalls++
_, _ = w.Write([]byte(`{"places":[{"id":"abc","displayName":{"text":"Cafe"}}]}`))
default:
t.Fatalf("unexpected path: %s", r.URL.Path)
}
}))
defer server.Close()
client := NewClient(Options{APIKey: "test-key", BaseURL: server.URL, RoutesBaseURL: server.URL})
response, err := client.Route(context.Background(), RouteRequest{
Query: "coffee",
From: "Seattle",
To: "Portland",
})
if err != nil {
t.Fatalf("route error: %v", err)
}
if len(response.Waypoints) == 0 {
t.Fatalf("expected waypoints")
}
if searchCalls == 0 {
t.Fatalf("expected search calls")
}
}
func TestRouteSearchError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case routesPath:
_, _ = w.Write([]byte("{\"routes\": [{\"polyline\": {\"encodedPolyline\": \"_p~iF~ps|U_ulLnnqC_mqNvxq`@\"}}]}"))
case "/places:searchText":
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("bad"))
default:
t.Fatalf("unexpected path: %s", r.URL.Path)
}
}))
defer server.Close()
client := NewClient(Options{APIKey: "test-key", BaseURL: server.URL, RoutesBaseURL: server.URL})
_, err := client.Route(context.Background(), RouteRequest{
Query: "coffee",
From: "Seattle",
To: "Portland",
})
if err == nil {
t.Fatalf("expected route error")
}
}
func TestRouteComputeRouteError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != routesPath {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
_, _ = w.Write([]byte(`{"routes":[]}`))
}))
defer server.Close()
client := NewClient(Options{APIKey: "test-key", RoutesBaseURL: server.URL})
_, err := client.Route(context.Background(), RouteRequest{
Query: "coffee",
From: "A",
To: "B",
})
if err == nil {
t.Fatalf("expected route error")
}
}