goplaces/internal/cli/cli_test.go
2026-02-14 05:53:13 +01:00

952 lines
26 KiB
Go

package cli
import (
"bytes"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/steipete/goplaces"
)
const (
placesSearchPath = "/places:searchText"
placesNearbyPath = "/places:searchNearby"
routesComputePath = "/directions/v2:computeRoutes"
directionsPath = routesComputePath
directionsModeWalkAPI = "WALK"
directionsModeDriveAPI = "DRIVE"
directionsModeWalking = "walking"
directionsModeDriving = "driving"
)
func TestRunSearchJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != placesSearchPath {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
_, _ = w.Write([]byte(`{"places": [{"id": "abc"}]}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"search",
"coffee",
"--api-key", "test-key",
"--base-url", server.URL,
"--json",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d (stdout=%s stderr=%s)", exitCode, stdout.String(), stderr.String())
}
if stderr.Len() != 0 {
t.Fatalf("unexpected stderr: %s", stderr.String())
}
// JSON output should be an array, not an object with "results" key
if !strings.HasPrefix(strings.TrimSpace(stdout.String()), "[") {
t.Fatalf("expected JSON array output, got: %s", stdout.String())
}
}
func TestRunSearchHuman(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"places": [{"id": "abc", "displayName": {"text": "Cafe"}}]}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"search",
"coffee",
"--api-key", "test-key",
"--base-url", server.URL,
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d (stdout=%s stderr=%s)", exitCode, stdout.String(), stderr.String())
}
if !strings.Contains(stdout.String(), "Cafe") {
t.Fatalf("unexpected stdout: %s", stdout.String())
}
}
func TestRunSearchWithFilters(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var payload map[string]any
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request: %v", err)
}
if payload["includedType"] != "cafe" {
t.Fatalf("unexpected includedType: %#v", payload["includedType"])
}
if payload["languageCode"] != "en" {
t.Fatalf("unexpected languageCode: %#v", payload["languageCode"])
}
if payload["regionCode"] != "US" {
t.Fatalf("unexpected regionCode: %#v", payload["regionCode"])
}
_, _ = w.Write([]byte(`{"places": [{"id": "abc"}]}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"search",
"coffee",
"--api-key", "test-key",
"--base-url", server.URL,
"--json",
"--keyword", "best",
"--type", "cafe",
"--open-now=true",
"--min-rating", "4.2",
"--price-level", "1",
"--lat", "40.0",
"--lng=-70.0",
"--radius-m", "500",
"--language", "en",
"--region", "US",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d (stdout=%s stderr=%s)", exitCode, stdout.String(), stderr.String())
}
if stderr.Len() != 0 {
t.Fatalf("unexpected stderr: %s", stderr.String())
}
// JSON output should be an array, not an object with "results" key
if !strings.HasPrefix(strings.TrimSpace(stdout.String()), "[") {
t.Fatalf("expected JSON array output, got: %s", stdout.String())
}
}
func TestRunAutocompleteJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/places:autocomplete" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
_, _ = w.Write([]byte(`{"suggestions": [{"placePrediction": {"placeId": "abc"}}]}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"autocomplete",
"coffee",
"--api-key", "test-key",
"--base-url", server.URL,
"--json",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d (stdout=%s stderr=%s)", exitCode, stdout.String(), stderr.String())
}
// JSON output should be an array, not an object with "suggestions" key
if !strings.HasPrefix(strings.TrimSpace(stdout.String()), "[") {
t.Fatalf("expected JSON array output, got: %s", stdout.String())
}
}
func TestRunAutocompleteHuman(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"suggestions": [{"placePrediction": {"placeId": "abc", "text": {"text": "Cafe"}}}]}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"autocomplete",
"coffee",
"--api-key", "test-key",
"--base-url", server.URL,
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
if !strings.Contains(stdout.String(), "Cafe") {
t.Fatalf("unexpected stdout: %s", stdout.String())
}
if stderr.Len() != 0 {
t.Fatalf("unexpected stderr: %s", stderr.String())
}
}
func TestRunNearbyJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != placesNearbyPath {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
_, _ = w.Write([]byte(`{"places": [{"id": "abc"}]}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"nearby",
"--lat", "1",
"--lng", "2",
"--radius-m", "3",
"--api-key", "test-key",
"--base-url", server.URL,
"--json",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
// JSON output should be an array, not an object with "results" key
if !strings.HasPrefix(strings.TrimSpace(stdout.String()), "[") {
t.Fatalf("expected JSON array output, got: %s", stdout.String())
}
}
func TestRunNearbyHuman(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"places": [{"id": "abc", "displayName": {"text": "Cafe"}}]}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"nearby",
"--lat", "1",
"--lng", "2",
"--radius-m", "3",
"--api-key", "test-key",
"--base-url", server.URL,
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
if !strings.Contains(stdout.String(), "Cafe") {
t.Fatalf("unexpected stdout: %s", stdout.String())
}
if stderr.Len() != 0 {
t.Fatalf("unexpected stderr: %s", stderr.String())
}
}
func TestRunRouteJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case routesComputePath:
_, _ = 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 routesComputePath:
_, _ = 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 TestRunRouteValidationError(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"route",
"coffee",
"--from", "A",
"--to", "B",
"--mode", "FLY",
"--api-key", "test-key",
}, &stdout, &stderr)
if exitCode != 2 {
t.Fatalf("expected validation error exit code 2, got %d", exitCode)
}
}
func TestRunRouteMissingFrom(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"route",
"coffee",
"--to", "B",
"--api-key", "test-key",
}, &stdout, &stderr)
if exitCode != 2 {
t.Fatalf("expected validation error exit code 2, got %d", exitCode)
}
}
func TestRunDirectionsJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != directionsPath {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if r.Header.Get("X-Goog-Api-Key") != "test-key" {
t.Fatalf("unexpected api key header: %s", r.Header.Get("X-Goog-Api-Key"))
}
var payload map[string]any
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode payload: %v", err)
}
if payload["travelMode"] != directionsModeWalkAPI {
t.Fatalf("unexpected mode: %#v", payload["travelMode"])
}
if payload["units"] != "METRIC" {
t.Fatalf("unexpected units: %#v", payload["units"])
}
_, _ = w.Write([]byte(`{
"routes":[{"description":"Main","legs":[{"distanceMeters":1000,"duration":"600s","localizedValues":{"distance":{"text":"1 km"},"duration":{"text":"10 mins"}},"steps":[]}]}]
}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"directions",
"--from", "A",
"--to", "B",
"--api-key", "test-key",
"--directions-base-url", server.URL,
"--json",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d (stdout=%s stderr=%s)", exitCode, stdout.String(), stderr.String())
}
if !strings.Contains(stdout.String(), "\"mode\": \"WALKING\"") {
t.Fatalf("unexpected stdout: %s", stdout.String())
}
if stderr.Len() != 0 {
t.Fatalf("unexpected stderr: %s", stderr.String())
}
}
func TestRunDirectionsCompareJSON(t *testing.T) {
seenModes := make(map[string]int)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != directionsPath {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
var payload map[string]any
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode payload: %v", err)
}
mode, _ := payload["travelMode"].(string)
seenModes[mode]++
responseBody := `{
"routes":[{"description":"Main","legs":[{"distanceMeters":1000,"duration":"600s","localizedValues":{"distance":{"text":"1 km"},"duration":{"text":"10 mins"}},"steps":[]}]}]
}`
if mode == directionsModeDriveAPI {
responseBody = `{
"routes":[{"description":"Main","legs":[{"distanceMeters":1000,"duration":"240s","localizedValues":{"distance":{"text":"1 km"},"duration":{"text":"4 mins"}},"steps":[]}]}]
}`
}
_, _ = w.Write([]byte(responseBody))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"directions",
"--from", "A",
"--to", "B",
"--compare", "drive",
"--api-key", "test-key",
"--directions-base-url", server.URL,
"--json",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d (stdout=%s stderr=%s)", exitCode, stdout.String(), stderr.String())
}
var results []goplaces.DirectionsResponse
if err := json.Unmarshal(stdout.Bytes(), &results); err != nil {
t.Fatalf("decode output: %v (stdout=%s)", err, stdout.String())
}
if len(results) != 2 {
t.Fatalf("expected 2 directions results, got %d", len(results))
}
if results[0].Mode != "WALKING" || results[1].Mode != "DRIVING" {
t.Fatalf("unexpected mode order: %#v", results)
}
if seenModes[directionsModeWalkAPI] != 1 || seenModes[directionsModeDriveAPI] != 1 {
t.Fatalf("expected both modes requested once, got: %#v", seenModes)
}
}
func TestRunDirectionsHumanCompareWithSteps(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != directionsPath {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
var payload map[string]any
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode payload: %v", err)
}
mode, _ := payload["travelMode"].(string)
if mode == "" {
t.Fatalf("missing mode")
}
_, _ = w.Write([]byte(`{
"routes":[{"description":"Main","legs":[{"distanceMeters":1000,"duration":"600s","localizedValues":{"distance":{"text":"1 km"},"duration":{"text":"10 mins"}},"steps":[{"distanceMeters":200,"staticDuration":"120s","localizedValues":{"distance":{"text":"0.2 km"},"staticDuration":{"text":"2 mins"}},"navigationInstruction":{"instructions":"Head north"}}]}]}]
}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"directions",
"--from", "A",
"--to", "B",
"--compare", "drive",
"--steps",
"--api-key", "test-key",
"--directions-base-url", server.URL,
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d (stdout=%s stderr=%s)", exitCode, stdout.String(), stderr.String())
}
if !strings.Contains(stdout.String(), "Directions (WALKING)") || !strings.Contains(stdout.String(), "Directions (DRIVING)") {
t.Fatalf("missing compare output: %s", stdout.String())
}
if !strings.Contains(stdout.String(), "Head north") {
t.Fatalf("missing steps output: %s", stdout.String())
}
}
func TestRunDirectionsValidationErrors(t *testing.T) {
tests := []struct {
name string
args []string
}{
{
name: "invalid mode",
args: []string{"directions", "--from", "A", "--to", "B", "--mode", "plane", "--api-key", "x"},
},
{
name: "invalid compare",
args: []string{"directions", "--from", "A", "--to", "B", "--compare", "plane", "--api-key", "x"},
},
{
name: "same compare mode",
args: []string{"directions", "--from", "A", "--to", "B", "--mode", "walk", "--compare", directionsModeWalking, "--api-key", "x"},
},
{
name: "partial from latlng",
args: []string{"directions", "--from-lat", "1", "--to", "B", "--api-key", "x"},
},
{
name: "partial to latlng",
args: []string{"directions", "--from", "A", "--to-lng", "2", "--api-key", "x"},
},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run(testCase.args, &stdout, &stderr)
if exitCode != 2 {
t.Fatalf("expected validation exit code 2, got %d (stdout=%s stderr=%s)", exitCode, stdout.String(), stderr.String())
}
})
}
}
func TestNormalizeDirectionsMode(t *testing.T) {
cases := map[string]string{
"walk": directionsModeWalking,
"walking": directionsModeWalking,
"drive": directionsModeDriving,
"driving": directionsModeDriving,
"bike": "bicycling",
"bicycle": "bicycling",
"bicycling": "bicycling",
"transit": "transit",
"plane": "",
}
for input, want := range cases {
if got := normalizeDirectionsMode(input); got != want {
t.Fatalf("normalizeDirectionsMode(%q) = %q, want %q", input, got, want)
}
}
}
func TestRunDetailsJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/places/place-1" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
_, _ = w.Write([]byte(`{"id": "place-1"}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"details",
"place-1",
"--api-key", "test-key",
"--base-url", server.URL,
"--json",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
if !strings.Contains(stdout.String(), "\"place_id\"") {
t.Fatalf("unexpected stdout: %s", stdout.String())
}
}
func TestRunDetailsWithReviews(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("X-Goog-FieldMask"), "reviews") {
t.Fatalf("expected reviews in field mask: %s", r.Header.Get("X-Goog-FieldMask"))
}
_, _ = w.Write([]byte(`{"id": "place-1", "reviews": [{"name": "reviews/1"}]}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"details",
"place-1",
"--api-key", "test-key",
"--base-url", server.URL,
"--reviews",
"--json",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
if !strings.Contains(stdout.String(), "\"reviews\"") {
t.Fatalf("unexpected stdout: %s", stdout.String())
}
}
func TestRunDetailsWithPhotos(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("X-Goog-FieldMask"), "photos") {
t.Fatalf("expected photos in field mask: %s", r.Header.Get("X-Goog-FieldMask"))
}
_, _ = w.Write([]byte(`{"id": "place-1", "photos": [{"name": "places/place-1/photos/photo-1"}]}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"details",
"place-1",
"--api-key", "test-key",
"--base-url", server.URL,
"--photos",
"--json",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
if !strings.Contains(stdout.String(), "\"photos\"") {
t.Fatalf("unexpected stdout: %s", stdout.String())
}
}
func TestRunDetailsHuman(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"id": "place-2", "displayName": {"text": "Park"}}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"details",
"place-2",
"--api-key", "test-key",
"--base-url", server.URL,
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
if !strings.Contains(stdout.String(), "Park") {
t.Fatalf("unexpected stdout: %s", stdout.String())
}
}
func TestRunPhotoJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/places/place-1/photos/photo-1/media" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
_, _ = w.Write([]byte(`{"photoUri": "https://example.com/photo.jpg"}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"photo",
"places/place-1/photos/photo-1",
"--api-key", "test-key",
"--base-url", server.URL,
"--json",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
if !strings.Contains(stdout.String(), "\"photo_uri\"") {
t.Fatalf("unexpected stdout: %s", stdout.String())
}
}
func TestRunPhotoHuman(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"photoUri": "https://example.com/photo.jpg"}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"photo",
"places/place-1/photos/photo-1",
"--api-key", "test-key",
"--base-url", server.URL,
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
if !strings.Contains(stdout.String(), "photo.jpg") {
t.Fatalf("unexpected stdout: %s", stdout.String())
}
if stderr.Len() != 0 {
t.Fatalf("unexpected stderr: %s", stderr.String())
}
}
func TestRunResolveHuman(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != placesSearchPath {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
_, _ = w.Write([]byte(`{"places": [{"id": "loc-1", "displayName": {"text": "Downtown"}}]}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"resolve",
"Downtown",
"--api-key", "test-key",
"--base-url", server.URL,
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
if !strings.Contains(stdout.String(), "Downtown") {
t.Fatalf("unexpected stdout: %s", stdout.String())
}
if stderr.Len() != 0 {
t.Fatalf("unexpected stderr: %s", stderr.String())
}
}
func TestRunResolveJSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"places": [{"id": "loc-2"}]}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"resolve",
"Downtown",
"--api-key", "test-key",
"--base-url", server.URL,
"--json",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
// JSON output should be an array, not an object with "results" key
if !strings.HasPrefix(strings.TrimSpace(stdout.String()), "[") {
t.Fatalf("expected JSON array output, got: %s", stdout.String())
}
}
func TestRunVersion(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{"--version"}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
if strings.TrimSpace(stdout.String()) != Version {
t.Fatalf("unexpected version: %s", stdout.String())
}
}
func TestRunMissingCommand(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{}, &stdout, &stderr)
if exitCode == 0 {
t.Fatalf("expected non-zero exit code")
}
}
func TestRunParseError(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{"search", "--api-key", "x"}, &stdout, &stderr)
if exitCode == 0 {
t.Fatalf("expected parse error")
}
}
func TestRunLocationBiasError(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{"search", "coffee", "--lat", "1", "--api-key", "x"}, &stdout, &stderr)
if exitCode != 2 {
t.Fatalf("expected validation error exit code 2, got %d", exitCode)
}
}
func TestRunNearbyLocationRestrictionError(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{"nearby", "--lat", "1", "--api-key", "x"}, &stdout, &stderr)
if exitCode != 2 {
t.Fatalf("expected validation error exit code 2, got %d", exitCode)
}
}
func TestRunHelp(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{"--help"}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
if stdout.Len() == 0 {
t.Fatalf("expected help output")
}
}
func TestVersionFlagIsBool(t *testing.T) {
var flag VersionFlag
if !flag.IsBool() {
t.Fatalf("expected IsBool true")
}
}
func TestRunSearchJSONWithNextPageToken(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != placesSearchPath {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
_, _ = w.Write([]byte(`{"places": [{"id": "abc"}], "nextPageToken": "token123"}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"search",
"coffee",
"--api-key", "test-key",
"--base-url", server.URL,
"--json",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d (stdout=%s stderr=%s)", exitCode, stdout.String(), stderr.String())
}
// JSON output should be an array
if !strings.HasPrefix(strings.TrimSpace(stdout.String()), "[") {
t.Fatalf("expected JSON array output, got: %s", stdout.String())
}
// next_page_token should be written to stderr
if !strings.Contains(stderr.String(), "next_page_token: token123") {
t.Fatalf("expected next_page_token in stderr, got: %s", stderr.String())
}
}
func TestRunNearbyJSONWithNextPageToken(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != placesNearbyPath {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
_, _ = w.Write([]byte(`{"places": [{"id": "abc"}], "nextPageToken": "nearby-token"}`))
}))
defer server.Close()
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"nearby",
"--lat", "1",
"--lng", "2",
"--radius-m", "3",
"--api-key", "test-key",
"--base-url", server.URL,
"--json",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}
// JSON output should be an array
if !strings.HasPrefix(strings.TrimSpace(stdout.String()), "[") {
t.Fatalf("expected JSON array output, got: %s", stdout.String())
}
// next_page_token should be written to stderr
if !strings.Contains(stderr.String(), "next_page_token: nearby-token") {
t.Fatalf("expected next_page_token in stderr, got: %s", stderr.String())
}
}
func TestWriteJSONError(t *testing.T) {
err := writeJSON(&bytes.Buffer{}, map[string]any{"bad": func() {}})
if err == nil {
t.Fatalf("expected json error")
}
}
func TestWriteJSON(t *testing.T) {
var out bytes.Buffer
if err := writeJSON(&out, map[string]string{"ok": "true"}); err != nil {
t.Fatalf("writeJSON error: %v", err)
}
if !strings.Contains(out.String(), "\"ok\"") {
t.Fatalf("unexpected json output: %s", out.String())
}
}
func TestHandleError(t *testing.T) {
if code := handleError(&bytes.Buffer{}, nil); code != 0 {
t.Fatalf("expected 0")
}
if code := handleError(&bytes.Buffer{}, goplaces.ValidationError{Field: "x", Message: "bad"}); code != 2 {
t.Fatalf("expected validation exit 2")
}
if code := handleError(&bytes.Buffer{}, goplaces.ErrMissingAPIKey); code != 2 {
t.Fatalf("expected missing api key exit 2")
}
if code := handleError(&bytes.Buffer{}, errors.New("boom")); code != 1 {
t.Fatalf("expected generic exit 1")
}
}