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:
parent
a98b887471
commit
44fc017653
14 changed files with 196 additions and 195 deletions
|
|
@ -1 +1 @@
|
|||
/nix/store/4xlkmdd947h5qlhj2rmbyavq08grkz27-pre-commit-config.json
|
||||
/nix/store/zx636v118rzzqzwafgrw2aybh7l0szrl-pre-commit-config.json
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue