From 6c5aa1953f0f4c7fd5b382ba83aac5960c9f9fbb Mon Sep 17 00:00:00 2001 From: Eddie Maldonado Date: Tue, 18 Jul 2023 09:18:09 -0400 Subject: [PATCH] Geocoding service backend (#2127) * chore: pull in place index name from environment variable * chore: pull AWS credentials from awscli configuration in dev * chore: add AWS_PLACE_INDEX to envrc template * feat: request and parse search results * feat: expose search results via API endpoint * fix: correct FilterBBox * fix: make map component bounds match tileset bounds * feat: request and parse suggestions * feat: API endpoint for suggestions * refactor: have one source of truth for map bounds * refactor: make search/1 and suggest/1 return parsed value --- .envrc.template | 3 + assets/src/components/map.tsx | 15 +- assets/src/mapLimits.ts | 24 +++ assets/tests/components/map.test.tsx | 20 +-- assets/tests/mapLimits.test.ts | 45 ++++++ config/config.exs | 6 + config/dev.exs | 4 + config/runtime.exs | 3 +- .../location_search/aws_location_request.ex | 99 ++++++++++++ lib/skate/location_search/search_result.ex | 25 +++ .../controllers/location_search_controller.ex | 23 +++ lib/skate_web/controllers/page_controller.ex | 3 + lib/skate_web/router.ex | 2 + lib/skate_web/templates/page/index.html.eex | 1 + mix.exs | 1 + mix.lock | 1 + .../aws_location_request_test.exs | 150 ++++++++++++++++++ .../location_search_controller_test.exs | 74 +++++++++ .../controllers/page_controller_test.exs | 10 ++ test/support/factory.ex | 30 ++++ 20 files changed, 524 insertions(+), 15 deletions(-) create mode 100644 assets/src/mapLimits.ts create mode 100644 assets/tests/mapLimits.test.ts create mode 100644 lib/skate/location_search/aws_location_request.ex create mode 100644 lib/skate/location_search/search_result.ex create mode 100644 lib/skate_web/controllers/location_search_controller.ex create mode 100644 test/skate/location_search/aws_location_request_test.exs create mode 100644 test/skate_web/controllers/location_search_controller_test.exs diff --git a/.envrc.template b/.envrc.template index 6b9f7941b..1f0626687 100644 --- a/.envrc.template +++ b/.envrc.template @@ -29,6 +29,9 @@ export BUSLOC_URL= ## Source of GTFS-realtime enhanced TripUpdates json data file (optional) export TRIP_UPDATES_URL= +## Amazon Location Service place index to use for location search +export AWS_PLACE_INDEX= + ## Erlang/OTP settings, pass "+MIscs 2048" to allocate enough memory for literals in your local dev environment export ERL_FLAGS="+MIscs 2048" diff --git a/assets/src/components/map.tsx b/assets/src/components/map.tsx index 79eebb24c..c92d32f72 100644 --- a/assets/src/components/map.tsx +++ b/assets/src/components/map.tsx @@ -51,6 +51,7 @@ import { StreetViewControl } from "./map/controls/StreetViewSwitch" import StreetViewModeEnabledContext from "../contexts/streetViewModeEnabledContext" import { TileType, tilesetUrlForType } from "../tilesetUrls" import { TileTypeContext } from "../contexts/tileTypeContext" +import getMapLimits from "../mapLimits" export interface Props { reactLeafletRef?: MutableRefObject @@ -312,6 +313,8 @@ const Map = (props: Props): ReactElement => { const [streetViewEnabled, setStreetViewEnabled] = useState( props.streetViewInitiallyEnabled || false ) + + const mapLimits = getMapLimits() const { allowFullscreen = true } = props const stateClasses = joinClasses([ @@ -329,10 +332,14 @@ const Map = (props: Props): ReactElement => { + +const getMapLimits = (): MapLimits | null => { + const mapLimitsJson = appData()?.mapLimits + + if (mapLimitsJson === undefined) { + return null + } + + const mapLimits = create(JSON.parse(mapLimitsJson) as unknown, MapLimits) + + return mapLimits +} + +export default getMapLimits diff --git a/assets/tests/components/map.test.tsx b/assets/tests/components/map.test.tsx index 10af3f82b..73a53c692 100644 --- a/assets/tests/components/map.test.tsx +++ b/assets/tests/components/map.test.tsx @@ -497,7 +497,7 @@ const animationFramePromise = (): Promise => { describe("auto centering", () => { test("auto centers on a vehicle", async () => { - const location = { lat: 42, lng: -71 } + const location = { lat: 42.25, lng: -71 } const vehicle: VehicleInScheduledService = vehicleFactory.build({ latitude: location.lat, longitude: location.lng, @@ -517,7 +517,7 @@ describe("auto centering", () => { test("tracks a vehicle when it moves", async () => { const vehicle = vehicleFactory.build({}) const mapRef: MutableRefObject = { current: null } - const oldLatLng = { lat: 42, lng: -71 } + const oldLatLng = { lat: 42.25, lng: -71 } const oldVehicle = { ...vehicle, latitude: oldLatLng.lat, @@ -530,7 +530,7 @@ describe("auto centering", () => { /> ) await animationFramePromise() - const newLatLng = { lat: 42.1, lng: -71.1 } + const newLatLng = { lat: 42.35, lng: -71.1 } const newVehicle = { ...vehicle, latitude: newLatLng.lat, @@ -559,7 +559,7 @@ describe("auto centering", () => { expect(container.firstChild).toHaveClass( "c-vehicle-map-state--auto-centering" ) - const manualLatLng = { lat: 41.9, lng: -70.9 } + const manualLatLng = { lat: 42.25, lng: -70.9 } act(() => { mapRef.current!.fire("dragstart") @@ -567,7 +567,7 @@ describe("auto centering", () => { }) await animationFramePromise() - const newLatLng = { lat: 42.1, lng: -71.1 } + const newLatLng = { lat: 42.35, lng: -71.1 } const newVehicle = { ...vehicle, latitude: newLatLng.lat, @@ -589,9 +589,9 @@ describe("auto centering", () => { test("auto recentering does not disable auto centering", async () => { const vehicle = vehicleFactory.build({}) const mapRef: MutableRefObject = { current: null } - const latLng1 = { lat: 42, lng: -71 } - const latLng2 = { lat: 42.1, lng: -71.1 } - const latLng3 = { lat: 42.2, lng: -71.2 } + const latLng1 = { lat: 42.1, lng: -71 } + const latLng2 = { lat: 42.2, lng: -71.1 } + const latLng3 = { lat: 42.3, lng: -71.2 } const vehicle1 = { ...vehicle, latitude: latLng1.lat, @@ -642,7 +642,7 @@ describe("auto centering", () => { await animationFramePromise() // Manual move to turn off auto centering - const manualLatLng = { lat: 41.9, lng: -70.9 } + const manualLatLng = { lat: 42.25, lng: -70.9 } act(() => { mapRef.current!.fire("dragstart") mapRef.current!.panTo(manualLatLng) @@ -677,7 +677,7 @@ describe("auto centering", () => { await animationFramePromise() // Manual move to turn off auto centering - const manualLatLng = { lat: 41.9, lng: -70.9 } + const manualLatLng = { lat: 42.35, lng: -70.9 } act(() => { mapRef.current!.fire("dragstart") mapRef.current!.panTo(manualLatLng) diff --git a/assets/tests/mapLimits.test.ts b/assets/tests/mapLimits.test.ts new file mode 100644 index 000000000..328fb9d17 --- /dev/null +++ b/assets/tests/mapLimits.test.ts @@ -0,0 +1,45 @@ +import { StructError } from "superstruct" +import appData from "../src/appData" +import getMapLimits from "../src/mapLimits" + +jest.mock("appData") + +describe("getMapLimits", () => { + test("returns map limits when properly formatted", () => { + const limits = { north: 1, south: 2, east: 3, west: 4 } + ;(appData as jest.Mock).mockImplementation(() => ({ + mapLimits: JSON.stringify(limits), + })) + + expect(getMapLimits()).toEqual(limits) + }) + + test.each([ + // JSON.parse should throw when given invalid json + { expectedError: SyntaxError, mockTestGroupData: "" }, + { expectedError: SyntaxError, mockTestGroupData: "test 1234 !@#$ []{}" }, + { expectedError: SyntaxError, mockTestGroupData: [1, 2, 3] }, + + // Superstruct catches valid json but invalid types + { expectedError: StructError, mockTestGroupData: null }, + { expectedError: StructError, mockTestGroupData: JSON.stringify(null) }, + { expectedError: StructError, mockTestGroupData: true }, + { expectedError: StructError, mockTestGroupData: JSON.stringify(true) }, + { + expectedError: StructError, + mockTestGroupData: JSON.stringify([1, 2, 3]), + }, + { + expectedError: StructError, + mockTestGroupData: JSON.stringify({ irrelevant: "keys" }), + }, + ])( + "raise error when test group data is not the correct type: case %#", + ({ mockTestGroupData, expectedError: expectedError }) => { + ;(appData as jest.Mock).mockImplementationOnce(() => ({ + mapLimits: mockTestGroupData, + })) + expect(() => getMapLimits()).toThrowError(expectedError) + } + ) +}) diff --git a/config/config.exs b/config/config.exs index c5b8481da..49a249a6f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -33,6 +33,12 @@ config :skate, sentry_frontend_dsn: {:system, "SENTRY_FRONTEND_DSN"}, sentry_environment: {:system, "SENTRY_ENV"}, log_duration_timing: true, + map_limits: %{ + north: 42.65, + south: 42.05, + east: -70.6, + west: -71.55 + }, redirect_http?: false, static_href: {SkateWeb.Router.Helpers, :static_path}, timezone: "America/New_York", diff --git a/config/dev.exs b/config/dev.exs index 86ef4f162..d6e3e59f4 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -31,6 +31,10 @@ config :skate, SkateWeb.Endpoint, config :skate, SkateWeb.AuthManager, secret_key: "dev key" +config :ex_aws, + access_key_id: [{:system, "AWS_ACCESS_KEY_ID"}, {:awscli, "default", 30}], + secret_access_key: [{:system, "AWS_SECRET_ACCESS_KEY"}, {:awscli, "default", 30}] + config :ueberauth, Ueberauth, providers: [ cognito: {Skate.Ueberauth.Strategy.Fake, [groups: ["skate-dispatcher", "skate-admin"]]} diff --git a/config/runtime.exs b/config/runtime.exs index 589c7c378..d0780ce43 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -8,7 +8,8 @@ config :skate, secret_key_base: System.get_env("SECRET_KEY_BASE"), restrict_environment_access?: System.get_env("RESTRICT_ENVIRONMENT_ACCESS") == "true", base_tileset_url: System.get_env("BASE_TILESET_URL"), - satellite_tileset_url: System.get_env("SATELLITE_TILESET_URL") + satellite_tileset_url: System.get_env("SATELLITE_TILESET_URL"), + aws_place_index: System.get_env("AWS_PLACE_INDEX") config :ueberauth, Ueberauth.Strategy.Cognito, client_secret: System.get_env("COGNITO_CLIENT_SECRET") diff --git a/lib/skate/location_search/aws_location_request.ex b/lib/skate/location_search/aws_location_request.ex new file mode 100644 index 000000000..7a40c6ea5 --- /dev/null +++ b/lib/skate/location_search/aws_location_request.ex @@ -0,0 +1,99 @@ +defmodule Skate.LocationSearch.AwsLocationRequest do + alias Skate.LocationSearch.SearchResult + + @spec search(String.t()) :: {:ok, map()} | {:error, term()} + def search(text) do + request_fn = Application.get_env(:skate, :aws_request_fn, &ExAws.request/1) + + path = + "/places/v0/indexes/" <> Application.get_env(:skate, :aws_place_index) <> "/search/text" + + case %ExAws.Operation.RestQuery{ + http_method: :post, + path: path, + body: Map.merge(base_arguments(), %{Text: text}), + service: :places + } + |> request_fn.() do + {:ok, response} -> parse_search_response(response) + {:error, error} -> {:error, error} + end + end + + @spec suggest(String.t()) :: {:ok, map()} | {:error, term()} + def suggest(text) do + request_fn = Application.get_env(:skate, :aws_request_fn, &ExAws.request/1) + + path = + "/places/v0/indexes/" <> + Application.get_env(:skate, :aws_place_index) <> "/search/suggestions" + + case %ExAws.Operation.RestQuery{ + http_method: :post, + path: path, + body: Map.merge(base_arguments(), %{Text: text}), + service: :places + } + |> request_fn.() do + {:ok, response} -> parse_suggest_response(response) + {:error, error} -> {:error, error} + end + end + + defp parse_search_response(%{status_code: 200, body: body}) do + %{"Results" => results} = Jason.decode!(body) + + Enum.map(results, fn result -> + %{"PlaceId" => id, "Place" => place} = result + + %{"Label" => label, "Geometry" => %{"Point" => [longitude, latitude]}} = place + + {name, address} = + separate_label_text(label, Map.get(place, "AddressNumber"), Map.get(place, "Street")) + + %SearchResult{ + id: id, + name: name, + address: address, + latitude: latitude, + longitude: longitude + } + end) + end + + defp parse_suggest_response(%{status_code: 200, body: body}) do + %{"Results" => results} = Jason.decode!(body) + + Enum.map(results, fn result -> Map.get(result, "Text") end) + end + + @spec separate_label_text(String.t(), String.t() | nil, String.t() | nil) :: + {String.t() | nil, String.t()} + defp separate_label_text(label, nil, nil), do: {nil, label} + + defp separate_label_text(label, address_number, street) do + address_prefix = address_number || street + + case address_prefix + |> Regex.escape() + |> Regex.compile!() + |> Regex.split(label, parts: 2, include_captures: true) do + ["", prefix, address] -> + {nil, prefix <> address} + + [name, prefix, address] -> + {Regex.replace(~r/, $/, name, ""), prefix <> address} + + [prefix, address] -> + {nil, prefix <> address} + end + end + + defp base_arguments do + map_limits = Application.get_env(:skate, :map_limits) + + %{ + FilterBBox: [map_limits[:west], map_limits[:south], map_limits[:east], map_limits[:north]] + } + end +end diff --git a/lib/skate/location_search/search_result.ex b/lib/skate/location_search/search_result.ex new file mode 100644 index 000000000..8e3755349 --- /dev/null +++ b/lib/skate/location_search/search_result.ex @@ -0,0 +1,25 @@ +defmodule Skate.LocationSearch.SearchResult do + @type t :: %__MODULE__{ + id: String.t(), + name: String.t() | nil, + address: String.t(), + latitude: float(), + longitude: float() + } + + @enforce_keys [ + :id, + :address, + :latitude, + :longitude + ] + + @derive {Jason.Encoder, only: [:id, :name, :address, :latitude, :longitude]} + defstruct [ + :id, + :name, + :address, + :latitude, + :longitude + ] +end diff --git a/lib/skate_web/controllers/location_search_controller.ex b/lib/skate_web/controllers/location_search_controller.ex new file mode 100644 index 000000000..6bf69e2f7 --- /dev/null +++ b/lib/skate_web/controllers/location_search_controller.ex @@ -0,0 +1,23 @@ +defmodule SkateWeb.LocationSearchController do + use SkateWeb, :controller + + alias Skate.LocationSearch.AwsLocationRequest + + @spec search(Plug.Conn.t(), map()) :: Plug.Conn.t() + def search(conn, %{"query" => query}) do + seach_fn = Application.get_env(:skate, :location_search_fn, &AwsLocationRequest.search/1) + + {:ok, result} = seach_fn.(query) + + json(conn, %{data: result}) + end + + @spec suggest(Plug.Conn.t(), map()) :: Plug.Conn.t() + def suggest(conn, %{"query" => query}) do + suggest_fn = Application.get_env(:skate, :location_suggest_fn, &AwsLocationRequest.suggest/1) + + {:ok, result} = suggest_fn.(query) + + json(conn, %{data: result}) + end +end diff --git a/lib/skate_web/controllers/page_controller.ex b/lib/skate_web/controllers/page_controller.ex index e1f00e6f6..b342b2408 100644 --- a/lib/skate_web/controllers/page_controller.ex +++ b/lib/skate_web/controllers/page_controller.ex @@ -20,6 +20,8 @@ defmodule SkateWeb.PageController do dispatcher_flag = conn |> Guardian.Plug.current_claims() |> AuthManager.claims_grant_dispatcher_access?() + map_limits = Application.get_env(:skate, :map_limits) + conn |> assign(:username, username) |> assign(:user_uuid, user.uuid) @@ -34,6 +36,7 @@ defmodule SkateWeb.PageController do satellite: Application.get_env(:skate, :satellite_tileset_url) }) |> assign(:user_test_groups, user.test_groups |> Enum.map(& &1.name)) + |> assign(:map_limits, map_limits) |> render("index.html") end diff --git a/lib/skate_web/router.ex b/lib/skate_web/router.ex index bdb28f4d0..18b7e238c 100644 --- a/lib/skate_web/router.ex +++ b/lib/skate_web/router.ex @@ -123,6 +123,8 @@ defmodule SkateWeb.Router do put "/route_tabs", RouteTabsController, :update put "/notification_read_state", NotificationReadStatesController, :update get "/swings", SwingsController, :index + get "/location_search/search", LocationSearchController, :search + get "/location_search/suggest", LocationSearchController, :suggest end scope "/_flags" do diff --git a/lib/skate_web/templates/page/index.html.eex b/lib/skate_web/templates/page/index.html.eex index eef62cbc0..5030d9c2c 100644 --- a/lib/skate_web/templates/page/index.html.eex +++ b/lib/skate_web/templates/page/index.html.eex @@ -8,4 +8,5 @@ data-tileset-urls="<%= Jason.encode!(@tileset_urls) %>" data-dispatcher-flag="<%= Jason.encode!(@dispatcher_flag) %>" data-user-test-groups="<%= Jason.encode!(@user_test_groups) %>" + data-map-limits="<%= Jason.encode!(@map_limits) %>" > diff --git a/mix.exs b/mix.exs index 13c766248..a51fc4551 100644 --- a/mix.exs +++ b/mix.exs @@ -50,6 +50,7 @@ defmodule Skate.MixProject do [ {:bypass, "~> 2.1.0", only: :test}, {:castore, "~> 0.1.5"}, + {:configparser_ex, "~> 4.0", only: :dev}, {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, {:csv, "~> 2.4.1"}, {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false}, diff --git a/mix.lock b/mix.lock index 4c1ab4f01..da0dd2da9 100644 --- a/mix.lock +++ b/mix.lock @@ -4,6 +4,7 @@ "castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"}, "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, + "configparser_ex": {:hex, :configparser_ex, "4.0.0", "17e2b831cfa33a08c56effc610339b2986f0d82a9caa0ed18880a07658292ab6", [:mix], [], "hexpm", "02e6d1a559361a063cba7b75bc3eb2d6ad7e62730c551cc4703541fd11e65e5b"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, diff --git a/test/skate/location_search/aws_location_request_test.exs b/test/skate/location_search/aws_location_request_test.exs new file mode 100644 index 000000000..456f77303 --- /dev/null +++ b/test/skate/location_search/aws_location_request_test.exs @@ -0,0 +1,150 @@ +defmodule Skate.LocationSearch.AwsLocationRequestTest do + use ExUnit.Case + + import Skate.Factory + import Test.Support.Helpers + + alias Skate.LocationSearch.AwsLocationRequest + alias Skate.LocationSearch.SearchResult + + setup do + reassign_env(:skate, :aws_place_index, "test-index") + end + + describe "search/1" do + test "transforms result with name into SearchResult structs" do + name = "Some Landmark" + address_number = "123" + street = "Test St" + address_suffix = "MA 02201, United States" + + response = %{ + status_code: 200, + body: + Jason.encode!(%{ + "Results" => [ + build(:amazon_location_search_result, %{ + name: name, + address_number: address_number, + street: street, + address_suffix: address_suffix + }) + ] + }) + } + + reassign_env(:skate, :aws_request_fn, fn %{ + path: "/places/v0/indexes/test-index/search/text" + } -> + {:ok, response} + end) + + expected_address = "#{address_number} #{street}, #{address_suffix}" + + assert [%SearchResult{name: ^name, address: ^expected_address}] = + AwsLocationRequest.search("search text") + end + + test "transforms result without name into SearchResult structs" do + address_number = "123" + street = "Test St" + address_suffix = "MA 02201, United States" + + response = %{ + status_code: 200, + body: + Jason.encode!(%{ + "Results" => [ + build(:amazon_location_search_result, %{ + name: nil, + address_number: address_number, + street: street, + address_suffix: address_suffix + }) + ] + }) + } + + reassign_env(:skate, :aws_request_fn, fn %{ + path: "/places/v0/indexes/test-index/search/text" + } -> + {:ok, response} + end) + + expected_address = "#{address_number} #{street}, #{address_suffix}" + + assert [%SearchResult{name: nil, address: ^expected_address}] = + AwsLocationRequest.search("search text") + end + + test "transforms result without address prefix information to go on into SearchResult structs" do + address_suffix = "Some Neighborhood, Boston, MA" + + response = %{ + status_code: 200, + body: + Jason.encode!(%{ + "Results" => [ + build(:amazon_location_search_result, %{ + name: nil, + address_number: nil, + street: nil, + address_suffix: address_suffix + }) + ] + }) + } + + reassign_env(:skate, :aws_request_fn, fn %{ + path: "/places/v0/indexes/test-index/search/text" + } -> + {:ok, response} + end) + + assert [%SearchResult{name: nil, address: ^address_suffix}] = + AwsLocationRequest.search("search text") + end + + test "returns errors" do + reassign_env(:skate, :aws_request_fn, fn %{ + path: "/places/v0/indexes/test-index/search/text" + } -> + {:error, "error"} + end) + + assert {:error, "error"} = AwsLocationRequest.search("search text") + end + end + + describe "suggest/1" do + test "pulls out suggested search text" do + response = %{ + status_code: 200, + body: + Jason.encode!(%{ + "Results" => [build(:amazon_location_suggest_result, %{"Text" => "some place"})] + }) + } + + reassign_env(:skate, :aws_request_fn, fn %{ + path: + "/places/v0/indexes/test-index/search/suggestions" + } -> + {:ok, response} + end) + + assert ["some place"] = AwsLocationRequest.suggest("text") + end + + test "returns errors" do + reassign_env(:skate, :aws_request_fn, fn %{ + path: + "/places/v0/indexes/test-index/search/suggestions" + } -> + {:error, "error"} + end) + + assert {:error, "error"} = AwsLocationRequest.suggest("search text") + end + end +end diff --git a/test/skate_web/controllers/location_search_controller_test.exs b/test/skate_web/controllers/location_search_controller_test.exs new file mode 100644 index 000000000..049d3e05b --- /dev/null +++ b/test/skate_web/controllers/location_search_controller_test.exs @@ -0,0 +1,74 @@ +defmodule SkateWeb.LocationSearchControllerTest do + use SkateWeb.ConnCase + import Test.Support.Helpers + + alias Skate.LocationSearch.SearchResult + + describe "GET /api/location_search/search" do + test "when logged out, redirects you to cognito auth", %{conn: conn} do + conn = + conn + |> api_headers() + |> get("/api/location_search/search?query=test") + + assert redirected_to(conn) == "/auth/cognito" + end + + @tag :authenticated + test "returns data", %{conn: conn} do + result = %SearchResult{ + id: "test_id", + name: "Landmark", + address: "123 Fake St", + latitude: 0, + longitude: 0 + } + + reassign_env(:skate, :location_search_fn, fn _query -> {:ok, [result]} end) + + conn = + conn + |> api_headers() + |> get("/api/location_search/search?query=test") + + assert json_response(conn, 200) == %{ + "data" => [ + result + |> Map.from_struct() + |> Map.new(fn {key, value} -> {Atom.to_string(key), value} end) + ] + } + end + end + + describe "GET /api/location_search/suggest" do + test "when logged out, redirects you to cognito auth", %{conn: conn} do + conn = + conn + |> api_headers() + |> get("/api/location_search/suggest?query=test") + + assert redirected_to(conn) == "/auth/cognito" + end + + @tag :authenticated + test "returns data", %{conn: conn} do + result = "suggested search" + + reassign_env(:skate, :location_suggest_fn, fn _query -> {:ok, [result]} end) + + conn = + conn + |> api_headers() + |> get("/api/location_search/suggest?query=test") + + assert json_response(conn, 200) == %{"data" => [result]} + end + end + + defp api_headers(conn) do + conn + |> put_req_header("accept", "application/json") + |> put_req_header("content-type", "application/json") + end +end diff --git a/test/skate_web/controllers/page_controller_test.exs b/test/skate_web/controllers/page_controller_test.exs index 308ae3a11..5a5676393 100644 --- a/test/skate_web/controllers/page_controller_test.exs +++ b/test/skate_web/controllers/page_controller_test.exs @@ -160,6 +160,16 @@ defmodule SkateWeb.PageControllerTest do assert html_response(conn, 200) =~ "data-tileset-url=\"tilesets.com/osm\"" end + @tag :authenticated + test "correct map limits set", %{conn: conn} do + reassign_env(:skate, :map_limits, %{north: 1, south: 2, east: 3, west: 4}) + + conn = get(conn, "/") + + assert html_response(conn, 200) =~ + "data-map-limits=\"{"east":3,"north":1,"south":2,"west":4}\"" + end + @tag :authenticated test "correct username set", %{conn: conn} do conn = get(conn, "/") diff --git a/test/support/factory.ex b/test/support/factory.ex index 629eeaae8..2798afb2a 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -198,4 +198,34 @@ defmodule Skate.Factory do save_changes_to_tab_uuid: nil } end + + def amazon_location_search_result_factory(attrs) do + address_number = Map.get(attrs, :address_number, sequence(:address_number, &to_string/1)) + street = Map.get(attrs, :street, "Test St") + name = Map.get(attrs, :name, "Landmark") + address_suffix = Map.get(attrs, :address_suffix, "MA 02201, United States") + + result = %{ + "Place" => %{ + "AddressNumber" => address_number, + "Geometry" => %{ + "Point" => [0, 0] + }, + "Label" => + "#{name && name <> ", "}#{address_number && address_number <> " "}#{street && street <> ", "}#{address_suffix}", + "Street" => street + }, + "PlaceId" => "test_id_#{sequence(:place_id, &to_string/1)}" + } + + result |> merge_attributes(attrs) |> evaluate_lazy_attributes() + end + + def amazon_location_suggest_result_factory do + %{ + "Text" => + "#{sequence(:address_number, &to_string/1)} Test St, Boston, MA 02201, United States", + "PlaceId" => "test_id_#{sequence(:place_id, &to_string/1)}" + } + end end