This is an automated email from the ASF dual-hosted git repository.

iilyak pushed a commit to branch couch-stats-resource-tracker-v3-rebase-http-2
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit 07d0af7c3a192ee568bfbeecbc3295473562c3c4
Author: ILYA Khlopotov <[email protected]>
AuthorDate: Fri Jun 27 16:03:43 2025 -0700

    Add csrt_httpd_tests.erl
---
 src/couch_stats/test/eunit/csrt_httpd_tests.erl | 375 ++++++++++++++++++++++++
 1 file changed, 375 insertions(+)

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 000000000..9dd2c1139
--- /dev/null
+++ b/src/couch_stats/test/eunit/csrt_httpd_tests.erl
@@ -0,0 +1,375 @@
+% 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").
+
+-import(
+    csrt_test_helper,
+    [
+        rctx_gen/0,
+        rctx_gen/1,
+        rctxs/0,
+        jrctx/1
+    ]
+).
+
+-include_lib("couch/include/couch_db.hrl").
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("couch_mrview/include/couch_mrview.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}).
+
+%% Use different values than default configs to ensure they're picked up
+-define(THRESHOLD_DBNAME_IO, 91).
+-define(THRESHOLD_DOCS_READ, 123).
+-define(THRESHOLD_DOCS_WRITTEN, 12).
+-define(THRESHOLD_IOQ_CALLS, 439).
+-define(THRESHOLD_ROWS_READ, 43).
+-define(THRESHOLD_CHANGES, 79).
+-define(THRESHOLD_LONG_REQS, 432).
+
+-define(TEST_QUERY_LIMIT, 98).
+
+csrt_httpd_test_() ->
+    {
+        foreach,
+        fun setup/0,
+        fun teardown/1,
+        [
+            ?TDEF_FE(t_query_group_by),
+            ?TDEF_FE(t_query_count_by),
+            ?TDEF_FE(t_query_sort_by)
+        ]
+    }.
+
+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"),
+    DbName = binary_to_list(?tempdb()),
+    Port = mochiweb_socket_server:get(chttpd, port),
+    Url = lists:concat(["http://";, Addr, ":", Port, "/"]),
+    {Ctx, Url, DbName}.
+
+teardown(#{dbname := DbName, ctx := Ctx}) ->
+    meck:unload(ioq),
+    ok = fabric:delete_db(DbName, [?ADMIN_CTX]),
+    Persist = false,
+    ok = config:delete("admins", ?USER, Persist),
+    test_util:stop_couch(Ctx).
+
+create_db(Top, Db) ->
+    case req(put, Top ++ Db) of
+        {201, #{}} -> ok;
+        Error -> error({failed_to_create_test_db, Db, Error})
+    end.
+
+req(Method, Url) ->
+    Headers = [?JSON_CT, ?AUTH, ?ACCEPT_JSON],
+    {ok, Code, _, Res} = test_request:request(Method, Url, Headers),
+    {Code, json_decode(Res)}.
+
+req(Method, Url, #{} = Body) ->
+    req(Method, Url, jiffy:encode(Body));
+req(Method, Url, Body) ->
+    Headers = [?JSON_CT, ?AUTH, ?ACCEPT_JSON],
+    {ok, Code, _, Res} = test_request:request(Method, Url, Headers, Body),
+    {Code, json_decode(Res)}.
+
+json_decode(Bin) when is_binary(Bin) ->
+    jiffy:decode(Bin, [return_maps]).
+
+setup() ->
+    {Ctx, Url, DbName} = setup_ctx(),
+    configure(),
+    ok = create_db(Url, DbName),
+    create_docs(DbName),
+    PidRef = mock_all_docs_req(DbName),
+    MArgs = #mrargs{include_docs = false},
+    _Res = fabric:all_docs(DbName, [?ADMIN_CTX], fun view_cb/2, [], MArgs),
+    Rctx = load_rctx(PidRef),
+    Rctxs = rctxs(),
+    #{ctx => Ctx, dbname => DbName, rctx => Rctx, rctxs => Rctxs, url => Url}.
+
+
+create_docs(DbName) ->
+    Docs = make_docs(100),
+    Opts = [],
+    {ok, _} = fabric:update_docs(DbName, Docs, Opts),
+    ok.
+
+configure() ->
+    config:set_boolean(?CSRT, "randomize_testing", false, false),
+    config:set_boolean(?CSRT, "enable_reporting", true, false),
+    config:set_boolean(?CSRT, "enable_rpc_reporting", true, false),
+
+    ok = meck:new(ioq, [passthrough]),
+    ok = meck:expect(ioq, bypass, fun(_, _) -> false end),
+
+    ok = config:set(
+        "csrt_logger.matchers_threshold", "docs_read", 
integer_to_list(?THRESHOLD_DOCS_READ), false
+    ),
+    ok = config:set(
+        "csrt_logger.matchers_threshold",
+        "docs_written",
+        integer_to_list(?THRESHOLD_DOCS_WRITTEN),
+        false
+    ),
+    ok = config:set(
+        "csrt_logger.matchers_threshold", "ioq_calls", 
integer_to_list(?THRESHOLD_IOQ_CALLS), false
+    ),
+    ok = config:set(
+        "csrt_logger.matchers_threshold", "rows_read", 
integer_to_list(?THRESHOLD_ROWS_READ), false
+    ),
+    ok = config:set(
+        "csrt_logger.matchers_threshold",
+        "changes_processed",
+        integer_to_list(?THRESHOLD_CHANGES),
+        false
+    ),
+    ok = config:set(
+        "csrt_logger.matchers_threshold", "long_reqs", 
integer_to_list(?THRESHOLD_LONG_REQS), false
+    ),
+    ok = config:set("csrt_logger.dbnames_io", "foo", 
integer_to_list(?THRESHOLD_DBNAME_IO), false),
+    ok = config:set("csrt_logger.dbnames_io", "bar", 
integer_to_list(?THRESHOLD_DBNAME_IO), false),
+    ok = config:set(
+        "csrt_logger.dbnames_io", "foo/bar", 
integer_to_list(?THRESHOLD_DBNAME_IO), false
+    ),
+    ok = config:set(?CSRT, "query_limit", integer_to_list(?TEST_QUERY_LIMIT)),
+    csrt_logger:reload_matchers(),
+    ok.
+
+%% we cannot use normal http request, because the `chttpd` calls
+%% `csrt:destroy_context()` and we would remove entries from `ets`.
+mock_all_docs_req(DbName) ->
+    Method = 'GET',
+    Path = "/" ++ DbName ++ "/_all_docs",
+    Nonce = couch_util:to_hex(crypto:strong_rand_bytes(5)),
+    Req = #httpd{method = Method, nonce = Nonce},
+    {_, _} = PidRef = csrt:create_coordinator_context(Req, Path),
+    csrt:set_context_username(<<"user_foo">>),
+    csrt:set_context_dbname(?l2b(DbName)),
+    PidRef.
+
+load_rctx(PidRef) ->
+    %% Add slight delay to accumulate RPC response deltas
+    timer:sleep(50),
+    csrt:get_resource(PidRef).
+
+make_docs(Count) ->
+    lists:map(
+        fun(I) ->
+            #doc{
+                id = ?l2b("foo_" ++ integer_to_list(I)),
+                body = {[{<<"value">>, I}]}
+            }
+        end,
+        lists:seq(1, Count)
+    ).
+
+view_cb({row, Row}, Acc) ->
+    {ok, [Row | Acc]};
+view_cb(_Msg, Acc) ->
+    {ok, Acc}.
+
+active_resources(Url, MatchName, Body) ->
+    req(post, Url ++ "/_active_resources/_match/" ++ MatchName, Body).
+
+
+t_query_group_by(#{rctx := Rctx, dbname := DbName, url := Url}) ->
+    DbNameBin = ?l2b(DbName),
+    IoqCalls = Rctx#rctx.ioq_calls,
+    Req = fun(AggregationKeys, CounterKey) ->
+        #{
+            <<"group_by">> => #{
+                <<"aggregate_keys">> => AggregationKeys,
+                <<"counter_key">> => CounterKey
+            }
+        }
+    end,
+    ?assertMatch(
+        {200, [#{
+            <<"key">> := #{<<"dbname">> := DbNameBin, <<"username">> := 
<<"user_foo">>},
+            <<"value">> := IoqCalls}]},
+        active_resources(Url, "rows_read", Req([<<"username">>, <<"dbname">>], 
<<"ioq_calls">>)),
+        "Should handle 'AggregationKeys :: [binary(), ...]'"
+    ),
+    ?assertMatch(
+        {200, [#{
+            <<"key">> := #{<<"username">> := <<"user_foo">>},
+            <<"value">> := IoqCalls}]},
+        active_resources(Url, "rows_read", Req([<<"username">>], 
<<"ioq_calls">>)),
+        "Should handle 'AggregationKeys :: [binary()]'"
+    ),
+    ?assertMatch(
+        {200, [#{
+            <<"key">> := #{<<"username">> := <<"user_foo">>},
+            <<"value">> := IoqCalls}]},
+        active_resources(Url, "rows_read", Req(<<"username">>, 
<<"ioq_calls">>)),
+        "Should handle 'AggregationKeys :: binary()'"
+    ),
+    ?assertMatch(
+        {400,
+            #{<<"error">> := <<"bad_request">>,
+            <<"reason">> := <<"Unknown matcher 'unknown_matcher'">>}},
+        active_resources(Url, "unknown_matcher", Req([<<"username">>], 
<<"ioq_calls">>)),
+        "Should return error if 'matcher' is unknown"
+    ),
+    ?assertMatch(
+        {400,
+            #{<<"error">> := <<"bad_request">>,
+            <<"reason">> := <<"Unknown field name 'unknown_field'">>}},
+        active_resources(Url, "rows_read", Req([<<"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(Url, "rows_read", Req(<<"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(Url, "rows_read", Req(<<"username">>, 
<<"unknown_field">>)),
+        "Should return error if 'ValueKey' contain unknown field"
+    ),
+    ok.
+
+t_query_count_by(#{dbname := DbName, url := Url}) ->
+    DbNameBin = ?l2b(DbName),
+    IoqCount = 1,
+    Req = fun(AggregationKeys) ->
+        #{
+            <<"count_by">> => #{
+                <<"aggregate_keys">> => AggregationKeys
+            }
+        }
+    end,
+    ?assertMatch(
+        {200, [#{
+            <<"key">> := #{<<"dbname">> := DbNameBin, <<"username">> := 
<<"user_foo">>},
+            <<"value">> := IoqCount}]},
+        active_resources(Url, "rows_read", Req([<<"username">>, 
<<"dbname">>])),
+        "Should handle 'AggregationKeys :: [binary(), ...]'"
+    ),
+    ?assertMatch(
+        {200, [#{
+            <<"key">> := #{<<"username">> := <<"user_foo">>},
+            <<"value">> := IoqCount}]},
+        active_resources(Url, "rows_read", Req([<<"username">>])),
+        "Should handle 'AggregationKeys :: [binary()]'"
+    ),
+    ?assertMatch(
+        {200, [#{
+            <<"key">> := #{<<"username">> := <<"user_foo">>},
+            <<"value">> := IoqCount}]},
+        active_resources(Url, "rows_read", Req(<<"username">>)),
+        "Should handle 'AggregationKeys :: binary()'"
+    ),
+    ?assertMatch(
+        {400,
+            #{<<"error">> := <<"bad_request">>,
+            <<"reason">> := <<"Unknown matcher 'unknown_matcher'">>}},
+        active_resources(Url, "unknown_matcher", Req([<<"username">>])),
+        "Should return error if 'matcher' is unknown"
+    ),
+    ?assertMatch(
+        {400,
+            #{<<"error">> := <<"bad_request">>,
+            <<"reason">> := <<"Unknown field name 'unknown_field'">>}},
+        active_resources(Url, "rows_read", Req([<<"unknown_field">>])),
+        "Should return error if 'AggregationKeys' contain unknown field"
+    ),
+    ?assertMatch(
+        {400,
+            #{<<"error">> := <<"bad_request">>,
+            <<"reason">> := <<"Unknown field name 'unknown_field'">>}},
+        active_resources(Url, "rows_read", Req(<<"unknown_field">>)),
+        "Should return error if 'AggregationKeys' is unknown field"
+    ),
+    ok.
+
+t_query_sort_by(#{rctx := Rctx, dbname := DbName, url := Url}) ->
+    DbNameBin = ?l2b(DbName),
+    IoqCalls = Rctx#rctx.ioq_calls,
+    Req = fun(AggregationKeys, CounterKey) ->
+        #{
+            <<"sort_by">> => #{
+                <<"aggregate_keys">> => AggregationKeys,
+                <<"counter_key">> => CounterKey
+            }
+        }
+    end,
+    ?assertMatch(
+        {200, [#{
+            <<"key">> := #{<<"dbname">> := DbNameBin, <<"username">> := 
<<"user_foo">>},
+            <<"value">> := IoqCalls}]},
+        active_resources(Url, "rows_read", Req([<<"username">>, <<"dbname">>], 
<<"ioq_calls">>)),
+        "Should handle 'AggregationKeys :: [binary(), ...]'"
+    ),
+    ?assertMatch(
+        {200, [#{
+            <<"key">> := #{<<"username">> := <<"user_foo">>},
+            <<"value">> := IoqCalls}]},
+        active_resources(Url, "rows_read", Req([<<"username">>], 
<<"ioq_calls">>)),
+        "Should handle 'AggregationKeys :: [binary()]'"
+    ),
+    ?assertMatch(
+        {200, [#{
+            <<"key">> := #{<<"username">> := <<"user_foo">>},
+            <<"value">> := IoqCalls}]},
+        active_resources(Url, "rows_read", Req(<<"username">>, 
<<"ioq_calls">>)),
+        "Should handle 'AggregationKeys :: binary()'"
+    ),
+    ?assertMatch(
+        {400,
+            #{<<"error">> := <<"bad_request">>,
+            <<"reason">> := <<"Unknown matcher 'unknown_matcher'">>}},
+        active_resources(Url, "unknown_matcher", Req([<<"username">>], 
<<"ioq_calls">>)),
+        "Should return error if 'matcher' is unknown"
+    ),
+    ?assertMatch(
+        {400,
+            #{<<"error">> := <<"bad_request">>,
+            <<"reason">> := <<"Unknown field name 'unknown_field'">>}},
+        active_resources(Url, "rows_read", Req([<<"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(Url, "rows_read", Req(<<"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(Url, "rows_read", Req(<<"username">>, 
<<"unknown_field">>)),
+        "Should return error if 'ValueKey' contain unknown field"
+    ),
+    ok.

Reply via email to