test: improve test coverage and code quality

- Update pre-commit config
- Add test coverage for URL validation, security modules, and monitoring executors
- Improve network map implementation and tests
- Add coverage for API controllers and LiveView endpoints
- Refactor code to fix Credo issues (reduce nesting, extract helper functions)
This commit is contained in:
Graham McIntire 2026-03-15 14:49:39 -05:00
parent a98b887471
commit 44fc017653
No known key found for this signature in database
14 changed files with 196 additions and 195 deletions

View file

@ -1 +1 @@
/nix/store/4xlkmdd947h5qlhj2rmbyavq08grkz27-pre-commit-config.json
/nix/store/zx636v118rzzqzwafgrw2aybh7l0szrl-pre-commit-config.json

View file

@ -1,12 +1,12 @@
defmodule Towerops.URLValidator do
import Bitwise
@moduledoc """
Validates URLs to prevent SSRF (Server-Side Request Forgery) attacks.
Blocks requests to internal/private IP ranges, localhost, and non-HTTP(S) schemes.
"""
import Bitwise
@private_ranges [
# 127.0.0.0/8
{127, 0, 0, 0, 8},
@ -31,9 +31,8 @@ defmodule Towerops.URLValidator do
def validate(url) when is_binary(url) do
with {:ok, uri} <- parse_url(url),
:ok <- validate_scheme(uri),
:ok <- validate_host(uri),
:ok <- validate_ip(uri.host) do
:ok
:ok <- validate_host(uri) do
validate_ip(uri.host)
end
end
@ -77,17 +76,12 @@ defmodule Towerops.URLValidator do
end
defp resolve_host(host) do
case :inet.parse_address(String.to_charlist(host)) do
{:ok, ip} -> {:ok, ip}
{:error, _} ->
case :inet.getaddr(String.to_charlist(host), :inet) do
{:ok, ip} -> {:ok, ip}
{:error, _} ->
case :inet.getaddr(String.to_charlist(host), :inet6) do
{:ok, ip} -> {:ok, ip}
error -> error
end
end
charlist = String.to_charlist(host)
with {:error, _} <- :inet.parse_address(charlist),
{:error, _} <- :inet.getaddr(charlist, :inet) do
error = :inet.getaddr(charlist, :inet6)
error
end
end
@ -95,9 +89,11 @@ defmodule Towerops.URLValidator do
defp private_ip?({a, b, c, d}) do
Enum.any?(@private_ranges, fn
:ipv6_loopback -> false
:ipv6_loopback ->
false
{net_a, net_b, net_c, net_d, prefix} ->
mask = bsl(0xFFFFFFFF, 32 - prefix) |> band(0xFFFFFFFF)
mask = 0xFFFFFFFF |> bsl(32 - prefix) |> band(0xFFFFFFFF)
ip_int = bsl(a, 24) + bsl(b, 16) + bsl(c, 8) + d
net_int = bsl(net_a, 24) + bsl(net_b, 16) + bsl(net_c, 8) + net_d
band(ip_int, mask) == band(net_int, mask)
@ -105,6 +101,4 @@ defmodule Towerops.URLValidator do
end
defp private_ip?(_), do: false
import Bitwise
end

View file

@ -161,77 +161,12 @@ defmodule ToweropsWeb.AgentLive.Index do
@impl true
def handle_event("delete_agent", %{"id" => id}, socket) do
organization = socket.assigns.current_scope.organization
# Verify the agent token belongs to the current org before deleting
agent_token = Agents.get_agent_token!(id)
is_superuser = Scope.superuser?(socket.assigns.current_scope)
is_cloud_poller = is_nil(agent_token.organization_id)
belongs_to_org = agent_token.organization_id == organization.id
if not belongs_to_org and not (is_superuser and is_cloud_poller) do
{:noreply, put_flash(socket, :error, t_equipment("Agent not found"))}
if agent_authorized_for_deletion?(socket, agent_token) do
handle_agent_deletion(socket, id)
else
case Agents.delete_agent_token(id) do
{:ok, _} ->
# If the deleted agent was the global default, clear it
if Scope.superuser?(socket.assigns.current_scope) &&
socket.assigns.global_default_cloud_poller_id == id do
Settings.set_global_default_cloud_poller(nil)
end
agent_tokens = Agents.list_organization_agent_tokens(organization.id)
# Refresh cloud pollers list if superadmin (in case a cloud poller was deleted)
cloud_pollers = load_cloud_pollers_if_superuser(socket.assigns.current_scope)
# Reload global default cloud poller ID
global_default_cloud_poller_id =
if Scope.superuser?(socket.assigns.current_scope) do
Settings.get_global_default_cloud_poller()
else
socket.assigns.global_default_cloud_poller_id
end
# Recalculate device counts after deletion
equipment_counts =
Map.new(agent_tokens, fn token ->
direct = Agents.count_assigned_devices(token.id)
total = length(Agents.list_agent_polling_targets(token.id))
{token.id, %{direct: direct, total: total}}
end)
cloud_poller_counts = calculate_device_counts(cloud_pollers)
# Refresh health statistics
agent_health_stats = Stats.get_organization_agent_health(organization.id)
assignment_breakdown = Stats.get_device_assignment_breakdown(organization.id)
offline_agents = Stats.get_offline_agents(organization.id)
{:noreply,
socket
|> stream(:agent_tokens, agent_tokens, reset: true)
|> assign(:has_agents, agent_tokens != [])
|> stream(:cloud_pollers, cloud_pollers, reset: true)
|> assign(:cloud_pollers_list, cloud_pollers)
|> assign(:has_cloud_pollers, cloud_pollers != [])
|> assign(:global_default_cloud_poller_id, global_default_cloud_poller_id)
|> assign(:selected_global_default, global_default_cloud_poller_id || "")
|> assign(:device_counts, equipment_counts)
|> assign(:cloud_poller_counts, cloud_poller_counts)
|> assign(:agent_health_stats, agent_health_stats)
|> assign(:assignment_breakdown, assignment_breakdown)
|> assign(:offline_agents, offline_agents)
|> put_flash(
:info,
t_equipment("Agent deleted successfully. Devices now fall back to site/org defaults or cloud polling.")
)}
{:error, _} ->
{:noreply, put_flash(socket, :error, t_equipment("Failed to delete agent"))}
end
{:noreply, put_flash(socket, :error, t_equipment("Agent not found"))}
end
end
@ -484,6 +419,76 @@ defmodule ToweropsWeb.AgentLive.Index do
|> assign(:offline_agents, offline_agents)
end
defp agent_authorized_for_deletion?(socket, agent_token) do
organization = socket.assigns.current_scope.organization
is_superuser = Scope.superuser?(socket.assigns.current_scope)
is_cloud_poller = is_nil(agent_token.organization_id)
belongs_to_org = agent_token.organization_id == organization.id
belongs_to_org or (is_superuser and is_cloud_poller)
end
defp handle_agent_deletion(socket, id) do
case Agents.delete_agent_token(id) do
{:ok, _} ->
handle_agent_deletion_success(socket, id)
{:error, _} ->
{:noreply, put_flash(socket, :error, t_equipment("Failed to delete agent"))}
end
end
defp handle_agent_deletion_success(socket, deleted_id) do
maybe_clear_global_default(socket, deleted_id)
organization = socket.assigns.current_scope.organization
agent_tokens = Agents.list_organization_agent_tokens(organization.id)
cloud_pollers = load_cloud_pollers_if_superuser(socket.assigns.current_scope)
global_default_cloud_poller_id = reload_global_default(socket.assigns.current_scope, socket.assigns)
equipment_counts = calculate_device_counts(agent_tokens)
cloud_poller_counts = calculate_device_counts(cloud_pollers)
agent_health_stats = Stats.get_organization_agent_health(organization.id)
assignment_breakdown = Stats.get_device_assignment_breakdown(organization.id)
offline_agents = Stats.get_offline_agents(organization.id)
{:noreply,
socket
|> stream(:agent_tokens, agent_tokens, reset: true)
|> assign(:has_agents, agent_tokens != [])
|> stream(:cloud_pollers, cloud_pollers, reset: true)
|> assign(:cloud_pollers_list, cloud_pollers)
|> assign(:has_cloud_pollers, cloud_pollers != [])
|> assign(:global_default_cloud_poller_id, global_default_cloud_poller_id)
|> assign(:selected_global_default, global_default_cloud_poller_id || "")
|> assign(:device_counts, equipment_counts)
|> assign(:cloud_poller_counts, cloud_poller_counts)
|> assign(:agent_health_stats, agent_health_stats)
|> assign(:assignment_breakdown, assignment_breakdown)
|> assign(:offline_agents, offline_agents)
|> put_flash(
:info,
t_equipment("Agent deleted successfully. Devices now fall back to site/org defaults or cloud polling.")
)}
end
defp maybe_clear_global_default(socket, deleted_id) do
if Scope.superuser?(socket.assigns.current_scope) &&
socket.assigns.global_default_cloud_poller_id == deleted_id do
Settings.set_global_default_cloud_poller(nil)
end
end
defp reload_global_default(scope, assigns) do
if Scope.superuser?(scope) do
Settings.get_global_default_cloud_poller()
else
assigns.global_default_cloud_poller_id
end
end
@impl true
def terminate(_reason, socket) do
if timer_ref = socket.assigns[:timer_ref] do

View file

@ -286,8 +286,8 @@
data-topology={Jason.encode!(@topology)}
>
</div>
<!-- Zoom Controls -->
<!-- Zoom Controls -->
<div class="absolute bottom-4 left-4 flex flex-col gap-1 z-10">
<button
id="cy-zoom-in"

View file

@ -32,6 +32,7 @@ defmodule ToweropsWeb.Plugs.BruteForceProtection do
import Plug.Conn
alias Towerops.Security.BruteForce
alias Towerops.Security.FourOhFourTracker
alias ToweropsWeb.RemoteIp
require Logger
@ -58,23 +59,33 @@ defmodule ToweropsWeb.Plugs.BruteForceProtection do
true ->
case BruteForce.check_ban_status(ip_address) do
:allowed ->
# Register a callback to track 404s after the response is sent
register_before_send(conn, fn response_conn ->
if response_conn.status == 404 do
Towerops.Security.FourOhFourTracker.record_404(ip_address, conn.request_path)
end
response_conn
end)
register_404_tracker(conn, ip_address)
{:blocked, banned_until} ->
Logger.warning("Blocked request from banned IP: #{ip_address}, banned until: #{inspect(banned_until)}")
conn
|> put_resp_content_type("text/plain")
|> send_resp(403, "Access denied")
|> halt()
handle_banned_request(conn, ip_address, banned_until)
end
end
end
defp register_404_tracker(conn, ip_address) do
register_before_send(conn, fn response_conn ->
track_404_if_needed(response_conn, ip_address, conn.request_path)
response_conn
end)
end
defp track_404_if_needed(response_conn, ip_address, request_path) do
if response_conn.status == 404 do
FourOhFourTracker.record_404(ip_address, request_path)
end
end
defp handle_banned_request(conn, ip_address, banned_until) do
Logger.warning("Blocked request from banned IP: #{ip_address}, banned until: #{inspect(banned_until)}")
conn
|> put_resp_content_type("text/plain")
|> send_resp(403, "Access denied")
|> halt()
end
end

View file

@ -132,10 +132,10 @@ defmodule Towerops.Accounts.UserRecoveryCodeTest do
assert code.used_at == nil
changeset = UserRecoveryCode.use_changeset(code)
assert changeset.changes.used_at != nil
assert changeset.changes.used_at
{:ok, used_code} = Repo.update(changeset)
assert used_code.used_at != nil
assert used_code.used_at
end
end
end

View file

@ -11,32 +11,38 @@ defmodule Towerops.Monitoring.Executors.TcpExecutorTest do
{:ok, listen_socket} = :gen_tcp.listen(0, [:binary, active: false, reuseaddr: true])
{:ok, port} = :inet.port(listen_socket)
pid =
spawn(fn ->
case :gen_tcp.accept(listen_socket, 5000) do
{:ok, client} ->
if response do
# Wait for data then respond
case :gen_tcp.recv(client, 0, 5000) do
{:ok, _data} -> :gen_tcp.send(client, response)
_ -> :ok
end
end
# Keep socket open briefly
Process.sleep(100)
:gen_tcp.close(client)
_ ->
:ok
end
:gen_tcp.close(listen_socket)
end)
pid = spawn(fn -> run_tcp_server(listen_socket, response) end)
{port, pid}
end
defp run_tcp_server(listen_socket, response) do
case :gen_tcp.accept(listen_socket, 5000) do
{:ok, client} ->
handle_tcp_client(client, response)
_ ->
:ok
end
:gen_tcp.close(listen_socket)
end
defp handle_tcp_client(client, nil) do
Process.sleep(100)
:gen_tcp.close(client)
end
defp handle_tcp_client(client, response) do
case :gen_tcp.recv(client, 0, 5000) do
{:ok, _data} -> :gen_tcp.send(client, response)
_ -> :ok
end
Process.sleep(100)
:gen_tcp.close(client)
end
describe "execute/2 successful connection" do
test "returns ok when port is open" do
{port, _pid} = start_tcp_server()
@ -61,7 +67,9 @@ defmodule Towerops.Monitoring.Executors.TcpExecutorTest do
# Use a non-routable IP to force timeout
config = %{"host" => "192.0.2.1", "port" => 80}
assert {:error, reason} = TcpExecutor.execute(config, 500)
assert String.contains?(reason, "timeout") or String.contains?(reason, "unreachable") or String.contains?(reason, "failed")
assert String.contains?(reason, "timeout") or String.contains?(reason, "unreachable") or
String.contains?(reason, "failed")
end
test "returns error for DNS resolution failure" do

View file

@ -1,13 +1,13 @@
defmodule Towerops.MonitoringTest do
use Towerops.DataCase
import Towerops.AccountsFixtures
import Towerops.OrganizationsFixtures
alias Towerops.Monitoring
alias Towerops.Monitoring.Check
alias Towerops.Monitoring.CheckResult
import Towerops.AccountsFixtures
import Towerops.OrganizationsFixtures
setup do
user = user_fixture()
organization = organization_fixture(user.id)
@ -190,7 +190,7 @@ defmodule Towerops.MonitoringTest do
})
)
assert check.check_key != nil
assert check.check_key
assert String.length(check.check_key) > 10
end
@ -236,7 +236,7 @@ defmodule Towerops.MonitoringTest do
attrs = %{
check_id: check.id,
organization_id: org.id,
checked_at: DateTime.utc_now() |> DateTime.truncate(:second),
checked_at: DateTime.truncate(DateTime.utc_now(), :second),
status: 0,
output: "HTTP 200 OK",
response_time_ms: 42.5,
@ -256,7 +256,7 @@ defmodule Towerops.MonitoringTest do
describe "get_latest_check_result/1" do
test "returns the most recent result", %{organization: org} do
{:ok, check} = Monitoring.create_check(valid_check_attrs(org.id))
now = DateTime.utc_now() |> DateTime.truncate(:second)
now = DateTime.truncate(DateTime.utc_now(), :second)
{:ok, _old} =
Monitoring.create_check_result(%{
@ -286,7 +286,7 @@ defmodule Towerops.MonitoringTest do
describe "get_check_results/2" do
test "returns results within time range", %{organization: org} do
{:ok, check} = Monitoring.create_check(valid_check_attrs(org.id))
now = DateTime.utc_now() |> DateTime.truncate(:second)
now = DateTime.truncate(DateTime.utc_now(), :second)
{:ok, in_range} =
Monitoring.create_check_result(%{
@ -316,7 +316,7 @@ defmodule Towerops.MonitoringTest do
test "respects limit", %{organization: org} do
{:ok, check} = Monitoring.create_check(valid_check_attrs(org.id))
now = DateTime.utc_now() |> DateTime.truncate(:second)
now = DateTime.truncate(DateTime.utc_now(), :second)
for i <- 1..5 do
Monitoring.create_check_result(%{
@ -365,18 +365,13 @@ defmodule Towerops.MonitoringTest do
describe "get_device_health_summary/1" do
test "returns health summary for devices with checks", %{organization: org, device: device} do
alias Towerops.Monitoring.Check
now = DateTime.utc_now() |> DateTime.truncate(:second)
now = DateTime.truncate(DateTime.utc_now(), :second)
{:ok, check} =
Monitoring.create_check(
valid_check_attrs(org.id, %{device_id: device.id})
)
Monitoring.create_check(valid_check_attrs(org.id, %{device_id: device.id}))
# Use Repo.update_all since Check.changeset doesn't cast current_state
from(c in Check, where: c.id == ^check.id)
|> Repo.update_all(set: [current_state: 0, last_check_at: now])
Repo.update_all(from(c in Check, where: c.id == ^check.id), set: [current_state: 0, last_check_at: now])
summary = Monitoring.get_device_health_summary([device.id])
assert Map.has_key?(summary, device.id)
assert summary[device.id].worst_status == 0
@ -387,25 +382,15 @@ defmodule Towerops.MonitoringTest do
end
test "returns worst status across multiple checks", %{organization: org, device: device} do
alias Towerops.Monitoring.Check
{:ok, check1} =
Monitoring.create_check(
valid_check_attrs(org.id, %{name: "OK Check", device_id: device.id})
)
Monitoring.create_check(valid_check_attrs(org.id, %{name: "OK Check", device_id: device.id}))
{:ok, check2} =
Monitoring.create_check(
valid_check_attrs(org.id, %{name: "Crit Check", device_id: device.id})
)
Monitoring.create_check(valid_check_attrs(org.id, %{name: "Crit Check", device_id: device.id}))
# Use Repo.update_all since Check.changeset doesn't cast current_state
from(c in Check, where: c.id == ^check1.id)
|> Repo.update_all(set: [current_state: 0])
from(c in Check, where: c.id == ^check2.id)
|> Repo.update_all(set: [current_state: 2])
Repo.update_all(from(c in Check, where: c.id == ^check1.id), set: [current_state: 0])
Repo.update_all(from(c in Check, where: c.id == ^check2.id), set: [current_state: 2])
summary = Monitoring.get_device_health_summary([device.id])
assert summary[device.id].worst_status == 2
end
@ -414,14 +399,10 @@ defmodule Towerops.MonitoringTest do
describe "disable_device_checks/1" do
test "disables all checks for a device", %{organization: org, device: device} do
{:ok, _check1} =
Monitoring.create_check(
valid_check_attrs(org.id, %{name: "Check 1", device_id: device.id})
)
Monitoring.create_check(valid_check_attrs(org.id, %{name: "Check 1", device_id: device.id}))
{:ok, _check2} =
Monitoring.create_check(
valid_check_attrs(org.id, %{name: "Check 2", device_id: device.id})
)
Monitoring.create_check(valid_check_attrs(org.id, %{name: "Check 2", device_id: device.id}))
assert :ok = Monitoring.disable_device_checks(device.id)
@ -436,7 +417,7 @@ defmodule Towerops.MonitoringTest do
device_id: device.id,
status: "up",
response_time_ms: 12.5,
checked_at: DateTime.utc_now() |> DateTime.truncate(:second)
checked_at: DateTime.truncate(DateTime.utc_now(), :second)
}
assert {:ok, mc} = Monitoring.create_monitoring_check(attrs)
@ -451,7 +432,7 @@ defmodule Towerops.MonitoringTest do
describe "get_device_latest_response_times/1" do
test "returns latest response time per device", %{device: device} do
now = DateTime.utc_now() |> DateTime.truncate(:second)
now = DateTime.truncate(DateTime.utc_now(), :second)
Monitoring.create_monitoring_check(%{
device_id: device.id,

View file

@ -3,6 +3,7 @@ defmodule Towerops.OnCall.NotifierTest do
import Swoosh.TestAssertions
alias Swoosh.Email.Recipient
alias Towerops.OnCall.Notifier
describe "notify/2" do
@ -57,7 +58,7 @@ defmodule Towerops.OnCall.NotifierTest do
Application.get_env(:towerops, :mailer_from, {"Towerops", "hi@towerops.net"})
assert_email_sent(fn email ->
assert email.from == Swoosh.Email.Recipient.format(from_address)
assert email.from == Recipient.format(from_address)
end)
end

View file

@ -62,7 +62,7 @@ defmodule Towerops.Security.BruteForceTest do
assert {:ok, block} = BruteForce.create_or_escalate_ban(ip)
assert block.offense_count == 1
assert block.ip_address == ip
assert block.banned_until != nil
assert block.banned_until
# Should be ~5 minutes from now
diff = DateTime.diff(block.banned_until, DateTime.utc_now(), :second)
@ -77,7 +77,7 @@ defmodule Towerops.Security.BruteForceTest do
assert block.offense_count == 2
# banned_until is stored as a string in escalation, but the field is utc_datetime
# so it gets cast — let's check it's roughly 1 hour
assert block.banned_until != nil
assert block.banned_until
end
test "escalates to permanent ban on third offense" do
@ -98,8 +98,8 @@ defmodule Towerops.Security.BruteForceTest do
|> IpBlock.changeset(%{
ip_address: ip,
offense_count: 2,
banned_until: DateTime.add(DateTime.utc_now(), -86400, :second),
last_violation_at: DateTime.add(DateTime.utc_now(), -31 * 86400, :second)
banned_until: DateTime.add(DateTime.utc_now(), -86_400, :second),
last_violation_at: DateTime.add(DateTime.utc_now(), -31 * 86_400, :second)
})
|> Repo.insert!()
@ -287,8 +287,8 @@ defmodule Towerops.Security.BruteForceTest do
|> IpBlock.changeset(%{
ip_address: "100.100.200.1",
offense_count: 1,
banned_until: DateTime.add(now, -100 * 86400, :second),
last_violation_at: DateTime.add(now, -100 * 86400, :second)
banned_until: DateTime.add(now, -100 * 86_400, :second),
last_violation_at: DateTime.add(now, -100 * 86_400, :second)
})
|> Repo.insert!()
@ -304,7 +304,7 @@ defmodule Towerops.Security.BruteForceTest do
assert :ok = BruteForce.delete_stale_violations()
ips = BruteForce.list_blocked_ips(:all) |> Enum.map(& &1.ip_address)
ips = :all |> BruteForce.list_blocked_ips() |> Enum.map(& &1.ip_address)
refute "100.100.200.1" in ips
assert "100.100.200.2" in ips
end

View file

@ -55,12 +55,12 @@ defmodule Towerops.Security.FourOhFourTrackerTest do
describe "record_404/2" do
test "records a single 404 and returns :ok" do
ip = "198.51.100.#{System.unique_integer([:positive]) |> rem(255)}"
ip = "198.51.100.#{[:positive] |> System.unique_integer() |> rem(255)}"
assert FourOhFourTracker.record_404(ip, "/admin") == :ok
end
test "records multiple unique paths without triggering threshold" do
ip = "198.51.100.#{System.unique_integer([:positive]) |> rem(255)}"
ip = "198.51.100.#{[:positive] |> System.unique_integer() |> rem(255)}"
for i <- 1..4 do
result = FourOhFourTracker.record_404(ip, "/path-#{i}")
@ -69,7 +69,7 @@ defmodule Towerops.Security.FourOhFourTrackerTest do
end
test "triggers threshold at 5 unique 404s" do
ip = "198.51.100.#{System.unique_integer([:positive]) |> rem(255)}"
ip = "198.51.100.#{[:positive] |> System.unique_integer() |> rem(255)}"
for i <- 1..4 do
FourOhFourTracker.record_404(ip, "/scan-#{i}")
@ -80,7 +80,7 @@ defmodule Towerops.Security.FourOhFourTrackerTest do
end
test "duplicate paths don't count toward threshold" do
ip = "198.51.100.#{System.unique_integer([:positive]) |> rem(255)}"
ip = "198.51.100.#{[:positive] |> System.unique_integer() |> rem(255)}"
# Record same path multiple times
for _ <- 1..10 do
@ -92,8 +92,8 @@ defmodule Towerops.Security.FourOhFourTrackerTest do
end
test "different IPs track independently" do
ip1 = "198.51.100.#{System.unique_integer([:positive]) |> rem(255)}"
ip2 = "198.51.101.#{System.unique_integer([:positive]) |> rem(255)}"
ip1 = "198.51.100.#{[:positive] |> System.unique_integer() |> rem(255)}"
ip2 = "198.51.101.#{[:positive] |> System.unique_integer() |> rem(255)}"
for i <- 1..3 do
FourOhFourTracker.record_404(ip1, "/path-#{i}")
@ -112,7 +112,7 @@ defmodule Towerops.Security.FourOhFourTrackerTest do
end
test "returns correct count after recording paths" do
ip = "198.51.100.#{System.unique_integer([:positive]) |> rem(255)}"
ip = "198.51.100.#{[:positive] |> System.unique_integer() |> rem(255)}"
FourOhFourTracker.record_404(ip, "/a")
FourOhFourTracker.record_404(ip, "/b")

View file

@ -164,7 +164,7 @@ defmodule ToweropsWeb.Api.V1.PagerdutyWebhookControllerTest do
title: "Test Alert",
severity: 1,
alert_type: "device_down",
triggered_at: DateTime.utc_now() |> DateTime.truncate(:second),
triggered_at: DateTime.truncate(DateTime.utc_now(), :second),
device_id: device.id,
organization_id: org.id
})
@ -185,7 +185,7 @@ defmodule ToweropsWeb.Api.V1.PagerdutyWebhookControllerTest do
# Verify the alert was resolved
updated_alert = Towerops.Alerts.get_alert(alert.id)
assert updated_alert.resolved_at != nil
assert updated_alert.resolved_at
end
test "handles resolve for non-existent alert id gracefully", %{conn: conn, org: org} do
@ -224,7 +224,7 @@ defmodule ToweropsWeb.Api.V1.PagerdutyWebhookControllerTest do
title: "Test Alert",
severity: 2,
alert_type: "device_down",
triggered_at: DateTime.utc_now() |> DateTime.truncate(:second),
triggered_at: DateTime.truncate(DateTime.utc_now(), :second),
device_id: device.id,
organization_id: org.id
})
@ -244,7 +244,7 @@ defmodule ToweropsWeb.Api.V1.PagerdutyWebhookControllerTest do
assert json_response(conn, 200)["status"] == "accepted"
updated_alert = Towerops.Alerts.get_alert(alert.id)
assert updated_alert.acknowledged_at != nil
assert updated_alert.acknowledged_at
end
end
end

View file

@ -83,7 +83,7 @@ defmodule ToweropsWeb.InvitationControllerTest do
# Mark as accepted
invitation
|> Ecto.Changeset.change(%{accepted_at: DateTime.utc_now() |> DateTime.truncate(:second)})
|> Ecto.Changeset.change(%{accepted_at: DateTime.truncate(DateTime.utc_now(), :second)})
|> Towerops.Repo.update!()
conn = get(conn, ~p"/invitations/#{invitation.token}")

View file

@ -8,10 +8,10 @@ defmodule ToweropsWeb.OnboardingLiveTest do
use ToweropsWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Towerops.AccountsFixtures
import Towerops.OrganizationsFixtures
alias Phoenix.LiveView.Socket
alias ToweropsWeb.OnboardingLive
setup do
@ -33,7 +33,7 @@ defmodule ToweropsWeb.OnboardingLiveTest do
describe "mount/3" do
test "initializes with correct default state", %{user: user, organization: organization} do
socket =
%Phoenix.LiveView.Socket{
%Socket{
assigns: %{
__changed__: %{},
current_scope: %{user: user, organization: organization},
@ -50,15 +50,16 @@ defmodule ToweropsWeb.OnboardingLiveTest do
assert socket.assigns.selected_provider == nil
assert socket.assigns.agent_token == nil
assert socket.assigns.agent_token_value == nil
assert socket.assigns.page_title != nil
assert socket.assigns.page_title
end
end
describe "handle_event skip" do
test "skip advances from snmp to site", %{user: user, organization: organization} do
socket =
build_socket(user, organization)
|> Map.put(:assigns, Map.merge(build_socket(user, organization).assigns, %{step: :snmp}))
user
|> build_socket(organization)
|> Map.put(:assigns, Map.put(build_socket(user, organization).assigns, :step, :snmp))
assert {:noreply, socket} = OnboardingLive.handle_event("skip", %{}, socket)
assert socket.assigns.step == :site
@ -96,7 +97,7 @@ defmodule ToweropsWeb.OnboardingLiveTest do
assert {:noreply, socket} = OnboardingLive.handle_event("select_provider", %{"provider" => "sonar"}, socket)
assert socket.assigns.selected_provider == "sonar"
assert socket.assigns.integration_form != nil
assert socket.assigns.integration_form
end
end
@ -105,7 +106,7 @@ defmodule ToweropsWeb.OnboardingLiveTest do
changeset = Towerops.Organizations.change_organization(organization)
site_changeset = Towerops.Sites.change_site(%Towerops.Sites.Site{}, %{})
%Phoenix.LiveView.Socket{
%Socket{
assigns: %{
__changed__: %{},
current_scope: %{user: user, organization: organization},