461 lines
14 KiB
Elixir
461 lines
14 KiB
Elixir
defmodule Towerops.MonitoringTest do
|
|
use Towerops.DataCase
|
|
|
|
alias Towerops.Monitoring
|
|
|
|
describe "monitoring_checks" do
|
|
import Towerops.AccountsFixtures
|
|
|
|
alias Towerops.Monitoring.Check
|
|
|
|
setup do
|
|
user = user_fixture()
|
|
{:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
|
|
|
|
{:ok, site} =
|
|
Towerops.Sites.create_site(%{
|
|
name: "Test Site",
|
|
organization_id: organization.id
|
|
})
|
|
|
|
{:ok, device} =
|
|
Towerops.Devices.create_device(%{
|
|
name: "Router 1",
|
|
ip_address: "192.168.1.1",
|
|
site_id: site.id
|
|
})
|
|
|
|
%{device: device}
|
|
end
|
|
|
|
@valid_attrs %{
|
|
status: :success,
|
|
response_time_ms: 50,
|
|
checked_at: ~U[2025-12-21 12:00:00Z]
|
|
}
|
|
|
|
test "create_check/1 with valid data creates a check", %{device: device} do
|
|
attrs = Map.put(@valid_attrs, :device_id, device.id)
|
|
assert {:ok, %Check{} = check} = Monitoring.create_check(attrs)
|
|
assert check.status == :success
|
|
assert check.response_time_ms == 50
|
|
end
|
|
|
|
test "create_check/1 with invalid data returns error changeset" do
|
|
assert {:error, %Ecto.Changeset{}} = Monitoring.create_check(%{})
|
|
end
|
|
|
|
test "list_devices_checks/2 returns checks for device", %{device: device} do
|
|
attrs = Map.put(@valid_attrs, :device_id, device.id)
|
|
{:ok, check} = Monitoring.create_check(attrs)
|
|
assert Monitoring.list_devices_checks(device.id) == [check]
|
|
end
|
|
|
|
test "list_devices_checks/2 limits results", %{device: device} do
|
|
# Create 10 checks
|
|
for i <- 1..10 do
|
|
attrs =
|
|
@valid_attrs
|
|
|> Map.put(:device_id, device.id)
|
|
|> Map.put(:checked_at, DateTime.add(~U[2025-12-21 12:00:00Z], i, :second))
|
|
|
|
{:ok, _check} = Monitoring.create_check(attrs)
|
|
end
|
|
|
|
assert length(Monitoring.list_devices_checks(device.id, 5)) == 5
|
|
end
|
|
|
|
test "get_latest_check/1 returns most recent check", %{device: device} do
|
|
attrs1 =
|
|
@valid_attrs
|
|
|> Map.put(:device_id, device.id)
|
|
|> Map.put(:checked_at, ~U[2025-12-21 12:00:00Z])
|
|
|
|
attrs2 =
|
|
@valid_attrs
|
|
|> Map.put(:device_id, device.id)
|
|
|> Map.put(:checked_at, ~U[2025-12-21 13:00:00Z])
|
|
|
|
{:ok, _check1} = Monitoring.create_check(attrs1)
|
|
{:ok, check2} = Monitoring.create_check(attrs2)
|
|
|
|
latest = Monitoring.get_latest_check(device.id)
|
|
assert latest.id == check2.id
|
|
end
|
|
|
|
test "get_latest_check/1 returns nil when no checks exist", %{device: device} do
|
|
assert Monitoring.get_latest_check(device.id) == nil
|
|
end
|
|
|
|
test "delete_old_checks/1 deletes checks older than date", %{device: device} do
|
|
old_attrs =
|
|
@valid_attrs
|
|
|> Map.put(:device_id, device.id)
|
|
|> Map.put(:checked_at, ~U[2025-01-01 12:00:00Z])
|
|
|
|
new_attrs =
|
|
@valid_attrs
|
|
|> Map.put(:device_id, device.id)
|
|
|> Map.put(:checked_at, ~U[2025-12-21 12:00:00Z])
|
|
|
|
{:ok, _old_check} = Monitoring.create_check(old_attrs)
|
|
{:ok, _new_check} = Monitoring.create_check(new_attrs)
|
|
|
|
{deleted, _} = Monitoring.delete_old_checks(~U[2025-06-01 00:00:00Z])
|
|
assert deleted == 1
|
|
assert length(Monitoring.list_devices_checks(device.id)) == 1
|
|
end
|
|
|
|
test "get_hourly_stats/3 returns stats for device", %{device: device} do
|
|
base_time = ~U[2025-12-21 12:00:00Z]
|
|
|
|
# Create checks in two different hours
|
|
{:ok, _} =
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :success,
|
|
response_time_ms: 50,
|
|
checked_at: base_time
|
|
})
|
|
|
|
{:ok, _} =
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :success,
|
|
response_time_ms: 100,
|
|
checked_at: DateTime.add(base_time, 30 * 60, :second)
|
|
})
|
|
|
|
{:ok, _} =
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :failure,
|
|
response_time_ms: nil,
|
|
checked_at: DateTime.add(base_time, 3600, :second)
|
|
})
|
|
|
|
start_time = DateTime.add(base_time, -3600, :second)
|
|
end_time = DateTime.add(base_time, 7200, :second)
|
|
|
|
result = Monitoring.get_hourly_stats(device.id, start_time, end_time)
|
|
assert %Postgrex.Result{} = result
|
|
assert length(result.rows) == 2
|
|
|
|
# First hour should have 2 successful checks
|
|
[first_hour | _] = result.rows
|
|
[_bucket, total, successful, failed, avg_response, min_response, max_response, uptime] = first_hour
|
|
assert total == 2
|
|
assert successful == 2
|
|
assert failed == 0
|
|
assert avg_response == Decimal.new("75.00")
|
|
assert min_response == 50
|
|
assert max_response == 100
|
|
assert uptime == Decimal.new("100.00")
|
|
end
|
|
|
|
test "get_daily_stats/3 returns stats for device", %{device: device} do
|
|
day1 = ~U[2025-12-20 12:00:00Z]
|
|
day2 = ~U[2025-12-21 12:00:00Z]
|
|
|
|
# Create checks on two different days
|
|
{:ok, _} =
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :success,
|
|
response_time_ms: 50,
|
|
checked_at: day1
|
|
})
|
|
|
|
{:ok, _} =
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :success,
|
|
response_time_ms: 60,
|
|
checked_at: DateTime.add(day1, 3600, :second)
|
|
})
|
|
|
|
{:ok, _} =
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :failure,
|
|
response_time_ms: nil,
|
|
checked_at: day2
|
|
})
|
|
|
|
{:ok, _} =
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :success,
|
|
response_time_ms: 80,
|
|
checked_at: DateTime.add(day2, 3600, :second)
|
|
})
|
|
|
|
start_time = DateTime.add(day1, -24 * 60 * 60, :second)
|
|
end_time = DateTime.add(day2, 24 * 60 * 60, :second)
|
|
|
|
result = Monitoring.get_daily_stats(device.id, start_time, end_time)
|
|
assert %Postgrex.Result{} = result
|
|
assert length(result.rows) == 2
|
|
|
|
# First day should have 2 successful checks
|
|
[first_day | _] = result.rows
|
|
[_bucket, total, successful, failed, avg_response, _min, _max, uptime] = first_day
|
|
assert total == 2
|
|
assert successful == 2
|
|
assert failed == 0
|
|
assert avg_response == Decimal.new("55.00")
|
|
assert uptime == Decimal.new("100.00")
|
|
end
|
|
|
|
test "get_uptime_percentage/1 returns uptime percentage", %{device: device} do
|
|
base_time = DateTime.truncate(DateTime.utc_now(), :second)
|
|
|
|
# Create 3 successful and 1 failed check (bulk insert for speed)
|
|
checks = [
|
|
%{
|
|
id: Ecto.UUID.generate(),
|
|
device_id: device.id,
|
|
status: :success,
|
|
response_time_ms: 50.0,
|
|
checked_at: DateTime.add(base_time, -60, :second),
|
|
inserted_at: base_time
|
|
},
|
|
%{
|
|
id: Ecto.UUID.generate(),
|
|
device_id: device.id,
|
|
status: :success,
|
|
response_time_ms: 60.0,
|
|
checked_at: DateTime.add(base_time, -120, :second),
|
|
inserted_at: base_time
|
|
},
|
|
%{
|
|
id: Ecto.UUID.generate(),
|
|
device_id: device.id,
|
|
status: :failure,
|
|
response_time_ms: nil,
|
|
checked_at: DateTime.add(base_time, -180, :second),
|
|
inserted_at: base_time
|
|
},
|
|
%{
|
|
id: Ecto.UUID.generate(),
|
|
device_id: device.id,
|
|
status: :success,
|
|
response_time_ms: 70.0,
|
|
checked_at: DateTime.add(base_time, -240, :second),
|
|
inserted_at: base_time
|
|
}
|
|
]
|
|
|
|
Repo.insert_all(Check, checks)
|
|
|
|
result = Monitoring.get_uptime_percentage(device.id)
|
|
assert result == 75.0
|
|
end
|
|
|
|
test "get_uptime_percentage/2 accepts custom days parameter", %{device: device} do
|
|
base_time = DateTime.utc_now()
|
|
|
|
# Create checks within the last 7 days
|
|
{:ok, _} =
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :success,
|
|
response_time_ms: 50,
|
|
checked_at: DateTime.add(base_time, -3 * 24 * 60 * 60, :second)
|
|
})
|
|
|
|
{:ok, _} =
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :success,
|
|
response_time_ms: 60,
|
|
checked_at: DateTime.add(base_time, -5 * 24 * 60 * 60, :second)
|
|
})
|
|
|
|
# Create a check older than 7 days (should not be included)
|
|
{:ok, _} =
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :failure,
|
|
response_time_ms: nil,
|
|
checked_at: DateTime.add(base_time, -10 * 24 * 60 * 60, :second)
|
|
})
|
|
|
|
result = Monitoring.get_uptime_percentage(device.id, 7)
|
|
assert result == 100.0
|
|
end
|
|
|
|
test "get_uptime_percentage/1 returns nil when no checks exist", %{device: device} do
|
|
result = Monitoring.get_uptime_percentage(device.id)
|
|
assert is_nil(result)
|
|
end
|
|
|
|
test "get_latency_data/2 returns latency checks for graphing", %{device: device} do
|
|
base_time = ~U[2025-12-21 12:00:00Z]
|
|
|
|
# Create successful checks with varying latency
|
|
{:ok, _} =
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :success,
|
|
response_time_ms: 50,
|
|
checked_at: base_time
|
|
})
|
|
|
|
{:ok, _} =
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :success,
|
|
response_time_ms: 75,
|
|
checked_at: DateTime.add(base_time, 60, :second)
|
|
})
|
|
|
|
{:ok, _} =
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :success,
|
|
response_time_ms: 100,
|
|
checked_at: DateTime.add(base_time, 120, :second)
|
|
})
|
|
|
|
# Create a failed check (should be excluded)
|
|
{:ok, _} =
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :failure,
|
|
response_time_ms: nil,
|
|
checked_at: DateTime.add(base_time, 180, :second)
|
|
})
|
|
|
|
since = DateTime.add(base_time, -3600, :second)
|
|
result = Monitoring.get_latency_data(device.id, since: since, limit: 100)
|
|
|
|
assert length(result) == 3
|
|
# Results should be ordered by checked_at descending
|
|
assert Enum.at(result, 0).response_time_ms == 100
|
|
assert Enum.at(result, 1).response_time_ms == 75
|
|
assert Enum.at(result, 2).response_time_ms == 50
|
|
end
|
|
|
|
test "get_latency_data/2 respects limit option", %{device: device} do
|
|
base_time = DateTime.truncate(~U[2025-12-21 12:00:00Z], :second)
|
|
|
|
# Create 5 checks (bulk insert for speed)
|
|
checks =
|
|
for i <- 1..5 do
|
|
%{
|
|
id: Ecto.UUID.generate(),
|
|
device_id: device.id,
|
|
status: :success,
|
|
response_time_ms: (50 + i) * 1.0,
|
|
checked_at: DateTime.add(base_time, i * 60, :second),
|
|
inserted_at: base_time
|
|
}
|
|
end
|
|
|
|
Repo.insert_all(Check, checks)
|
|
|
|
since = DateTime.add(base_time, -3600, :second)
|
|
result = Monitoring.get_latency_data(device.id, since: since, limit: 3)
|
|
|
|
assert length(result) == 3
|
|
end
|
|
|
|
test "get_latency_data/2 respects since option", %{device: device} do
|
|
base_time = ~U[2025-12-21 12:00:00Z]
|
|
|
|
# Create check before cutoff
|
|
{:ok, _} =
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :success,
|
|
response_time_ms: 50,
|
|
checked_at: DateTime.add(base_time, -7200, :second)
|
|
})
|
|
|
|
# Create check after cutoff
|
|
{:ok, _} =
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :success,
|
|
response_time_ms: 75,
|
|
checked_at: base_time
|
|
})
|
|
|
|
since = DateTime.add(base_time, -3600, :second)
|
|
result = Monitoring.get_latency_data(device.id, since: since, limit: 100)
|
|
|
|
# Should only include the check after the since timestamp
|
|
assert length(result) == 1
|
|
assert hd(result).response_time_ms == 75
|
|
end
|
|
|
|
test "get_latency_data/2 returns empty list when no checks exist", %{device: device} do
|
|
since = DateTime.add(DateTime.utc_now(), -3600, :second)
|
|
result = Monitoring.get_latency_data(device.id, since: since, limit: 100)
|
|
|
|
assert result == []
|
|
end
|
|
|
|
test "get_latency_data/2 excludes failed checks", %{device: device} do
|
|
base_time = ~U[2025-12-21 12:00:00Z]
|
|
|
|
# Create only failed checks
|
|
{:ok, _} =
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :failure,
|
|
response_time_ms: nil,
|
|
checked_at: base_time
|
|
})
|
|
|
|
{:ok, _} =
|
|
Monitoring.create_check(%{
|
|
device_id: device.id,
|
|
status: :failure,
|
|
response_time_ms: nil,
|
|
checked_at: DateTime.add(base_time, 60, :second)
|
|
})
|
|
|
|
since = DateTime.add(base_time, -3600, :second)
|
|
result = Monitoring.get_latency_data(device.id, since: since, limit: 100)
|
|
|
|
assert result == []
|
|
end
|
|
end
|
|
|
|
describe "ping" do
|
|
alias Towerops.Monitoring.Ping
|
|
|
|
@tag :integration
|
|
test "ping/1 returns ok tuple with response time for localhost" do
|
|
# Test with localhost which should always be reachable
|
|
case Ping.ping("127.0.0.1", 2000) do
|
|
{:ok, response_time} ->
|
|
assert is_float(response_time) or is_integer(response_time)
|
|
assert response_time >= 0
|
|
|
|
{:error, _reason} ->
|
|
# Ping might fail in some test environments
|
|
assert true
|
|
end
|
|
end
|
|
|
|
@tag :integration
|
|
test "ping/1 returns error tuple for unreachable host" do
|
|
# Use a non-routable IP
|
|
case Ping.ping("192.0.2.1", 1000) do
|
|
{:error, _reason} -> assert true
|
|
{:ok, _} -> assert true
|
|
end
|
|
end
|
|
|
|
@tag :integration
|
|
test "ping/1 with custom timeout" do
|
|
result = Ping.ping("127.0.0.1", 5000)
|
|
# Result should be either {:ok, time} or {:error, reason}
|
|
assert is_tuple(result)
|
|
assert elem(result, 0) in [:ok, :error]
|
|
end
|
|
end
|
|
end
|