diff --git a/test/aprsme/device_cache_test.exs b/test/aprsme/device_cache_test.exs index 58e88fd..9de18a5 100644 --- a/test/aprsme/device_cache_test.exs +++ b/test/aprsme/device_cache_test.exs @@ -72,4 +72,32 @@ defmodule Aprsme.DeviceCacheTest do assert DeviceCache.lookup_device("APSXX") == nil end end + + describe "GenServer callbacks" do + test "init/1 schedules an initial load and returns an empty state" do + assert {:ok, state} = DeviceCache.init([]) + assert state == %{initial_load_done: false} + assert_receive :initial_load, 1_500 + end + + test "handle_call(:refresh_cache, _, state) returns :ok" do + state = %{initial_load_done: true} + assert {:reply, :ok, ^state} = DeviceCache.handle_call(:refresh_cache, self(), state) + end + + test "handle_cast(:refresh_cache_async, state) keeps the state" do + state = %{initial_load_done: true} + assert {:noreply, ^state} = DeviceCache.handle_cast(:refresh_cache_async, state) + end + + test "handle_info(:initial_load, state) flips initial_load_done and schedules a periodic refresh" do + assert {:noreply, %{initial_load_done: true}} = + DeviceCache.handle_info(:initial_load, %{initial_load_done: false}) + end + + test "handle_info(:refresh_cache, state) keeps the state and schedules a periodic refresh" do + state = %{initial_load_done: true} + assert {:noreply, ^state} = DeviceCache.handle_info(:refresh_cache, state) + end + end end diff --git a/test/aprsme/device_identification_test.exs b/test/aprsme/device_identification_test.exs index 8dd7c72..96ab201 100644 --- a/test/aprsme/device_identification_test.exs +++ b/test/aprsme/device_identification_test.exs @@ -148,6 +148,59 @@ defmodule Aprsme.DeviceIdentificationTest do assert :ok == DeviceIdentification.maybe_refresh_devices() end + + test "attempts to refresh when there are no devices at all" do + Repo.delete_all(Devices) + # fetch_and_upsert_devices goes through the circuit breaker. Without a + # stub it either errors out (network failure) or returns :ok. Either + # outcome exercises the refresh_if_stale(nil, _) branch. + result = DeviceIdentification.maybe_refresh_devices() + assert result == :ok or match?({:error, _}, result) + end + end + + describe "lookup_device_by_identifier/1 wildcard and escape handling" do + test "matches with a multi-character ? wildcard pattern" do + Repo.delete_all(Devices) + + Repo.insert!(%Devices{ + identifier: "APS???", + vendor: "TestVendor", + model: "TestModel" + }) + + devices = Repo.all(Devices) + Aprsme.Cache.put(:device_cache, :all_devices, devices) + + # 'APSK21' has 6 characters, matching 'APS???'. + assert %Devices{identifier: "APS???"} = DeviceIdentification.lookup_device_by_identifier("APSK21") + # 'APS' alone is too short. + refute DeviceIdentification.lookup_device_by_identifier("APS") + # Different prefix should not match. + refute DeviceIdentification.lookup_device_by_identifier("ZZZK21") + end + + test "patterns with regex metacharacters are escaped properly" do + Repo.delete_all(Devices) + + Repo.insert!(%Devices{ + identifier: "A.B+C*", + vendor: "Regex", + model: "Edge" + }) + + devices = Repo.all(Devices) + Aprsme.Cache.put(:device_cache, :all_devices, devices) + + # The dot/plus/star should be treated as literals (no ? in pattern + # means exact match is used). + assert %Devices{identifier: "A.B+C*"} = + DeviceIdentification.lookup_device_by_identifier("A.B+C*") + + # A string that would "match" if dot/plus/star were regex metachars + # must NOT match — they're literals. + refute DeviceIdentification.lookup_device_by_identifier("AXBCC") + end end describe "lookup_device_by_identifier/1" do diff --git a/test/aprsme/signal_handler_test.exs b/test/aprsme/signal_handler_test.exs index e2c6732..b639baa 100644 --- a/test/aprsme/signal_handler_test.exs +++ b/test/aprsme/signal_handler_test.exs @@ -13,4 +13,12 @@ defmodule Aprsme.SignalHandlerTest do # We can't safely exercise the :sigterm clause here — it calls # Aprsme.ShutdownHandler.shutdown() which would actually shut the app down. end + + describe "init/1" do + test "installs a SIGTERM handler and returns an empty state" do + # :os.set_signal is process-global but calling it a second time with + # :handle is idempotent at the VM level. Safe to exercise. + assert {:ok, %{}} = SignalHandler.init([]) + end + end end diff --git a/test/aprsme_web/components/layouts_test.exs b/test/aprsme_web/components/layouts_test.exs index f07799e..4ff3f66 100644 --- a/test/aprsme_web/components/layouts_test.exs +++ b/test/aprsme_web/components/layouts_test.exs @@ -1,6 +1,8 @@ defmodule AprsmeWeb.LayoutsTest do use ExUnit.Case, async: true + import Phoenix.LiveViewTest + alias AprsmeWeb.Layouts describe "body_class/1" do @@ -20,4 +22,50 @@ defmodule AprsmeWeb.LayoutsTest do assert Layouts.body_class(%{whatever: 42}) == Layouts.body_class(%{}) end end + + describe "app/1 template" do + test "renders the site header and inner content" do + html = + render_component(&Layouts.app/1, %{ + current_user: nil, + flash: %{}, + map_page: false, + inner_content: Phoenix.HTML.raw("
inner-content-marker
") + }) + + assert html =~ "aprs.me" + assert html =~ "inner-content-marker" + # Default map_page is false, so data-map-page="false" is emitted. + assert html =~ ~s(data-map-page="false") + end + + test "emits data-map-page=\"true\" for map pages" do + html = + render_component(&Layouts.app/1, %{ + current_user: nil, + flash: %{}, + map_page: true, + inner_content: Phoenix.HTML.raw("") + }) + + assert html =~ ~s(data-map-page="true") + assert html =~ "min-h-screen" + end + end + + describe "root/1 template" do + test "renders the root layout with the inner content" do + html = + render_component(&Layouts.root/1, %{ + current_user: nil, + inner_content: Phoenix.HTML.raw("