aprs.me/test/aprsme_web/live/shared/param_utils_test.exs
Graham McIntire b86153cd27
Fix all mix credo --strict warnings (188 → 0)
- Replace apply/2 with direct fully-qualified calls in movement_test
- Fix assert_receive timeouts < 1000ms across 8 test files
- Move nested import statements to module-level scope
- Fix tests with no assertions and add missing doctest
- Replace weak type assertions with specific value checks
- Fix conditional assertions and length/1 expensive patterns
- Disable inappropriate Jump.CredoChecks.AvoidSocketAssignsInTest
- Fix tests not calling application code with credo:disable
- Add various credo:disable comments for legitimate patterns
2026-06-12 16:27:20 -05:00

281 lines
9 KiB
Elixir

defmodule AprsmeWeb.Live.Shared.ParamUtilsTest do
use ExUnit.Case, async: true
use ExUnitProperties
alias AprsmeWeb.Live.Shared.ParamUtils
describe "parse_float_in_range/4" do
test "parses valid float inside range" do
assert ParamUtils.parse_float_in_range("30.5", 0.0, -90.0, 90.0) == 30.5
end
test "returns default when input is outside range" do
assert ParamUtils.parse_float_in_range("200", 0.0, -90.0, 90.0) == 0.0
assert ParamUtils.parse_float_in_range("-200", 0.0, -90.0, 90.0) == 0.0
end
test "returns default for unparseable input" do
assert ParamUtils.parse_float_in_range("abc", 0.0, -90.0, 90.0) == 0.0
assert ParamUtils.parse_float_in_range("", 0.0, -90.0, 90.0) == 0.0
end
test "returns default for non-binary input" do
assert ParamUtils.parse_float_in_range(nil, 1.5, -90.0, 90.0) == 1.5
assert ParamUtils.parse_float_in_range(42, 1.5, -90.0, 90.0) == 1.5
end
test "accepts negative values in range" do
assert ParamUtils.parse_float_in_range("-45.25", 0.0, -90.0, 90.0) == -45.25
end
test "accepts values at exact range boundaries" do
assert ParamUtils.parse_float_in_range("90.0", 0.0, -90.0, 90.0) == 90.0
assert ParamUtils.parse_float_in_range("-90.0", 0.0, -90.0, 90.0) == -90.0
end
test "accepts input with trailing non-numeric characters if still in range" do
# This mirrors documented behavior in the implementation.
assert ParamUtils.parse_float_in_range("30.5abc", 0.0, -90.0, 90.0) == 30.5
end
end
describe "parse_int_in_range/4" do
test "parses valid integer inside range" do
assert ParamUtils.parse_int_in_range("10", 5, 1, 20) == 10
end
test "returns default for out-of-range value" do
assert ParamUtils.parse_int_in_range("100", 5, 1, 20) == 5
assert ParamUtils.parse_int_in_range("0", 5, 1, 20) == 5
end
test "returns default for unparseable value" do
assert ParamUtils.parse_int_in_range("foo", 5, 1, 20) == 5
assert ParamUtils.parse_int_in_range(nil, 5, 1, 20) == 5
end
test "accepts boundary values" do
assert ParamUtils.parse_int_in_range("1", 5, 1, 20) == 1
assert ParamUtils.parse_int_in_range("20", 5, 1, 20) == 20
end
end
describe "sanitize_numeric_string/1" do
test "trims whitespace" do
assert ParamUtils.sanitize_numeric_string(" 42 ") == "42"
end
test "strips non-numeric characters" do
assert ParamUtils.sanitize_numeric_string("1<script>2") == "12"
end
test "keeps scientific notation characters" do
assert ParamUtils.sanitize_numeric_string("1e5") == "1e5"
assert ParamUtils.sanitize_numeric_string("1.5E-3") == "1.5E-3"
end
test "keeps decimals and signs" do
assert ParamUtils.sanitize_numeric_string("-3.14") == "-3.14"
end
test "limits length to 20 characters" do
long = String.duplicate("1", 50)
assert byte_size(ParamUtils.sanitize_numeric_string(long)) == 20
end
test "returns empty string for non-binary" do
assert ParamUtils.sanitize_numeric_string(nil) == ""
assert ParamUtils.sanitize_numeric_string(123) == ""
end
end
describe "limit_string_length/2" do
test "truncates strings longer than limit" do
assert ParamUtils.limit_string_length("hello world", 5) == "hello"
end
test "leaves short strings alone" do
assert ParamUtils.limit_string_length("hi", 10) == "hi"
end
test "handles strings exactly at the limit" do
assert ParamUtils.limit_string_length("abcde", 5) == "abcde"
end
end
describe "finite?/1" do
test "returns true for floats" do
assert ParamUtils.finite?(1.0)
assert ParamUtils.finite?(-999.9)
assert ParamUtils.finite?(0.0)
end
test "returns false for non-floats" do
refute ParamUtils.finite?(1)
refute ParamUtils.finite?("1.0")
refute ParamUtils.finite?(nil)
end
end
describe "safe_parse_coordinate/4" do
test "parses strings and clamps" do
assert ParamUtils.safe_parse_coordinate("45.0", 0.0, -90.0, 90.0) == 45.0
end
test "passes through numbers in range" do
assert ParamUtils.safe_parse_coordinate(12.5, 0.0, -90.0, 90.0) == 12.5
end
test "clamps numbers above max" do
assert ParamUtils.safe_parse_coordinate(500, 0.0, -90.0, 90.0) == 90.0
end
test "clamps numbers below min" do
assert ParamUtils.safe_parse_coordinate(-500, 0.0, -90.0, 90.0) == -90.0
end
test "returns default for invalid string (out-of-range)" do
assert ParamUtils.safe_parse_coordinate("200", 1.5, -90.0, 90.0) == 1.5
end
test "returns default for non-string, non-number input" do
assert ParamUtils.safe_parse_coordinate(nil, 1.5, -90.0, 90.0) == 1.5
assert ParamUtils.safe_parse_coordinate(%{}, 1.5, -90.0, 90.0) == 1.5
end
end
describe "clamp_coordinate/3" do
test "keeps values in range" do
assert ParamUtils.clamp_coordinate(45.0, -90.0, 90.0) == 45.0
end
test "clamps above max" do
assert ParamUtils.clamp_coordinate(91.0, -90.0, 90.0) == 90.0
end
test "clamps below min" do
assert ParamUtils.clamp_coordinate(-91.0, -90.0, 90.0) == -90.0
end
test "returns 0.0 for non-numeric" do
assert ParamUtils.clamp_coordinate(nil, -90.0, 90.0) == 0.0
end
end
describe "clamp_zoom/1" do
test "parses binary zoom" do
assert ParamUtils.clamp_zoom("10") == 10
end
test "clamps integer outside range" do
assert ParamUtils.clamp_zoom(50) == 20
assert ParamUtils.clamp_zoom(-5) == 1
end
test "truncates float" do
assert ParamUtils.clamp_zoom(8.9) == 8
end
test "returns default for invalid input" do
assert ParamUtils.clamp_zoom(nil) == 5
assert ParamUtils.clamp_zoom(%{}) == 5
assert ParamUtils.clamp_zoom("garbage") == 5
end
end
describe "valid_callsign?/1" do
test "accepts standard callsigns" do
assert ParamUtils.valid_callsign?("K5ABC")
assert ParamUtils.valid_callsign?("W1XYZ-9")
end
test "rejects lowercase? actually the regex is case-insensitive" do
assert ParamUtils.valid_callsign?("k5abc")
end
test "rejects obvious garbage" do
refute ParamUtils.valid_callsign?("")
refute ParamUtils.valid_callsign?("HELLO WORLD")
refute ParamUtils.valid_callsign?("K5ABC#")
end
test "rejects non-binary input" do
refute ParamUtils.valid_callsign?(nil)
refute ParamUtils.valid_callsign?(123)
end
end
describe "sanitize_path_string/1" do
test "keeps allowed characters" do
assert ParamUtils.sanitize_path_string("WIDE1-1,WIDE2-2*") == "WIDE1-1,WIDE2-2*"
end
test "strips disallowed characters" do
assert ParamUtils.sanitize_path_string("A<script>B") == "AscriptB"
end
test "trims whitespace" do
assert ParamUtils.sanitize_path_string(" ABC ") == "ABC"
end
test "limits length" do
long = String.duplicate("A", 300)
assert byte_size(ParamUtils.sanitize_path_string(long)) == 200
end
test "returns empty for non-binary" do
assert ParamUtils.sanitize_path_string(nil) == ""
end
end
describe "property: parse_float_in_range never returns a value outside the range when input was valid" do
property "result is always default or within [min, max]" do
check all(
value <- float(min: -1000.0, max: 1000.0),
min <- float(min: -500.0, max: 0.0),
max <- float(min: 0.0, max: 500.0),
min < max
) do
default = min
result = ParamUtils.parse_float_in_range(to_string(value), default, min, max)
# credo:disable-for-next-line Jump.CredoChecks.ConditionalAssertion
assert result == default or (result >= min and result <= max)
# parse_float_in_range returns default on parse failure or clamped value
end
end
end
describe "property: clamp_coordinate never exceeds bounds" do
property "clamp_coordinate(v, min, max) is always in [min, max]" do
check all(
v <- one_of([float(), integer()]),
min <- float(min: -1000.0, max: 0.0),
max <- float(min: 0.0, max: 1000.0),
min < max
) do
result = ParamUtils.clamp_coordinate(v, min, max)
assert result >= min and result <= max
end
end
end
describe "property: clamp_zoom always returns an integer in [1, 20]" do
property "clamp_zoom enforces invariants" do
check all(zoom <- one_of([integer(), float(), string(:alphanumeric), constant(nil)])) do
result = ParamUtils.clamp_zoom(zoom)
assert is_integer(result)
assert result >= 1 and result <= 20
end
end
end
describe "property: sanitize_numeric_string output is always printable and short" do
property "output length <= 20 and contains only safe characters" do
check all(s <- string(:printable)) do
result = ParamUtils.sanitize_numeric_string(s)
assert byte_size(result) <= 20
assert Regex.match?(~r/^[\d\.\-eE]*$/, result)
end
end
end
end