towerops/lib/towerops_web/live/onboarding_live.ex
Graham McIntire 3ca0834ef0 tests: raise coverage to 70% via helper promotion + new unit/property tests
Promoted pure presentation and utility helpers from `defp` to `def @doc false`
across ~20 LiveViews, Oban workers, and sync modules so they're reachable from
unit tests. Refactored several `cond` blocks into idiomatic function heads with
guards. Added ~250 new test cases in new files under test/towerops and
test/towerops_web, including DB-backed tests for CnMaestro.Sync and
AlertNotificationWorker, and removed dead LiveView tab components and
CapacityLive (no callers anywhere in lib/test).

Configured mix.exs test_coverage.ignore_modules to exclude vendored third-party
code (SnmpKit, protobuf-generated Towerops.Agent.*, Absinthe GraphQL types,
Phoenix HTML modules, Inspect protocol impls) from coverage calculations —
these are not our project code.

Coverage: 66.93% → 70.09%. Full suite: 10,127 tests, 0 failures.
2026-04-24 09:49:06 -05:00

259 lines
7.1 KiB
Elixir

defmodule ToweropsWeb.OnboardingLive do
@moduledoc false
use ToweropsWeb, :live_view
alias Towerops.Agents
alias Towerops.Integrations
alias Towerops.Integrations.Integration
alias Towerops.Organizations
alias Towerops.Sites
alias Towerops.Sites.Site
@steps [:snmp, :site, :billing, :agent]
@billing_providers [
%{id: "gaiia", name: "Gaiia", icon: "hero-user-group"},
%{id: "sonar", name: "Sonar", icon: "hero-currency-dollar"},
%{id: "splynx", name: "Splynx", icon: "hero-banknotes"},
%{id: "visp", name: "VISP", icon: "hero-cloud"}
]
@impl true
def mount(_params, _session, socket) do
organization = socket.assigns.current_scope.organization
changeset = Organizations.change_organization(organization)
site_changeset = Sites.change_site(%Site{}, %{})
{:ok,
socket
|> assign(:page_title, t("Setup"))
|> assign(:organization, organization)
|> assign(:step, :snmp)
|> assign(:steps, @steps)
|> assign(:form, to_form(changeset))
|> assign(:site_form, to_form(site_changeset, as: "site"))
|> assign(:billing_providers, @billing_providers)
|> assign(:selected_provider, nil)
|> assign(:integration_form, nil)
|> assign(:agent_token, nil)
|> assign(:agent_token_value, nil)}
end
@impl true
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
# SNMP step events
@impl true
def handle_event("validate_snmp", %{"organization" => params}, socket) do
changeset =
socket.assigns.organization
|> Organizations.change_organization(params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :form, to_form(changeset))}
end
@impl true
def handle_event("save_snmp", %{"organization" => params}, socket) do
case Organizations.update_organization(socket.assigns.organization, params) do
{:ok, organization} ->
{:noreply,
socket
|> assign(:organization, organization)
|> put_flash(:info, t("SNMP configuration saved"))
|> assign(:step, :site)}
{:error, changeset} ->
{:noreply, assign(socket, :form, to_form(changeset))}
end
end
# Site step events
@impl true
def handle_event("validate_site", %{"site" => params}, socket) do
changeset =
%Site{}
|> Sites.change_site(params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :site_form, to_form(changeset, as: "site"))}
end
@impl true
def handle_event("save_site", %{"site" => params}, socket) do
org = socket.assigns.organization
attrs = Map.put(params, "organization_id", org.id)
case Sites.create_site(attrs) do
{:ok, _site} ->
# Enable sites on the org if not already
if !org.use_sites do
Organizations.update_organization(org, %{use_sites: true})
end
{:noreply,
socket
|> put_flash(:info, t("Site created"))
|> assign(:step, :billing)}
{:error, changeset} ->
{:noreply, assign(socket, :site_form, to_form(changeset, as: "site"))}
end
end
# Billing step events
@impl true
def handle_event("select_provider", %{"provider" => provider}, socket) do
form =
%Integration{}
|> Integrations.change_integration(%{provider: provider})
|> to_form()
{:noreply,
socket
|> assign(:selected_provider, provider)
|> assign(:integration_form, form)}
end
@impl true
def handle_event("validate_integration", %{"integration" => params}, socket) do
changeset =
%Integration{}
|> Integrations.change_integration(normalize_integration_params(params, socket.assigns.selected_provider))
|> Map.put(:action, :validate)
{:noreply, assign(socket, :integration_form, to_form(changeset))}
end
@impl true
def handle_event("save_integration", %{"integration" => params}, socket) do
org = socket.assigns.organization
provider = socket.assigns.selected_provider
attrs = params |> normalize_integration_params(provider) |> Map.put(:provider, provider) |> Map.put(:enabled, true)
case Integrations.create_integration(org.id, attrs) do
{:ok, _integration} ->
{:noreply,
socket
|> put_flash(:info, t("Integration saved"))
|> assign(:step, :agent)
|> load_agent_token()}
{:error, changeset} ->
{:noreply, assign(socket, :integration_form, to_form(changeset))}
end
end
# Navigation events
@impl true
def handle_event("skip", _params, socket) do
next = next_step(socket.assigns.step)
socket =
if next == :done do
complete_onboarding(socket)
else
socket = assign(socket, :step, next)
if next == :agent, do: load_agent_token(socket), else: socket
end
{:noreply, socket}
end
@impl true
def handle_event("finish", _params, socket) do
{:noreply, complete_onboarding(socket)}
end
@impl true
def handle_event("go_to_step", %{"step" => step}, socket) do
step = String.to_existing_atom(step)
socket =
if step == :agent do
socket |> assign(:step, step) |> load_agent_token()
else
assign(socket, :step, step)
end
{:noreply, socket}
end
# Private helpers
@doc false
def next_step(:snmp), do: :site
def next_step(:site), do: :billing
def next_step(:billing), do: :agent
def next_step(:agent), do: :done
@doc false
def step_index(:snmp), do: 0
def step_index(:site), do: 1
def step_index(:billing), do: 2
def step_index(:agent), do: 3
defp step_label(:snmp), do: t("SNMP")
defp step_label(:site), do: t("Site")
defp step_label(:billing), do: t("Billing")
defp step_label(:agent), do: t("Agent")
defp complete_onboarding(socket) do
org = socket.assigns.organization
Organizations.update_organization(org, %{onboarding_completed: true})
socket
|> put_flash(:info, t("Setup complete"))
|> push_navigate(to: ~p"/dashboard")
end
defp load_agent_token(socket) do
org = socket.assigns.organization
tokens = Agents.list_organization_agent_tokens(org.id)
case tokens do
[token | _] ->
assign(socket, agent_token: token, agent_token_value: nil)
[] ->
case Agents.create_agent_token(org.id, "Default Agent") do
{:ok, token, token_value} ->
assign(socket, agent_token: token, agent_token_value: token_value)
{:error, _} ->
assign(socket, agent_token: nil, agent_token_value: nil)
end
end
end
@doc false
def normalize_integration_params(params, "sonar") do
%{
credentials: %{
"instance_url" => String.trim(Map.get(params, "instance_url", "")),
"api_token" => Map.get(params, "api_token", "")
}
}
end
def normalize_integration_params(params, "splynx") do
%{
credentials: %{
"instance_url" => String.trim(Map.get(params, "instance_url", "")),
"api_key" => Map.get(params, "api_key", ""),
"api_secret" => Map.get(params, "api_secret", "")
}
}
end
def normalize_integration_params(params, _provider) do
%{credentials: %{"api_key" => Map.get(params, "api_key", "")}}
end
end