towerops/test/towerops/url_validator_test.exs
Graham McIntire efaf5558ff refactor: convert 6 Gleam modules to idiomatic Elixir with TDD (#196)
Phase 1: Foundation Types (100% complete)
- query_helpers: SQL LIKE sanitization with pipe operators
- numeric: Integer parsing with pattern matching guards
- result: Pure Elixir Result monad (map, and_then, unwrap_or)

Phase 2: Ecto Domain Types (100% complete)
- ip_address: IPv4/IPv6 validation using :inet directly
- mac_address: Multi-format MAC parsing (colon/hyphen/dot/compact)
- snmp_oid: OID parsing/manipulation with recursive pattern matching

All 198 tests passing across converted modules.
API changed from Gleam-style {:some/:none to idiomatic {:ok/:error.
Refactored parse_numeric_oid to use with statement, reducing nesting depth.

Reviewed-on: graham/towerops-web#196
2026-03-28 09:52:07 -05:00

134 lines
4.2 KiB
Elixir

defmodule Towerops.URLValidatorTest do
use ExUnit.Case, async: true
alias Towerops.URLValidator
describe "validate/1" do
test "allows valid HTTP URL" do
assert :ok = URLValidator.validate("http://example.com")
end
test "allows valid HTTPS URL" do
assert :ok = URLValidator.validate("https://example.com")
end
test "allows URL with path" do
assert :ok = URLValidator.validate("https://example.com/path/to/resource")
end
test "allows URL with query params" do
assert :ok = URLValidator.validate("https://example.com/search?q=test&page=1")
end
test "allows URL with port" do
assert :ok = URLValidator.validate("https://example.com:8443/api")
end
# Scheme enforcement
test "rejects FTP scheme" do
assert {:error, "Only http and https schemes are allowed"} =
URLValidator.validate("ftp://example.com")
end
test "rejects file scheme" do
assert {:error, _} = URLValidator.validate("file:///etc/passwd")
end
test "rejects javascript scheme" do
assert {:error, _} = URLValidator.validate("javascript:alert(1)")
end
test "rejects URL without scheme" do
assert {:error, "URL must include a scheme (http or https)"} =
URLValidator.validate("example.com")
end
test "rejects URL without host" do
assert {:error, _} = URLValidator.validate("http://")
end
# Localhost blocking
test "rejects localhost" do
assert {:error, "Requests to localhost are not allowed"} =
URLValidator.validate("http://localhost")
end
test "rejects localhost with port" do
assert {:error, "Requests to localhost are not allowed"} =
URLValidator.validate("http://localhost:3000")
end
test "rejects .local domains" do
assert {:error, "Requests to localhost are not allowed"} =
URLValidator.validate("http://myservice.local")
end
test "rejects LOCALHOST (case insensitive)" do
assert {:error, "Requests to localhost are not allowed"} =
URLValidator.validate("http://LOCALHOST")
end
# Private IP blocking
test "rejects 127.0.0.1 (loopback)" do
assert {:error, "Requests to private/internal IP addresses are not allowed"} =
URLValidator.validate("http://127.0.0.1")
end
test "rejects 127.x.x.x range" do
assert {:error, "Requests to private/internal IP addresses are not allowed"} =
URLValidator.validate("http://127.0.0.2")
end
test "rejects 10.x.x.x (private)" do
assert {:error, "Requests to private/internal IP addresses are not allowed"} =
URLValidator.validate("http://10.0.0.1")
end
test "rejects 172.16.x.x (private)" do
assert {:error, "Requests to private/internal IP addresses are not allowed"} =
URLValidator.validate("http://172.16.0.1")
end
test "rejects 192.168.x.x (private)" do
assert {:error, "Requests to private/internal IP addresses are not allowed"} =
URLValidator.validate("http://192.168.1.1")
end
test "rejects 169.254.x.x (link-local)" do
assert {:error, "Requests to private/internal IP addresses are not allowed"} =
URLValidator.validate("http://169.254.169.254")
end
test "allows public IP addresses" do
assert :ok = URLValidator.validate("http://8.8.8.8")
end
test "allows 172.32.x.x (outside private range)" do
assert :ok = URLValidator.validate("http://172.32.0.1")
end
# Non-string input
test "rejects non-string input" do
assert {:error, "URL must be a string"} = URLValidator.validate(123)
end
test "rejects nil input" do
assert {:error, "URL must be a string"} = URLValidator.validate(nil)
end
test "rejects atom input" do
assert {:error, "URL must be a string"} = URLValidator.validate(:not_a_url)
end
# Edge cases
test "rejects empty string host" do
assert {:error, _} = URLValidator.validate("http:///path")
end
# IPv6 loopback
test "rejects IPv6 loopback ::1" do
assert {:error, "Requests to private/internal IP addresses are not allowed"} =
URLValidator.validate("http://[::1]")
end
end
end