test: refactor LiveView tests to use HTML-aware element/form helpers
Apply patterns from testingliveview.com — replace render_click/submit/change event-name calls with element/form-targeted equivalents that validate the actual DOM wiring, not just handler logic. Adds stable test selectors (id="...") to interactive elements across 10 LiveView templates. Surfaces (and fixes) several real issues that the bypass-style tests masked: - Removes dead regenerate_token handler in agent_live/index.ex (no UI ever triggered it) and its corresponding test. - Removes dead refresh_topology test in network_map_live_test (event was referenced only in the test — no handler, no button, no JS anywhere). - Corrects "shows notification tab with add device modal" — modal lives on the security tab; original test mounted ?tab=notifications and only worked because direct event calls bypassed tab gating. - Corrects form input shape mismatches in agent_live/index_test that the handler's defensive case clauses had been masking. - Expands setup data for org/settings_live_test (snmp_community, default_agent_token_id, multi-org membership) so the gated buttons actually render. 11 direct render_click/submit/change calls remain — all intentional, documented server-side guard tests (tampered POSTs, race conditions, defensive idempotent handlers). Full LiveView test suite: 1300 tests, 0 failures.
This commit is contained in:
parent
17bb5a9117
commit
aa7fc830e5
21 changed files with 257 additions and 183 deletions
|
|
@ -271,7 +271,7 @@ defmodule Towerops.Agents do
|
|||
Updates an agent token with the given attributes.
|
||||
|
||||
Currently only supports updating the name and allow_remote_debug fields.
|
||||
Other fields like the token itself cannot be changed (use regenerate_token instead).
|
||||
Other fields like the token itself cannot be changed.
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ defmodule ToweropsWeb.Admin.AuditLive.Index do
|
|||
<.icon name="hero-magnifying-glass" class="h-4 w-4 mr-2" /> Search
|
||||
</button>
|
||||
<button
|
||||
id="audit-clear-filters"
|
||||
type="button"
|
||||
phx-click="clear_filters"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
|
|
@ -284,6 +285,7 @@ defmodule ToweropsWeb.Admin.AuditLive.Index do
|
|||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
id="audit-prev-page"
|
||||
type="button"
|
||||
phx-click="prev_page"
|
||||
disabled={@page == 1}
|
||||
|
|
@ -292,6 +294,7 @@ defmodule ToweropsWeb.Admin.AuditLive.Index do
|
|||
Previous
|
||||
</button>
|
||||
<button
|
||||
id="audit-next-page"
|
||||
type="button"
|
||||
phx-click="next_page"
|
||||
disabled={length(@audit_logs) < @per_page}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
<div class="border-b border-gray-200 dark:border-white/10">
|
||||
<nav class="-mb-px flex space-x-8">
|
||||
<button
|
||||
id="security-tab-whitelist"
|
||||
phx-click="change_tab"
|
||||
phx-value-tab="whitelist"
|
||||
class={[
|
||||
|
|
@ -26,6 +27,7 @@
|
|||
</button>
|
||||
|
||||
<button
|
||||
id="security-tab-blocked"
|
||||
phx-click="change_tab"
|
||||
phx-value-tab="blocked"
|
||||
class={[
|
||||
|
|
@ -49,7 +51,7 @@
|
|||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{t("Allowed IPs and CIDR Ranges")}
|
||||
</h2>
|
||||
<.button phx-click="show_whitelist_form">
|
||||
<.button id="show-whitelist-form" phx-click="show_whitelist_form">
|
||||
<.icon name="hero-plus" class="w-4 h-4 mr-1" /> Add to Allowlist
|
||||
</.button>
|
||||
</div>
|
||||
|
|
@ -80,7 +82,7 @@
|
|||
|
||||
<div class="flex gap-2">
|
||||
<.button type="submit">{t("Add to Allowlist")}</.button>
|
||||
<.button type="button" phx-click="hide_whitelist_form">
|
||||
<.button id="hide-whitelist-form" type="button" phx-click="hide_whitelist_form">
|
||||
{t("Cancel")}
|
||||
</.button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -130,35 +130,6 @@ defmodule ToweropsWeb.AgentLive.Index do
|
|||
|> push_patch(to: ~p"/agents?modal=setup")}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("regenerate_token", %{"id" => id}, socket) do
|
||||
agent_token = Agents.get_agent_token!(id)
|
||||
organization = socket.assigns.current_scope.organization
|
||||
|
||||
# Revoke the old token and create a new one
|
||||
case Agents.revoke_agent_token(id) do
|
||||
{:ok, _} ->
|
||||
case Agents.create_agent_token(organization.id, agent_token.name) do
|
||||
{:ok, new_agent_token, token} ->
|
||||
agent_tokens = Agents.list_organization_agent_tokens(organization.id)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> stream(:agent_tokens, agent_tokens, reset: true)
|
||||
|> assign(:has_agents, agent_tokens != [])
|
||||
|> assign(:new_token, %{agent_token: new_agent_token, token: token})
|
||||
|> assign(:show_token_modal, true)
|
||||
|> put_flash(:info, t_equipment("New token generated successfully"))}
|
||||
|
||||
{:error, _changeset} ->
|
||||
{:noreply, put_flash(socket, :error, t_equipment("Failed to generate new token"))}
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
{:noreply, put_flash(socket, :error, t_equipment("Failed to revoke old token"))}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete_agent", %{"id" => id}, socket) do
|
||||
agent_token = Agents.get_agent_token!(id)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
{t("Create New Agent")}
|
||||
</h3>
|
||||
<.form for={@agent_form} phx-submit="create_agent">
|
||||
<.form id="create-agent-form" for={@agent_form} phx-submit="create_agent">
|
||||
<div class="space-y-4">
|
||||
<.input
|
||||
field={@agent_form[:name]}
|
||||
|
|
@ -143,6 +143,7 @@
|
|||
<%= if agent.enabled do %>
|
||||
<div class="flex items-center gap-3">
|
||||
<.button
|
||||
id={"show-setup-#{agent.id}"}
|
||||
type="button"
|
||||
phx-click="show_setup"
|
||||
phx-value-id={agent.id}
|
||||
|
|
@ -151,6 +152,7 @@
|
|||
{t("View Setup")}
|
||||
</.button>
|
||||
<.button
|
||||
id={"delete-agent-#{agent.id}"}
|
||||
type="button"
|
||||
phx-click="delete_agent"
|
||||
phx-value-id={agent.id}
|
||||
|
|
@ -260,6 +262,7 @@
|
|||
<%= if agent.enabled do %>
|
||||
<div class="flex items-center gap-3">
|
||||
<.button
|
||||
id={"show-setup-#{agent.id}"}
|
||||
type="button"
|
||||
phx-click="show_setup"
|
||||
phx-value-id={agent.id}
|
||||
|
|
@ -268,6 +271,7 @@
|
|||
{t("View Setup")}
|
||||
</.button>
|
||||
<.button
|
||||
id={"delete-agent-#{agent.id}"}
|
||||
type="button"
|
||||
phx-click="delete_agent"
|
||||
phx-value-id={agent.id}
|
||||
|
|
@ -303,7 +307,12 @@
|
|||
)}
|
||||
</p>
|
||||
|
||||
<.form for={%{}} phx-change="update_selected_global_default" class="max-w-md">
|
||||
<.form
|
||||
id="global-default-form"
|
||||
for={%{}}
|
||||
phx-change="update_selected_global_default"
|
||||
class="max-w-md"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
|
||||
|
|
@ -351,6 +360,7 @@
|
|||
|
||||
<div class="flex gap-3">
|
||||
<.button
|
||||
id="save-global-default"
|
||||
type="button"
|
||||
phx-click="save_global_default"
|
||||
variant="primary"
|
||||
|
|
@ -529,7 +539,7 @@
|
|||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<.button phx-click="close_token_modal" variant="primary">
|
||||
<.button id="close-token-modal" phx-click="close_token_modal" variant="primary">
|
||||
{t("Close")}
|
||||
</.button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -339,6 +339,7 @@
|
|||
<div class="border-b border-gray-200 dark:border-white/10">
|
||||
<nav class="-mb-px flex space-x-8" aria-label={t("Monitoring mode")}>
|
||||
<button
|
||||
id="monitoring-mode-snmp-and-icmp"
|
||||
type="button"
|
||||
phx-click="switch_monitoring_mode"
|
||||
phx-value-mode="snmp_and_icmp"
|
||||
|
|
@ -353,6 +354,7 @@
|
|||
{t("SNMP & ICMP")}
|
||||
</button>
|
||||
<button
|
||||
id="monitoring-mode-icmp-only"
|
||||
type="button"
|
||||
phx-click="switch_monitoring_mode"
|
||||
phx-value-mode="icmp_only"
|
||||
|
|
@ -573,6 +575,7 @@
|
|||
|
||||
<%= if @live_action == :edit do %>
|
||||
<.button
|
||||
id="trigger-discovery"
|
||||
type="button"
|
||||
phx-click="trigger_discovery"
|
||||
phx-disable-with={t("Discovering...")}
|
||||
|
|
@ -778,6 +781,7 @@
|
|||
)}
|
||||
</p>
|
||||
<.button
|
||||
id="delete-device"
|
||||
phx-click="delete"
|
||||
data-confirm={
|
||||
t(
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@
|
|||
<%= if @has_devices do %>
|
||||
<%= if @reorder_mode do %>
|
||||
<.button
|
||||
id="reset-order"
|
||||
type="button"
|
||||
phx-click="reset_order"
|
||||
data-confirm={t("Reset all sites and devices to alphabetical order?")}
|
||||
|
|
@ -60,6 +61,7 @@
|
|||
</.button>
|
||||
<% else %>
|
||||
<.button
|
||||
id="toggle-reorder-mode"
|
||||
type="button"
|
||||
phx-click="toggle_reorder_mode"
|
||||
title="Reorder devices and sites"
|
||||
|
|
@ -75,6 +77,7 @@
|
|||
</.button>
|
||||
<%= if @has_devices and not @reorder_mode do %>
|
||||
<.button
|
||||
id="force-rediscover-all"
|
||||
type="button"
|
||||
phx-click="force_rediscover_all"
|
||||
title="Rediscover all devices"
|
||||
|
|
|
|||
|
|
@ -1507,6 +1507,7 @@
|
|||
{capacity_source_label(interface.capacity_source)}
|
||||
</span>
|
||||
<button
|
||||
id={"clear-capacity-#{interface.id}"}
|
||||
phx-click="clear_capacity"
|
||||
phx-value-interface_id={interface.id}
|
||||
class="text-gray-400 hover:text-red-500 transition-colors ml-0.5"
|
||||
|
|
@ -1517,6 +1518,7 @@
|
|||
</div>
|
||||
<% else %>
|
||||
<button
|
||||
id={"set-capacity-#{interface.id}"}
|
||||
phx-click={
|
||||
JS.push("set_capacity",
|
||||
value: %{
|
||||
|
|
|
|||
|
|
@ -329,6 +329,7 @@
|
|||
{t("Node Details")}
|
||||
</h3>
|
||||
<button
|
||||
id="close-detail-panel"
|
||||
phx-click="close_detail_panel"
|
||||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@
|
|||
</div>
|
||||
<% else %>
|
||||
<button
|
||||
id="toggle-default-org"
|
||||
type="button"
|
||||
phx-click="toggle_default_org"
|
||||
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:shadow-none dark:hover:bg-indigo-400"
|
||||
|
|
@ -487,6 +488,7 @@
|
|||
)}
|
||||
</p>
|
||||
<button
|
||||
id="apply-snmp-to-all"
|
||||
type="button"
|
||||
phx-click="apply_snmp_to_all"
|
||||
data-confirm={
|
||||
|
|
@ -674,6 +676,7 @@
|
|||
)}
|
||||
</p>
|
||||
<button
|
||||
id="apply-agent-to-all"
|
||||
type="button"
|
||||
phx-click="apply_agent_to_all"
|
||||
data-confirm={
|
||||
|
|
|
|||
|
|
@ -675,6 +675,7 @@
|
|||
</p>
|
||||
<div class="mt-6">
|
||||
<button
|
||||
id="show-add-token-modal-empty"
|
||||
type="button"
|
||||
phx-click="show_add_token_modal"
|
||||
class="inline-flex items-center gap-x-1.5 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400"
|
||||
|
|
@ -1204,6 +1205,7 @@
|
|||
</p>
|
||||
<div class="mt-6">
|
||||
<button
|
||||
id="show-add-device-modal"
|
||||
type="button"
|
||||
phx-click="show_add_device_modal"
|
||||
class="inline-flex items-center gap-x-1.5 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400"
|
||||
|
|
@ -1265,6 +1267,7 @@
|
|||
|
||||
<div class="mt-6 flex">
|
||||
<button
|
||||
id="show-add-device-modal-with-devices"
|
||||
type="button"
|
||||
phx-click="show_add_device_modal"
|
||||
class="inline-flex items-center gap-x-1.5 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400"
|
||||
|
|
@ -1440,7 +1443,7 @@
|
|||
</span>
|
||||
|
||||
<div class="relative z-10 inline-block transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left align-bottom shadow-xl transition-all dark:bg-gray-800/95 sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
|
||||
<.form for={%{}} as={:token} phx-submit="create_api_token">
|
||||
<.form id="create-api-token-form" for={%{}} as={:token} phx-submit="create_api_token">
|
||||
<div>
|
||||
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900">
|
||||
<.icon name="hero-code-bracket" class="h-6 w-6 text-blue-600 dark:text-blue-400" />
|
||||
|
|
@ -1502,6 +1505,7 @@
|
|||
{t("Create Token")}
|
||||
</button>
|
||||
<button
|
||||
id="cancel-add-token"
|
||||
type="button"
|
||||
phx-click="cancel_add_token"
|
||||
class="mt-3 inline-flex w-full justify-center rounded-lg bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-white dark:ring-white/10 dark:hover:bg-gray-800 sm:col-start-1 sm:mt-0"
|
||||
|
|
@ -1660,6 +1664,7 @@
|
|||
{t("Continue")}
|
||||
</button>
|
||||
<button
|
||||
id="cancel-add-device"
|
||||
type="button"
|
||||
phx-click="cancel_add_device"
|
||||
class="mt-3 inline-flex w-full justify-center rounded-lg bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-white dark:ring-white/10 dark:hover:bg-gray-800 sm:col-start-1 sm:mt-0"
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ defmodule ToweropsWeb.Admin.AuditLive.IndexTest do
|
|||
|> render_submit()
|
||||
|
||||
# Then clear filters
|
||||
html = render_click(view, "clear_filters")
|
||||
html = view |> element("#audit-clear-filters") |> render_click()
|
||||
|
||||
# Both logs should be visible again
|
||||
assert html =~ "Impersonate start"
|
||||
|
|
@ -151,7 +151,7 @@ defmodule ToweropsWeb.Admin.AuditLive.IndexTest do
|
|||
|
||||
assert html =~ "Page 1"
|
||||
|
||||
html = render_click(view, "next_page")
|
||||
html = view |> element("#audit-next-page") |> render_click()
|
||||
|
||||
assert html =~ "Page 2"
|
||||
end
|
||||
|
|
@ -167,8 +167,8 @@ defmodule ToweropsWeb.Admin.AuditLive.IndexTest do
|
|||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/audit")
|
||||
|
||||
render_click(view, "next_page")
|
||||
html = render_click(view, "prev_page")
|
||||
view |> element("#audit-next-page") |> render_click()
|
||||
html = view |> element("#audit-prev-page") |> render_click()
|
||||
|
||||
assert html =~ "Page 1"
|
||||
end
|
||||
|
|
@ -183,6 +183,8 @@ defmodule ToweropsWeb.Admin.AuditLive.IndexTest do
|
|||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/audit")
|
||||
|
||||
# The "Previous" button is disabled when @page == 1, so the UI prevents
|
||||
# navigation. Test the server-side guard against a tampered event.
|
||||
html = render_click(view, "prev_page")
|
||||
|
||||
assert html =~ "Page 1"
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ defmodule ToweropsWeb.Admin.SecurityLive.IndexTest do
|
|||
test "change_tab event patches URL", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/security")
|
||||
|
||||
render_click(view, "change_tab", %{"tab" => "blocked"})
|
||||
view |> element("#security-tab-blocked") |> render_click()
|
||||
|
||||
assert_patch(view, ~p"/admin/security?tab=blocked")
|
||||
end
|
||||
|
|
@ -87,7 +87,7 @@ defmodule ToweropsWeb.Admin.SecurityLive.IndexTest do
|
|||
test "shows whitelist form when button clicked", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/security")
|
||||
|
||||
html = render_click(view, "show_whitelist_form")
|
||||
html = view |> element("#show-whitelist-form") |> render_click()
|
||||
|
||||
assert html =~ "IP Address or CIDR"
|
||||
assert html =~ "Description"
|
||||
|
|
@ -96,12 +96,12 @@ defmodule ToweropsWeb.Admin.SecurityLive.IndexTest do
|
|||
test "hides whitelist form when cancel clicked", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/security")
|
||||
|
||||
render_click(view, "show_whitelist_form")
|
||||
view |> element("#show-whitelist-form") |> render_click()
|
||||
|
||||
# Verify form is visible
|
||||
assert render(view) =~ "IP Address or CIDR"
|
||||
|
||||
render_click(view, "hide_whitelist_form")
|
||||
view |> element("#hide-whitelist-form") |> render_click()
|
||||
html = render(view)
|
||||
|
||||
# The form fields should no longer be present
|
||||
|
|
@ -111,7 +111,7 @@ defmodule ToweropsWeb.Admin.SecurityLive.IndexTest do
|
|||
test "add_whitelist form renders with correct fields", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/security")
|
||||
|
||||
render_click(view, "show_whitelist_form")
|
||||
view |> element("#show-whitelist-form") |> render_click()
|
||||
|
||||
# Known issue: the form uses to_form(%{}) without an :as option, producing
|
||||
# flat field names, but the handler expects %{"whitelist" => params}.
|
||||
|
|
|
|||
|
|
@ -57,11 +57,13 @@ defmodule ToweropsWeb.AgentLive.IndexTest do
|
|||
|
||||
{:ok, view, _html} = live(conn)
|
||||
|
||||
# Trigger create_agent event with nested params
|
||||
html = render_submit(view, "create_agent", %{"agent_form" => %{"name" => "New Agent"}})
|
||||
html =
|
||||
view
|
||||
|> form("#create-agent-form", %{"name" => "New Agent"})
|
||||
|> render_submit()
|
||||
|
||||
assert has_element?(view, "#token-modal", "New Agent")
|
||||
assert html =~ "Agent created successfully"
|
||||
assert html =~ "New Agent"
|
||||
|
||||
# Verify agent was actually created
|
||||
agent_tokens = Agents.list_organization_agent_tokens(organization.id)
|
||||
|
|
@ -69,21 +71,6 @@ defmodule ToweropsWeb.AgentLive.IndexTest do
|
|||
assert hd(agent_tokens).name == "New Agent"
|
||||
end
|
||||
|
||||
test "creates agent with flat params format", %{conn: conn, user: user, organization: _organization} do
|
||||
conn =
|
||||
conn
|
||||
|> log_in_user(user)
|
||||
|> get("/agents")
|
||||
|
||||
{:ok, view, _html} = live(conn)
|
||||
|
||||
# Trigger create_agent event with flat params
|
||||
html = render_submit(view, "create_agent", %{"name" => "Flat Agent"})
|
||||
|
||||
assert html =~ "Agent created successfully"
|
||||
assert html =~ "Flat Agent"
|
||||
end
|
||||
|
||||
test "shows error on creation failure", %{conn: conn, user: user, organization: _organization} do
|
||||
conn =
|
||||
conn
|
||||
|
|
@ -92,8 +79,10 @@ defmodule ToweropsWeb.AgentLive.IndexTest do
|
|||
|
||||
{:ok, view, _html} = live(conn)
|
||||
|
||||
# Trigger create_agent with invalid params (empty name)
|
||||
html = render_submit(view, "create_agent", %{"agent_form" => %{"name" => ""}})
|
||||
html =
|
||||
view
|
||||
|> form("#create-agent-form", %{"name" => ""})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "Failed to create agent"
|
||||
end
|
||||
|
|
@ -108,14 +97,16 @@ defmodule ToweropsWeb.AgentLive.IndexTest do
|
|||
|
||||
{:ok, view, _html} = live(conn)
|
||||
|
||||
# First create an agent to show the modal
|
||||
render_submit(view, "create_agent", %{"name" => "Test Agent"})
|
||||
view
|
||||
|> form("#create-agent-form", %{"name" => "Test Agent"})
|
||||
|> render_submit()
|
||||
|
||||
# Now close the modal
|
||||
_html = render_click(view, "close_token_modal", %{})
|
||||
assert has_element?(view, "#token-modal")
|
||||
|
||||
# Modal should be closed - verify by checking that we can still interact with the view
|
||||
assert render(view) =~ "Test Agent"
|
||||
view |> element("#close-token-modal") |> render_click()
|
||||
|
||||
refute has_element?(view, "#token-modal")
|
||||
assert has_element?(view, "#agents-table", "Test Agent")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -130,7 +121,10 @@ defmodule ToweropsWeb.AgentLive.IndexTest do
|
|||
|
||||
{:ok, view, _html} = live(conn)
|
||||
|
||||
html = render_click(view, "delete_agent", %{"id" => agent_token.id})
|
||||
html =
|
||||
view
|
||||
|> element("#delete-agent-#{agent_token.id}")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "Agent deleted successfully"
|
||||
|
||||
|
|
@ -239,9 +233,9 @@ defmodule ToweropsWeb.AgentLive.IndexTest do
|
|||
|
||||
# Create a cloud poller
|
||||
html =
|
||||
render_submit(view, "create_agent", %{
|
||||
"agent_form" => %{"name" => "Test Cloud Poller", "is_cloud_poller" => "true"}
|
||||
})
|
||||
view
|
||||
|> form("#create-agent-form", %{"name" => "Test Cloud Poller", "is_cloud_poller" => "true"})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "Cloud poller created successfully"
|
||||
assert html =~ "Test Cloud Poller"
|
||||
|
|
@ -269,7 +263,9 @@ defmodule ToweropsWeb.AgentLive.IndexTest do
|
|||
# Regular user should NOT see the cloud poller checkbox
|
||||
refute html =~ "Cloud Poller (Application-wide agent"
|
||||
|
||||
# Attempt to create a cloud poller (bypassing UI)
|
||||
# The cloud_poller checkbox is intentionally not rendered for non-superadmins, so
|
||||
# form/3 cannot set it (which validates the UI-level guard works). To test the
|
||||
# server-side guard against a tampered POST, bypass the form helper here.
|
||||
html =
|
||||
render_submit(view, "create_agent", %{
|
||||
"agent_form" => %{"name" => "Unauthorized Cloud Poller", "is_cloud_poller" => "true"}
|
||||
|
|
@ -298,11 +294,11 @@ defmodule ToweropsWeb.AgentLive.IndexTest do
|
|||
|
||||
{:ok, view, _html} = live(conn)
|
||||
|
||||
# Create a regular agent (is_cloud_poller = false)
|
||||
# Create a regular agent (is_cloud_poller defaults to false from the hidden input)
|
||||
html =
|
||||
render_submit(view, "create_agent", %{
|
||||
"agent_form" => %{"name" => "Regular Agent", "is_cloud_poller" => "false"}
|
||||
})
|
||||
view
|
||||
|> form("#create-agent-form", %{"name" => "Regular Agent"})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "Agent created successfully"
|
||||
assert html =~ "Regular Agent"
|
||||
|
|
@ -440,7 +436,10 @@ defmodule ToweropsWeb.AgentLive.IndexTest do
|
|||
|
||||
{:ok, view, _html} = live(conn)
|
||||
|
||||
html = render_click(view, "delete_agent", %{"id" => cloud_poller.id})
|
||||
html =
|
||||
view
|
||||
|> element("#delete-agent-#{cloud_poller.id}")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "Agent deleted successfully"
|
||||
|
||||
|
|
@ -449,25 +448,6 @@ defmodule ToweropsWeb.AgentLive.IndexTest do
|
|||
assert Enum.empty?(cloud_pollers)
|
||||
end
|
||||
|
||||
test "superadmin can regenerate cloud poller token", %{
|
||||
conn: conn,
|
||||
superadmin: superadmin,
|
||||
organization: _organization,
|
||||
cloud_poller: cloud_poller
|
||||
} do
|
||||
conn =
|
||||
conn
|
||||
|> log_in_user(superadmin)
|
||||
|> get("/agents")
|
||||
|
||||
{:ok, view, _html} = live(conn)
|
||||
|
||||
html = render_click(view, "regenerate_token", %{"id" => cloud_poller.id})
|
||||
|
||||
assert html =~ "New token generated successfully"
|
||||
assert html =~ "Test Cloud Poller"
|
||||
end
|
||||
|
||||
test "superadmin can view cloud poller setup instructions", %{
|
||||
conn: conn,
|
||||
superadmin: superadmin,
|
||||
|
|
@ -481,11 +461,13 @@ defmodule ToweropsWeb.AgentLive.IndexTest do
|
|||
|
||||
{:ok, view, _html} = live(conn)
|
||||
|
||||
html = render_click(view, "show_setup", %{"id" => cloud_poller.id})
|
||||
html =
|
||||
view
|
||||
|> element("#show-setup-#{cloud_poller.id}")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "Test Cloud Poller"
|
||||
# Setup modal should show token and instructions
|
||||
assert html =~ cloud_poller.token or html =~ "docker run" or html =~ "Setup"
|
||||
assert has_element?(view, "#token-modal")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -529,12 +511,11 @@ defmodule ToweropsWeb.AgentLive.IndexTest do
|
|||
assert html =~ "Global Default Cloud Poller"
|
||||
|
||||
# Update dropdown selection (doesn't save yet)
|
||||
render_change(view, "update_selected_global_default", %{
|
||||
"agent_token_id" => cloud_poller.id
|
||||
})
|
||||
view
|
||||
|> form("#global-default-form", %{"agent_token_id" => cloud_poller.id})
|
||||
|> render_change()
|
||||
|
||||
# Click save button
|
||||
html = render_click(view, "save_global_default", %{})
|
||||
html = view |> element("#save-global-default") |> render_click()
|
||||
|
||||
assert html =~ "Global default cloud poller set successfully"
|
||||
|
||||
|
|
@ -556,10 +537,10 @@ defmodule ToweropsWeb.AgentLive.IndexTest do
|
|||
|
||||
fake_id = Ecto.UUID.generate()
|
||||
|
||||
# Update dropdown to fake ID
|
||||
# Tests a race-condition guard: the agent was deleted between page load and save.
|
||||
# form/3 validates against the rendered <option> list, which by definition can't
|
||||
# include the deleted agent — so we drive the events directly to reach the guard.
|
||||
render_change(view, "update_selected_global_default", %{"agent_token_id" => fake_id})
|
||||
|
||||
# Attempt to save
|
||||
html = render_click(view, "save_global_default", %{})
|
||||
|
||||
assert html =~ "Selected agent no longer exists"
|
||||
|
|
@ -588,10 +569,11 @@ defmodule ToweropsWeb.AgentLive.IndexTest do
|
|||
assert html =~ "Currently using"
|
||||
|
||||
# Select "No global default"
|
||||
render_change(view, "update_selected_global_default", %{"agent_token_id" => ""})
|
||||
view
|
||||
|> form("#global-default-form", %{"agent_token_id" => ""})
|
||||
|> render_change()
|
||||
|
||||
# Save
|
||||
html = render_click(view, "save_global_default", %{})
|
||||
html = view |> element("#save-global-default") |> render_click()
|
||||
|
||||
assert html =~ "Global default cloud poller cleared"
|
||||
|
||||
|
|
@ -616,7 +598,10 @@ defmodule ToweropsWeb.AgentLive.IndexTest do
|
|||
{:ok, view, _html} = live(conn)
|
||||
|
||||
# Delete the cloud poller
|
||||
html = render_click(view, "delete_agent", %{"id" => cloud_poller.id})
|
||||
html =
|
||||
view
|
||||
|> element("#delete-agent-#{cloud_poller.id}")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "Agent deleted successfully"
|
||||
|
||||
|
|
|
|||
|
|
@ -311,11 +311,13 @@ defmodule ToweropsWeb.AgentLiveTest do
|
|||
# Regular user should not see the cloud poller UI at all
|
||||
refute html =~ "Global Default Cloud Poller"
|
||||
|
||||
# Try to trigger save_global_default event directly - should be blocked by permission check
|
||||
# This simulates an attacker trying to call the event handler
|
||||
# Tests a server-side guard: the Global Default UI isn't rendered for non-superadmins
|
||||
# (verified by the refute above). To exercise the handler's permission check against
|
||||
# a tampered POST, drive the event directly — bypassing form/element wiring is the
|
||||
# whole point of this test.
|
||||
capture_log(fn ->
|
||||
result = render_click(view, "save_global_default", %{})
|
||||
# Should get an error or be rejected
|
||||
|
||||
assert result =~ "Only superadmins can set the global default cloud poller" or
|
||||
result =~ "Remote Agents"
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -196,7 +196,11 @@ defmodule ToweropsWeb.DeviceLive.FormTest do
|
|||
test "switches monitoring mode", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/devices/new")
|
||||
|
||||
html = render_click(view, "switch_monitoring_mode", %{"mode" => "icmp_only"})
|
||||
html =
|
||||
view
|
||||
|> element("#monitoring-mode-icmp-only")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "New Device"
|
||||
end
|
||||
end
|
||||
|
|
@ -257,7 +261,7 @@ defmodule ToweropsWeb.DeviceLive.FormTest do
|
|||
test "deletes device", %{conn: conn, device: device} do
|
||||
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/edit")
|
||||
|
||||
result = render_click(view, "delete")
|
||||
result = view |> element("#delete-device") |> render_click()
|
||||
|
||||
case result do
|
||||
{:error, {:live_redirect, %{to: "/devices" <> _}}} ->
|
||||
|
|
@ -281,6 +285,9 @@ defmodule ToweropsWeb.DeviceLive.FormTest do
|
|||
|
||||
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}/edit")
|
||||
|
||||
# The "Rediscover" button is hidden when monitoring_mode == "icmp_only" (i.e.
|
||||
# when SNMP is disabled), so it cannot be clicked through the UI in this state.
|
||||
# This test exercises the handler's defensive guard against being invoked anyway.
|
||||
html = render_click(view, "trigger_discovery")
|
||||
assert html =~ "SNMP is not enabled"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ defmodule ToweropsWeb.DeviceLive.IndexTest do
|
|||
|
||||
{:ok, view, _html} = live(conn, ~p"/devices")
|
||||
|
||||
html = render_click(view, "toggle_reorder_mode")
|
||||
html = view |> element("#toggle-reorder-mode") |> render_click()
|
||||
assert html =~ "Reset"
|
||||
end
|
||||
|
||||
|
|
@ -287,7 +287,9 @@ defmodule ToweropsWeb.DeviceLive.IndexTest do
|
|||
|
||||
{:ok, view, _html} = live(conn, ~p"/devices")
|
||||
|
||||
html = render_click(view, "reset_order")
|
||||
# Reset button only renders inside reorder mode — toggle in first.
|
||||
view |> element("#toggle-reorder-mode") |> render_click()
|
||||
html = view |> element("#reset-order") |> render_click()
|
||||
assert html =~ "Order reset to alphabetical"
|
||||
end
|
||||
|
||||
|
|
@ -303,15 +305,32 @@ defmodule ToweropsWeb.DeviceLive.IndexTest do
|
|||
|
||||
{:ok, view, _html} = live(conn, ~p"/devices")
|
||||
|
||||
html = render_click(view, "force_rediscover_all")
|
||||
html = view |> element("#force-rediscover-all") |> render_click()
|
||||
assert html =~ "No SNMP-enabled devices found"
|
||||
end
|
||||
|
||||
test "reorder_device with invalid device_id shows error", %{conn: conn} do
|
||||
test "reorder_device with invalid device_id shows error", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
organization: organization
|
||||
} do
|
||||
# The #device-list hook target only renders when has_devices is true.
|
||||
{:ok, _device} =
|
||||
Devices.create_device(%{
|
||||
name: "Router",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/devices")
|
||||
|
||||
# Fired by the DeviceListReorder JS hook on drag-drop. Using render_hook
|
||||
# validates the hook is wired to #device-list, even though it can't run JS.
|
||||
html =
|
||||
render_click(view, "reorder_device", %{
|
||||
view
|
||||
|> element("#device-list")
|
||||
|> render_hook("reorder_device", %{
|
||||
"device_id" => Ecto.UUID.generate(),
|
||||
"new_position" => "1"
|
||||
})
|
||||
|
|
@ -319,11 +338,25 @@ defmodule ToweropsWeb.DeviceLive.IndexTest do
|
|||
assert html =~ "not found"
|
||||
end
|
||||
|
||||
test "reorder_site with invalid site_id shows error", %{conn: conn} do
|
||||
test "reorder_site with invalid site_id shows error", %{
|
||||
conn: conn,
|
||||
site: site,
|
||||
organization: organization
|
||||
} do
|
||||
{:ok, _device} =
|
||||
Devices.create_device(%{
|
||||
name: "Router",
|
||||
ip_address: "192.168.1.1",
|
||||
site_id: site.id,
|
||||
organization_id: organization.id
|
||||
})
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/devices")
|
||||
|
||||
html =
|
||||
render_click(view, "reorder_site", %{
|
||||
view
|
||||
|> element("#device-list")
|
||||
|> render_hook("reorder_site", %{
|
||||
"site_id" => Ecto.UUID.generate(),
|
||||
"new_position" => "1"
|
||||
})
|
||||
|
|
|
|||
|
|
@ -504,7 +504,31 @@ defmodule ToweropsWeb.DeviceLive.ShowTest do
|
|||
assert html =~ "Utilization"
|
||||
end
|
||||
|
||||
test "set capacity via event handler", %{
|
||||
test "set capacity via UI button", %{
|
||||
conn: conn,
|
||||
user: user,
|
||||
device: device,
|
||||
interface: interface
|
||||
} do
|
||||
# The "Set" button only appears when the interface has no configured_capacity yet.
|
||||
# Clear it first so the button is visible, then click — the button submits the
|
||||
# if_speed-derived value (1_000_000_000 / 1M = 1000 Mbps).
|
||||
interface
|
||||
|> Interface.changeset(%{configured_capacity_bps: nil, capacity_source: "if_speed"})
|
||||
|> Towerops.Repo.update!()
|
||||
|
||||
conn = log_in_user(conn, user)
|
||||
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=ports")
|
||||
|
||||
html =
|
||||
view
|
||||
|> element("#set-capacity-#{interface.id}")
|
||||
|> render_click()
|
||||
|
||||
assert html =~ "1000.0 Mbps" or html =~ "1.0 Gbps"
|
||||
end
|
||||
|
||||
test "clear capacity via UI button", %{
|
||||
conn: conn,
|
||||
user: user,
|
||||
device: device,
|
||||
|
|
@ -514,24 +538,9 @@ defmodule ToweropsWeb.DeviceLive.ShowTest do
|
|||
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=ports")
|
||||
|
||||
html =
|
||||
render_click(view, "set_capacity", %{
|
||||
"interface_id" => interface.id,
|
||||
"capacity_mbps" => "500"
|
||||
})
|
||||
|
||||
assert html =~ "500.0 Mbps"
|
||||
end
|
||||
|
||||
test "clear capacity via event handler", %{
|
||||
conn: conn,
|
||||
user: user,
|
||||
device: device,
|
||||
interface: interface
|
||||
} do
|
||||
conn = log_in_user(conn, user)
|
||||
{:ok, view, _html} = live(conn, ~p"/devices/#{device.id}?tab=ports")
|
||||
|
||||
html = render_click(view, "clear_capacity", %{"interface_id" => interface.id})
|
||||
view
|
||||
|> element("#clear-capacity-#{interface.id}")
|
||||
|> render_click()
|
||||
|
||||
# After clearing, the capacity badge should not show 300 Mbps anymore
|
||||
refute html =~ "300.0 Mbps"
|
||||
|
|
|
|||
|
|
@ -86,16 +86,6 @@ defmodule ToweropsWeb.NetworkMapLiveTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "events" do
|
||||
test "refresh_topology reloads data and shows flash", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/network-map")
|
||||
|
||||
html = render_click(view, "refresh_topology")
|
||||
|
||||
assert html =~ "Network map refreshed"
|
||||
end
|
||||
end
|
||||
|
||||
describe "topology update events" do
|
||||
test "pushes update_topology event on mount", %{conn: conn, organization: organization} do
|
||||
{:ok, site} = Towerops.Sites.create_site(%{name: "Test Site", organization_id: organization.id})
|
||||
|
|
@ -166,22 +156,28 @@ defmodule ToweropsWeb.NetworkMapLiveTest do
|
|||
test "node_clicked event assigns selected node detail", %{conn: conn, device: device} do
|
||||
{:ok, view, _html} = live(conn, ~p"/network-map")
|
||||
|
||||
html = render_click(view, "node_clicked", %{"node_id" => device.id, "node_type" => "managed"})
|
||||
html =
|
||||
view
|
||||
|> element("#cy-container")
|
||||
|> render_hook("node_clicked", %{"node_id" => device.id, "node_type" => "managed"})
|
||||
|
||||
assert html =~ "Test-Router"
|
||||
assert has_element?(view, "#node-detail-panel", "Test-Router")
|
||||
assert html =~ "10.0.0.1"
|
||||
end
|
||||
|
||||
test "close button clears selection", %{conn: conn, device: device} do
|
||||
{:ok, view, _html} = live(conn, ~p"/network-map")
|
||||
|
||||
# Open panel
|
||||
render_click(view, "node_clicked", %{"node_id" => device.id, "node_type" => "managed"})
|
||||
# Open panel via the JS hook (cytoscape graph node click)
|
||||
view
|
||||
|> element("#cy-container")
|
||||
|> render_hook("node_clicked", %{"node_id" => device.id, "node_type" => "managed"})
|
||||
|
||||
# Close panel
|
||||
html = render_click(view, "close_detail_panel")
|
||||
assert has_element?(view, "#node-detail-panel")
|
||||
|
||||
refute html =~ "Node Details"
|
||||
view |> element("#close-detail-panel") |> render_click()
|
||||
|
||||
refute has_element?(view, "#node-detail-panel")
|
||||
end
|
||||
|
||||
test "?node=<id> param opens detail panel on mount", %{conn: conn, device: device} do
|
||||
|
|
|
|||
|
|
@ -161,34 +161,49 @@ defmodule ToweropsWeb.Org.SettingsLiveTest do
|
|||
end
|
||||
|
||||
test "applies SNMP config to all devices", %{conn: conn, user: user, organization: org} do
|
||||
# The "Apply to All" button is only shown when org has an snmp_community set,
|
||||
# otherwise there's nothing to apply.
|
||||
{:ok, org} = Organizations.update_organization(org, %{snmp_community: "public"})
|
||||
|
||||
{:ok, view, _html} =
|
||||
conn
|
||||
|> log_in_user(user)
|
||||
|> live(~p"/orgs/#{org.slug}/settings")
|
||||
|> live(~p"/orgs/#{org.slug}/settings?tab=snmp")
|
||||
|
||||
html = render_click(view, "apply_snmp_to_all")
|
||||
html = view |> element("#apply-snmp-to-all") |> render_click()
|
||||
assert html =~ "Applied SNMP configuration to"
|
||||
end
|
||||
|
||||
test "applies default agent to all devices", %{conn: conn, user: user, organization: org} do
|
||||
# The "Apply to All" button is only shown when org has a default agent set.
|
||||
{:ok, agent_token, _} = Towerops.Agents.create_agent_token(org.id, "Default Agent")
|
||||
{:ok, org} = Organizations.update_organization(org, %{default_agent_token_id: agent_token.id})
|
||||
|
||||
{:ok, view, _html} =
|
||||
conn
|
||||
|> log_in_user(user)
|
||||
|> live(~p"/orgs/#{org.slug}/settings")
|
||||
|> live(~p"/orgs/#{org.slug}/settings?tab=agents")
|
||||
|
||||
html = render_click(view, "apply_agent_to_all")
|
||||
html = view |> element("#apply-agent-to-all") |> render_click()
|
||||
assert html =~ "Applied default agent to"
|
||||
end
|
||||
|
||||
test "toggles default organization", %{conn: conn, user: user, organization: org} do
|
||||
# The toggle button only renders when (a) @user_orgs_count > 1 and (b) the
|
||||
# current membership is NOT the user's default. Create a second org and clear
|
||||
# the default flag on this org's membership so the button is visible.
|
||||
_other_org = organization_fixture(user.id)
|
||||
|
||||
membership = Organizations.get_membership!(org.id, user.id)
|
||||
{:ok, _} = Organizations.update_membership(membership, %{is_default: false})
|
||||
|
||||
{:ok, view, _html} =
|
||||
conn
|
||||
|> log_in_user(user)
|
||||
|> live(~p"/orgs/#{org.slug}/settings")
|
||||
|
||||
html = render_click(view, "toggle_default_org")
|
||||
html = view |> element("#toggle-default-org") |> render_click()
|
||||
|
||||
# Either sets as default or shows already default message
|
||||
assert html =~ "default organization" or html =~ "already your default"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -143,6 +143,10 @@ defmodule ToweropsWeb.UserSettingsLiveTest do
|
|||
test "opens and cancels revoke all modal", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=sessions")
|
||||
|
||||
# The "Revoke All Other Sessions" button only renders when there are 2+ active
|
||||
# browser sessions for the user (UX: nothing to revoke if you only have one).
|
||||
# The test fixture creates a single session, so the trigger button isn't in the
|
||||
# DOM. We exercise the modal-open/cancel handler logic directly here.
|
||||
html = render_click(view, "show_revoke_all_modal")
|
||||
assert html =~ "Revoke All"
|
||||
|
||||
|
|
@ -169,7 +173,7 @@ defmodule ToweropsWeb.UserSettingsLiveTest do
|
|||
test "opens add token modal", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=api")
|
||||
|
||||
html = render_click(view, "show_add_token_modal")
|
||||
html = view |> element("#show-add-token-modal-empty") |> render_click()
|
||||
|
||||
assert html =~ "Create API Token"
|
||||
end
|
||||
|
|
@ -177,8 +181,8 @@ defmodule ToweropsWeb.UserSettingsLiveTest do
|
|||
test "cancels add token modal", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=api")
|
||||
|
||||
render_click(view, "show_add_token_modal")
|
||||
html = render_click(view, "cancel_add_token")
|
||||
view |> element("#show-add-token-modal-empty") |> render_click()
|
||||
html = view |> element("#cancel-add-token") |> render_click()
|
||||
|
||||
refute html =~ "Create API Token"
|
||||
end
|
||||
|
|
@ -208,12 +212,12 @@ defmodule ToweropsWeb.UserSettingsLiveTest do
|
|||
|
||||
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=api")
|
||||
|
||||
render_click(view, "show_add_token_modal")
|
||||
view |> element("#show-add-token-modal-empty") |> render_click()
|
||||
|
||||
html =
|
||||
render_click(view, "create_api_token", %{
|
||||
"token" => %{"name" => "My Token", "organization_id" => org.id}
|
||||
})
|
||||
view
|
||||
|> form("#create-api-token-form", %{"name" => "My Token", "organization_id" => org.id})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "My Token" or html =~ "API Tokens"
|
||||
end
|
||||
|
|
@ -221,6 +225,9 @@ defmodule ToweropsWeb.UserSettingsLiveTest do
|
|||
test "create_api_token without name shows error", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=api")
|
||||
|
||||
# Defensive handler test: the UI marks `name` as `required`, so a real browser
|
||||
# blocks empty submission. The handler still has to defend against tampered POSTs
|
||||
# — so we drive the event directly to verify the server-side guard.
|
||||
html = render_click(view, "create_api_token", %{})
|
||||
assert html =~ "required" or html =~ "API Tokens"
|
||||
end
|
||||
|
|
@ -228,6 +235,9 @@ defmodule ToweropsWeb.UserSettingsLiveTest do
|
|||
test "close_token_modal clears token", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=api")
|
||||
|
||||
# The Close button only renders when the token-display modal is open (which only
|
||||
# happens after a token is successfully created). This tests the handler's idempotent
|
||||
# behavior when the close event fires without the modal being open.
|
||||
html = render_click(view, "close_token_modal")
|
||||
assert html =~ "API Tokens"
|
||||
end
|
||||
|
|
@ -236,14 +246,19 @@ defmodule ToweropsWeb.UserSettingsLiveTest do
|
|||
describe "UserSettingsLive session management with sudo mode" do
|
||||
setup :register_and_log_in_user_with_sudo
|
||||
|
||||
test "shows notification tab with add device modal", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=notifications")
|
||||
test "shows security tab with add device modal", %{conn: conn} do
|
||||
# show_add_device_modal is on the SECURITY tab (TOTP authenticator devices),
|
||||
# not notifications. Original test mounted ?tab=notifications and only worked
|
||||
# because direct render_click bypassed tab gating.
|
||||
# The fixture user has enable_totp: true (one TOTP device), so the
|
||||
# "with-devices" Add button renders, not the empty-state one.
|
||||
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=security")
|
||||
|
||||
html = render_click(view, "show_add_device_modal")
|
||||
assert html =~ "Mobile" or html =~ "Device"
|
||||
html = view |> element("#show-add-device-modal") |> render_click()
|
||||
assert html =~ "Device" or html =~ "authenticator"
|
||||
|
||||
html = render_click(view, "cancel_add_device")
|
||||
assert html =~ "Mobile Devices"
|
||||
html = view |> element("#cancel-add-device") |> render_click()
|
||||
assert html =~ "TOTP" or html =~ "authenticator" or html =~ "Devices"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -253,13 +268,19 @@ defmodule ToweropsWeb.UserSettingsLiveTest do
|
|||
test "check_password_breach with empty value", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=account")
|
||||
|
||||
html = render_click(view, "check_password_breach", %{"value" => ""})
|
||||
# Fired by phx-blur on #new-password — render_blur drives that path through the
|
||||
# actual element, validating the wiring. Original test used render_click which
|
||||
# silently bypassed the blur attribute.
|
||||
html = view |> element("#new-password") |> render_blur(%{"value" => ""})
|
||||
assert html =~ "Account Settings"
|
||||
end
|
||||
|
||||
test "check_password_breach with no value param", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/users/settings?tab=account")
|
||||
|
||||
# Defensive handler test: the phx-blur event always carries the input value, so
|
||||
# an empty params map can't be produced from the UI. Drive directly to verify
|
||||
# the no-value branch of the handler.
|
||||
html = render_click(view, "check_password_breach", %{})
|
||||
assert html =~ "Account Settings"
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue