## Summary - **Ping checks never scheduled**: `ensure_default_ping_check` created check records but never called `schedule_check`, so auto-created ping checks never executed - **Skipped checks permanently orphaned**: `CheckExecutorWorker` only rescheduled checks that actually executed — skipped checks (agent-assigned, SNMP disabled) broke the scheduling chain permanently - **No recovery mechanism**: `JobHealthCheckWorker` only recovered `DeviceMonitorWorker` jobs, not `CheckExecutorWorker` jobs — any lost check job was gone forever ## Changes - `monitoring.ex`: Call `schedule_check` after creating ping check in `ensure_default_ping_check` - `check_executor_worker.ex`: Reschedule skipped-but-enabled checks so conditions are re-evaluated next cycle - `job_health_check_worker.ex`: Recover orphaned `CheckExecutorWorker` jobs for enabled checks (runs every 10 min) - `devices.ex`: Wrap `ensure_default_ping_check` in try/rescue for deadlock resilience ## Test plan - [x] New tests: ping check scheduling on creation, no duplicate scheduling on idempotent call - [x] Updated tests: skipped checks now assert rescheduling instead of orphaning - [x] New tests: health check worker recovers orphaned check executor jobs, skips disabled checks - [x] All 177 targeted tests pass - [x] Compile clean with warnings-as-errors - [x] Pre-commit hooks pass (credo, format) Reviewed-on: graham/towerops-web#71
628 lines
22 KiB
Elixir
628 lines
22 KiB
Elixir
defmodule Towerops.MonitoringTest do
|
|
use Towerops.DataCase
|
|
|
|
import Ecto.Query
|
|
import Towerops.AccountsFixtures
|
|
import Towerops.AgentsFixtures
|
|
import Towerops.OrganizationsFixtures
|
|
|
|
alias Towerops.Monitoring
|
|
alias Towerops.Monitoring.Check
|
|
alias Towerops.Monitoring.CheckResult
|
|
alias Towerops.Repo
|
|
|
|
setup do
|
|
user = user_fixture()
|
|
organization = organization_fixture(user.id)
|
|
site = site_fixture(organization.id)
|
|
|
|
{:ok, device} =
|
|
Towerops.Devices.create_device(
|
|
%{
|
|
name: "Test Device",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
},
|
|
bypass_limits: true
|
|
)
|
|
|
|
%{organization: organization, device: device, user: user, site: site}
|
|
end
|
|
|
|
defp valid_check_attrs(organization_id, opts \\ %{}) do
|
|
Map.merge(
|
|
%{
|
|
name: "Test HTTP Check",
|
|
check_type: "http",
|
|
organization_id: organization_id,
|
|
config: %{"url" => "https://example.com"},
|
|
interval_seconds: 60,
|
|
max_check_attempts: 3,
|
|
timeout_ms: 5000
|
|
},
|
|
opts
|
|
)
|
|
end
|
|
|
|
describe "list_checks/2" do
|
|
test "returns checks for organization", %{organization: org} do
|
|
{:ok, check} = Monitoring.create_check(valid_check_attrs(org.id))
|
|
checks = Monitoring.list_checks(org.id)
|
|
assert Enum.any?(checks, &(&1.id == check.id))
|
|
end
|
|
|
|
test "does not return checks from other organizations", %{organization: org} do
|
|
other_user = user_fixture()
|
|
other_org = organization_fixture(other_user.id)
|
|
{:ok, _check} = Monitoring.create_check(valid_check_attrs(other_org.id))
|
|
|
|
# Only the auto-created ping check from setup device should be present
|
|
checks = Monitoring.list_checks(org.id)
|
|
assert Enum.all?(checks, &(&1.organization_id == org.id))
|
|
refute Enum.any?(checks, &(&1.check_type == "http"))
|
|
end
|
|
|
|
test "filters by device_id", %{organization: org, device: device} do
|
|
{:ok, check_with_device} =
|
|
Monitoring.create_check(valid_check_attrs(org.id, %{device_id: device.id}))
|
|
|
|
{:ok, _check_without} = Monitoring.create_check(valid_check_attrs(org.id, %{name: "Other"}))
|
|
|
|
checks = Monitoring.list_checks(org.id, device_id: device.id)
|
|
assert Enum.any?(checks, &(&1.id == check_with_device.id))
|
|
refute Enum.any?(checks, &(&1.name == "Other"))
|
|
end
|
|
|
|
test "filters by check_type", %{organization: org} do
|
|
{:ok, _http} = Monitoring.create_check(valid_check_attrs(org.id))
|
|
|
|
{:ok, dns} =
|
|
Monitoring.create_check(
|
|
valid_check_attrs(org.id, %{
|
|
name: "DNS Check",
|
|
check_type: "dns",
|
|
config: %{"hostname" => "example.com"}
|
|
})
|
|
)
|
|
|
|
assert [found] = Monitoring.list_checks(org.id, check_type: "dns")
|
|
assert found.id == dns.id
|
|
end
|
|
|
|
test "filters by enabled", %{organization: org} do
|
|
{:ok, enabled} = Monitoring.create_check(valid_check_attrs(org.id))
|
|
|
|
{:ok, _disabled} =
|
|
Monitoring.create_check(valid_check_attrs(org.id, %{name: "Disabled", enabled: false}))
|
|
|
|
enabled_checks = Monitoring.list_checks(org.id, enabled: true)
|
|
assert Enum.any?(enabled_checks, &(&1.id == enabled.id))
|
|
refute Enum.any?(enabled_checks, &(&1.name == "Disabled"))
|
|
end
|
|
|
|
test "orders by name ascending", %{organization: org} do
|
|
{:ok, _b} = Monitoring.create_check(valid_check_attrs(org.id, %{name: "Bravo"}))
|
|
{:ok, _a} = Monitoring.create_check(valid_check_attrs(org.id, %{name: "Alpha"}))
|
|
{:ok, _c} = Monitoring.create_check(valid_check_attrs(org.id, %{name: "Charlie"}))
|
|
|
|
names = org.id |> Monitoring.list_checks() |> Enum.map(& &1.name)
|
|
assert names == Enum.sort(names)
|
|
assert "Alpha" in names
|
|
assert "Bravo" in names
|
|
assert "Charlie" in names
|
|
end
|
|
end
|
|
|
|
describe "list_checks_for_agent/2" do
|
|
test "returns service checks assigned to an agent", %{organization: org} do
|
|
{:ok, agent_token, _token_string} = agent_token_fixture(org.id)
|
|
|
|
{:ok, http_check} =
|
|
Monitoring.create_check(
|
|
valid_check_attrs(org.id, %{name: "HTTP Check", check_type: "http", agent_token_id: agent_token.id})
|
|
)
|
|
|
|
{:ok, tcp_check} =
|
|
Monitoring.create_check(
|
|
valid_check_attrs(org.id, %{
|
|
name: "TCP Check",
|
|
check_type: "tcp",
|
|
config: %{"host" => "10.0.0.1", "port" => 80},
|
|
agent_token_id: agent_token.id
|
|
})
|
|
)
|
|
|
|
checks = Monitoring.list_checks_for_agent(agent_token.id)
|
|
ids = Enum.map(checks, & &1.id)
|
|
assert http_check.id in ids
|
|
assert tcp_check.id in ids
|
|
end
|
|
|
|
test "excludes non-service check types", %{organization: org} do
|
|
{:ok, agent_token, _token_string} = agent_token_fixture(org.id)
|
|
|
|
{:ok, _snmp} =
|
|
Monitoring.create_check(
|
|
valid_check_attrs(org.id, %{
|
|
name: "SNMP Sensor",
|
|
check_type: "snmp_sensor",
|
|
config: %{"oid" => "1.3.6.1"},
|
|
agent_token_id: agent_token.id
|
|
})
|
|
)
|
|
|
|
{:ok, _http} =
|
|
Monitoring.create_check(valid_check_attrs(org.id, %{name: "HTTP Check", agent_token_id: agent_token.id}))
|
|
|
|
checks = Monitoring.list_checks_for_agent(agent_token.id)
|
|
assert length(checks) == 1
|
|
assert hd(checks).check_type == "http"
|
|
end
|
|
|
|
test "filters by enabled", %{organization: org} do
|
|
{:ok, agent_token, _token_string} = agent_token_fixture(org.id)
|
|
|
|
{:ok, _enabled} =
|
|
Monitoring.create_check(valid_check_attrs(org.id, %{name: "Enabled", agent_token_id: agent_token.id}))
|
|
|
|
{:ok, _disabled} =
|
|
Monitoring.create_check(
|
|
valid_check_attrs(org.id, %{name: "Disabled", enabled: false, agent_token_id: agent_token.id})
|
|
)
|
|
|
|
checks = Monitoring.list_checks_for_agent(agent_token.id, enabled: true)
|
|
assert length(checks) == 1
|
|
assert hd(checks).name == "Enabled"
|
|
end
|
|
|
|
test "does not return checks for other agents", %{organization: org} do
|
|
{:ok, agent1, _} = agent_token_fixture(org.id)
|
|
{:ok, agent2, _} = agent_token_fixture(org.id)
|
|
|
|
{:ok, _check} =
|
|
Monitoring.create_check(valid_check_attrs(org.id, %{name: "Agent 1 Check", agent_token_id: agent1.id}))
|
|
|
|
assert Monitoring.list_checks_for_agent(agent2.id) == []
|
|
end
|
|
end
|
|
|
|
describe "get_check/1 and get_check!/1" do
|
|
test "returns the check", %{organization: org} do
|
|
{:ok, check} = Monitoring.create_check(valid_check_attrs(org.id))
|
|
assert Monitoring.get_check(check.id).id == check.id
|
|
assert Monitoring.get_check!(check.id).id == check.id
|
|
end
|
|
|
|
test "get_check/1 returns nil for missing id" do
|
|
assert Monitoring.get_check(Ecto.UUID.generate()) == nil
|
|
end
|
|
|
|
test "get_check!/1 raises for missing id" do
|
|
assert_raise Ecto.NoResultsError, fn ->
|
|
Monitoring.get_check!(Ecto.UUID.generate())
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "get_check_by_key/1" do
|
|
test "returns passive check by key", %{organization: org} do
|
|
{:ok, check} =
|
|
Monitoring.create_check(
|
|
valid_check_attrs(org.id, %{
|
|
name: "Passive Check",
|
|
check_type: "passive",
|
|
config: %{}
|
|
})
|
|
)
|
|
|
|
assert found = Monitoring.get_check_by_key(check.check_key)
|
|
assert found.id == check.id
|
|
end
|
|
|
|
test "returns nil for non-existent key" do
|
|
assert Monitoring.get_check_by_key("nonexistent") == nil
|
|
end
|
|
end
|
|
|
|
describe "create_check/1" do
|
|
test "creates with valid attrs", %{organization: org} do
|
|
assert {:ok, %Check{} = check} = Monitoring.create_check(valid_check_attrs(org.id))
|
|
assert check.name == "Test HTTP Check"
|
|
assert check.check_type == "http"
|
|
assert check.config == %{"url" => "https://example.com"}
|
|
assert check.enabled == true
|
|
assert check.current_state == 3
|
|
assert check.current_state_type == "soft"
|
|
end
|
|
|
|
test "fails without required fields" do
|
|
assert {:error, %Ecto.Changeset{}} = Monitoring.create_check(%{})
|
|
end
|
|
|
|
test "validates check_type inclusion", %{organization: org} do
|
|
attrs = valid_check_attrs(org.id, %{check_type: "invalid"})
|
|
assert {:error, changeset} = Monitoring.create_check(attrs)
|
|
assert errors_on(changeset).check_type
|
|
end
|
|
|
|
test "validates HTTP config requires url", %{organization: org} do
|
|
attrs = valid_check_attrs(org.id, %{config: %{}})
|
|
assert {:error, changeset} = Monitoring.create_check(attrs)
|
|
assert errors_on(changeset).config
|
|
end
|
|
|
|
test "validates TCP config requires host and port", %{organization: org} do
|
|
attrs = valid_check_attrs(org.id, %{check_type: "tcp", config: %{}})
|
|
assert {:error, changeset} = Monitoring.create_check(attrs)
|
|
assert errors_on(changeset).config
|
|
end
|
|
|
|
test "validates DNS config requires hostname", %{organization: org} do
|
|
attrs = valid_check_attrs(org.id, %{check_type: "dns", config: %{}})
|
|
assert {:error, changeset} = Monitoring.create_check(attrs)
|
|
assert errors_on(changeset).config
|
|
end
|
|
|
|
test "generates check_key for passive checks", %{organization: org} do
|
|
{:ok, check} =
|
|
Monitoring.create_check(
|
|
valid_check_attrs(org.id, %{
|
|
name: "Passive",
|
|
check_type: "passive",
|
|
config: %{}
|
|
})
|
|
)
|
|
|
|
assert check.check_key
|
|
assert String.length(check.check_key) > 10
|
|
end
|
|
|
|
test "validates positive interval_seconds", %{organization: org} do
|
|
attrs = valid_check_attrs(org.id, %{interval_seconds: 0})
|
|
assert {:error, changeset} = Monitoring.create_check(attrs)
|
|
assert errors_on(changeset).interval_seconds
|
|
end
|
|
end
|
|
|
|
describe "update_check/2" do
|
|
test "updates with valid attrs", %{organization: org} do
|
|
{:ok, check} = Monitoring.create_check(valid_check_attrs(org.id))
|
|
assert {:ok, updated} = Monitoring.update_check(check, %{name: "Updated Name"})
|
|
assert updated.name == "Updated Name"
|
|
end
|
|
|
|
test "fails with invalid attrs", %{organization: org} do
|
|
{:ok, check} = Monitoring.create_check(valid_check_attrs(org.id))
|
|
assert {:error, %Ecto.Changeset{}} = Monitoring.update_check(check, %{check_type: "invalid"})
|
|
end
|
|
end
|
|
|
|
describe "delete_check/1" do
|
|
test "deletes the check", %{organization: org} do
|
|
{:ok, check} = Monitoring.create_check(valid_check_attrs(org.id))
|
|
assert {:ok, _} = Monitoring.delete_check(check)
|
|
assert Monitoring.get_check(check.id) == nil
|
|
end
|
|
end
|
|
|
|
describe "change_check/2" do
|
|
test "returns a changeset", %{organization: org} do
|
|
{:ok, check} = Monitoring.create_check(valid_check_attrs(org.id))
|
|
assert %Ecto.Changeset{} = Monitoring.change_check(check)
|
|
end
|
|
end
|
|
|
|
describe "create_check_result/1" do
|
|
test "creates a valid check result", %{organization: org} do
|
|
{:ok, check} = Monitoring.create_check(valid_check_attrs(org.id))
|
|
|
|
attrs = %{
|
|
check_id: check.id,
|
|
organization_id: org.id,
|
|
checked_at: DateTime.truncate(DateTime.utc_now(), :second),
|
|
status: 0,
|
|
output: "HTTP 200 OK",
|
|
response_time_ms: 42.5,
|
|
value: 42.5
|
|
}
|
|
|
|
assert {:ok, %CheckResult{} = result} = Monitoring.create_check_result(attrs)
|
|
assert result.status == 0
|
|
assert result.output == "HTTP 200 OK"
|
|
end
|
|
|
|
test "fails without required fields" do
|
|
assert {:error, %Ecto.Changeset{}} = Monitoring.create_check_result(%{})
|
|
end
|
|
end
|
|
|
|
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.truncate(DateTime.utc_now(), :second)
|
|
|
|
{:ok, _old} =
|
|
Monitoring.create_check_result(%{
|
|
check_id: check.id,
|
|
organization_id: org.id,
|
|
checked_at: DateTime.add(now, -60, :second),
|
|
status: 0
|
|
})
|
|
|
|
{:ok, new} =
|
|
Monitoring.create_check_result(%{
|
|
check_id: check.id,
|
|
organization_id: org.id,
|
|
checked_at: now,
|
|
status: 2
|
|
})
|
|
|
|
assert Monitoring.get_latest_check_result(check.id).id == new.id
|
|
end
|
|
|
|
test "returns nil when no results exist", %{organization: org} do
|
|
{:ok, check} = Monitoring.create_check(valid_check_attrs(org.id))
|
|
assert Monitoring.get_latest_check_result(check.id) == nil
|
|
end
|
|
end
|
|
|
|
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.truncate(DateTime.utc_now(), :second)
|
|
|
|
{:ok, in_range} =
|
|
Monitoring.create_check_result(%{
|
|
check_id: check.id,
|
|
organization_id: org.id,
|
|
checked_at: now,
|
|
status: 0
|
|
})
|
|
|
|
{:ok, _out_of_range} =
|
|
Monitoring.create_check_result(%{
|
|
check_id: check.id,
|
|
organization_id: org.id,
|
|
checked_at: DateTime.add(now, -7200, :second),
|
|
status: 0
|
|
})
|
|
|
|
results =
|
|
Monitoring.get_check_results(check.id,
|
|
from: DateTime.add(now, -3600, :second),
|
|
to: now
|
|
)
|
|
|
|
assert length(results) == 1
|
|
assert hd(results).id == in_range.id
|
|
end
|
|
|
|
test "respects limit", %{organization: org} do
|
|
{:ok, check} = Monitoring.create_check(valid_check_attrs(org.id))
|
|
now = DateTime.truncate(DateTime.utc_now(), :second)
|
|
|
|
for i <- 1..5 do
|
|
Monitoring.create_check_result(%{
|
|
check_id: check.id,
|
|
organization_id: org.id,
|
|
checked_at: DateTime.add(now, -i, :second),
|
|
status: 0
|
|
})
|
|
end
|
|
|
|
results =
|
|
Monitoring.get_check_results(check.id,
|
|
from: DateTime.add(now, -3600, :second),
|
|
to: now,
|
|
limit: 3
|
|
)
|
|
|
|
assert length(results) == 3
|
|
end
|
|
end
|
|
|
|
describe "update_check_state/3" do
|
|
# NOTE: Check.changeset does not cast state fields (current_state,
|
|
# current_state_type, current_check_attempt, last_check_at, etc.).
|
|
# update_check_state computes correct transitions but the changeset
|
|
# silently drops them. These tests document the actual behavior.
|
|
|
|
test "succeeds but state fields are not persisted via changeset", %{organization: org} do
|
|
{:ok, check} = Monitoring.create_check(valid_check_attrs(org.id))
|
|
|
|
# The function returns {:ok, check} but state fields remain at defaults
|
|
assert {:ok, updated} = Monitoring.update_check_state(check, 0, "OK")
|
|
# current_state stays at 3 (default) because changeset doesn't cast it
|
|
assert updated.current_state == 3
|
|
assert updated.current_state_type == "soft"
|
|
end
|
|
|
|
test "does not crash on repeated calls", %{organization: org} do
|
|
{:ok, check} = Monitoring.create_check(valid_check_attrs(org.id))
|
|
|
|
assert {:ok, check} = Monitoring.update_check_state(check, 0, "OK")
|
|
assert {:ok, check} = Monitoring.update_check_state(check, 2, "CRITICAL")
|
|
assert {:ok, _check} = Monitoring.update_check_state(check, 0, "OK again")
|
|
end
|
|
end
|
|
|
|
describe "get_device_health_summary/1" do
|
|
test "returns health summary for devices with checks", %{organization: org, device: device} do
|
|
now = DateTime.truncate(DateTime.utc_now(), :second)
|
|
|
|
{:ok, _check} =
|
|
Monitoring.create_check(valid_check_attrs(org.id, %{device_id: device.id}))
|
|
|
|
# Set ALL device checks to state 0 (including auto-created ping check)
|
|
Repo.update_all(from(c in Check, where: c.device_id == ^device.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
|
|
end
|
|
|
|
test "returns empty map for empty device list" do
|
|
assert Monitoring.get_device_health_summary([]) == %{}
|
|
end
|
|
|
|
test "returns worst status across multiple checks", %{organization: org, device: device} do
|
|
{:ok, _check1} =
|
|
Monitoring.create_check(valid_check_attrs(org.id, %{name: "OK Check", device_id: device.id}))
|
|
|
|
{:ok, crit_check} =
|
|
Monitoring.create_check(valid_check_attrs(org.id, %{name: "Crit Check", device_id: device.id}))
|
|
|
|
# Set all device checks to OK first, then set crit_check to CRITICAL
|
|
Repo.update_all(from(c in Check, where: c.device_id == ^device.id), set: [current_state: 0])
|
|
Repo.update_all(from(c in Check, where: c.id == ^crit_check.id), set: [current_state: 2])
|
|
summary = Monitoring.get_device_health_summary([device.id])
|
|
assert summary[device.id].worst_status == 2
|
|
end
|
|
end
|
|
|
|
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}))
|
|
|
|
{:ok, _check2} =
|
|
Monitoring.create_check(valid_check_attrs(org.id, %{name: "Check 2", device_id: device.id}))
|
|
|
|
assert :ok = Monitoring.disable_device_checks(device.id)
|
|
|
|
checks = Monitoring.list_checks(org.id, device_id: device.id)
|
|
assert Enum.all?(checks, fn c -> c.enabled == false end)
|
|
end
|
|
end
|
|
|
|
describe "create_monitoring_check/1" do
|
|
test "creates a monitoring check record", %{device: device} do
|
|
attrs = %{
|
|
device_id: device.id,
|
|
status: "up",
|
|
response_time_ms: 12.5,
|
|
checked_at: DateTime.truncate(DateTime.utc_now(), :second)
|
|
}
|
|
|
|
assert {:ok, mc} = Monitoring.create_monitoring_check(attrs)
|
|
assert mc.status == "up"
|
|
assert mc.response_time_ms == 12.5
|
|
end
|
|
|
|
test "fails without required fields" do
|
|
assert {:error, %Ecto.Changeset{}} = Monitoring.create_monitoring_check(%{})
|
|
end
|
|
end
|
|
|
|
describe "get_device_latest_response_times/1" do
|
|
test "returns latest response time per device", %{device: device} do
|
|
now = DateTime.truncate(DateTime.utc_now(), :second)
|
|
|
|
Monitoring.create_monitoring_check(%{
|
|
device_id: device.id,
|
|
status: "up",
|
|
response_time_ms: 10.0,
|
|
checked_at: DateTime.add(now, -60, :second)
|
|
})
|
|
|
|
Monitoring.create_monitoring_check(%{
|
|
device_id: device.id,
|
|
status: "up",
|
|
response_time_ms: 20.0,
|
|
checked_at: now
|
|
})
|
|
|
|
result = Monitoring.get_device_latest_response_times([device.id])
|
|
assert Map.has_key?(result, device.id)
|
|
assert result[device.id].response_time_ms == 20.0
|
|
end
|
|
|
|
test "returns empty map for empty list" do
|
|
assert Monitoring.get_device_latest_response_times([]) == %{}
|
|
end
|
|
end
|
|
|
|
describe "get_latency_data/2" do
|
|
test "returns empty list (legacy stub)" do
|
|
assert Monitoring.get_latency_data(Ecto.UUID.generate(), []) == []
|
|
end
|
|
end
|
|
|
|
describe "ensure_default_ping_check/1" do
|
|
test "creates a ping check for a device that has none", %{organization: org, device: device} do
|
|
assert {:ok, check} = Monitoring.ensure_default_ping_check(device)
|
|
assert check.check_type == "ping"
|
|
assert check.name == "ICMP Ping"
|
|
assert check.source_type == "auto_discovery"
|
|
assert check.device_id == device.id
|
|
assert check.organization_id == org.id
|
|
assert check.config["host"] == device.ip_address
|
|
assert check.config["count"] == 3
|
|
assert check.enabled == true
|
|
assert check.interval_seconds == 60
|
|
assert check.timeout_ms == 5000
|
|
end
|
|
|
|
test "schedules a CheckExecutorWorker job when creating a new ping check", %{device: device} do
|
|
# Delete existing ping check created by device setup, and all jobs
|
|
Repo.delete_all(from(c in Check, where: c.device_id == ^device.id and c.check_type == "ping"))
|
|
Repo.delete_all(Oban.Job)
|
|
|
|
{:ok, check} = Monitoring.ensure_default_ping_check(device)
|
|
|
|
jobs =
|
|
Repo.all(
|
|
from(j in Oban.Job,
|
|
where: j.worker == "Towerops.Workers.CheckExecutorWorker",
|
|
where: fragment("args->>'check_id' = ?", ^check.id)
|
|
)
|
|
)
|
|
|
|
assert length(jobs) == 1
|
|
end
|
|
|
|
test "does not schedule duplicate job when check already exists", %{device: device} do
|
|
# Delete existing ping check created by device setup, and all jobs
|
|
Repo.delete_all(from(c in Check, where: c.device_id == ^device.id and c.check_type == "ping"))
|
|
Repo.delete_all(Oban.Job)
|
|
|
|
{:ok, _check} = Monitoring.ensure_default_ping_check(device)
|
|
{:ok, _check} = Monitoring.ensure_default_ping_check(device)
|
|
|
|
jobs =
|
|
Repo.all(
|
|
from(j in Oban.Job,
|
|
where: j.worker == "Towerops.Workers.CheckExecutorWorker"
|
|
)
|
|
)
|
|
|
|
# Only one job from the initial creation, not a second from the idempotent call
|
|
assert length(jobs) == 1
|
|
end
|
|
|
|
test "is idempotent - does not create duplicate", %{device: device} do
|
|
{:ok, first} = Monitoring.ensure_default_ping_check(device)
|
|
{:ok, second} = Monitoring.ensure_default_ping_check(device)
|
|
assert first.id == second.id
|
|
end
|
|
|
|
test "updates host config when device IP changed", %{device: device} do
|
|
{:ok, check} = Monitoring.ensure_default_ping_check(device)
|
|
assert check.config["host"] == "192.168.1.1"
|
|
|
|
# Simulate device with changed IP
|
|
updated_device = %{device | ip_address: "10.0.0.1"}
|
|
{:ok, updated_check} = Monitoring.ensure_default_ping_check(updated_device)
|
|
|
|
assert updated_check.id == check.id
|
|
assert updated_check.config["host"] == "10.0.0.1"
|
|
end
|
|
|
|
test "does not update check when IP is unchanged", %{device: device} do
|
|
{:ok, check} = Monitoring.ensure_default_ping_check(device)
|
|
{:ok, same_check} = Monitoring.ensure_default_ping_check(device)
|
|
assert same_check.id == check.id
|
|
assert same_check.updated_at == check.updated_at
|
|
end
|
|
end
|
|
end
|