- 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
281 lines
9 KiB
Elixir
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
|