Skip to content

Commit f2a6f31

Browse files
authored
sort query params in verified routes during tests (#6536)
By having the query parameters being serialized in order (during tests only) when using VerifiedRoutes, we can make some test assertions, like e.g. `assert_redirect/3` from Phoenix.Liveview, less brittle when the query params are constructed dynamically in the backend and it's not easy to predict / maintain exact query param order in tested urls. This behaviour will be enabled in newly generated apps by default. For existing apps, add the following to `confing/test.exs` ```elixir config :phoenix, sort_verified_routes_query_params: true ```
1 parent 593d499 commit f2a6f31

File tree

5 files changed

+38
-5
lines changed

5 files changed

+38
-5
lines changed

config/config.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ config :logger, :console,
77
config :phoenix,
88
json_library: Jason,
99
stacktrace_depth: 20,
10-
trim_on_html_eex_engine: false
10+
trim_on_html_eex_engine: false,
11+
sort_verified_routes_query_params: true
1112

1213
if Mix.env() == :dev do
1314
esbuild = fn args ->

installer/templates/phx_single/config/test.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ config :phoenix, :plug_init_mode, :runtime<%= if @html do %>
2323
# Enable helpful, but potentially expensive runtime checks
2424
config :phoenix_live_view,
2525
enable_expensive_runtime_checks: true<% end %>
26+
27+
# Sort query params output of verified routes for robust url comparisons
28+
config :phoenix,
29+
sort_verified_routes_query_params: true

installer/templates/phx_umbrella/config/test.exs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@ config :phoenix, :plug_init_mode, :runtime<%= if @html do %>
1616
# Enable helpful, but potentially expensive runtime checks
1717
config :phoenix_live_view,
1818
enable_expensive_runtime_checks: true<% end %>
19+
20+
# Sort query params output of verified routes for robust url comparisons
21+
config :phoenix,
22+
sort_verified_routes_query_params: true

lib/phoenix/verified_routes.ex

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ defmodule Phoenix.VerifiedRoutes do
4242
Like path segments, query strings params are proper URL encoded and may be interpolated
4343
directly into the ~p string.
4444
45+
To ease url comparisons during tests (e.g. when using `assert_redirect/3`) query params
46+
will be sorted. This is controlled by the `phoenix: [sort_verified_routes_query_params: true]`
47+
configuration option.
48+
4549
## What about named routes?
4650
4751
Many web frameworks, and early versions of Phoenix, provided a feature called "named routes".
@@ -821,7 +825,11 @@ defmodule Phoenix.VerifiedRoutes do
821825
"interpolated query string params must be separated by &, got: #{Macro.to_string(route)}"
822826
end
823827

824-
rewrite = {:"::", m1, [{{:., m2, [__MODULE__, :__encode_query__]}, m3, [arg]}, bin]}
828+
sort_params? = Application.get_env(:phoenix, :sort_verified_routes_query_params, false)
829+
830+
rewrite =
831+
{:"::", m1, [{{:., m2, [__MODULE__, :__encode_query__]}, m3, [arg, sort_params?]}, bin]}
832+
825833
verify_query(rest, route, [rewrite | acc])
826834
end
827835

@@ -880,14 +888,22 @@ defmodule Phoenix.VerifiedRoutes do
880888
end
881889

882890
@doc false
883-
def __encode_query__(dict) when is_list(dict) or (is_map(dict) and not is_struct(dict)) do
891+
def __encode_query__(dict, sort? \\ false)
892+
893+
def __encode_query__(dict, sort?)
894+
when is_list(dict) or (is_map(dict) and not is_struct(dict)) do
884895
case Plug.Conn.Query.encode(dict, &to_param/1) do
885896
"" -> ""
886-
query_str -> query_str
897+
query_str -> maybe_sort_query(query_str, sort?)
887898
end
888899
end
889900

890-
def __encode_query__(val), do: val |> to_param() |> URI.encode_www_form()
901+
def __encode_query__(val, _sort?), do: val |> to_param() |> URI.encode_www_form()
902+
903+
defp maybe_sort_query(query_str, false), do: query_str
904+
905+
defp maybe_sort_query(query, true),
906+
do: query |> String.split("&") |> Enum.sort() |> Enum.join("&")
891907

892908
defp to_param(int) when is_integer(int), do: Integer.to_string(int)
893909
defp to_param(bin) when is_binary(bin), do: bin

test/phoenix/verified_routes_test.exs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,14 @@ defmodule Phoenix.VerifiedRoutesTest do
371371

372372
assert ~p"/posts/5?#{[foo: %{__struct__: Foo, id: 5}]}" ==
373373
"/posts/5?foo=5"
374+
375+
# {key, value} params pairs are sorted
376+
assert ~p"/posts/5?#{[b: 2, a: 1, c: 3]}" == "/posts/5?a=1&b=2&c=3"
377+
assert ~p"/posts/5?#{%{b: 2, a: 1, c: 3}}" == "/posts/5?a=1&b=2&c=3"
378+
# array values are sorted
379+
assert ~p"/posts/5?#{[foo: ~w(b a)]}" == "/posts/5?foo[]=a&foo[]=b"
380+
# ampersands are escaped and won't mess with splitting query at '&'
381+
assert ~p"/posts/5?#{[foo: "bar", "a&b": "e&f"]}" == "/posts/5?a%26b=e%26f&foo=bar"
374382
end
375383

376384
test "~p mixed query string interpolation" do

0 commit comments

Comments
 (0)