defmodule Microwaveprop.BeaconMeasurementsTest do use Microwaveprop.DataCase import Microwaveprop.AccountsFixtures import Microwaveprop.BeaconsFixtures alias Microwaveprop.BeaconMeasurements alias Microwaveprop.BeaconMeasurements.BeaconMeasurement alias Microwaveprop.BeaconMonitors alias Microwaveprop.Beacons defp setup_actors(_) do user = user_fixture() {:ok, monitor} = BeaconMonitors.create_monitor(user, %{"name" => "Shack Pi"}) beacon = beacon_fixture(user) {:ok, beacon} = Beacons.approve_beacon(beacon) %{user: user, monitor: monitor, beacon: beacon} end defp wire_attrs(beacon, overrides \\ %{}) do Map.merge( %{ "beacon_id" => beacon.id, "frequency_hz" => 28_330_000, "measured_at" => "2026-05-13T15:30:00Z", "integration_s" => 60, "passband_hz" => 300.0, "gain_db" => 10.0, "noise_floor_dbfs" => -110.2, "signal_peak_dbfs" => -88.4, "signal_avg_dbfs" => -89.1, "snr_peak_db" => 21.8, "snr_avg_db" => 21.1, "signal_active_fraction" => 0.48, "propmonitor_version" => "0.1.0" }, overrides ) end describe "create_measurement/2" do setup :setup_actors test "inserts a measurement for an approved on-air beacon", %{monitor: m, beacon: b} do assert {:ok, %BeaconMeasurement{} = meas} = BeaconMeasurements.create_measurement(m, wire_attrs(b)) assert meas.monitor_id == m.id assert meas.beacon_id == b.id assert meas.frequency_hz == 28_330_000 assert meas.signal_active_fraction == 0.48 assert meas.propmonitor_version == "0.1.0" end test "returns :beacon_not_found for an unknown beacon UUID", %{monitor: m} do attrs = wire_attrs(%{id: "00000000-0000-0000-0000-000000000000"}) assert {:error, :beacon_not_found} = BeaconMeasurements.create_measurement(m, attrs) end test "returns :beacon_not_found for a malformed beacon UUID", %{monitor: m, beacon: b} do attrs = b |> wire_attrs() |> Map.put("beacon_id", "not-a-uuid") assert {:error, :beacon_not_found} = BeaconMeasurements.create_measurement(m, attrs) end test "returns :beacon_not_found for an unapproved beacon", %{monitor: m, user: u} do pending = beacon_fixture(u, callsign: "K5HOLD") attrs = wire_attrs(pending) assert {:error, :beacon_not_found} = BeaconMeasurements.create_measurement(m, attrs) end test "returns :beacon_not_found for a beacon marked off-the-air", %{monitor: m, beacon: b} do {:ok, _} = Beacons.update_beacon(b, %{"on_the_air" => false}) assert {:error, :beacon_not_found} = BeaconMeasurements.create_measurement(m, wire_attrs(b)) end test "rejects out-of-range signal_active_fraction", %{monitor: m, beacon: b} do attrs = wire_attrs(b, %{"signal_active_fraction" => 1.5}) assert {:error, changeset} = BeaconMeasurements.create_measurement(m, attrs) assert %{signal_active_fraction: [_ | _]} = errors_on(changeset) end test "rejects integration_s below the floor", %{monitor: m, beacon: b} do attrs = wire_attrs(b, %{"integration_s" => 1}) assert {:error, changeset} = BeaconMeasurements.create_measurement(m, attrs) assert %{integration_s: [_ | _]} = errors_on(changeset) end test "rejects missing required fields", %{monitor: m, beacon: b} do attrs = Map.delete(wire_attrs(b), "noise_floor_dbfs") assert {:error, changeset} = BeaconMeasurements.create_measurement(m, attrs) assert %{noise_floor_dbfs: ["can't be blank"]} = errors_on(changeset) end test "duplicate (monitor, beacon, measured_at) returns a unique-constraint error", %{monitor: m, beacon: b} do attrs = wire_attrs(b) assert {:ok, _} = BeaconMeasurements.create_measurement(m, attrs) assert {:error, %Ecto.Changeset{} = changeset} = BeaconMeasurements.create_measurement(m, attrs) assert Enum.any?(changeset.errors, fn {_, {_, opts}} -> Keyword.get(opts, :constraint) == :unique end) end test "two monitors can report for the same beacon at the same instant", %{monitor: m, beacon: b, user: u} do {:ok, m2} = BeaconMonitors.create_monitor(u, %{"name" => "Other Pi"}) attrs = wire_attrs(b) assert {:ok, _} = BeaconMeasurements.create_measurement(m, attrs) assert {:ok, _} = BeaconMeasurements.create_measurement(m2, attrs) end end describe "list_recent_for_beacon/2 and list_recent_for_monitor/2" do setup :setup_actors test "returns measurements newest first", %{monitor: m, beacon: b} do a1 = wire_attrs(b, %{"measured_at" => "2026-05-13T15:30:00Z"}) a2 = wire_attrs(b, %{"measured_at" => "2026-05-13T15:31:00Z"}) {:ok, _} = BeaconMeasurements.create_measurement(m, a1) {:ok, _} = BeaconMeasurements.create_measurement(m, a2) [latest, prev] = BeaconMeasurements.list_recent_for_beacon(b.id, 10) assert latest.measured_at > prev.measured_at [latest_m, _] = BeaconMeasurements.list_recent_for_monitor(m.id, 10) assert latest_m.measured_at == latest.measured_at end end end