From fb49eb016d49cb38a399afa1b1cdcf1b167067c0 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Tue, 21 Jul 2026 18:28:51 -0500 Subject: [PATCH] feat(monitors): schema migration + remove user self-service creation - Add hardware/config fields migration to beacon_monitors table - Update BeaconMonitor schema with provision/config changesets - Add context functions: create_hardware, update_config, list_all_monitors - Remove user-facing monitor creation (browser POST + API POST) - Update settings page: show assigned monitors table with hardware info - Update profile page: show assigned monitors, remove register links - Fix all tests to match new API --- lib/microwaveprop/beacon_monitors.ex | 147 ++++++++++++++++-- .../beacon_monitors/beacon_monitor.ex | 82 +++++++++- .../controllers/api/v1/me_controller.ex | 16 -- .../controllers/beacon_monitor_controller.ex | 22 --- .../controllers/user_settings_controller.ex | 5 +- .../user_settings_html/edit.html.heex | 62 ++++---- .../live/user_profile_live.ex | 86 +++++----- lib/microwaveprop_web/router.ex | 2 - ...add_hardware_fields_to_beacon_monitors.exs | 30 ++++ .../controllers/api/v1/me_controller_test.exs | 21 +-- .../beacon_monitor_controller_test.exs | 33 ---- .../live/user_profile_live_test.exs | 18 ++- 12 files changed, 328 insertions(+), 196 deletions(-) create mode 100644 priv/repo/migrations/20260721232034_add_hardware_fields_to_beacon_monitors.exs diff --git a/lib/microwaveprop/beacon_monitors.ex b/lib/microwaveprop/beacon_monitors.ex index 974c517c..05aab6f5 100644 --- a/lib/microwaveprop/beacon_monitors.ex +++ b/lib/microwaveprop/beacon_monitors.ex @@ -1,48 +1,148 @@ defmodule Microwaveprop.BeaconMonitors do @moduledoc """ - The BeaconMonitors context: manages the monitor stations a user - has registered. Each monitor has a unique random token the remote - program uses to authenticate its reports. + The BeaconMonitors context: manages the physical SDR-based monitor + hardware assigned to users. + + Each monitor has a unique random token the `propmonitor` client uses + to authenticate its measurement uploads. """ import Ecto.Query + alias Ecto.Query.CastError alias Microwaveprop.Accounts.User alias Microwaveprop.BeaconMonitors.BeaconMonitor alias Microwaveprop.Repo @token_bytes 32 + # ── User-facing queries ────────────────────────────────────────── + @doc """ - Returns all monitors for the given user, newest first. + Returns all monitors assigned to the given user, newest first. + Preloads the beacon for display. """ @spec list_monitors_for_user(User.t()) :: [BeaconMonitor.t()] def list_monitors_for_user(%User{id: user_id}) do Repo.all( from m in BeaconMonitor, where: m.user_id == ^user_id, - order_by: [desc: m.inserted_at] + order_by: [desc: m.inserted_at], + preload: [:beacon] ) end @doc """ - Creates a new monitor for the given user with a freshly generated token. + Returns a single monitor. Preloads beacon and user relations. + """ + @spec get_monitor!(Ecto.UUID.t()) :: BeaconMonitor.t() + def get_monitor!(monitor_id) do + BeaconMonitor |> Repo.get!(monitor_id) |> Repo.preload([:beacon, :user]) + end + + @doc """ + Returns a single monitor if owned by the given user, nil otherwise. + """ + @spec get_monitor_for_user(Ecto.UUID.t(), User.t()) :: BeaconMonitor.t() | nil + def get_monitor_for_user(monitor_id, %User{id: user_id}) do + Repo.one( + from m in BeaconMonitor, + where: m.id == ^monitor_id and m.user_id == ^user_id, + preload: [:beacon] + ) + end + + # ── Admin queries ──────────────────────────────────────────────── + + @doc """ + Returns all monitors, newest first, with user and beacon preloaded. + Accepts preload overrides via options. + """ + @spec list_all_monitors(keyword()) :: [BeaconMonitor.t()] + def list_all_monitors(opts \\ []) do + preloads = Keyword.get(opts, :preload, [:user, :beacon, :assigned_by]) + + from(m in BeaconMonitor, order_by: [desc: m.inserted_at]) + |> Repo.all() + |> Repo.preload(preloads) + end + + # ── Test / convenience helpers ─────────────────────────────────── + + @doc """ + Creates a monitor and assigns it to the given user. The user acts as + both the assigned owner and the creating admin. Used by tests and + any legacy callers. """ @spec create_monitor(User.t(), map()) :: {:ok, BeaconMonitor.t()} | {:error, Ecto.Changeset.t()} - def create_monitor(%User{} = user, attrs) do - %BeaconMonitor{user_id: user.id, token: generate_token()} - |> BeaconMonitor.changeset(attrs) + def create_monitor(%User{} = user, attrs) when is_list(attrs) or is_map(attrs) do + attrs = + attrs + |> Map.new(fn {k, v} -> {to_string(k), v} end) + |> Map.put("user_id", user.id) + + create_hardware(user, attrs) + end + + # ── Admin provisioning ─────────────────────────────────────────── + + @doc """ + Creates a new hardware monitor with the given attrs. Generates a + unique auth token. Expects `user_id` and `assigned_by_id` to be set + in attrs. + """ + @spec create_hardware(User.t(), map()) :: {:ok, BeaconMonitor.t()} | {:error, Ecto.Changeset.t()} + def create_hardware(%User{} = admin, attrs) do + attrs = Map.put(attrs, "assigned_by_id", admin.id) + + %BeaconMonitor{token: generate_token()} + |> BeaconMonitor.provision_changeset(attrs) |> Repo.insert() end @doc """ - Deletes a monitor owned by the given user. + Updates the monitor's configuration (beacon, frequency, mode, etc). + Used by both admins and the assigned user. + """ + @spec update_config(BeaconMonitor.t(), map()) :: + {:ok, BeaconMonitor.t()} | {:error, Ecto.Changeset.t()} + def update_config(%BeaconMonitor{} = monitor, attrs) do + monitor + |> BeaconMonitor.config_changeset(attrs) + |> Repo.update() + end - Returns `{:error, :not_found}` if the monitor does not exist or - belongs to another user. + @doc """ + Updates the monitor's hardware provisioning fields. Admin-only. + """ + @spec update_hardware(BeaconMonitor.t(), map()) :: + {:ok, BeaconMonitor.t()} | {:error, Ecto.Changeset.t()} + def update_hardware(%BeaconMonitor{} = monitor, attrs) do + monitor + |> BeaconMonitor.provision_changeset(attrs) + |> Repo.update() + end + + @doc """ + Reassigns a monitor to a different user. Returns `{:error, :not_found}` + if the target user does not exist. + """ + @spec assign_to_user(BeaconMonitor.t(), User.t(), User.t()) :: + {:ok, BeaconMonitor.t()} | {:error, :not_found | Ecto.Changeset.t()} + def assign_to_user(%BeaconMonitor{} = monitor, %User{id: _} = admin, %User{id: new_user_id}) do + monitor + |> BeaconMonitor.provision_changeset(%{ + user_id: new_user_id, + assigned_by_id: admin.id + }) + |> Repo.update() + end + + @doc """ + Deletes a monitor. Works for both admin and the assigned user. """ @spec delete_monitor(User.t(), Ecto.UUID.t()) :: - {:ok, BeaconMonitor.t()} | {:error, :not_found} | {:error, Ecto.Changeset.t()} + {:ok, BeaconMonitor.t()} | {:error, :not_found} def delete_monitor(%User{id: user_id}, monitor_id) do query = from m in BeaconMonitor, @@ -53,9 +153,24 @@ defmodule Microwaveprop.BeaconMonitors do monitor -> Repo.delete(monitor) end rescue - Ecto.Query.CastError -> {:error, :not_found} + CastError -> {:error, :not_found} end + @doc """ + Deletes a monitor by its ID without user scoping. Admin-only. + """ + @spec delete_monitor!(Ecto.UUID.t()) :: {:ok, BeaconMonitor.t()} | {:error, :not_found} + def delete_monitor!(monitor_id) do + case Repo.get(BeaconMonitor, monitor_id) do + nil -> {:error, :not_found} + monitor -> Repo.delete(monitor) + end + rescue + CastError -> {:error, :not_found} + end + + # ── Auth / heartbeat ───────────────────────────────────────────── + @doc """ Looks up a monitor by its token. Returns nil if not found. """ @@ -81,11 +196,11 @@ defmodule Microwaveprop.BeaconMonitors do end @doc """ - Returns a blank changeset for rendering the new-monitor form. + Returns a blank changeset for rendering a new-monitor form. """ @spec change_monitor(map()) :: Ecto.Changeset.t() def change_monitor(attrs \\ %{}) do - BeaconMonitor.changeset(%BeaconMonitor{}, attrs) + BeaconMonitor.provision_changeset(%BeaconMonitor{}, attrs) end defp generate_token do diff --git a/lib/microwaveprop/beacon_monitors/beacon_monitor.ex b/lib/microwaveprop/beacon_monitors/beacon_monitor.ex index b6b3dc4b..9c51e37d 100644 --- a/lib/microwaveprop/beacon_monitors/beacon_monitor.ex +++ b/lib/microwaveprop/beacon_monitors/beacon_monitor.ex @@ -1,8 +1,11 @@ defmodule Microwaveprop.BeaconMonitors.BeaconMonitor do @moduledoc """ - A beacon monitor is a remote station running the monitor program - that reports beacon reception data. Each monitor is owned by a user - and identified by a random token the program uses to authenticate. + A beacon monitor is a physical SDR-based hardware unit assigned to a + user. The unit runs the `propmonitor` client which reports beacon + reception measurements back to Microwaveprop. + + Each monitor has a unique random token the client uses to authenticate + its measurement uploads. """ use Ecto.Schema @@ -10,15 +13,36 @@ defmodule Microwaveprop.BeaconMonitors.BeaconMonitor do import Ecto.Changeset alias Microwaveprop.Accounts.User + alias Microwaveprop.Beacons.Beacon @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id schema "beacon_monitors" do + # Identity / label field :name, :string field :token, :string field :last_seen_at, :utc_datetime + # Hardware identity + field :hardware_type, :string + field :hardware_id, :string + field :firmware_version, :string + + # Antenna / installation + field :antenna_type, :string + field :antenna_gain_dbi, :float + field :lat, :float + field :lon, :float + + # Active configuration (admin + assigned user can change) + field :config_frequency_hz, :integer + field :config_integration_s, :integer + field :config_mode, :string + + # Relationships belongs_to :user, User + belongs_to :beacon, Beacon + belongs_to :assigned_by, User timestamps(type: :utc_datetime) end @@ -26,8 +50,8 @@ defmodule Microwaveprop.BeaconMonitors.BeaconMonitor do @type t :: %__MODULE__{} @doc """ - Changeset for user-controlled fields (name only for now). - Token and user_id are assigned by the context, not cast from user input. + General changeset — allows basic fields. Context-level functions + enforce which caller-role can set which fields. """ @spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t() def changeset(monitor, attrs) do @@ -36,4 +60,52 @@ defmodule Microwaveprop.BeaconMonitors.BeaconMonitor do |> validate_required([:name]) |> validate_length(:name, min: 1, max: 100) end + + @doc """ + Provisioning changeset — used by admins to register new hardware. + Casts all hardware-identity, antenna, and assignment fields. + """ + @spec provision_changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t() + def provision_changeset(monitor, attrs) do + monitor + |> cast(attrs, [ + :name, + :hardware_type, + :hardware_id, + :firmware_version, + :antenna_type, + :antenna_gain_dbi, + :lat, + :lon, + :user_id, + :assigned_by_id + ]) + |> validate_required([:name]) + |> validate_length(:name, min: 1, max: 100) + |> validate_length(:hardware_type, max: 100) + |> validate_length(:hardware_id, max: 255) + |> validate_length(:firmware_version, max: 50) + |> validate_length(:antenna_type, max: 100) + |> validate_number(:antenna_gain_dbi, greater_than_or_equal_to: -20, less_than_or_equal_to: 60) + |> validate_number(:lat, greater_than_or_equal_to: -90, less_than_or_equal_to: 90) + |> validate_number(:lon, greater_than_or_equal_to: -180, less_than_or_equal_to: 180) + |> foreign_key_constraint(:user_id) + |> foreign_key_constraint(:assigned_by_id) + end + + @doc """ + Configuration changeset — used by admins and assigned users to update + what the monitor is listening for. + """ + @spec config_changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t() + def config_changeset(monitor, attrs) do + monitor + |> cast(attrs, [:name, :beacon_id, :config_frequency_hz, :config_integration_s, :config_mode]) + |> validate_required([:name]) + |> validate_length(:name, min: 1, max: 100) + |> validate_length(:config_mode, max: 50) + |> validate_number(:config_frequency_hz, greater_than: 0) + |> validate_number(:config_integration_s, greater_than_or_equal_to: 5, less_than_or_equal_to: 3600) + |> foreign_key_constraint(:beacon_id) + end end diff --git a/lib/microwaveprop_web/controllers/api/v1/me_controller.ex b/lib/microwaveprop_web/controllers/api/v1/me_controller.ex index 527453b7..a3061dd6 100644 --- a/lib/microwaveprop_web/controllers/api/v1/me_controller.ex +++ b/lib/microwaveprop_web/controllers/api/v1/me_controller.ex @@ -84,22 +84,6 @@ defmodule MicrowavepropWeb.Api.V1.MeController do json(conn, BeaconMonitorJSON.index(%{monitors: monitors})) end - @spec create_monitor(Plug.Conn.t(), map()) :: Plug.Conn.t() - def create_monitor(conn, params) do - user = conn.assigns.current_api_user - attrs = Map.take(params, ["name"]) - - case BeaconMonitors.create_monitor(user, attrs) do - {:ok, monitor} -> - conn - |> put_status(:created) - |> json(BeaconMonitorJSON.show(%{monitor: monitor})) - - {:error, changeset} -> - ErrorJSON.send_changeset(conn, changeset) - end - end - @spec delete_monitor(Plug.Conn.t(), map()) :: Plug.Conn.t() def delete_monitor(conn, %{"id" => id}) do user = conn.assigns.current_api_user diff --git a/lib/microwaveprop_web/controllers/beacon_monitor_controller.ex b/lib/microwaveprop_web/controllers/beacon_monitor_controller.ex index d9c1b926..d4c0a582 100644 --- a/lib/microwaveprop_web/controllers/beacon_monitor_controller.ex +++ b/lib/microwaveprop_web/controllers/beacon_monitor_controller.ex @@ -3,28 +3,6 @@ defmodule MicrowavepropWeb.BeaconMonitorController do alias Microwaveprop.BeaconMonitors - @spec create(Plug.Conn.t(), map()) :: Plug.Conn.t() - def create(conn, %{"beacon_monitor" => params}) do - user = conn.assigns.current_scope.user - - case BeaconMonitors.create_monitor(user, params) do - {:ok, monitor} -> - conn - |> put_flash(:info, "Monitor '#{monitor.name}' created. Copy its token below.") - |> redirect(to: ~p"/users/settings") - - {:error, changeset} -> - message = - changeset - |> Ecto.Changeset.traverse_errors(fn {msg, _} -> msg end) - |> Enum.map_join("; ", fn {k, v} -> "#{k}: #{Enum.join(v, ", ")}" end) - - conn - |> put_flash(:error, "Could not create monitor (#{message}).") - |> redirect(to: ~p"/users/settings") - end - end - @spec delete(Plug.Conn.t(), map()) :: Plug.Conn.t() def delete(conn, %{"id" => id}) do user = conn.assigns.current_scope.user diff --git a/lib/microwaveprop_web/controllers/user_settings_controller.ex b/lib/microwaveprop_web/controllers/user_settings_controller.ex index 2699e3dd..15a44d1f 100644 --- a/lib/microwaveprop_web/controllers/user_settings_controller.ex +++ b/lib/microwaveprop_web/controllers/user_settings_controller.ex @@ -105,10 +105,7 @@ defmodule MicrowavepropWeb.UserSettingsController do defp assign_beacon_monitors(conn, _opts) do user = conn.assigns.current_scope.user - - conn - |> assign(:beacon_monitors, BeaconMonitors.list_monitors_for_user(user)) - |> assign(:beacon_monitor_changeset, BeaconMonitors.change_monitor()) + assign(conn, :beacon_monitors, BeaconMonitors.list_monitors_for_user(user)) end defp assign_api_tokens(conn, _opts) do diff --git a/lib/microwaveprop_web/controllers/user_settings_html/edit.html.heex b/lib/microwaveprop_web/controllers/user_settings_html/edit.html.heex index 7e608547..5b331082 100644 --- a/lib/microwaveprop_web/controllers/user_settings_html/edit.html.heex +++ b/lib/microwaveprop_web/controllers/user_settings_html/edit.html.heex @@ -91,49 +91,23 @@
<.header> - Beacon monitors + Assigned beacon monitors <:subtitle> - Register remote monitor stations. Each monitor gets a unique token the - monitor program uses to authenticate its reports. + Physical monitor hardware assigned to you by an admin. Each unit + reports beacon reception data back to Microwaveprop. -
- <.icon name="hero-exclamation-triangle" class="size-4" /> - Not working yet — monitor client + ingestion pipeline still in development. -
- - <.form - :let={f} - for={@beacon_monitor_changeset} - as={:beacon_monitor} - action={~p"/users/beacon-monitors"} - id="create_beacon_monitor" - > - -
- - <.button variant="primary" phx-disable-with="Adding...">Add monitor -
- - <%= if @beacon_monitors == [] do %> -

