test: cover repo_listener

10 characterization tests for PostgreSQL NOTIFY → PubSub relay:
contact_status_changed channel → db:contact_status topic, oban_job_changed
→ db:oban_jobs, channel isolation, invalid JSON silent-swallow, catch-all
handling of unknown channels and info messages.
This commit is contained in:
Graham McIntire 2026-04-16 14:38:41 -05:00
parent 4f956e4606
commit cc480652aa
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59

View file

@ -0,0 +1,137 @@
defmodule Microwaveprop.RepoListenerTest do
@moduledoc """
Characterization tests for `Microwaveprop.RepoListener`.
The module is a GenServer that subscribes to two PostgreSQL `NOTIFY`
channels via `Postgrex.Notifications` and rebroadcasts decoded
payloads via `Phoenix.PubSub`. These tests exercise the `handle_info/2`
seam directly using the exact message shape `Postgrex.Notifications`
delivers no database connection required.
"""
use ExUnit.Case, async: false
alias Microwaveprop.RepoListener
# Shape of the message that `Postgrex.Notifications` delivers to a
# listening process. Ref. https://hexdocs.pm/postgrex/Postgrex.Notifications.html
defp notification(channel, payload) do
{:notification, self(), make_ref(), channel, payload}
end
describe "handle_info/2 — contact_status_changed" do
setup do
:ok = Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "db:contact_status")
on_exit(fn -> Phoenix.PubSub.unsubscribe(Microwaveprop.PubSub, "db:contact_status") end)
:ok
end
test "broadcasts decoded payload on db:contact_status topic" do
payload = Jason.encode!(%{"contact_id" => "abc-123", "status" => "queued"})
msg = notification("contact_status_changed", payload)
assert {:noreply, %{pid: nil}} = RepoListener.handle_info(msg, %{pid: nil})
assert_receive {:contact_status_changed, %{"contact_id" => "abc-123", "status" => "queued"}}
end
test "preserves full decoded payload structure (nested maps/lists)" do
data = %{
"contact_id" => "xyz",
"fields" => ["weather", "hrrr"],
"meta" => %{"source" => "notify"}
}
msg = notification("contact_status_changed", Jason.encode!(data))
assert {:noreply, _} = RepoListener.handle_info(msg, %{pid: nil})
assert_receive {:contact_status_changed, ^data}
end
test "silently ignores invalid JSON payload" do
msg = notification("contact_status_changed", "not-json-at-all{")
assert {:noreply, %{pid: nil}} = RepoListener.handle_info(msg, %{pid: nil})
refute_receive {:contact_status_changed, _}, 50
end
end
describe "handle_info/2 — oban_job_changed" do
setup do
:ok = Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "db:oban_jobs")
on_exit(fn -> Phoenix.PubSub.unsubscribe(Microwaveprop.PubSub, "db:oban_jobs") end)
:ok
end
test "broadcasts decoded payload on db:oban_jobs topic" do
payload = Jason.encode!(%{"id" => 42, "state" => "completed", "queue" => "weather"})
msg = notification("oban_job_changed", payload)
assert {:noreply, %{pid: nil}} = RepoListener.handle_info(msg, %{pid: nil})
assert_receive {:oban_job_changed, %{"id" => 42, "state" => "completed", "queue" => "weather"}}
end
test "silently ignores invalid JSON payload" do
msg = notification("oban_job_changed", "{\"broken\":")
assert {:noreply, %{pid: nil}} = RepoListener.handle_info(msg, %{pid: nil})
refute_receive {:oban_job_changed, _}, 50
end
end
describe "handle_info/2 — channel isolation" do
test "contact_status_changed does not broadcast on db:oban_jobs" do
:ok = Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "db:oban_jobs")
on_exit(fn -> Phoenix.PubSub.unsubscribe(Microwaveprop.PubSub, "db:oban_jobs") end)
msg = notification("contact_status_changed", Jason.encode!(%{"contact_id" => "abc"}))
assert {:noreply, _} = RepoListener.handle_info(msg, %{pid: nil})
refute_receive {:oban_job_changed, _}, 50
refute_receive {:contact_status_changed, _}, 50
end
test "oban_job_changed does not broadcast on db:contact_status" do
:ok = Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "db:contact_status")
on_exit(fn -> Phoenix.PubSub.unsubscribe(Microwaveprop.PubSub, "db:contact_status") end)
msg = notification("oban_job_changed", Jason.encode!(%{"id" => 1}))
assert {:noreply, _} = RepoListener.handle_info(msg, %{pid: nil})
refute_receive {:contact_status_changed, _}, 50
refute_receive {:oban_job_changed, _}, 50
end
end
describe "handle_info/2 — unknown / unrelated messages" do
test "ignores notifications on unknown channels without crashing" do
:ok = Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "db:contact_status")
:ok = Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "db:oban_jobs")
on_exit(fn ->
Phoenix.PubSub.unsubscribe(Microwaveprop.PubSub, "db:contact_status")
Phoenix.PubSub.unsubscribe(Microwaveprop.PubSub, "db:oban_jobs")
end)
msg = notification("some_other_channel", Jason.encode!(%{"x" => 1}))
assert {:noreply, %{pid: nil}} = RepoListener.handle_info(msg, %{pid: nil})
refute_receive {:contact_status_changed, _}, 50
refute_receive {:oban_job_changed, _}, 50
end
test "ignores arbitrary info messages without crashing" do
assert {:noreply, %{pid: nil}} = RepoListener.handle_info(:random_message, %{pid: nil})
assert {:noreply, %{pid: nil}} = RepoListener.handle_info({:foo, :bar}, %{pid: nil})
end
test "returns state unchanged regardless of message" do
state = %{pid: nil, extra: :keep_me}
assert {:noreply, ^state} = RepoListener.handle_info(:whatever, state)
msg = notification("contact_status_changed", Jason.encode!(%{"a" => 1}))
assert {:noreply, ^state} = RepoListener.handle_info(msg, state)
end
end
end