Two critical production bugs fixed: 1. **Alert type enum → string conversion** - Changed Alert.alert_type from Ecto.Enum to :string for flexibility - Updated all queries to use "device_down"/"device_up" strings instead of atoms - Fixed pattern matching in alerts.ex and device_monitor_worker.ex - Updated 44+ test files to use string literals 2. **SQL array indexing syntax error in activity feed** - Fixed PostgreSQL syntax: `array_agg(...)[1]` → `(array_agg(...))[1]` - Prevents "syntax error at or near [" in activity feed queries 3. **Added comprehensive timer cleanup tests** - Tests for mobile_qr_live.ex timer cleanup on terminate - Tests for agent_live index timer cleanup - Verifies memory leak fixes from previous commits Files changed: - lib/towerops/alerts.ex - lib/towerops/alerts/alert.ex - lib/towerops/activity_feed.ex - lib/towerops/workers/device_monitor_worker.ex - test/**/*_test.exs (44+ files with alert_type references) - test/towerops_web/live/mobile_qr_live_test.exs - test/towerops_web/live/agent_live_test.exs - test/towerops/workers/device_poller_worker_test.exs All tests passing except 1 unrelated brute force protection test.
639 lines
22 KiB
Elixir
639 lines
22 KiB
Elixir
defmodule ToweropsWeb.AgentLiveTest do
|
|
use ToweropsWeb.ConnCase
|
|
|
|
import Ecto.Query
|
|
import ExUnit.CaptureLog
|
|
import Phoenix.LiveViewTest
|
|
|
|
alias Towerops.Agents
|
|
|
|
setup :register_and_log_in_user
|
|
|
|
setup %{user: user} do
|
|
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
|
|
|
%{organization: organization}
|
|
end
|
|
|
|
describe "Index - Basic Functionality" do
|
|
test "lists all agent tokens", %{conn: conn, organization: organization} do
|
|
{:ok, _agent_token, _token} = Agents.create_agent_token(organization.id, "Agent 1")
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/agents")
|
|
|
|
assert html =~ "Remote Agents"
|
|
assert html =~ "Agent 1"
|
|
end
|
|
|
|
test "displays empty state when no agents", %{conn: conn, organization: _organization} do
|
|
{:ok, _view, html} = live(conn, ~p"/agents")
|
|
|
|
assert html =~ "No agents"
|
|
assert html =~ "Get started by creating your first remote agent"
|
|
end
|
|
|
|
test "creates new agent and shows token modal", %{conn: conn, organization: _organization} do
|
|
{:ok, view, _html} = live(conn, ~p"/agents")
|
|
|
|
html =
|
|
view
|
|
|> form("#new-agent-form form", %{name: "Test Agent"})
|
|
|> render_submit()
|
|
|
|
assert html =~ "Agent Setup Instructions"
|
|
assert html =~ "Test Agent"
|
|
assert html =~ "Authentication Token"
|
|
end
|
|
|
|
test "deletes agent", %{conn: conn, organization: organization} do
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(organization.id, "Agent 1")
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/agents")
|
|
|
|
html =
|
|
view
|
|
|> element("button", "Delete")
|
|
|> render_click()
|
|
|
|
assert html =~ "Agent deleted successfully"
|
|
|
|
# Verify agent was actually deleted
|
|
assert is_nil(Towerops.Repo.get(Agents.AgentToken, agent_token.id))
|
|
end
|
|
|
|
test "closes token modal", %{conn: conn, organization: _organization} do
|
|
{:ok, view, _html} = live(conn, ~p"/agents")
|
|
|
|
# Create agent to show modal
|
|
view
|
|
|> form("#new-agent-form form", %{name: "Test Agent"})
|
|
|> render_submit()
|
|
|
|
# Close modal by clicking the primary button (bg-blue-600 class indicates primary button)
|
|
html =
|
|
view
|
|
|> element("button.bg-blue-600[phx-click='close_token_modal']")
|
|
|> render_click()
|
|
|
|
refute html =~ "Agent Created Successfully"
|
|
end
|
|
|
|
test "requires authentication", %{organization: _organization} do
|
|
conn = build_conn()
|
|
{:error, redirect} = live(conn, ~p"/agents")
|
|
|
|
assert {:redirect, %{to: path}} = redirect
|
|
assert path == ~p"/users/log-in"
|
|
end
|
|
|
|
test "displays agent with warning status", %{conn: conn, organization: organization} do
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(organization.id, "Agent 1")
|
|
|
|
# Update last_seen_at to 3 minutes ago (warning status)
|
|
three_minutes_ago = DateTime.add(DateTime.utc_now(), -180, :second)
|
|
|
|
Agents.update_agent_token_heartbeat(agent_token.id, "127.0.0.1")
|
|
|
|
Towerops.Repo.update_all(
|
|
from(t in Agents.AgentToken, where: t.id == ^agent_token.id),
|
|
set: [last_seen_at: three_minutes_ago]
|
|
)
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/agents")
|
|
|
|
assert html =~ "Warning"
|
|
assert html =~ "3m ago"
|
|
end
|
|
|
|
test "displays agent with offline status", %{conn: conn, organization: organization} do
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(organization.id, "Agent 1")
|
|
|
|
# Update last_seen_at to 10 minutes ago (offline status)
|
|
ten_minutes_ago = DateTime.add(DateTime.utc_now(), -600, :second)
|
|
|
|
Agents.update_agent_token_heartbeat(agent_token.id, "127.0.0.1")
|
|
|
|
Towerops.Repo.update_all(
|
|
from(t in Agents.AgentToken, where: t.id == ^agent_token.id),
|
|
set: [last_seen_at: ten_minutes_ago]
|
|
)
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/agents")
|
|
|
|
assert html =~ "Offline"
|
|
assert html =~ "10m ago"
|
|
end
|
|
|
|
test "displays agent last seen time in hours", %{conn: conn, organization: organization} do
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(organization.id, "Agent 1")
|
|
|
|
# Update last_seen_at to 2 hours ago
|
|
two_hours_ago = DateTime.add(DateTime.utc_now(), -7200, :second)
|
|
|
|
Agents.update_agent_token_heartbeat(agent_token.id, "127.0.0.1")
|
|
|
|
Towerops.Repo.update_all(
|
|
from(t in Agents.AgentToken, where: t.id == ^agent_token.id),
|
|
set: [last_seen_at: two_hours_ago]
|
|
)
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/agents")
|
|
|
|
assert html =~ "2h ago"
|
|
end
|
|
|
|
test "displays agent last seen time in days", %{conn: conn, organization: organization} do
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(organization.id, "Agent 1")
|
|
|
|
# Update last_seen_at to 2 days ago
|
|
two_days_ago = DateTime.add(DateTime.utc_now(), -172_800, :second)
|
|
|
|
Agents.update_agent_token_heartbeat(agent_token.id, "127.0.0.1")
|
|
|
|
Towerops.Repo.update_all(
|
|
from(t in Agents.AgentToken, where: t.id == ^agent_token.id),
|
|
set: [last_seen_at: two_days_ago]
|
|
)
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/agents")
|
|
|
|
assert html =~ "2d ago"
|
|
end
|
|
end
|
|
|
|
describe "Index - Token Management" do
|
|
test "shows setup instructions for existing agent", %{conn: conn, organization: organization} do
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(organization.id, "Test Agent")
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/agents")
|
|
|
|
html =
|
|
view
|
|
|> element("button[phx-click='show_setup'][phx-value-id='#{agent_token.id}']")
|
|
|> render_click()
|
|
|
|
assert html =~ "Agent Setup Instructions"
|
|
assert html =~ "Test Agent"
|
|
assert html =~ "Authentication Token"
|
|
end
|
|
end
|
|
|
|
describe "Index - Cloud Pollers (Superuser)" do
|
|
setup %{user: user} do
|
|
# Make user a superuser
|
|
Towerops.Repo.update!(Ecto.Changeset.change(user, %{is_superuser: true}))
|
|
:ok
|
|
end
|
|
|
|
test "superuser can create cloud poller", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/agents")
|
|
|
|
html =
|
|
view
|
|
|> form("#new-agent-form form", %{name: "Cloud Poller 1", is_cloud_poller: "true"})
|
|
|> render_submit()
|
|
|
|
assert html =~ "Cloud poller created successfully"
|
|
assert html =~ "Agent Setup Instructions"
|
|
assert html =~ "Cloud Poller 1"
|
|
end
|
|
|
|
test "superuser can see cloud pollers section", %{conn: conn} do
|
|
{:ok, _cloud_poller, _token} = Agents.create_cloud_poller("Cloud Poller 1")
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/agents")
|
|
|
|
assert html =~ "Cloud Pollers"
|
|
assert html =~ "Cloud Poller 1"
|
|
end
|
|
|
|
test "superuser can set global default cloud poller", %{conn: conn} do
|
|
{:ok, cloud_poller, _token} = Agents.create_cloud_poller("Default Poller")
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/agents")
|
|
|
|
# Update the selected dropdown value using the event handler
|
|
render_hook(view, "update_selected_global_default", %{"agent_token_id" => cloud_poller.id})
|
|
|
|
# Save the selection
|
|
html =
|
|
view
|
|
|> element("button[phx-click='save_global_default']")
|
|
|> render_click()
|
|
|
|
assert html =~ "Global default cloud poller set successfully"
|
|
end
|
|
|
|
test "superuser can clear global default cloud poller", %{conn: conn} do
|
|
{:ok, cloud_poller, _token} = Agents.create_cloud_poller("Default Poller")
|
|
{:ok, _} = Towerops.Settings.set_global_default_cloud_poller(cloud_poller.id)
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/agents")
|
|
|
|
# Select empty value using the event handler
|
|
render_hook(view, "update_selected_global_default", %{"agent_token_id" => ""})
|
|
|
|
# Save the selection
|
|
html =
|
|
view
|
|
|> element("button[phx-click='save_global_default']")
|
|
|> render_click()
|
|
|
|
assert html =~ "Global default cloud poller cleared"
|
|
end
|
|
|
|
test "superuser sees error when setting non-existent agent as default", %{conn: conn} do
|
|
non_existent_id = Ecto.UUID.generate()
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/agents")
|
|
|
|
# Update dropdown to non-existent ID
|
|
render_hook(view, "update_selected_global_default", %{"agent_token_id" => non_existent_id})
|
|
|
|
# Try to save - should fail validation
|
|
html =
|
|
view
|
|
|> element("button[phx-click='save_global_default']")
|
|
|> render_click()
|
|
|
|
assert html =~ "Selected agent no longer exists"
|
|
end
|
|
|
|
test "deleting global default cloud poller clears the setting", %{conn: conn} do
|
|
{:ok, cloud_poller, _token} = Agents.create_cloud_poller("Default Poller")
|
|
{:ok, _} = Towerops.Settings.set_global_default_cloud_poller(cloud_poller.id)
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/agents")
|
|
|
|
# Delete the cloud poller
|
|
html =
|
|
view
|
|
|> element("button[phx-click='delete_agent'][phx-value-id='#{cloud_poller.id}']")
|
|
|> render_click()
|
|
|
|
assert html =~ "Agent deleted successfully"
|
|
|
|
# Verify global default was cleared
|
|
assert Towerops.Settings.get_global_default_cloud_poller() == nil
|
|
end
|
|
end
|
|
|
|
describe "Index - Cloud Pollers (Regular User)" do
|
|
test "regular user cannot create cloud poller", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/agents")
|
|
|
|
# Regular users don't have the is_cloud_poller checkbox in the form,
|
|
# so we need to test the backend permission check directly
|
|
# by triggering the event with is_cloud_poller parameter
|
|
html = render_hook(view, "create_agent", %{"name" => "Test", "is_cloud_poller" => "true"})
|
|
|
|
assert html =~ "Only superadmins can create cloud pollers"
|
|
end
|
|
|
|
test "regular user does not see cloud pollers section", %{conn: conn} do
|
|
{:ok, _cloud_poller, _token} = Agents.create_cloud_poller("Cloud Poller 1")
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/agents")
|
|
|
|
refute html =~ "Cloud Pollers"
|
|
refute html =~ "Global Default Cloud Poller"
|
|
end
|
|
|
|
test "regular user cannot set global default cloud poller", %{conn: conn} do
|
|
{:ok, cloud_poller, _token} = Agents.create_cloud_poller("Default Poller")
|
|
|
|
# First make the user a superuser temporarily to set the global default
|
|
# Then we'll test that a regular user can't access the save_global_default event
|
|
{:ok, _} = Towerops.Settings.set_global_default_cloud_poller(cloud_poller.id)
|
|
|
|
{:ok, view, html} = live(conn, ~p"/agents")
|
|
|
|
# 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
|
|
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)
|
|
end
|
|
end
|
|
|
|
describe "Index - Device Counts" do
|
|
test "displays device counts for agents", %{conn: conn, organization: organization} do
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(organization.id, "Test Agent")
|
|
|
|
# Create a site and device assigned to this agent
|
|
{:ok, site} =
|
|
Towerops.Sites.create_site(%{
|
|
name: "Test Site",
|
|
address: "123 Test St",
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, device} =
|
|
Towerops.Devices.create_device(%{
|
|
name: "Test Device",
|
|
ip_address: "192.168.1.100",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
|
|
Agents.assign_device_to_agent(agent_token.id, device.id)
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/agents")
|
|
|
|
# Should show device count (displays as "1 total" in template)
|
|
assert html =~ "1 total"
|
|
assert html =~ "1 direct"
|
|
end
|
|
|
|
test "displays zero when no devices assigned", %{conn: conn, organization: organization} do
|
|
{:ok, _agent_token, _token} = Agents.create_agent_token(organization.id, "Test Agent")
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/agents")
|
|
|
|
# Displays as "0 total" in template
|
|
assert html =~ "0 total"
|
|
assert html =~ "0 direct"
|
|
end
|
|
end
|
|
|
|
describe "Index - Error Handling" do
|
|
test "handles agent creation failure", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/agents")
|
|
|
|
# Try to create agent with empty name
|
|
html =
|
|
view
|
|
|> form("#new-agent-form form", %{name: ""})
|
|
|> render_submit()
|
|
|
|
assert html =~ "Failed to create agent"
|
|
end
|
|
end
|
|
|
|
describe "Index - Multiple Agents" do
|
|
test "displays multiple agents", %{conn: conn, organization: organization} do
|
|
{:ok, _agent1, _token1} = Agents.create_agent_token(organization.id, "Agent 1")
|
|
{:ok, _agent2, _token2} = Agents.create_agent_token(organization.id, "Agent 2")
|
|
{:ok, _agent3, _token3} = Agents.create_agent_token(organization.id, "Agent 3")
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/agents")
|
|
|
|
assert html =~ "Agent 1"
|
|
assert html =~ "Agent 2"
|
|
assert html =~ "Agent 3"
|
|
end
|
|
|
|
test "deleting one agent keeps others", %{conn: conn, organization: organization} do
|
|
{:ok, agent1, _token1} = Agents.create_agent_token(organization.id, "Agent 1")
|
|
{:ok, _agent2, _token2} = Agents.create_agent_token(organization.id, "Agent 2")
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/agents")
|
|
|
|
html =
|
|
view
|
|
|> element("button[phx-click='delete_agent'][phx-value-id='#{agent1.id}']")
|
|
|> render_click()
|
|
|
|
assert html =~ "Agent deleted successfully"
|
|
assert html =~ "Agent 2"
|
|
refute html =~ "Agent 1"
|
|
end
|
|
end
|
|
|
|
describe "Edit" do
|
|
test "loads edit page with existing agent name", %{conn: conn, organization: organization} do
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(organization.id, "Old Agent Name")
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/agents/#{agent_token.id}/edit")
|
|
|
|
assert html =~ "Edit Agent"
|
|
assert html =~ "Old Agent Name"
|
|
assert html =~ "Update the agent name"
|
|
end
|
|
|
|
test "validates empty name shows error on submit", %{conn: conn, organization: organization} do
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(organization.id, "Test Agent")
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/agents/#{agent_token.id}/edit")
|
|
|
|
html =
|
|
view
|
|
|> form("#edit-agent-form", agent_token: %{name: ""})
|
|
|> render_submit()
|
|
|
|
assert html =~ "can't be blank"
|
|
|
|
# Verify agent was not updated
|
|
agent = Agents.get_agent_token!(agent_token.id)
|
|
assert agent.name == "Test Agent"
|
|
end
|
|
|
|
test "successful save updates agent and redirects to show page", %{
|
|
conn: conn,
|
|
organization: organization
|
|
} do
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(organization.id, "Old Name")
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/agents/#{agent_token.id}/edit")
|
|
|
|
view
|
|
|> form("#edit-agent-form", agent_token: %{name: "New Name"})
|
|
|> render_submit()
|
|
|
|
# Verify redirect to show page with flash message
|
|
assert_redirect(view, ~p"/agents/#{agent_token.id}")
|
|
|
|
# Verify agent was updated
|
|
updated_agent = Agents.get_agent_token!(agent_token.id)
|
|
assert updated_agent.name == "New Name"
|
|
end
|
|
|
|
test "cancel button navigates back to show page", %{conn: conn, organization: organization} do
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(organization.id, "Test Agent")
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/agents/#{agent_token.id}/edit")
|
|
|
|
assert html =~ "Cancel"
|
|
|
|
# Find cancel link and verify it points to show page
|
|
assert html =~
|
|
~p"/agents/#{agent_token.id}"
|
|
end
|
|
|
|
test "requires authentication", %{organization: organization} do
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(organization.id, "Test Agent")
|
|
|
|
conn = build_conn()
|
|
{:error, redirect} = live(conn, ~p"/agents/#{agent_token.id}/edit")
|
|
|
|
assert {:redirect, %{to: path}} = redirect
|
|
assert path == ~p"/users/log-in"
|
|
end
|
|
|
|
test "verifies agent belongs to organization", %{conn: conn, organization: organization, user: user} do
|
|
# Create another organization
|
|
{:ok, other_org} = Towerops.Organizations.create_organization(%{name: "Other Org"}, user.id, bypass_limits: true)
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(other_org.id, "Other Agent")
|
|
|
|
# Scope session to the first organization while preserving auth
|
|
conn = Plug.Conn.put_session(conn, "current_organization_id", organization.id)
|
|
|
|
# Try to edit agent from other organization - capture_log suppresses expected Router exception log
|
|
capture_log(fn ->
|
|
assert_raise Ecto.NoResultsError, fn ->
|
|
live(conn, ~p"/agents/#{agent_token.id}/edit")
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
describe "Show - Superadmin Cross-Org Access" do
|
|
setup %{user: user} do
|
|
Towerops.Repo.update!(Ecto.Changeset.change(user, %{is_superuser: true}))
|
|
:ok
|
|
end
|
|
|
|
test "superadmin can view agent from another organization", %{conn: conn, organization: organization, user: user} do
|
|
{:ok, other_org} = Towerops.Organizations.create_organization(%{name: "Other Org"}, user.id, bypass_limits: true)
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(other_org.id, "Other Org Agent")
|
|
|
|
conn = Plug.Conn.put_session(conn, "current_organization_id", organization.id)
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/agents/#{agent_token.id}")
|
|
|
|
assert html =~ "Other Org Agent"
|
|
end
|
|
|
|
test "superadmin can edit agent from another organization", %{conn: conn, organization: organization, user: user} do
|
|
{:ok, other_org} = Towerops.Organizations.create_organization(%{name: "Other Org"}, user.id, bypass_limits: true)
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(other_org.id, "Other Org Agent")
|
|
|
|
conn = Plug.Conn.put_session(conn, "current_organization_id", organization.id)
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/agents/#{agent_token.id}/edit")
|
|
|
|
assert html =~ "Edit Agent"
|
|
assert html =~ "Other Org Agent"
|
|
end
|
|
end
|
|
|
|
describe "Show - Restart Agent" do
|
|
test "superuser sees restart button and can restart agent", %{conn: conn, organization: organization, user: user} do
|
|
Towerops.Repo.update!(Ecto.Changeset.change(user, %{is_superuser: true}))
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(organization.id, "Test Agent")
|
|
|
|
{:ok, view, html} = live(conn, ~p"/agents/#{agent_token.id}")
|
|
|
|
assert html =~ "Restart"
|
|
|
|
# Subscribe to lifecycle topic to verify broadcast
|
|
Phoenix.PubSub.subscribe(Towerops.PubSub, "agent:#{agent_token.id}:lifecycle")
|
|
|
|
view
|
|
|> element("button", "Restart")
|
|
|> render_click()
|
|
|
|
assert_receive :restart_requested
|
|
assert render(view) =~ "Restart command sent"
|
|
end
|
|
|
|
test "regular user does not see restart button", %{conn: conn, organization: organization} do
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(organization.id, "Test Agent")
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/agents/#{agent_token.id}")
|
|
|
|
refute html =~ "Restart"
|
|
end
|
|
end
|
|
|
|
describe "Show - Update Agent" do
|
|
test "superuser sees update button", %{conn: conn, organization: organization, user: user} do
|
|
Towerops.Repo.update!(Ecto.Changeset.change(user, %{is_superuser: true}))
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(organization.id, "Test Agent")
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/agents/#{agent_token.id}")
|
|
|
|
assert has_element?(view, "button", "Update")
|
|
end
|
|
|
|
test "superuser clicking update shows error when no releases available", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
user: user
|
|
} do
|
|
Towerops.Repo.update!(Ecto.Changeset.change(user, %{is_superuser: true}))
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(organization.id, "Test Agent")
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/agents/#{agent_token.id}")
|
|
|
|
html =
|
|
view
|
|
|> element("button", "Update")
|
|
|> render_click()
|
|
|
|
assert html =~ "Failed to fetch latest release"
|
|
end
|
|
|
|
test "regular user does not see update button", %{conn: conn, organization: organization} do
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(organization.id, "Test Agent")
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/agents/#{agent_token.id}")
|
|
|
|
refute has_element?(view, "button", "Update")
|
|
end
|
|
end
|
|
|
|
describe "Show - Regular User Cross-Org Access" do
|
|
test "regular user cannot view agent from another organization", %{conn: conn, organization: organization, user: user} do
|
|
{:ok, other_org} = Towerops.Organizations.create_organization(%{name: "Other Org"}, user.id, bypass_limits: true)
|
|
{:ok, agent_token, _token} = Agents.create_agent_token(other_org.id, "Other Org Agent")
|
|
|
|
conn = Plug.Conn.put_session(conn, "current_organization_id", organization.id)
|
|
|
|
capture_log(fn ->
|
|
assert_raise Ecto.NoResultsError, fn ->
|
|
live(conn, ~p"/agents/#{agent_token.id}")
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
describe "Index - Timer Cleanup (Reliability Fix)" do
|
|
test "cleans up timer on LiveView terminate", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, ~p"/agents")
|
|
|
|
# Verify LiveView mounted and is running
|
|
assert render(view) =~ "Remote Agents"
|
|
|
|
# Stop the LiveView process to trigger terminate/2
|
|
stop_live(view)
|
|
|
|
# The test passing means terminate/2 didn't crash
|
|
# In production, this prevents memory leaks from orphaned timers
|
|
end
|
|
|
|
test "handles nil timer ref in terminate gracefully", %{conn: conn} do
|
|
# Test the case where timer_ref might be nil
|
|
{:ok, view, _html} = live(conn, ~p"/agents")
|
|
|
|
# Even if timer_ref is nil, terminate should handle it gracefully
|
|
stop_live(view)
|
|
|
|
# No crash = success
|
|
end
|
|
|
|
defp stop_live(view) do
|
|
# Stop the LiveView process
|
|
Process.exit(view.pid, :kill)
|
|
# Wait for process to die
|
|
ref = Process.monitor(view.pid)
|
|
assert_receive {:DOWN, ^ref, :process, _, _}, 1000
|
|
end
|
|
end
|
|
end
|