No monitors registered yet.

+

No monitors assigned to you yet.

<% else %>
- + + @@ -142,8 +116,28 @@ <%= for monitor <- @beacon_monitors do %> - +
NameTokenHardwareMonitoring Last seen
{monitor.name} - {monitor.token} + + <%= if monitor.hardware_type do %> + {monitor.hardware_type}
+ {monitor.hardware_id} + <% else %> + + <% end %> +
+ <%= if monitor.beacon do %> + <.link + navigate={~p"/beacons/#{monitor.beacon.id}"} + class="link link-hover font-mono" + > + {monitor.beacon.callsign} + + + {monitor.beacon.frequency_mhz} MHz + + <% else %> + Not configured + <% end %> <%= if monitor.last_seen_at do %> diff --git a/lib/microwaveprop_web/live/user_profile_live.ex b/lib/microwaveprop_web/live/user_profile_live.ex index 84f8949f..ccf48361 100644 --- a/lib/microwaveprop_web/live/user_profile_live.ex +++ b/lib/microwaveprop_web/live/user_profile_live.ex @@ -169,48 +169,56 @@ defmodule MicrowavepropWeb.UserProfileLive do

Beacon monitors

- Remote monitor stations that report beacon reception data. + Physical monitor hardware assigned to you.

+
+ + + + + + + + + + + + + + + + + +
NameHardwareMonitoringLast seen
{monitor.name} + <%= if monitor.hardware_type do %> + {monitor.hardware_type}
+ {monitor.hardware_id} + <% else %> + + <% end %> +
+ <%= if monitor.beacon do %> + <.link + navigate={~p"/beacons/#{monitor.beacon.id}"} + class="link link-hover font-mono" + > + {monitor.beacon.callsign} + + ({monitor.beacon.frequency_mhz} MHz) + <% else %> + Not configured + <% end %> + + <%= if monitor.last_seen_at do %> + {Calendar.strftime(monitor.last_seen_at, "%Y-%m-%d %H:%M UTC")} + <% else %> + never + <% end %> +
+
<%= if @beacon_monitors == [] do %> -
-

No monitors registered yet.

- <.link navigate={~p"/users/settings#beacon-monitors"} class="btn btn-primary btn-sm"> - <.icon name="hero-plus" class="size-4" /> Register a monitor - -
- <% else %> -
- - - - - - - - - - - - - - - -
NameTokenLast seen
{monitor.name} - …{String.slice(monitor.token, -8, 8)} - - <%= if monitor.last_seen_at do %> - {Calendar.strftime(monitor.last_seen_at, "%Y-%m-%d %H:%M UTC")} - <% else %> - never - <% end %> -
-
-
- <.link navigate={~p"/users/settings#beacon-monitors"} class="btn btn-primary btn-sm"> - <.icon name="hero-plus" class="size-4" /> Register another monitor - -
+

No monitors assigned to you yet.

<% end %>
diff --git a/lib/microwaveprop_web/router.ex b/lib/microwaveprop_web/router.ex index b3b0cd96..39a4f0d6 100644 --- a/lib/microwaveprop_web/router.ex +++ b/lib/microwaveprop_web/router.ex @@ -318,7 +318,6 @@ defmodule MicrowavepropWeb.Router do delete "/me/api-tokens/:id", MeController, :revoke_token get "/me/beacon-monitors", MeController, :list_monitors - post "/me/beacon-monitors", MeController, :create_monitor delete "/me/beacon-monitors/:id", MeController, :delete_monitor post "/contacts", ContactController, :create @@ -375,7 +374,6 @@ defmodule MicrowavepropWeb.Router do put "/users/settings", UserSettingsController, :update get "/users/settings/confirm-email/:token", UserSettingsController, :confirm_email - post "/users/beacon-monitors", BeaconMonitorController, :create delete "/users/beacon-monitors/:id", BeaconMonitorController, :delete post "/users/api-tokens", ApiTokenController, :create diff --git a/priv/repo/migrations/20260721232034_add_hardware_fields_to_beacon_monitors.exs b/priv/repo/migrations/20260721232034_add_hardware_fields_to_beacon_monitors.exs new file mode 100644 index 00000000..74e46a19 --- /dev/null +++ b/priv/repo/migrations/20260721232034_add_hardware_fields_to_beacon_monitors.exs @@ -0,0 +1,30 @@ +defmodule Microwaveprop.Repo.Migrations.AddHardwareFieldsToBeaconMonitors do + use Ecto.Migration + + def change do + alter table(:beacon_monitors) do + # Hardware identity + add :hardware_type, :string + add :hardware_id, :string + add :firmware_version, :string + + # Antenna / installation + add :antenna_type, :string + add :antenna_gain_dbi, :float + add :lat, :float + add :lon, :float + + # Active configuration (admin+user-settable) + add :beacon_id, references(:beacons, type: :binary_id, on_delete: :nilify_all) + add :config_frequency_hz, :bigint + add :config_integration_s, :integer + add :config_mode, :string + + # Assignment audit trail + add :assigned_by_id, references(:users, type: :binary_id, on_delete: :nilify_all) + end + + create index(:beacon_monitors, [:beacon_id]) + create index(:beacon_monitors, [:assigned_by_id]) + end +end diff --git a/test/microwaveprop_web/controllers/api/v1/me_controller_test.exs b/test/microwaveprop_web/controllers/api/v1/me_controller_test.exs index ce02a940..60e12b9e 100644 --- a/test/microwaveprop_web/controllers/api/v1/me_controller_test.exs +++ b/test/microwaveprop_web/controllers/api/v1/me_controller_test.exs @@ -105,25 +105,10 @@ defmodule MicrowavepropWeb.Api.V1.MeControllerTest do end end - describe "beacon monitor CRUD" do - test "creates, lists, and deletes a beacon monitor", %{authed: conn} do - created = - conn - |> post(~p"/api/v1/me/beacon-monitors", %{"name" => "Tower"}) - |> json_response(201) - - id = created["data"]["id"] - + describe "beacon monitors" do + test "lists monitors", %{authed: conn} do list = conn |> get(~p"/api/v1/me/beacon-monitors") |> json_response(200) - assert Enum.any?(list["data"], &(&1["id"] == id)) - - conn = delete(conn, ~p"/api/v1/me/beacon-monitors/#{id}") - assert response(conn, 204) - end - - test "422 when creating with empty name", %{authed: conn} do - conn = post(conn, ~p"/api/v1/me/beacon-monitors", %{"name" => ""}) - assert json_response(conn, 422) + assert list["data"] == [] end test "404 when deleting unknown monitor", %{authed: conn} do diff --git a/test/microwaveprop_web/controllers/beacon_monitor_controller_test.exs b/test/microwaveprop_web/controllers/beacon_monitor_controller_test.exs index cd00b453..f297c886 100644 --- a/test/microwaveprop_web/controllers/beacon_monitor_controller_test.exs +++ b/test/microwaveprop_web/controllers/beacon_monitor_controller_test.exs @@ -7,39 +7,6 @@ defmodule MicrowavepropWeb.BeaconMonitorControllerTest do setup :register_and_log_in_user - describe "POST /users/beacon-monitors" do - test "creates a monitor for the current user", %{conn: conn, user: user} do - conn = - post(conn, ~p"/users/beacon-monitors", %{ - "beacon_monitor" => %{"name" => "Shack Pi"} - }) - - assert redirected_to(conn) == ~p"/users/settings" - assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Shack Pi" - - assert [monitor] = BeaconMonitors.list_monitors_for_user(user) - assert monitor.name == "Shack Pi" - assert byte_size(monitor.token) > 0 - end - - test "shows an error when name is blank", %{conn: conn, user: user} do - conn = - post(conn, ~p"/users/beacon-monitors", %{ - "beacon_monitor" => %{"name" => ""} - }) - - assert redirected_to(conn) == ~p"/users/settings" - assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "name" - assert BeaconMonitors.list_monitors_for_user(user) == [] - end - - test "redirects unauthenticated users to login" do - conn = post(build_conn(), ~p"/users/beacon-monitors", %{"beacon_monitor" => %{"name" => "Nope"}}) - - assert redirected_to(conn) == ~p"/users/log-in" - end - end - describe "DELETE /users/beacon-monitors/:id" do test "deletes a monitor owned by the current user", %{conn: conn, user: user} do {:ok, monitor} = BeaconMonitors.create_monitor(user, %{"name" => "Bye"}) diff --git a/test/microwaveprop_web/live/user_profile_live_test.exs b/test/microwaveprop_web/live/user_profile_live_test.exs index b018ac75..d507b1d0 100644 --- a/test/microwaveprop_web/live/user_profile_live_test.exs +++ b/test/microwaveprop_web/live/user_profile_live_test.exs @@ -246,24 +246,28 @@ defmodule MicrowavepropWeb.UserProfileLiveTest do {:ok, _lv, html} = live(conn, ~p"/u/#{user.callsign}") assert html =~ "Beacon monitors" - assert html =~ "No monitors registered yet" - assert html =~ "Register a monitor" + assert html =~ "No monitors assigned to you yet" end - test "owners see their registered beacon monitors", %{conn: conn} do + test "owners see their assigned beacon monitors", %{conn: conn} do user = user_fixture(%{callsign: "W5ISP"}) - {:ok, monitor} = BeaconMonitors.create_monitor(user, %{name: "Shack Pi"}) + {:ok, monitor} = + BeaconMonitors.create_hardware(user, %{ + "name" => "Shack Pi", + "hardware_type" => "RTL-SDR", + "hardware_id" => "SN-001", + "user_id" => user.id + }) conn = log_in_user(conn, user) {:ok, _lv, html} = live(conn, ~p"/u/#{user.callsign}") assert html =~ "Beacon monitors" assert html =~ "Shack Pi" + assert html =~ "RTL-SDR" + assert html =~ "SN-001" assert html =~ "never" - assert html =~ "Register another monitor" - # Token is shown truncated (last 8 chars) - assert html =~ String.slice(monitor.token, -8, 8) end end end