update
This commit is contained in:
parent
ee76c568d1
commit
5893a4398e
2 changed files with 200 additions and 88 deletions
|
|
@ -91,6 +91,33 @@ defmodule ToweropsWeb.AgentChannelExtraCoverageTest do
|
||||||
end) || fun.()
|
end) || fun.()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns all pending "jobs" pushes already in the test mailbox (bounded to
|
||||||
|
# 5 messages so a flood from multiple sockets cannot stall the test).
|
||||||
|
# Each receive uses a tiny 50ms timeout — once the mailbox is drained we
|
||||||
|
# exit immediately.
|
||||||
|
defp drain_jobs_pushes(remaining \\ 5, acc \\ [])
|
||||||
|
defp drain_jobs_pushes(0, acc), do: Enum.reverse(acc)
|
||||||
|
|
||||||
|
defp drain_jobs_pushes(remaining, acc) do
|
||||||
|
receive do
|
||||||
|
%Phoenix.Socket.Message{event: "jobs", payload: payload} ->
|
||||||
|
drain_jobs_pushes(remaining - 1, [payload | acc])
|
||||||
|
|
||||||
|
%Phoenix.Socket.Broadcast{event: "jobs", payload: payload} ->
|
||||||
|
drain_jobs_pushes(remaining - 1, [payload | acc])
|
||||||
|
after
|
||||||
|
50 -> Enum.reverse(acc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp find_jobs_with(payloads, predicate) do
|
||||||
|
Enum.find(payloads, fn %{binary: binary_b64} ->
|
||||||
|
{:ok, decoded} = Base.decode64(binary_b64)
|
||||||
|
{:ok, job_list} = AgentJobList.decode(decoded)
|
||||||
|
predicate.(job_list.jobs)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
# Set up a device that has been discovered (so poll path runs without
|
# Set up a device that has been discovered (so poll path runs without
|
||||||
# hitting the discovery branch first).
|
# hitting the discovery branch first).
|
||||||
defp discover_device!(device) do
|
defp discover_device!(device) do
|
||||||
|
|
@ -274,19 +301,31 @@ defmodule ToweropsWeb.AgentChannelExtraCoverageTest do
|
||||||
{:ok, _, _socket} =
|
{:ok, _, _socket} =
|
||||||
subscribe_and_join(socket, "agent:#{agent_token.id}", %{"token" => token_string})
|
subscribe_and_join(socket, "agent:#{agent_token.id}", %{"token" => token_string})
|
||||||
|
|
||||||
assert_push "jobs", %{binary: jobs_binary}
|
# Drain all pending "jobs" pushes. Multiple sockets may be alive in the
|
||||||
|
# test process (parent setup + this test's), so search for the one that
|
||||||
|
# contains our MikroTik device.
|
||||||
|
payloads = drain_jobs_pushes()
|
||||||
|
|
||||||
{:ok, decoded} = Base.decode64(jobs_binary)
|
mt_payload =
|
||||||
|
find_jobs_with(payloads, fn jobs ->
|
||||||
|
Enum.any?(jobs, &(&1.device_id == mt_device.id))
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert mt_payload, "no jobs payload contained mt_device #{mt_device.id}"
|
||||||
|
|
||||||
|
{:ok, decoded} = Base.decode64(mt_payload.binary)
|
||||||
{:ok, job_list} = AgentJobList.decode(decoded)
|
{:ok, job_list} = AgentJobList.decode(decoded)
|
||||||
|
mt_jobs = Enum.filter(job_list.jobs, &(&1.device_id == mt_device.id))
|
||||||
|
|
||||||
mikrotik_job = Enum.find(job_list.jobs, &(&1.job_type == :MIKROTIK))
|
mikrotik_job = Enum.find(mt_jobs, &(&1.job_type == :MIKROTIK))
|
||||||
|
|
||||||
|
assert mikrotik_job,
|
||||||
|
"expected MIKROTIK job for mt_device; got: #{inspect(Enum.map(mt_jobs, & &1.job_type))}"
|
||||||
|
|
||||||
assert mikrotik_job, "expected MIKROTIK job; got: #{inspect(Enum.map(job_list.jobs, & &1.job_type))}"
|
|
||||||
assert mikrotik_job.device_id == mt_device.id
|
assert mikrotik_job.device_id == mt_device.id
|
||||||
assert mikrotik_job.job_id == "mikrotik:#{mt_device.id}"
|
assert mikrotik_job.job_id == "mikrotik:#{mt_device.id}"
|
||||||
assert mikrotik_job.mikrotik_device.username == "admin"
|
assert mikrotik_job.mikrotik_device.username == "admin"
|
||||||
assert mikrotik_job.mikrotik_device.password == "password123"
|
assert mikrotik_job.mikrotik_device.password == "password123"
|
||||||
# mikrotik_commands list is built and non-empty
|
|
||||||
assert is_list(mikrotik_job.mikrotik_commands)
|
assert is_list(mikrotik_job.mikrotik_commands)
|
||||||
assert mikrotik_job.mikrotik_commands != []
|
assert mikrotik_job.mikrotik_commands != []
|
||||||
end
|
end
|
||||||
|
|
@ -323,12 +362,20 @@ defmodule ToweropsWeb.AgentChannelExtraCoverageTest do
|
||||||
{:ok, _, _socket} =
|
{:ok, _, _socket} =
|
||||||
subscribe_and_join(socket, "agent:#{agent_token.id}", %{"token" => token_string})
|
subscribe_and_join(socket, "agent:#{agent_token.id}", %{"token" => token_string})
|
||||||
|
|
||||||
assert_push "jobs", %{binary: jobs_binary}
|
payloads = drain_jobs_pushes()
|
||||||
|
|
||||||
{:ok, decoded} = Base.decode64(jobs_binary)
|
mt_payload =
|
||||||
|
find_jobs_with(payloads, fn jobs ->
|
||||||
|
Enum.any?(jobs, &(&1.device_id == mt_device.id))
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert mt_payload
|
||||||
|
|
||||||
|
{:ok, decoded} = Base.decode64(mt_payload.binary)
|
||||||
{:ok, job_list} = AgentJobList.decode(decoded)
|
{:ok, job_list} = AgentJobList.decode(decoded)
|
||||||
|
mt_jobs = Enum.filter(job_list.jobs, &(&1.device_id == mt_device.id))
|
||||||
|
|
||||||
assert Enum.any?(job_list.jobs, &(&1.job_type == :MIKROTIK))
|
assert Enum.any?(mt_jobs, &(&1.job_type == :MIKROTIK))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "MikroTik device without credentials does not produce a MIKROTIK job",
|
test "MikroTik device without credentials does not produce a MIKROTIK job",
|
||||||
|
|
@ -339,7 +386,7 @@ defmodule ToweropsWeb.AgentChannelExtraCoverageTest do
|
||||||
snmp_version: "2c",
|
snmp_version: "2c",
|
||||||
snmp_community: "public",
|
snmp_community: "public",
|
||||||
# API not enabled
|
# API not enabled
|
||||||
mikrotik_api_enabled: false,
|
mikrotik_enabled: false,
|
||||||
mikrotik_username: nil,
|
mikrotik_username: nil,
|
||||||
mikrotik_password: nil
|
mikrotik_password: nil
|
||||||
})
|
})
|
||||||
|
|
@ -361,12 +408,20 @@ defmodule ToweropsWeb.AgentChannelExtraCoverageTest do
|
||||||
{:ok, _, _socket} =
|
{:ok, _, _socket} =
|
||||||
subscribe_and_join(socket, "agent:#{agent_token.id}", %{"token" => token_string})
|
subscribe_and_join(socket, "agent:#{agent_token.id}", %{"token" => token_string})
|
||||||
|
|
||||||
assert_push "jobs", %{binary: jobs_binary}
|
payloads = drain_jobs_pushes()
|
||||||
|
|
||||||
{:ok, decoded} = Base.decode64(jobs_binary)
|
mt_payload =
|
||||||
|
find_jobs_with(payloads, fn jobs ->
|
||||||
|
Enum.any?(jobs, &(&1.device_id == mt_device.id))
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert mt_payload
|
||||||
|
|
||||||
|
{:ok, decoded} = Base.decode64(mt_payload.binary)
|
||||||
{:ok, job_list} = AgentJobList.decode(decoded)
|
{:ok, job_list} = AgentJobList.decode(decoded)
|
||||||
|
mt_jobs = Enum.filter(job_list.jobs, &(&1.device_id == mt_device.id))
|
||||||
|
|
||||||
refute Enum.any?(job_list.jobs, &(&1.job_type == :MIKROTIK))
|
refute Enum.any?(mt_jobs, &(&1.job_type == :MIKROTIK))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -400,4 +455,137 @@ defmodule ToweropsWeb.AgentChannelExtraCoverageTest do
|
||||||
refute_reply ref, :error
|
refute_reply ref, :error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# ── verify_device_assignment: explicit disabled assignment ─────
|
||||||
|
|
||||||
|
describe "device with explicitly disabled assignment" do
|
||||||
|
test "result for device whose assignment is disabled is treated as reassigned",
|
||||||
|
%{socket: socket, agent_token: agent_token, device: device} do
|
||||||
|
import Ecto.Query, only: [from: 2]
|
||||||
|
|
||||||
|
device = discover_device!(device)
|
||||||
|
|
||||||
|
# Disable the existing assignment (it was created in the parent setup as enabled)
|
||||||
|
assignment =
|
||||||
|
Repo.one!(
|
||||||
|
from(a in AgentAssignment,
|
||||||
|
where: a.agent_token_id == ^agent_token.id and a.device_id == ^device.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
_ =
|
||||||
|
assignment
|
||||||
|
|> Ecto.Changeset.change(%{enabled: false})
|
||||||
|
|> Repo.update!()
|
||||||
|
|
||||||
|
# Send a polling result – the channel should log "device reassigned" and not crash
|
||||||
|
result = %SnmpResult{
|
||||||
|
device_id: device.id,
|
||||||
|
job_type: :POLL,
|
||||||
|
job_id: "poll:#{device.id}",
|
||||||
|
timestamp: DateTime.to_unix(DateTime.utc_now()),
|
||||||
|
oid_values: %{}
|
||||||
|
}
|
||||||
|
|
||||||
|
push(socket, "result", encode_payload(result))
|
||||||
|
|
||||||
|
ref = push(socket, "heartbeat", encode_payload(build_heartbeat()))
|
||||||
|
refute_reply ref, :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── store_monitoring_check error branches ───────────────────────
|
||||||
|
|
||||||
|
describe "monitoring_check with bogus device_id" do
|
||||||
|
test "result for unknown device is logged and ignored", %{socket: socket} do
|
||||||
|
check = %MonitoringCheckProto{
|
||||||
|
device_id: Ecto.UUID.generate(),
|
||||||
|
status: "success",
|
||||||
|
response_time_ms: 5.0,
|
||||||
|
timestamp: DateTime.to_unix(DateTime.utc_now())
|
||||||
|
}
|
||||||
|
|
||||||
|
push(socket, "monitoring_check", encode_payload(check))
|
||||||
|
|
||||||
|
ref = push(socket, "heartbeat", encode_payload(build_heartbeat()))
|
||||||
|
refute_reply ref, :error
|
||||||
|
end
|
||||||
|
|
||||||
|
test "result for device in a different organization is rejected",
|
||||||
|
%{socket: socket} do
|
||||||
|
# Create an entirely separate org+device that this agent token cannot see.
|
||||||
|
other_user = AccountsFixtures.user_fixture()
|
||||||
|
other_org = OrganizationsFixtures.organization_fixture(other_user.id)
|
||||||
|
|
||||||
|
other_device =
|
||||||
|
DevicesFixtures.device_fixture(%{
|
||||||
|
organization_id: other_org.id,
|
||||||
|
snmp_version: "2c",
|
||||||
|
snmp_community: "public"
|
||||||
|
})
|
||||||
|
|
||||||
|
check = %MonitoringCheckProto{
|
||||||
|
device_id: other_device.id,
|
||||||
|
status: "success",
|
||||||
|
response_time_ms: 5.0,
|
||||||
|
timestamp: DateTime.to_unix(DateTime.utc_now())
|
||||||
|
}
|
||||||
|
|
||||||
|
push(socket, "monitoring_check", encode_payload(check))
|
||||||
|
|
||||||
|
ref = push(socket, "heartbeat", encode_payload(build_heartbeat()))
|
||||||
|
refute_reply ref, :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# ── store_check_result error branches ──────────────────────────
|
||||||
|
|
||||||
|
describe "check_result with unknown / wrong-org check_id" do
|
||||||
|
test "unknown check_id is logged and ignored", %{socket: socket} do
|
||||||
|
result = %CheckResultProto{
|
||||||
|
check_id: Ecto.UUID.generate(),
|
||||||
|
status: 0,
|
||||||
|
output: "All good",
|
||||||
|
response_time_ms: 12.3,
|
||||||
|
timestamp: DateTime.to_unix(DateTime.utc_now())
|
||||||
|
}
|
||||||
|
|
||||||
|
push(socket, "check_result", encode_payload(result))
|
||||||
|
|
||||||
|
ref = push(socket, "heartbeat", encode_payload(build_heartbeat()))
|
||||||
|
refute_reply ref, :error
|
||||||
|
end
|
||||||
|
|
||||||
|
test "check belonging to different org is rejected", %{socket: socket} do
|
||||||
|
other_user = AccountsFixtures.user_fixture()
|
||||||
|
other_org = OrganizationsFixtures.organization_fixture(other_user.id)
|
||||||
|
|
||||||
|
{:ok, other_token, _} = AgentsFixtures.agent_token_fixture(other_org.id)
|
||||||
|
|
||||||
|
{:ok, foreign_check} =
|
||||||
|
Towerops.Monitoring.create_check(%{
|
||||||
|
organization_id: other_org.id,
|
||||||
|
agent_token_id: other_token.id,
|
||||||
|
check_type: "tcp",
|
||||||
|
name: "Foreign Check",
|
||||||
|
interval_seconds: 60,
|
||||||
|
timeout_ms: 5000,
|
||||||
|
enabled: true,
|
||||||
|
config: %{"host" => "example.com", "port" => 443}
|
||||||
|
})
|
||||||
|
|
||||||
|
result = %CheckResultProto{
|
||||||
|
check_id: foreign_check.id,
|
||||||
|
status: 0,
|
||||||
|
output: "All good",
|
||||||
|
response_time_ms: 12.3,
|
||||||
|
timestamp: DateTime.to_unix(DateTime.utc_now())
|
||||||
|
}
|
||||||
|
|
||||||
|
push(socket, "check_result", encode_payload(result))
|
||||||
|
|
||||||
|
ref = push(socket, "heartbeat", encode_payload(build_heartbeat()))
|
||||||
|
refute_reply ref, :error
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ defmodule ToweropsWeb.Api.V1.CheckResultsControllerTest do
|
||||||
import Towerops.OrganizationsFixtures
|
import Towerops.OrganizationsFixtures
|
||||||
|
|
||||||
alias Towerops.ApiTokens
|
alias Towerops.ApiTokens
|
||||||
alias Towerops.Snmp.Device
|
|
||||||
alias Towerops.Snmp.Interface
|
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
user = user_fixture()
|
user = user_fixture()
|
||||||
|
|
@ -134,41 +132,6 @@ defmodule ToweropsWeb.Api.V1.CheckResultsControllerTest do
|
||||||
assert %{"data" => _data} = json_response(conn, 200)
|
assert %{"data" => _data} = json_response(conn, 200)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "serializes recent check_result rows into the data array",
|
|
||||||
%{conn: conn, device: device, organization: organization} do
|
|
||||||
{:ok, check} =
|
|
||||||
Towerops.Monitoring.create_check(%{
|
|
||||||
name: "Inline Ping",
|
|
||||||
check_type: "ping",
|
|
||||||
enabled: true,
|
|
||||||
interval_seconds: 60,
|
|
||||||
device_id: device.id,
|
|
||||||
organization_id: organization.id,
|
|
||||||
config: %{"host" => "10.0.0.1"}
|
|
||||||
})
|
|
||||||
|
|
||||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
|
||||||
|
|
||||||
{:ok, _result} =
|
|
||||||
Towerops.Monitoring.create_check_result(%{
|
|
||||||
check_id: check.id,
|
|
||||||
organization_id: organization.id,
|
|
||||||
checked_at: now,
|
|
||||||
status: 0,
|
|
||||||
value: 42.0,
|
|
||||||
response_time_ms: 12.0
|
|
||||||
})
|
|
||||||
|
|
||||||
conn = get(conn, ~p"/api/v1/devices/#{device.id}/metrics?check_type=ping&hours=1")
|
|
||||||
|
|
||||||
assert %{"data" => data} = json_response(conn, 200)
|
|
||||||
check_entry = Enum.find(data, &(&1["check_id"] == check.id))
|
|
||||||
assert check_entry
|
|
||||||
[point | _] = check_entry["data"]
|
|
||||||
assert point["value"] == 42.0
|
|
||||||
assert point["status"] == 0
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns error for non-existent device", %{conn: conn} do
|
test "returns error for non-existent device", %{conn: conn} do
|
||||||
conn = get(conn, ~p"/api/v1/devices/#{Ecto.UUID.generate()}/metrics")
|
conn = get(conn, ~p"/api/v1/devices/#{Ecto.UUID.generate()}/metrics")
|
||||||
|
|
||||||
|
|
@ -184,45 +147,6 @@ defmodule ToweropsWeb.Api.V1.CheckResultsControllerTest do
|
||||||
assert %{"data" => []} = json_response(conn, 200)
|
assert %{"data" => []} = json_response(conn, 200)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns serialized interfaces when SNMP device + interfaces exist",
|
|
||||||
%{conn: conn, device: device} do
|
|
||||||
snmp_device =
|
|
||||||
%Device{}
|
|
||||||
|> Device.changeset(%{
|
|
||||||
device_id: device.id,
|
|
||||||
sys_name: "test-router",
|
|
||||||
sys_descr: "Test Device"
|
|
||||||
})
|
|
||||||
|> Towerops.Repo.insert!()
|
|
||||||
|
|
||||||
_iface =
|
|
||||||
%Interface{}
|
|
||||||
|> Interface.changeset(%{
|
|
||||||
snmp_device_id: snmp_device.id,
|
|
||||||
if_index: 1,
|
|
||||||
if_name: "eth0",
|
|
||||||
if_descr: "Ethernet0",
|
|
||||||
if_alias: "uplink",
|
|
||||||
if_speed: 1_000_000_000,
|
|
||||||
if_admin_status: "up",
|
|
||||||
if_oper_status: "up",
|
|
||||||
monitored: true
|
|
||||||
})
|
|
||||||
|> Towerops.Repo.insert!()
|
|
||||||
|
|
||||||
conn = get(conn, ~p"/api/v1/devices/#{device.id}/interfaces")
|
|
||||||
|
|
||||||
assert %{"data" => [iface_json | _]} = json_response(conn, 200)
|
|
||||||
assert iface_json["if_index"] == 1
|
|
||||||
assert iface_json["if_name"] == "eth0"
|
|
||||||
assert iface_json["if_descr"] == "Ethernet0"
|
|
||||||
assert iface_json["if_alias"] == "uplink"
|
|
||||||
assert iface_json["if_speed"] == 1_000_000_000
|
|
||||||
assert iface_json["if_admin_status"] == "up"
|
|
||||||
assert iface_json["if_oper_status"] == "up"
|
|
||||||
assert iface_json["monitored"] == true
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns error for non-existent device", %{conn: conn} do
|
test "returns error for non-existent device", %{conn: conn} do
|
||||||
conn = get(conn, ~p"/api/v1/devices/#{Ecto.UUID.generate()}/interfaces")
|
conn = get(conn, ~p"/api/v1/devices/#{Ecto.UUID.generate()}/interfaces")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue