towerops/test/towerops/monitoring/ping_test.exs
2026-01-27 09:30:18 -06:00

165 lines
5.2 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
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
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 :integration
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 :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
end