From a035f6d31edbd02140e7ff2d63c80ca4d0e6f8e3 Mon Sep 17 00:00:00 2001 From: Adrian Kumpf Date: Sun, 1 Sep 2019 22:13:15 +0200 Subject: [PATCH] Add tests --- .../mqtt/pubsub/vehicle_subscriber.ex | 25 ++-- mix.exs | 3 +- mix.lock | 2 + test/support/mocks/mqtt_publisher.ex | 27 ++++ test/support/mocks/vehicles.ex | 7 + test/teslamate/locations/geocoder_test.exs | 65 +++++++++ .../mqtt/pubsub/vehicle_subscriber_test.exs | 135 ++++++++++++++++++ .../teslamate_web/live/geofence_live_test.exs | 18 ++- 8 files changed, 272 insertions(+), 10 deletions(-) create mode 100644 test/support/mocks/mqtt_publisher.ex create mode 100644 test/teslamate/locations/geocoder_test.exs create mode 100644 test/teslamate/mqtt/pubsub/vehicle_subscriber_test.exs diff --git a/lib/teslamate/mqtt/pubsub/vehicle_subscriber.ex b/lib/teslamate/mqtt/pubsub/vehicle_subscriber.ex index b39f562f..5eea5d0f 100644 --- a/lib/teslamate/mqtt/pubsub/vehicle_subscriber.ex +++ b/lib/teslamate/mqtt/pubsub/vehicle_subscriber.ex @@ -2,11 +2,12 @@ defmodule TeslaMate.Mqtt.PubSub.VehicleSubscriber do use GenServer require Logger + import Core.Dependency, only: [call: 3] alias TeslaMate.Mqtt.Publisher alias TeslaMate.Vehicles - defstruct [:car_id, :last_summary] + defstruct [:car_id, :last_summary, :deps] alias __MODULE__, as: State def child_spec(arg) do @@ -23,8 +24,15 @@ defmodule TeslaMate.Mqtt.PubSub.VehicleSubscriber do @impl true def init(opts) do car_id = Keyword.fetch!(opts, :car_id) - :ok = Vehicles.subscribe(car_id) - {:ok, %State{car_id: car_id}} + + deps = %{ + vehicles: Keyword.get(opts, :deps_vehicles, Vehicles), + publisher: Keyword.get(opts, :deps_publisher, Publisher) + } + + :ok = call(deps.vehicles, :subscribe, [car_id]) + + {:ok, %State{car_id: car_id, deps: deps}} end @impl true @@ -40,10 +48,11 @@ defmodule TeslaMate.Mqtt.PubSub.VehicleSubscriber do end) |> Task.async_stream( fn {key, value} -> - Publisher.publish("teslamate/cars/#{car_id}/#{key}", to_string(value), - retain: true, - qos: 1 - ) + call(state.deps.publisher, :publish, [ + "teslamate/cars/#{car_id}/#{key}", + to_string(value), + [retain: true, qos: 1] + ]) end, max_concurrency: 10, on_timeout: :kill_task, @@ -54,6 +63,6 @@ defmodule TeslaMate.Mqtt.PubSub.VehicleSubscriber do _ok -> nil end) - {:noreply, state} + {:noreply, %State{state | last_summary: summary}} end end diff --git a/mix.exs b/mix.exs index 3623b251..cec28ce0 100644 --- a/mix.exs +++ b/mix.exs @@ -55,7 +55,8 @@ defmodule TeslaMate.MixProject do {:mojito, "~> 0.5"}, {:geocalc, "~> 0.5"}, {:srtm, "~> 0.1"}, - {:fuse, "~> 2.4"} + {:fuse, "~> 2.4"}, + {:mock, "~> 0.3", only: :test} ] end diff --git a/mix.lock b/mix.lock index 55d6bc03..fc1139bf 100644 --- a/mix.lock +++ b/mix.lock @@ -18,10 +18,12 @@ "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, + "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, "mint": {:hex, :mint, "0.4.0", "b93a10192957624ed4a8b8641eff1819019c36487bdf49e2b505afd2cc9b7911", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm"}, + "mock": {:hex, :mock, "0.3.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, "mojito": {:hex, :mojito, "0.5.0", "de003eafc09633d236e487621f1a8d7d55ca0414a5a418e09ab70a595f9c9c12", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 0.4.0", [hex: :mint, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, "phoenix": {:hex, :phoenix, "1.4.9", "746d098e10741c334d88143d3c94cab1756435f94387a63441792e66ec0ee974", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/test/support/mocks/mqtt_publisher.ex b/test/support/mocks/mqtt_publisher.ex new file mode 100644 index 00000000..a4c5fc65 --- /dev/null +++ b/test/support/mocks/mqtt_publisher.ex @@ -0,0 +1,27 @@ +defmodule MqttPublisherMock do + use GenServer + + defstruct [:pid] + alias __MODULE__, as: State + + # API + + def start_link(opts) do + GenServer.start_link(__MODULE__, opts, name: Keyword.fetch!(opts, :name)) + end + + def publish(name, topic, msg, opts), do: GenServer.call(name, {:publish, topic, msg, opts}) + + # Callbacks + + @impl true + def init(opts) do + {:ok, %State{pid: Keyword.fetch!(opts, :pid)}} + end + + @impl true + def handle_call({:publish, _topic, _msg, _opts} = action, _from, %State{pid: pid} = state) do + send(pid, {MqttPublisherMock, action}) + {:reply, :ok, state} + end +end diff --git a/test/support/mocks/vehicles.ex b/test/support/mocks/vehicles.ex index 9652eea6..0e7a0029 100644 --- a/test/support/mocks/vehicles.ex +++ b/test/support/mocks/vehicles.ex @@ -12,6 +12,8 @@ defmodule VehiclesMock do def restart(name), do: GenServer.call(name, :restart) + def subscribe(name, car_id), do: GenServer.call(name, {:subscribe, car_id}) + # Callbacks @impl true @@ -20,6 +22,11 @@ defmodule VehiclesMock do end @impl true + def handle_call({:subscribe, _car_id} = action, _from, %State{pid: pid} = state) do + send(pid, {VehiclesMock, action}) + {:reply, :ok, state} + end + def handle_call(:restart, _from, %State{pid: pid} = state) do send(pid, {VehiclesMock, :restart}) {:reply, :ok, state} diff --git a/test/teslamate/locations/geocoder_test.exs b/test/teslamate/locations/geocoder_test.exs new file mode 100644 index 00000000..041f4a4a --- /dev/null +++ b/test/teslamate/locations/geocoder_test.exs @@ -0,0 +1,65 @@ +defmodule TeslaMate.Locations.GeocoderTest do + use ExUnit.Case, async: false + + alias TeslaMate.Locations.Geocoder + + import Mock + + @response {:ok, + %Mojito.Response{ + body: + "{\"place_id\":241575531,\"licence\":\"Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright\",\"osm_type\":\"node\",\"osm_id\":5983038298,\"lat\":\"37.8895442\",\"lon\":\"41.1288167\",\"place_rank\":30,\"category\":\"amenity\",\"type\":\"cafe\",\"importance\":0,\"addresstype\":\"amenity\",\"name\":\"Kahve Deryası\",\"display_name\":\"Kahve Deryası, Cihan Kavşağı, Batman, Ziyagökalp Mahallesi, Batman merkez, Batman, Southeastern Anatolia Region, 72060, Turkey\",\"address\":{\"cafe\":\"Kahve Deryası\",\"road\":\"Cihan Kavşağı\",\"residential\":\"Batman\",\"suburb\":\"Ziyagökalp Mahallesi\",\"city\":\"Batman merkez\",\"state\":\"Southeastern Anatolia Region\",\"postcode\":\"72060\",\"country\":\"Turkey\",\"country_code\":\"tr\"},\"extratags\":{},\"namedetails\":{\"name\":\"Kahve Deryası\"},\"boundingbox\":[\"37.8894442\",\"37.8896442\",\"41.1287167\",\"41.1289167\"]}", + complete: true, + headers: [ + {"date", "Sun, 01 Sep 2019 21:03:23 GMT"}, + {"server", "Apache/2.4.29 (Ubuntu)"}, + {"access-control-allow-origin", "*"}, + {"access-control-allow-methods", "OPTIONS,GET"}, + {"strict-transport-security", "max-age=31536000; includeSubDomains; preload"}, + {"expect-ct", + "max-age=0, report-uri=\"https://openstreetmap.report-uri.com/r/d/ct/reportOnly\""}, + {"content-type", "application/json; charset=UTF-8"} + ], + status_code: 200 + }} + + test "geocoders coordinates" do + with_mock Mojito, + get: + fn "https://nominatim.openstreetmap.org/reverse?format=jsonv2&addressdetails=1&extratags=1&namedetails=1&zoom=19&lat=37.889602&lon=41.129182", + _headers -> + @response + end do + assert Geocoder.reverse_lookup(37.889602, 41.129182) == + {:ok, + %{ + city: "Batman merkez", + country: "Turkey", + county: nil, + display_name: + "Kahve Deryası, Cihan Kavşağı, Batman, Ziyagökalp Mahallesi, Batman merkez, Batman, Southeastern Anatolia Region, 72060, Turkey", + house_number: nil, + latitude: "37.8895442", + longitude: "41.1288167", + name: "Kahve Deryası", + neighbourhood: "Ziyagökalp Mahallesi", + place_id: 241_575_531, + postcode: "72060", + raw: %{ + "cafe" => "Kahve Deryası", + "city" => "Batman merkez", + "country" => "Turkey", + "country_code" => "tr", + "postcode" => "72060", + "residential" => "Batman", + "road" => "Cihan Kavşağı", + "state" => "Southeastern Anatolia Region", + "suburb" => "Ziyagökalp Mahallesi" + }, + road: "Cihan Kavşağı", + state: "Southeastern Anatolia Region", + state_district: nil + }} + end + end +end diff --git a/test/teslamate/mqtt/pubsub/vehicle_subscriber_test.exs b/test/teslamate/mqtt/pubsub/vehicle_subscriber_test.exs new file mode 100644 index 00000000..a6223513 --- /dev/null +++ b/test/teslamate/mqtt/pubsub/vehicle_subscriber_test.exs @@ -0,0 +1,135 @@ +defmodule TeslaMate.Mqtt.PubSub.VehicleSubscriberTest do + use TeslaMate.DataCase, async: true + + alias TeslaMate.Mqtt.PubSub.VehicleSubscriber + alias TeslaMate.Vehicles.Vehicle.Summary + + defp start_subscriber(name, car_id) do + publisher_name = :"mqtt_publisher_#{name}" + vehicles_name = :"vehicles_#{name}" + + {:ok, _pid} = start_supervised({MqttPublisherMock, name: publisher_name, pid: self()}) + {:ok, _pid} = start_supervised({VehiclesMock, name: vehicles_name, pid: self()}) + + start_supervised( + {VehicleSubscriber, + [ + name: name, + car_id: car_id, + deps_publisher: {MqttPublisherMock, publisher_name}, + deps_vehicles: {VehiclesMock, vehicles_name} + ]} + ) + end + + test "", %{test: name} do + {:ok, pid} = start_subscriber(name, 0) + + assert_receive {VehiclesMock, {:subscribe, 0}} + + summary = %Summary{ + display_name: "Foo", + state: :online, + since: DateTime.utc_now(), + battery_level: 60.0, + ideal_battery_range_km: 230.5, + est_battery_range_km: 220.0, + battery_range_km: 230.5, + charge_energy_added: 25, + speed: 40, + outside_temp: 15, + inside_temp: 20.0, + locked: true, + sentry_mode: false, + plugged_in: false, + scheduled_charging_start_time: DateTime.utc_now() |> DateTime.add(60 * 60 * 10, :second), + charge_limit_soc: 90, + charger_power: 50 + } + + send(pid, summary) + + assert_receive {MqttPublisherMock, + {:publish, "teslamate/cars/0/charge_limit_soc", "90", [retain: true, qos: 1]}} + + assert_receive {MqttPublisherMock, + {:publish, "teslamate/cars/0/display_name", "Foo", [retain: true, qos: 1]}} + + assert_receive {MqttPublisherMock, + {:publish, "teslamate/cars/0/inside_temp", "20.0", [retain: true, qos: 1]}} + + assert_receive {MqttPublisherMock, + {:publish, "teslamate/cars/0/locked", "true", [retain: true, qos: 1]}} + + assert_receive {MqttPublisherMock, + {:publish, "teslamate/cars/0/outside_temp", "15", [retain: true, qos: 1]}} + + assert_receive {MqttPublisherMock, + {:publish, "teslamate/cars/0/plugged_in", "false", [retain: true, qos: 1]}} + + scheduled_charging_start_time_str = to_string(summary.scheduled_charging_start_time) + + assert_receive {MqttPublisherMock, + {:publish, "teslamate/cars/0/scheduled_charging_start_time", + ^scheduled_charging_start_time_str, [retain: true, qos: 1]}} + + assert_receive {MqttPublisherMock, + {:publish, "teslamate/cars/0/sentry_mode", "false", [retain: true, qos: 1]}} + + assert_receive {MqttPublisherMock, + {:publish, "teslamate/cars/0/speed", "40", [retain: true, qos: 1]}} + + assert_receive {MqttPublisherMock, + {:publish, "teslamate/cars/0/state", "online", [retain: true, qos: 1]}} + + assert_receive {MqttPublisherMock, + {:publish, "teslamate/cars/0/battery_level", "60.0", [retain: true, qos: 1]}} + + assert_receive {MqttPublisherMock, + {:publish, "teslamate/cars/0/battery_range_km", "230.5", + [retain: true, qos: 1]}} + + assert_receive {MqttPublisherMock, + {:publish, "teslamate/cars/0/est_battery_range_km", "220.0", + [retain: true, qos: 1]}} + + assert_receive {MqttPublisherMock, + {:publish, "teslamate/cars/0/ideal_battery_range_km", "230.5", + [retain: true, qos: 1]}} + + assert_receive {MqttPublisherMock, + {:publish, "teslamate/cars/0/charge_energy_added", "25", + [retain: true, qos: 1]}} + + assert_receive {MqttPublisherMock, + {:publish, "teslamate/cars/0/charger_power", "50", [retain: true, qos: 1]}} + + since_str = to_string(summary.since) + + assert_receive {MqttPublisherMock, + {:publish, "teslamate/cars/0/since", ^since_str, [retain: true, qos: 1]}} + + # send same summary again + send(pid, summary) + + refute_receive _ + end + + test "send empty string if scheduled_charging_start_time is nil", %{test: name} do + {:ok, pid} = start_subscriber(name, 0) + + assert_receive {VehiclesMock, {:subscribe, 0}} + + summary = %Summary{ + scheduled_charging_start_time: nil + } + + send(pid, summary) + + assert_receive {MqttPublisherMock, + {:publish, "teslamate/cars/0/scheduled_charging_start_time", "", + [retain: true, qos: 1]}} + + refute_receive _ + end +end diff --git a/test/teslamate_web/live/geofence_live_test.exs b/test/teslamate_web/live/geofence_live_test.exs index 9ca0112b..867c600c 100644 --- a/test/teslamate_web/live/geofence_live_test.exs +++ b/test/teslamate_web/live/geofence_live_test.exs @@ -1,7 +1,7 @@ defmodule TeslaMateWeb.GeoFenceLiveTest do use TeslaMateWeb.ConnCase - alias TeslaMate.{Locations, Settings} + alias TeslaMate.{Locations, Settings, Log} alias TeslaMate.Locations.GeoFence def geofence_fixture(attrs \\ %{}) do @@ -151,6 +151,22 @@ defmodule TeslaMateWeb.GeoFenceLiveTest do end describe "New" do + test "pre-fills the coordinates with the most recent position", %{conn: conn} do + assert {:ok, car} = Log.create_car(%{efficiency: 0.153, eid: 42, model: "3", vid: 42}) + + assert {:ok, _} = + Log.insert_position(car.id, %{ + date: DateTime.utc_now(), + latitude: 48.067612, + longitude: 12.862226 + }) + + assert {:ok, view, html} = live(conn, "/geo-fences/new") + + assert html =~ ~r// + assert html =~ ~r// + end + test "validates cahnges when creating a new geo-fence", %{conn: conn} do assert {:ok, view, html} = live(conn, "/geo-fences/new")