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
134 lines
4.2 KiB
Elixir
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
|