From c7c9f893a5cc073e7bbacd51e2e53303b6acb458 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 8 May 2026 11:23:08 -0500 Subject: [PATCH] =?UTF-8?q?test:=20add=20ping/escalation=20tests;=20covera?= =?UTF-8?q?ge=2084.99%=20=E2=86=92=2085.13%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/towerops/monitoring/ping_test.exs | 19 ++++- test/towerops/on_call/escalation_test.exs | 95 +++++++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/test/towerops/monitoring/ping_test.exs b/test/towerops/monitoring/ping_test.exs index 0a5a6a54..352113e6 100644 --- a/test/towerops/monitoring/ping_test.exs +++ b/test/towerops/monitoring/ping_test.exs @@ -133,7 +133,6 @@ defmodule Towerops.Monitoring.PingTest do # correctly handles various response formats by testing with valid IPs # that return quickly (localhost) - @tag :integration test "localhost ping returns valid response format" do result = Ping.ping("127.0.0.1", 2000) @@ -149,6 +148,24 @@ defmodule Towerops.Monitoring.PingTest do end end + test "default-arity ping/1 dispatches to ping/2" do + # ping/1 calls ping(ip, 5000); we mostly want to exercise the arity + # without slowing the suite down for a real timeout. + result = Ping.ping("127.0.0.1") + + case result do + {:ok, response_time} -> assert is_float(response_time) + {:error, reason} -> assert reason in [:timeout, :command_not_found, :parse_error] + end + end + + test "unreachable IP returns :timeout error" do + # 192.0.2.0/24 is reserved for documentation per RFC 5737 — never routed. + result = Ping.ping("192.0.2.1", 1000) + # Either the OS resolves immediately (timeout) or the binary is missing. + assert match?({:error, :timeout}, result) or match?({:error, _}, result) + end + @tag :integration test "IPv6 localhost ping returns valid response format" do result = Ping.ping("::1", 2000) diff --git a/test/towerops/on_call/escalation_test.exs b/test/towerops/on_call/escalation_test.exs index a74f69b7..0f2876f7 100644 --- a/test/towerops/on_call/escalation_test.exs +++ b/test/towerops/on_call/escalation_test.exs @@ -9,6 +9,7 @@ defmodule Towerops.OnCall.EscalationTest do alias Towerops.Alerts alias Towerops.OnCall.Escalation + alias Towerops.OnCall.Incident setup do user = user_fixture() @@ -146,4 +147,98 @@ defmodule Towerops.OnCall.EscalationTest do assert Escalation.find_incident_for_alert(Ecto.UUID.generate()) == nil end end + + describe "check_and_escalate/1" do + test "is a no-op when the incident no longer exists" do + assert :ok = Escalation.check_and_escalate(Ecto.UUID.generate()) + end + + test "is a no-op for acknowledged incidents", ctx do + policy = escalation_policy_fixture(ctx.organization.id) + rule = escalation_rule_fixture(policy.id, %{position: 0, timeout_minutes: 5}) + escalation_target_fixture(rule.id, %{target_type: "user", user_id: ctx.user.id}) + + {:ok, alert} = + Alerts.create_alert(%{ + alert_type: "device_down", + device_id: ctx.device.id, + triggered_at: DateTime.utc_now(), + message: "Device is down" + }) + + {:ok, incident} = Escalation.start_escalation(alert, policy.id) + {:ok, _} = Escalation.acknowledge_incident(incident.id, ctx.user.id) + + assert :ok = Escalation.check_and_escalate(incident.id) + end + + test "advances to the next rule when current is not the last", ctx do + policy = escalation_policy_fixture(ctx.organization.id) + rule0 = escalation_rule_fixture(policy.id, %{position: 0, timeout_minutes: 1}) + _rule1 = escalation_rule_fixture(policy.id, %{position: 1, timeout_minutes: 1}) + + escalation_target_fixture(rule0.id, %{target_type: "user", user_id: ctx.user.id}) + + {:ok, alert} = + Alerts.create_alert(%{ + alert_type: "device_down", + device_id: ctx.device.id, + triggered_at: DateTime.utc_now(), + message: "Device is down" + }) + + {:ok, incident} = Escalation.start_escalation(alert, policy.id) + + assert :ok = Escalation.check_and_escalate(incident.id) + + reloaded = Towerops.Repo.get!(Incident, incident.id) + assert reloaded.current_rule_position == 1 + end + + test "loops back to first rule when current is last and repeat_count not exhausted", + ctx do + policy = + escalation_policy_fixture(ctx.organization.id, %{repeat_count: 2}) + + rule0 = escalation_rule_fixture(policy.id, %{position: 0, timeout_minutes: 1}) + escalation_target_fixture(rule0.id, %{target_type: "user", user_id: ctx.user.id}) + + {:ok, alert} = + Alerts.create_alert(%{ + alert_type: "device_down", + device_id: ctx.device.id, + triggered_at: DateTime.utc_now(), + message: "Device is down" + }) + + {:ok, incident} = Escalation.start_escalation(alert, policy.id) + + assert :ok = Escalation.check_and_escalate(incident.id) + + reloaded = Towerops.Repo.get!(Incident, incident.id) + assert reloaded.current_loop == 2 + assert reloaded.current_rule_position == 0 + end + + test "exhausts when current is last rule and loop count met", ctx do + policy = + escalation_policy_fixture(ctx.organization.id, %{repeat_count: 1}) + + rule0 = escalation_rule_fixture(policy.id, %{position: 0, timeout_minutes: 1}) + escalation_target_fixture(rule0.id, %{target_type: "user", user_id: ctx.user.id}) + + {:ok, alert} = + Alerts.create_alert(%{ + alert_type: "device_down", + device_id: ctx.device.id, + triggered_at: DateTime.utc_now(), + message: "Device is down" + }) + + {:ok, incident} = Escalation.start_escalation(alert, policy.id) + + # current_loop is already 1 == repeat_count, so further escalation is exhausted + assert :ok = Escalation.check_and_escalate(incident.id) + end + end end