Add csrt_httpd_tests.erl suite
diff --git a/src/couch_stats/test/eunit/csrt_httpd_tests.erl b/src/couch_stats/test/eunit/csrt_httpd_tests.erl
new file mode 100644
index 0000000..a054373
--- /dev/null
+++ b/src/couch_stats/test/eunit/csrt_httpd_tests.erl
@@ -0,0 +1,660 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+%   http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(csrt_httpd_tests).
+
+-include_lib("stdlib/include/ms_transform.hrl").
+
+-include_lib("couch/include/couch_eunit.hrl").
+-include("../../src/couch_stats_resource_tracker.hrl").
+
+-define(USER, ?MODULE_STRING ++ "_admin").
+-define(PASS, "pass").
+-define(AUTH, {basic_auth, {?USER, ?PASS}}).
+
+-define(JSON, "application/json").
+-define(JSON_CT, {"Content-Type", ?JSON}).
+-define(ACCEPT_JSON, {"Accept", ?JSON}).
+
+csrt_httpd_test_() ->
+    {
+        foreach,
+        fun setup/0,
+        fun teardown/1,
+        [
+            ?TDEF_FE(t_query_group_by_multiple_keys),
+            ?TDEF_FE(t_query_group_by_single_key),
+            ?TDEF_FE(t_query_group_by_binary_key),
+            ?TDEF_FE(t_query_group_by_bad_request),
+            ?TDEF_FE(t_query_count_by_multiple_keys),
+            ?TDEF_FE(t_query_count_by_single_key),
+            ?TDEF_FE(t_query_count_by_binary_key),
+            ?TDEF_FE(t_query_count_by_bad_request),
+            ?TDEF_FE(t_query_sort_by_multiple_keys),
+            ?TDEF_FE(t_query_sort_by_single_key),
+            ?TDEF_FE(t_query_sort_by_binary_key),
+            ?TDEF_FE(t_query_sort_by_bad_request)
+        ]
+    }.
+
+setup_ctx() ->
+    Ctx = test_util:start_couch([chttpd, fabric, couch_stats]),
+    Hashed = couch_passwords:hash_admin_password(?PASS),
+    HashedList = binary_to_list(Hashed),
+    ok = config:set("admins", ?USER, HashedList, false),
+    Addr = config:get("chttpd", "bind_address", "127.0.0.1"),
+    Port = mochiweb_socket_server:get(chttpd, port),
+    Url = lists:concat(["http://", Addr, ":", Port, "/"]),
+    {Ctx, Url}.
+
+setup() ->
+    {Ctx, Url} = setup_ctx(),
+    Rctxs = [
+        rctx(#{dbname => <<"db1">>, ioq_calls => 123, username => <<"user_foo">>}),
+        rctx(#{dbname => <<"db1">>, ioq_calls => 321, username => <<"user_foo">>}),
+        rctx(#{dbname => <<"db2">>, ioq_calls => 345, username => <<"user_bar">>}),
+        rctx(#{dbname => <<"db2">>, ioq_calls => 543, username => <<"user_bar">>}),
+        rctx(#{dbname => <<"db1">>, ioq_calls => 678, username => <<"user_bar">>}),
+        rctx(#{dbname => <<"db2">>, ioq_calls => 987, username => <<"user_foo">>})
+    ],
+    ets:insert(?CSRT_ETS, Rctxs),
+    #{ctx => Ctx, url => Url, rctxs => Rctxs}.
+
+teardown(#{ctx := Ctx}) ->
+    Persist = false,
+    ok = config:delete("admins", ?USER, Persist),
+    test_util:stop_couch(Ctx).
+
+active_resources_group_by(Url, AggregationKeys, CounterKey) ->
+    active_resources_group_by("docs_read", Url, AggregationKeys, CounterKey).
+
+active_resources_group_by(MatcherName, Url, AggregationKeys, CounterKey) ->
+    Body = #{
+        <<"group_by">> => #{
+            <<"aggregate_keys">> => AggregationKeys,
+            <<"counter_key">> => CounterKey
+        }
+    },
+    active_resources(Url, MatcherName, Body).
+
+t_query_group_by_multiple_keys(#{rctxs := Rctxs, url := Url}) ->
+    Aggregated = aggregate([username, dbname], ioq_calls, Rctxs),
+    Grouped = group(Aggregated),
+    {RC, Results} = active_resources_group_by(Url, [<<"username">>, <<"dbname">>], <<"ioq_calls">>),
+    ?assertEqual(200, RC, format("Should have '200' return code, got ~p~n  ~p~n", [RC, Results])),
+    [
+        #{
+            <<"errors">> := [],
+            <<"node">> := _,
+            <<"result">> := Result
+        }
+    ] = Results,
+    ?assert(is_list(Result), format("Expected list of entries, got ~p~n", [Result])),
+    ?assertEqual(
+        4, length(Result), format("Expected four entries, got ~p~n  ~p~n", [length(Result), Result])
+    ),
+    ?assertMatch(
+        [
+            #{<<"key">> := #{<<"username">> := _, <<"dbname">> := _}, <<"value">> := _},
+            #{<<"key">> := #{<<"username">> := _, <<"dbname">> := _}, <<"value">> := _},
+            #{<<"key">> := #{<<"username">> := _, <<"dbname">> := _}, <<"value">> := _},
+            #{<<"key">> := #{<<"username">> := _, <<"dbname">> := _}, <<"value">> := _}
+        ],
+        Result,
+        "Unexpected shape of the result"
+    ),
+    OrderedByKey = order_by_key([username, dbname], Result),
+    V1 = maps:get({<<"user_bar">>, <<"db1">>}, Grouped),
+    V2 = maps:get({<<"user_bar">>, <<"db2">>}, Grouped),
+    V3 = maps:get({<<"user_foo">>, <<"db1">>}, Grouped),
+    V4 = maps:get({<<"user_foo">>, <<"db2">>}, Grouped),
+    ?assertMatch(
+        [
+            #{
+                <<"key">> := #{<<"username">> := <<"user_bar">>, <<"dbname">> := <<"db1">>},
+                <<"value">> := V1
+            },
+            #{
+                <<"key">> := #{<<"username">> := <<"user_bar">>, <<"dbname">> := <<"db2">>},
+                <<"value">> := V2
+            },
+            #{
+                <<"key">> := #{<<"username">> := <<"user_foo">>, <<"dbname">> := <<"db1">>},
+                <<"value">> := V3
+            },
+            #{
+                <<"key">> := #{<<"username">> := <<"user_foo">>, <<"dbname">> := <<"db2">>},
+                <<"value">> := V4
+            }
+        ],
+        OrderedByKey
+    ),
+    ok.
+
+t_query_group_by_single_key(#{rctxs := Rctxs, url := Url}) ->
+    Aggregated = aggregate([username], ioq_calls, Rctxs),
+    Grouped = group(Aggregated),
+    {RC, Results} = active_resources_group_by(Url, [<<"username">>], <<"ioq_calls">>),
+    ?assertEqual(200, RC, format("Should have '200' return code, got ~p~n  ~p~n", [RC, Results])),
+    [
+        #{
+            <<"errors">> := [],
+            <<"node">> := _,
+            <<"result">> := Result
+        }
+    ] = Results,
+    ?assert(is_list(Result), format("Expected list of entries, got ~p~n", [Result])),
+    ?assertEqual(
+        2, length(Result), format("Expected two entries, got ~p~n  ~p~n", [length(Result), Result])
+    ),
+    ?assertMatch(
+        [
+            #{<<"key">> := #{<<"username">> := _}, <<"value">> := _},
+            #{<<"key">> := #{<<"username">> := _}, <<"value">> := _}
+        ],
+        Result,
+        "Unexpected shape of the result"
+    ),
+    OrderedByKey = order_by_key([username], Result),
+    V1 = maps:get({<<"user_bar">>}, Grouped),
+    V2 = maps:get({<<"user_foo">>}, Grouped),
+    ?assertMatch(
+        [
+            #{<<"key">> := #{<<"username">> := <<"user_bar">>}, <<"value">> := V1},
+            #{<<"key">> := #{<<"username">> := <<"user_foo">>}, <<"value">> := V2}
+        ],
+        OrderedByKey
+    ),
+    ok.
+
+t_query_group_by_binary_key(#{rctxs := Rctxs, url := Url}) ->
+    Aggregated = aggregate([username], ioq_calls, Rctxs),
+    Grouped = group(Aggregated),
+    {RC, Results} = active_resources_group_by(Url, <<"username">>, <<"ioq_calls">>),
+    ?assertEqual(200, RC, format("Should have '200' return code, got ~p~n  ~p~n", [RC, Results])),
+    [
+        #{
+            <<"errors">> := [],
+            <<"node">> := _,
+            <<"result">> := Result
+        }
+    ] = Results,
+    ?assert(is_list(Result), format("Expected list of entries, got ~p~n", [Result])),
+    ?assertEqual(
+        2, length(Result), format("Expected two entries, got ~p~n  ~p~n", [length(Result), Result])
+    ),
+    ?assertMatch(
+        [
+            #{<<"key">> := #{<<"username">> := _}, <<"value">> := _},
+            #{<<"key">> := #{<<"username">> := _}, <<"value">> := _}
+        ],
+        Result,
+        format("Unexpected shape of the result~n  ~p~n", [Result])
+    ),
+    OrderedByKey = order_by_key([username], Result),
+    V1 = maps:get({<<"user_bar">>}, Grouped),
+    V2 = maps:get({<<"user_foo">>}, Grouped),
+    ?assertMatch(
+        [
+            #{<<"key">> := #{<<"username">> := <<"user_bar">>}, <<"value">> := V1},
+            #{<<"key">> := #{<<"username">> := <<"user_foo">>}, <<"value">> := V2}
+        ],
+        OrderedByKey
+    ),
+    ok.
+
+t_query_group_by_bad_request(#{url := Url}) ->
+    ?assertMatch(
+        {400, #{
+            <<"error">> := <<"bad_request">>,
+            <<"reason">> := <<"Unknown matcher 'unknown_matcher'">>
+        }},
+        active_resources_group_by("unknown_matcher", Url, <<"username">>, <<"ioq_calls">>),
+        "Should return error if 'matcher' is unknown"
+    ),
+    ?assertMatch(
+        {400, #{
+            <<"error">> := <<"bad_request">>,
+            <<"reason">> := <<"Unknown field name 'unknown_field'">>
+        }},
+        active_resources_group_by(Url, [<<"unknown_field">>], <<"ioq_calls">>),
+        "Should return error if 'AggregationKeys' contain unknown field"
+    ),
+    ?assertMatch(
+        {400, #{
+            <<"error">> := <<"bad_request">>,
+            <<"reason">> := <<"Unknown field name 'unknown_field'">>
+        }},
+        active_resources_group_by(Url, <<"unknown_field">>, <<"ioq_calls">>),
+        "Should return error if 'AggregationKeys' is unknown field"
+    ),
+    ?assertMatch(
+        {400, #{
+            <<"error">> := <<"bad_request">>,
+            <<"reason">> := <<"Unknown field name 'unknown_field'">>
+        }},
+        active_resources_group_by(Url, <<"username">>, <<"unknown_field">>),
+        "Should return error if 'ValueKey' contain unknown field"
+    ),
+    ok.
+
+active_resources_count_by(Url, AggregationKeys) ->
+    active_resources_count_by("docs_read", Url, AggregationKeys).
+
+active_resources_count_by(MatcherName, Url, AggregationKeys) ->
+    Body = #{
+        <<"count_by">> => #{
+            <<"aggregate_keys">> => AggregationKeys
+        }
+    },
+    active_resources(Url, MatcherName, Body).
+
+t_query_count_by_multiple_keys(#{rctxs := Rctxs, url := Url}) ->
+    Aggregated = aggregate([username, dbname], ioq_calls, Rctxs),
+    Grouped = count(Aggregated),
+    {RC, Results} = active_resources_count_by(Url, [<<"username">>, <<"dbname">>]),
+    ?assertEqual(200, RC, format("Should have '200' return code, got ~p~n  ~p~n", [RC, Results])),
+    [
+        #{
+            <<"errors">> := [],
+            <<"node">> := _,
+            <<"result">> := Result
+        }
+    ] = Results,
+    ?assert(is_list(Result), format("Expected list of entries, got ~p~n", [Result])),
+    ?assertEqual(
+        4, length(Result), format("Expected four entries, got ~p~n  ~p~n", [length(Result), Result])
+    ),
+    ?assertMatch(
+        [
+            #{<<"key">> := #{<<"username">> := _, <<"dbname">> := _}, <<"value">> := _},
+            #{<<"key">> := #{<<"username">> := _, <<"dbname">> := _}, <<"value">> := _},
+            #{<<"key">> := #{<<"username">> := _, <<"dbname">> := _}, <<"value">> := _},
+            #{<<"key">> := #{<<"username">> := _, <<"dbname">> := _}, <<"value">> := _}
+        ],
+        Result,
+        "Unexpected shape of the result"
+    ),
+    OrderedByKey = order_by_key([username, dbname], Result),
+    V1 = maps:get({<<"user_bar">>, <<"db1">>}, Grouped),
+    V2 = maps:get({<<"user_bar">>, <<"db2">>}, Grouped),
+    V3 = maps:get({<<"user_foo">>, <<"db1">>}, Grouped),
+    V4 = maps:get({<<"user_foo">>, <<"db2">>}, Grouped),
+    ?assertMatch(
+        [
+            #{
+                <<"key">> := #{<<"username">> := <<"user_bar">>, <<"dbname">> := <<"db1">>},
+                <<"value">> := V1
+            },
+            #{
+                <<"key">> := #{<<"username">> := <<"user_bar">>, <<"dbname">> := <<"db2">>},
+                <<"value">> := V2
+            },
+            #{
+                <<"key">> := #{<<"username">> := <<"user_foo">>, <<"dbname">> := <<"db1">>},
+                <<"value">> := V3
+            },
+            #{
+                <<"key">> := #{<<"username">> := <<"user_foo">>, <<"dbname">> := <<"db2">>},
+                <<"value">> := V4
+            }
+        ],
+        OrderedByKey
+    ),
+    ok.
+
+t_query_count_by_single_key(#{rctxs := Rctxs, url := Url}) ->
+    Aggregated = aggregate([username], ioq_calls, Rctxs),
+    Grouped = count(Aggregated),
+    {RC, Results} = active_resources_count_by(Url, [<<"username">>]),
+    ?assertEqual(200, RC, format("Should have '200' return code, got ~p~n  ~p~n", [RC, Results])),
+    [
+        #{
+            <<"errors">> := [],
+            <<"node">> := _,
+            <<"result">> := Result
+        }
+    ] = Results,
+    ?assert(is_list(Result), format("Expected list of entries, got ~p~n", [Result])),
+    ?assertEqual(
+        2, length(Result), format("Expected two entries, got ~p~n  ~p~n", [length(Result), Result])
+    ),
+    ?assertMatch(
+        [
+            #{<<"key">> := #{<<"username">> := _}, <<"value">> := _},
+            #{<<"key">> := #{<<"username">> := _}, <<"value">> := _}
+        ],
+        Result,
+        "Unexpected shape of the result"
+    ),
+    OrderedByKey = order_by_key([username], Result),
+    V1 = maps:get({<<"user_bar">>}, Grouped),
+    V2 = maps:get({<<"user_foo">>}, Grouped),
+    ?assertMatch(
+        [
+            #{<<"key">> := #{<<"username">> := <<"user_bar">>}, <<"value">> := V1},
+            #{<<"key">> := #{<<"username">> := <<"user_foo">>}, <<"value">> := V2}
+        ],
+        OrderedByKey
+    ),
+    ok.
+
+t_query_count_by_binary_key(#{rctxs := Rctxs, url := Url}) ->
+    Aggregated = aggregate([username], ioq_calls, Rctxs),
+    Grouped = count(Aggregated),
+    {RC, Results} = active_resources_count_by(Url, <<"username">>),
+    ?assertEqual(200, RC, format("Should have '200' return code, got ~p~n  ~p~n", [RC, Results])),
+    [
+        #{
+            <<"errors">> := [],
+            <<"node">> := _,
+            <<"result">> := Result
+        }
+    ] = Results,
+    ?assert(is_list(Result), format("Expected list of entries, got ~p~n", [Result])),
+    ?assertEqual(
+        2, length(Result), format("Expected two entries, got ~p~n  ~p~n", [length(Result), Result])
+    ),
+    ?assertMatch(
+        [
+            #{<<"key">> := #{<<"username">> := _}, <<"value">> := _},
+            #{<<"key">> := #{<<"username">> := _}, <<"value">> := _}
+        ],
+        Result,
+        "Unexpected shape of the result"
+    ),
+    OrderedByKey = order_by_key([username], Result),
+    V1 = maps:get({<<"user_bar">>}, Grouped),
+    V2 = maps:get({<<"user_foo">>}, Grouped),
+    ?assertMatch(
+        [
+            #{<<"key">> := #{<<"username">> := <<"user_bar">>}, <<"value">> := V1},
+            #{<<"key">> := #{<<"username">> := <<"user_foo">>}, <<"value">> := V2}
+        ],
+        OrderedByKey
+    ),
+    ok.
+
+t_query_count_by_bad_request(#{url := Url}) ->
+    ?assertMatch(
+        {400, #{
+            <<"error">> := <<"bad_request">>,
+            <<"reason">> := <<"Unknown matcher 'unknown_matcher'">>
+        }},
+        active_resources_count_by("unknown_matcher", Url, <<"username">>),
+        "Should return error if 'matcher' is unknown"
+    ),
+    ?assertMatch(
+        {400, #{
+            <<"error">> := <<"bad_request">>,
+            <<"reason">> := <<"Unknown field name 'unknown_field'">>
+        }},
+        active_resources_count_by(Url, [<<"unknown_field">>]),
+        "Should return error if 'AggregationKeys' contain unknown field"
+    ),
+    ?assertMatch(
+        {400, #{
+            <<"error">> := <<"bad_request">>,
+            <<"reason">> := <<"Unknown field name 'unknown_field'">>
+        }},
+        active_resources_count_by(Url, <<"unknown_field">>),
+        "Should return error if 'AggregationKeys' is unknown field"
+    ),
+    ok.
+
+active_resources_sort_by(Url, AggregationKeys, CounterKey) ->
+    active_resources_sort_by("docs_read", Url, AggregationKeys, CounterKey).
+
+active_resources_sort_by(MatcherName, Url, AggregationKeys, CounterKey) ->
+    Body = #{
+        <<"sort_by">> => #{
+            <<"aggregate_keys">> => AggregationKeys,
+            <<"counter_key">> => CounterKey
+        }
+    },
+    active_resources(Url, MatcherName, Body).
+
+t_query_sort_by_multiple_keys(#{rctxs := Rctxs, url := Url}) ->
+    Aggregated = aggregate([username, dbname], ioq_calls, Rctxs),
+    Grouped = group(Aggregated),
+    Ordered = order_by_value(Grouped),
+    {RC, Results} = active_resources_sort_by(Url, [<<"username">>, <<"dbname">>], <<"ioq_calls">>),
+    ?assertEqual(200, RC, format("Should have '200' return code, got ~p~n  ~p~n", [RC, Results])),
+    [
+        #{
+            <<"errors">> := [],
+            <<"node">> := _,
+            <<"result">> := Result
+        }
+    ] = Results,
+    ?assert(is_list(Result), format("Expected list of entries, got ~p~n", [Result])),
+    ?assertEqual(
+        4, length(Result), format("Expected four entries, got ~p~n  ~p~n", [length(Result), Result])
+    ),
+    ?assertMatch(
+        [
+            #{<<"key">> := #{<<"username">> := _, <<"dbname">> := _}, <<"value">> := _},
+            #{<<"key">> := #{<<"username">> := _, <<"dbname">> := _}, <<"value">> := _},
+            #{<<"key">> := #{<<"username">> := _, <<"dbname">> := _}, <<"value">> := _},
+            #{<<"key">> := #{<<"username">> := _, <<"dbname">> := _}, <<"value">> := _}
+        ],
+        Result,
+        "Unexpected shape of the result"
+    ),
+    [
+        {{<<"user_foo">>, <<"db2">>}, V1},
+        {{<<"user_bar">>, <<"db2">>}, V2},
+        {{<<"user_bar">>, <<"db1">>}, V3},
+        {{<<"user_foo">>, <<"db1">>}, V4}
+    ] = Ordered,
+    ?assertMatch(
+        [
+            #{
+                <<"key">> := #{<<"username">> := <<"user_foo">>, <<"dbname">> := <<"db2">>},
+                <<"value">> := V1
+            },
+            #{
+                <<"key">> := #{<<"username">> := <<"user_bar">>, <<"dbname">> := <<"db2">>},
+                <<"value">> := V2
+            },
+            #{
+                <<"key">> := #{<<"username">> := <<"user_bar">>, <<"dbname">> := <<"db1">>},
+                <<"value">> := V3
+            },
+            #{
+                <<"key">> := #{<<"username">> := <<"user_foo">>, <<"dbname">> := <<"db1">>},
+                <<"value">> := V4
+            }
+        ],
+        Result
+    ),
+    ok.
+
+t_query_sort_by_single_key(#{rctxs := Rctxs, url := Url}) ->
+    Aggregated = aggregate([username], ioq_calls, Rctxs),
+    Grouped = group(Aggregated),
+    Ordered = order_by_value(Grouped),
+    {RC, Results} = active_resources_sort_by(Url, [<<"username">>], <<"ioq_calls">>),
+    ?assertEqual(200, RC, format("Should have '200' return code, got ~p~n  ~p~n", [RC, Results])),
+    [
+        #{
+            <<"errors">> := [],
+            <<"node">> := _,
+            <<"result">> := Result
+        }
+    ] = Results,
+    ?assert(is_list(Result), format("Expected list of entries, got ~p~n", [Result])),
+    ?assertEqual(
+        2, length(Result), format("Expected two entries, got ~p~n  ~p~n", [length(Result), Result])
+    ),
+    ?assertMatch(
+        [
+            #{<<"key">> := #{<<"username">> := _}, <<"value">> := _},
+            #{<<"key">> := #{<<"username">> := _}, <<"value">> := _}
+        ],
+        Result,
+        "Unexpected shape of the result"
+    ),
+    [
+        {{<<"user_bar">>}, V1},
+        {{<<"user_foo">>}, V2}
+    ] = Ordered,
+    ?assertMatch(
+        [
+            #{<<"key">> := #{<<"username">> := <<"user_bar">>}, <<"value">> := V1},
+            #{<<"key">> := #{<<"username">> := <<"user_foo">>}, <<"value">> := V2}
+        ],
+        Result
+    ),
+    ok.
+
+t_query_sort_by_binary_key(#{rctxs := Rctxs, url := Url}) ->
+    Aggregated = aggregate([username], ioq_calls, Rctxs),
+    Grouped = group(Aggregated),
+    Ordered = order_by_value(Grouped),
+    {RC, Results} = active_resources_sort_by(Url, <<"username">>, <<"ioq_calls">>),
+    ?assertEqual(200, RC, format("Should have '200' return code, got ~p~n  ~p~n", [RC, Results])),
+    [
+        #{
+            <<"errors">> := [],
+            <<"node">> := _,
+            <<"result">> := Result
+        }
+    ] = Results,
+    ?assert(is_list(Result), format("Expected list of entries, got ~p~n", [Result])),
+    ?assertEqual(
+        2, length(Result), format("Expected two entries, got ~p~n  ~p~n", [length(Result), Result])
+    ),
+    ?assertMatch(
+        [
+            #{<<"key">> := #{<<"username">> := _}, <<"value">> := _},
+            #{<<"key">> := #{<<"username">> := _}, <<"value">> := _}
+        ],
+        Result,
+        "Unexpected shape of the result"
+    ),
+    [
+        {{<<"user_bar">>}, V1},
+        {{<<"user_foo">>}, V2}
+    ] = Ordered,
+    ?assertMatch(
+        [
+            #{<<"key">> := #{<<"username">> := <<"user_bar">>}, <<"value">> := V1},
+            #{<<"key">> := #{<<"username">> := <<"user_foo">>}, <<"value">> := V2}
+        ],
+        Result
+    ),
+    ok.
+
+t_query_sort_by_bad_request(#{url := Url}) ->
+    ?assertMatch(
+        {400, #{
+            <<"error">> := <<"bad_request">>,
+            <<"reason">> := <<"Unknown matcher 'unknown_matcher'">>
+        }},
+        active_resources_sort_by("unknown_matcher", Url, <<"username">>, <<"ioq_calls">>),
+        "Should return error if 'matcher' is unknown"
+    ),
+    ?assertMatch(
+        {400, #{
+            <<"error">> := <<"bad_request">>,
+            <<"reason">> := <<"Unknown field name 'unknown_field'">>
+        }},
+        active_resources_sort_by(Url, [<<"unknown_field">>], <<"ioq_calls">>),
+        "Should return error if 'AggregationKeys' contain unknown field"
+    ),
+    ?assertMatch(
+        {400, #{
+            <<"error">> := <<"bad_request">>,
+            <<"reason">> := <<"Unknown field name 'unknown_field'">>
+        }},
+        active_resources_sort_by(Url, <<"unknown_field">>, <<"ioq_calls">>),
+        "Should return error if 'AggregationKeys' is unknown field"
+    ),
+    ?assertMatch(
+        {400, #{
+            <<"error">> := <<"bad_request">>,
+            <<"reason">> := <<"Unknown field name 'unknown_field'">>
+        }},
+        active_resources_sort_by(Url, <<"username">>, <<"unknown_field">>),
+        "Should return error if 'ValueKey' contain unknown field"
+    ),
+    ok.
+
+format(Fmt, Args) ->
+    lists:flatten(io_lib:format(Fmt, Args)).
+
+aggregate(AggregationKeys, ValField, Records) ->
+    lists:foldl(
+        fun(Rctx, Acc) ->
+            Key = list_to_tuple([csrt_entry:value(Field, Rctx) || Field <- AggregationKeys]),
+            CurrVal = maps:get(Key, Acc, []),
+            maps:put(Key, [csrt_entry:value(ValField, Rctx) | CurrVal], Acc)
+        end,
+        #{},
+        Records
+    ).
+
+group(Aggregated) ->
+    maps:fold(
+        fun(Key, Val, Acc) ->
+            maps:put(Key, lists:foldl(fun erlang:'+'/2, 0, Val), Acc)
+        end,
+        #{},
+        Aggregated
+    ).
+
+count(Aggregated) ->
+    maps:fold(
+        fun(Key, Val, Acc) ->
+            maps:put(Key, lists:foldl(fun(_, A) -> A + 1 end, 0, Val), Acc)
+        end,
+        #{},
+        Aggregated
+    ).
+
+order_by_value(Grouped) ->
+    lists:reverse(lists:keysort(2, maps:to_list(Grouped))).
+
+% This function handles both representations of entries of the result
+% #{<<"key">> => #{<<"dbname">> => <<"db2">>, <<"username">> => <<"user_foo">>}, <<"value">> => 1}
+% and
+% {{<<"db2">>, <<"user_foo">>}, 1}
+order_by_key(AggregationKeys, Entries) when is_list(AggregationKeys) andalso is_list(Entries) ->
+    lists:sort(
+        fun(A, B) ->
+            get_key(AggregationKeys, A) =< get_key(AggregationKeys, B)
+        end,
+        Entries
+    ).
+
+% This function handles both representations of entries of the result
+% #{<<"key">> => #{<<"dbname">> => <<"db2">>, <<"username">> => <<"user_foo">>}, <<"value">> => 1}
+% and
+% {{<<"db2">>, <<"user_foo">>}, 1}
+get_key(AggregationKeys, #{<<"key">> := Key}) ->
+    list_to_tuple([maps:get(atom_to_binary(Field), Key) || Field <- AggregationKeys]);
+get_key(_AggregationKeys, {Key, _}) ->
+    Key.
+
+active_resources(Url, MatchName, Body) ->
+    EndpointUrl = Url ++ "/_active_resources/_match/" ++ MatchName,
+    Headers = [?JSON_CT, ?AUTH, ?ACCEPT_JSON],
+    {ok, Code, _, Res} = test_request:request(post, EndpointUrl, Headers, jiffy:encode(Body)),
+    {Code, jiffy:decode(Res, [return_maps])}.
+
+rctx(Opts) ->
+    % Update `docs_read` to make standard `{docs_read, fun matcher_on_docs_read/1, 1000}`
+    % matcher match.
+    Threshold = config:get("csrt_logger.matchers_threshold", "rows_read", 1000),
+    BaseOpts = #{docs_read => Threshold + 1, username => <<"user_foo">>},
+    csrt_test_helper:rctx_gen(maps:merge(BaseOpts, Opts)).