- Remove Mox.set_mox_global() in DPW extra test; rely on $callers
instead so concurrent vendor SNMP tests stop intermittently failing
with VerificationError.
- Make ToweropsWeb.Plugs.GraphQLIntrospectionTest async: false. It
mutates Application.put_env(:towerops, :env, :prod), which polluted
every async vendor test that reads :env via
Towerops.Snmp.Client.phoenix_snmp_disabled/0.
- Bump test pool queue_target/queue_interval. AgentChannelTest spawns
many `:proc_lib` channel processes that hold sandbox connections;
the default 50ms queue caused intermittent
'could not checkout the connection' errors.
- Tag real-System.cmd("ping") tests as :network so they're excluded
by default. Saves ~20s on every full run.
- Make JobCleanupTask's internal 1s settle sleep configurable via
:job_cleanup_settle_ms; the test overrides it to 0.
Plus the pending CoverageLive.Form edit-save / EIRP recompute and
TraceLive deep-link tests from before the compact.
244 lines
7.8 KiB
Elixir
244 lines
7.8 KiB
Elixir
defmodule Towerops.Monitoring.PingTest do
|
|
use ExUnit.Case, async: true
|
|
use ExUnitProperties
|
|
|
|
alias Towerops.Monitoring.Ping
|
|
|
|
# These tests focus on IP validation which happens BEFORE any system calls
|
|
describe "IP address validation" do
|
|
test "returns error for invalid IP address" do
|
|
assert {:error, :invalid_ip} = Ping.ping("not-an-ip")
|
|
end
|
|
|
|
test "returns error for empty string" do
|
|
assert {:error, :invalid_ip} = Ping.ping("")
|
|
end
|
|
|
|
test "returns error for malformed IP with out of range octets" do
|
|
assert {:error, :invalid_ip} = Ping.ping("256.256.256.256")
|
|
end
|
|
|
|
test "returns error for partial IP format" do
|
|
# Note: "192.168.1" is valid shorthand (192.168.0.1), so use trailing dot
|
|
assert {:error, :invalid_ip} = Ping.ping("192.168.1.")
|
|
end
|
|
|
|
test "returns error for IP with extra octets" do
|
|
assert {:error, :invalid_ip} = Ping.ping("192.168.1.1.1")
|
|
end
|
|
|
|
test "returns error for IP with letters" do
|
|
assert {:error, :invalid_ip} = Ping.ping("192.168.1.abc")
|
|
end
|
|
|
|
test "returns error for IP with special characters" do
|
|
assert {:error, :invalid_ip} = Ping.ping("192.168.1.1!")
|
|
end
|
|
|
|
test "rejects invalid IPv6 address" do
|
|
assert {:error, :invalid_ip} = Ping.ping("2001:0db8:85a3::8a2e:0370:gggg")
|
|
end
|
|
|
|
test "rejects malformed IPv6 address" do
|
|
assert {:error, :invalid_ip} = Ping.ping(":::1")
|
|
end
|
|
end
|
|
|
|
describe "error handling" do
|
|
test "handles nil IP address with FunctionClauseError" do
|
|
assert_raise FunctionClauseError, fn ->
|
|
Ping.ping(nil)
|
|
end
|
|
end
|
|
|
|
test "handles integer IP address with FunctionClauseError" do
|
|
assert_raise FunctionClauseError, fn ->
|
|
Ping.ping(123)
|
|
end
|
|
end
|
|
|
|
test "handles atom IP address with FunctionClauseError" do
|
|
assert_raise FunctionClauseError, fn ->
|
|
Ping.ping(:localhost)
|
|
end
|
|
end
|
|
|
|
test "handles list IP address with FunctionClauseError" do
|
|
assert_raise FunctionClauseError, fn ->
|
|
Ping.ping([192, 168, 1, 1])
|
|
end
|
|
end
|
|
|
|
test "handles non-integer timeout with FunctionClauseError" do
|
|
assert_raise FunctionClauseError, fn ->
|
|
Ping.ping("127.0.0.1", "1000")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "PingBehaviour implementation" do
|
|
test "implements ping/1 callback" do
|
|
exports = Ping.module_info(:exports)
|
|
assert {:ping, 1} in exports
|
|
end
|
|
|
|
test "implements ping/2 callback" do
|
|
exports = Ping.module_info(:exports)
|
|
assert {:ping, 2} in exports
|
|
end
|
|
end
|
|
|
|
describe "property-based tests for IP validation" do
|
|
@tag :property
|
|
property "invalid IP strings always return error from validation" do
|
|
# Use generators that produce definitely-invalid IPs (no expensive filter)
|
|
check all(
|
|
invalid_ip <-
|
|
one_of([
|
|
# Alphanumeric strings (no dots) are never valid IPs
|
|
string(:alphanumeric, min_length: 1, max_length: 15),
|
|
# IPs with invalid octets (>255)
|
|
map(integer(256..999), &"#{&1}.#{&1}.#{&1}.#{&1}"),
|
|
# IPs with letters in octet position (use ?a..?z range)
|
|
map(string(?a..?z, length: 3), &"192.168.#{&1}.1")
|
|
]),
|
|
max_runs: 10
|
|
) do
|
|
# These fail at validation, before any system call
|
|
result = Ping.ping(invalid_ip)
|
|
assert match?({:error, _}, result)
|
|
end
|
|
end
|
|
|
|
@tag :property
|
|
property "valid IPv4 addresses are accepted by inet parser" do
|
|
check all(
|
|
octet1 <- integer(0..255),
|
|
octet2 <- integer(0..255),
|
|
octet3 <- integer(0..255),
|
|
octet4 <- integer(0..255),
|
|
max_runs: 20
|
|
) do
|
|
ip = "#{octet1}.#{octet2}.#{octet3}.#{octet4}"
|
|
# Just verify IP validation passes (don't actually ping)
|
|
assert {:ok, _} = :inet.parse_address(String.to_charlist(ip))
|
|
end
|
|
end
|
|
end
|
|
|
|
# These tests verify the ping output parser behavior
|
|
# by testing the module's internal parsing logic indirectly
|
|
describe "ping output parsing (via mocked system calls)" do
|
|
# We can't easily mock System.cmd, but we can test that the module
|
|
# correctly handles various response formats by testing with valid IPs
|
|
# that return quickly (localhost)
|
|
|
|
@tag :network
|
|
test "localhost ping returns valid response format" do
|
|
result = Ping.ping("127.0.0.1", 2000)
|
|
|
|
case result do
|
|
{:ok, response_time} ->
|
|
assert is_float(response_time)
|
|
assert response_time >= 0.0
|
|
assert response_time < 2000.0
|
|
|
|
{:error, reason} ->
|
|
# Acceptable errors in test environment
|
|
assert reason in [:timeout, :command_not_found, :parse_error]
|
|
end
|
|
end
|
|
|
|
@tag :network
|
|
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
|
|
|
|
@tag :network
|
|
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)
|
|
|
|
case result do
|
|
{:ok, response_time} ->
|
|
assert is_float(response_time)
|
|
assert response_time >= 0.0
|
|
|
|
{:error, reason} ->
|
|
# IPv6 may not be available in all test environments
|
|
assert reason in [:timeout, :command_not_found, :parse_error]
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "validate_ip_address/1" do
|
|
test "empty string returns invalid_ip" do
|
|
assert {:error, :invalid_ip} == Ping.validate_ip_address("")
|
|
end
|
|
|
|
test "valid IPv4 is :ok" do
|
|
assert :ok == Ping.validate_ip_address("192.168.1.1")
|
|
assert :ok == Ping.validate_ip_address("0.0.0.0")
|
|
assert :ok == Ping.validate_ip_address("255.255.255.255")
|
|
end
|
|
|
|
test "valid IPv6 is :ok" do
|
|
assert :ok == Ping.validate_ip_address("::1")
|
|
assert :ok == Ping.validate_ip_address("2001:db8::1")
|
|
end
|
|
|
|
test "invalid string returns invalid_ip" do
|
|
assert {:error, :invalid_ip} == Ping.validate_ip_address("not.an.ip")
|
|
assert {:error, :invalid_ip} == Ping.validate_ip_address("256.256.256.256")
|
|
end
|
|
end
|
|
|
|
describe "parse_ping_output/1" do
|
|
test "parses macOS format" do
|
|
output = """
|
|
PING 127.0.0.1 (127.0.0.1): 56 data bytes
|
|
round-trip min/avg/max/stddev = 0.050/0.123/0.200/0.075 ms
|
|
"""
|
|
|
|
assert {:ok, 0.123} == Ping.parse_ping_output(output)
|
|
end
|
|
|
|
test "parses Linux format" do
|
|
output = """
|
|
--- 127.0.0.1 ping statistics ---
|
|
rtt min/avg/max/mdev = 0.010/0.025/0.050/0.020 ms
|
|
"""
|
|
|
|
assert {:ok, 0.025} == Ping.parse_ping_output(output)
|
|
end
|
|
|
|
test "parses simple time= format" do
|
|
output = "64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.123 ms"
|
|
assert {:ok, 0.123} == Ping.parse_ping_output(output)
|
|
end
|
|
|
|
test "parses time< fallback" do
|
|
output = "64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time<0.5 ms"
|
|
assert {:ok, 0.5} == Ping.parse_ping_output(output)
|
|
end
|
|
|
|
test "returns parse_error when no known pattern present" do
|
|
assert {:error, :parse_error} == Ping.parse_ping_output("total garbage")
|
|
assert {:error, :parse_error} == Ping.parse_ping_output("")
|
|
end
|
|
end
|
|
end
|