diff options
Diffstat (limited to 'deps/rabbitmq_management/test')
19 files changed, 9409 insertions, 0 deletions
diff --git a/deps/rabbitmq_management/test/cache_SUITE.erl b/deps/rabbitmq_management/test/cache_SUITE.erl new file mode 100644 index 0000000000..00f9cd56c8 --- /dev/null +++ b/deps/rabbitmq_management/test/cache_SUITE.erl @@ -0,0 +1,109 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2016-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(cache_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("proper/include/proper.hrl"). + +-compile(export_all). + +all() -> + [ + {group, non_parallel_tests} + ]. + +groups() -> + [ + {non_parallel_tests, [], [ + name, + fetch, + fetch_cached, + fetch_stale, + fetch_stale_after_expiry, + fetch_throws, + fetch_cached_with_same_args, + fetch_cached_with_different_args_invalidates_cache + ]} + ]. + +%% ------------------------------------------------------------------- +%% Testsuite setup/teardown. +%% ------------------------------------------------------------------- + +init_per_suite(Config) -> + Config. + +end_per_suite(Config) -> + Config. + +init_per_group(_, Config) -> Config. + +end_per_group(_, Config) -> Config. + +init_per_testcase(_Testcase, Config) -> + {ok, P} = rabbit_mgmt_db_cache:start_link(banana), + rabbit_ct_helpers:set_config(Config, {sut, P}). + +end_per_testcase(_Testcase, Config) -> + P = ?config(sut, Config), + _ = gen_server:stop(P), + Config. + +-define(DEFAULT_CACHE_TIME, 5000). + +%% tests + +name(Config) -> + ct:pal(?LOW_IMPORTANCE, "Priv: ~p", [?config(priv_dir, Config)]), + rabbit_mgmt_db_cache_banana = rabbit_mgmt_db_cache:process_name(banana). + +fetch_new_key(_Config) -> + {error, key_not_found} = rabbit_mgmt_db_cache:fetch(this_is_not_the_key_you_are_looking_for, + fun() -> 123 end). + +fetch(_Config) -> + {ok, 123} = rabbit_mgmt_db_cache:fetch(banana, fun() -> 123 end). + +fetch_cached(_Config) -> + {ok, 123} = rabbit_mgmt_db_cache:fetch(banana, fun() -> + timer:sleep(100), + 123 end), + {ok, 123} = rabbit_mgmt_db_cache:fetch(banana, fun() -> 321 end). + +fetch_stale(Config) -> + P = ?config(sut, Config), + {ok, 123} = rabbit_mgmt_db_cache:fetch(banana, fun() -> 123 end), + ok = gen_server:call(P, purge_cache), + {ok, 321} = rabbit_mgmt_db_cache:fetch(banana, fun() -> 321 end). + +fetch_stale_after_expiry(_Config) -> + {ok, 123} = rabbit_mgmt_db_cache:fetch(banana, fun() -> 123 end), % expire quickly + timer:sleep(500), + {ok, 321} = rabbit_mgmt_db_cache:fetch(banana, fun() -> 321 end). + +fetch_throws(_Config) -> + {error, {throw, banana_face}} = + rabbit_mgmt_db_cache:fetch(banana, fun() -> throw(banana_face) end), + {ok, 123} = rabbit_mgmt_db_cache:fetch(banana, fun() -> 123 end). + +fetch_cached_with_same_args(_Config) -> + {ok, 123} = rabbit_mgmt_db_cache:fetch(banana, fun(_) -> + timer:sleep(100), + 123 + end, [42]), + {ok, 123} = rabbit_mgmt_db_cache:fetch(banana, fun(_) -> 321 end, [42]). + +fetch_cached_with_different_args_invalidates_cache(_Config) -> + {ok, 123} = rabbit_mgmt_db_cache:fetch(banana, fun(_) -> + timer:sleep(100), + 123 + end, [42]), + {ok, 321} = rabbit_mgmt_db_cache:fetch(banana, fun(_) -> + timer:sleep(100), + 321 end, [442]), + {ok, 321} = rabbit_mgmt_db_cache:fetch(banana, fun(_) -> 456 end, [442]). diff --git a/deps/rabbitmq_management/test/clustering_SUITE.erl b/deps/rabbitmq_management/test/clustering_SUITE.erl new file mode 100644 index 0000000000..fc096962af --- /dev/null +++ b/deps/rabbitmq_management/test/clustering_SUITE.erl @@ -0,0 +1,874 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2016-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(clustering_SUITE). + +-include_lib("amqp_client/include/amqp_client.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("rabbit_common/include/rabbit_core_metrics.hrl"). +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_metrics.hrl"). +-include_lib("rabbitmq_ct_helpers/include/rabbit_mgmt_test.hrl"). + +-import(rabbit_ct_broker_helpers, [get_node_config/3, restart_node/2]). +-import(rabbit_mgmt_test_util, [http_get/2, http_put/4, http_delete/3]). +-import(rabbit_misc, [pget/2]). + +-compile(export_all). + +all() -> + [ + {group, non_parallel_tests} + ]. + +groups() -> + [{non_parallel_tests, [], [ + list_cluster_nodes_test, + multi_node_case1_test, + ha_queue_hosted_on_other_node, + ha_queue_with_multiple_consumers, + queue_on_other_node, + queue_with_multiple_consumers, + queue_consumer_cancelled, + queue_consumer_channel_closed, + queue, + queues_single, + queues_multiple, + queues_removed, + channels_multiple_on_different_nodes, + channel_closed, + channel, + channel_other_node, + channel_with_consumer_on_other_node, + channel_with_consumer_on_one_node, + consumers, + connections, + exchanges, + exchange, + vhosts, + nodes, + overview, + disable_plugin + ]} + ]. + +%% ------------------------------------------------------------------- +%% Testsuite setup/teardown. +%% ------------------------------------------------------------------- + +merge_app_env(Config) -> + Config1 = rabbit_ct_helpers:merge_app_env(Config, + {rabbit, [ + {collect_statistics, fine}, + {collect_statistics_interval, 500} + ]}), + rabbit_ct_helpers:merge_app_env(Config1, + {rabbitmq_management_agent, [ + {rates_mode, detailed}, + {sample_retention_policies, + %% List of {MaxAgeInSeconds, SampleEveryNSeconds} + [{global, [{605, 5}, {3660, 60}, {29400, 600}, {86400, 1800}]}, + {basic, [{605, 5}, {3600, 60}]}, + {detailed, [{10, 5}]}] }]}). + +init_per_suite(Config) -> + rabbit_ct_helpers:log_environment(), + inets:start(), + Config1 = rabbit_ct_helpers:set_config(Config, [ + {rmq_nodename_suffix, ?MODULE}, + {rmq_nodes_count, 2} + ]), + Config2 = merge_app_env(Config1), + rabbit_ct_helpers:run_setup_steps(Config2, + rabbit_ct_broker_helpers:setup_steps()). + +end_per_suite(Config) -> + rabbit_ct_helpers:run_teardown_steps(Config, + rabbit_ct_broker_helpers:teardown_steps()). + +init_per_group(_, Config) -> + Config. + +end_per_group(_, Config) -> + Config. + +init_per_testcase(multi_node_case1_test = Testcase, Config) -> + rabbit_ct_helpers:testcase_started(Config, Testcase); +init_per_testcase(Testcase, Config) -> + rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, clear_all_table_data, []), + rabbit_ct_broker_helpers:rpc(Config, 1, ?MODULE, clear_all_table_data, []), + rabbit_ct_broker_helpers:close_all_connections(Config, 0, <<"clustering_SUITE:init_per_testcase">>), + Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config), + Config1 = rabbit_ct_helpers:set_config(Config, {conn, Conn}), + rabbit_ct_helpers:testcase_started(Config1, Testcase). + +end_per_testcase(multi_node_case1_test = Testcase, Config) -> + rabbit_ct_broker_helpers:close_all_connections(Config, 0, <<"clustering_SUITE:end_per_testcase">>), + rabbit_ct_helpers:testcase_finished(Config, Testcase); +end_per_testcase(Testcase, Config) -> + rabbit_ct_client_helpers:close_connection(?config(conn, Config)), + rabbit_ct_broker_helpers:close_all_connections(Config, 0, <<"clustering_SUITE:end_per_testcase">>), + rabbit_ct_helpers:testcase_finished(Config, Testcase). + +%% ------------------------------------------------------------------- +%% Testcases. +%% ------------------------------------------------------------------- + +list_cluster_nodes_test(Config) -> + %% see rmq_nodes_count in init_per_suite + ?assertEqual(2, length(http_get(Config, "/nodes"))), + passed. + +multi_node_case1_test(Config) -> + Nodename1 = rabbit_data_coercion:to_binary(get_node_config(Config, 0, nodename)), + Nodename2 = rabbit_data_coercion:to_binary(get_node_config(Config, 1, nodename)), + Policy = [{pattern, <<".*">>}, + {definition, [{'ha-mode', <<"all">>}]}], + http_put(Config, "/policies/%2F/HA", Policy, [?CREATED, ?NO_CONTENT]), + http_delete(Config, "/queues/%2F/multi-node-test-queue", [?NO_CONTENT, ?NOT_FOUND]), + + Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 1), + {ok, Chan} = amqp_connection:open_channel(Conn), + _ = queue_declare(Chan, <<"multi-node-test-queue">>), + Q = wait_for_mirrored_queue(Config, "/queues/%2F/multi-node-test-queue"), + + ?assert(lists:member(maps:get(node, Q), [Nodename1, Nodename2])), + [Mirror] = maps:get(slave_nodes, Q), + [Mirror] = maps:get(synchronised_slave_nodes, Q), + ?assert(lists:member(Mirror, [Nodename1, Nodename2])), + + %% restart node2 so that queue master migrates + restart_node(Config, 1), + + Q2 = wait_for_mirrored_queue(Config, "/queues/%2F/multi-node-test-queue"), + http_delete(Config, "/queues/%2F/multi-node-test-queue", ?NO_CONTENT), + http_delete(Config, "/policies/%2F/HA", ?NO_CONTENT), + + ?assert(lists:member(maps:get(node, Q2), [Nodename1, Nodename2])), + + rabbit_ct_client_helpers:close_connection(Conn), + + passed. + +ha_queue_hosted_on_other_node(Config) -> + Policy = [{pattern, <<".*">>}, + {definition, [{'ha-mode', <<"all">>}]}], + http_put(Config, "/policies/%2F/HA", Policy, [?CREATED, ?NO_CONTENT]), + + Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 1), + {ok, Chan} = amqp_connection:open_channel(Conn), + _ = queue_declare_durable(Chan, <<"ha-queue">>), + _ = wait_for_mirrored_queue(Config, "/queues/%2F/ha-queue"), + + {ok, Chan2} = amqp_connection:open_channel(?config(conn, Config)), + consume(Chan, <<"ha-queue">>), + + timer:sleep(5100), + force_stats(), + Res = http_get(Config, "/queues/%2F/ha-queue"), + + % assert some basic data is there + [Cons] = maps:get(consumer_details, Res), + #{} = maps:get(channel_details, Cons), % channel details proplist must not be empty + 0 = maps:get(prefetch_count, Cons), % check one of the augmented properties + <<"ha-queue">> = maps:get(name, Res), + + amqp_channel:close(Chan), + amqp_channel:close(Chan2), + rabbit_ct_client_helpers:close_connection(Conn), + + http_delete(Config, "/queues/%2F/ha-queue", ?NO_CONTENT), + http_delete(Config, "/policies/%2F/HA", ?NO_CONTENT), + + ok. + +ha_queue_with_multiple_consumers(Config) -> + Policy = [{pattern, <<".*">>}, + {definition, [{'ha-mode', <<"all">>}]}], + http_put(Config, "/policies/%2F/HA", Policy, [?CREATED, ?NO_CONTENT]), + + {ok, Chan} = amqp_connection:open_channel(?config(conn, Config)), + _ = queue_declare_durable(Chan, <<"ha-queue3">>), + _ = wait_for_mirrored_queue(Config, "/queues/%2F/ha-queue3"), + + consume(Chan, <<"ha-queue3">>), + force_stats(), + + {ok, Chan2} = amqp_connection:open_channel(?config(conn, Config)), + consume(Chan2, <<"ha-queue3">>), + + timer:sleep(5100), + force_stats(), + + Res = http_get(Config, "/queues/%2F/ha-queue3"), + + % assert some basic data is there + [C1, C2] = maps:get(consumer_details, Res), + % channel details proplist must not be empty + #{} = maps:get(channel_details, C1), + #{} = maps:get(channel_details, C2), + % check one of the augmented properties + 0 = maps:get(prefetch_count, C1), + 0 = maps:get(prefetch_count, C2), + <<"ha-queue3">> = maps:get(name, Res), + + amqp_channel:close(Chan), + amqp_channel:close(Chan2), + + http_delete(Config, "/queues/%2F/ha-queue3", ?NO_CONTENT), + http_delete(Config, "/policies/%2F/HA", ?NO_CONTENT), + + ok. + +queue_on_other_node(Config) -> + Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 1), + {ok, Chan} = amqp_connection:open_channel(Conn), + _ = queue_declare(Chan, <<"some-queue">>), + _ = wait_for_queue(Config, "/queues/%2F/some-queue"), + + {ok, Chan2} = amqp_connection:open_channel(?config(conn, Config)), + consume(Chan2, <<"some-queue">>), + + timer:sleep(5100), + force_stats(), + Res = http_get(Config, "/queues/%2F/some-queue"), + + % assert some basic data is present + [Cons] = maps:get(consumer_details, Res), + #{} = maps:get(channel_details, Cons), % channel details proplist must not be empty + 0 = maps:get(prefetch_count, Cons), % check one of the augmented properties + <<"some-queue">> = maps:get(name, Res), + + http_delete(Config, "/queues/%2F/some-queue", ?NO_CONTENT), + + amqp_channel:close(Chan), + amqp_channel:close(Chan2), + rabbit_ct_client_helpers:close_connection(Conn), + + ok. + +queue_with_multiple_consumers(Config) -> + {ok, Chan} = amqp_connection:open_channel(?config(conn, Config)), + Q = <<"multi-consumer-queue1">>, + _ = queue_declare(Chan, Q), + _ = wait_for_queue(Config, "/queues/%2F/multi-consumer-queue1"), + + + Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 1), + {ok, Chan2} = amqp_connection:open_channel(Conn), + consume(Chan, Q), + consume(Chan2, Q), + publish(Chan2, Q), + publish(Chan, Q), + % ensure a message has been consumed and acked + receive + {#'basic.deliver'{delivery_tag = T}, _} -> + amqp_channel:cast(Chan, #'basic.ack'{delivery_tag = T}) + end, + + timer:sleep(5100), + force_stats(), + + Res = http_get(Config, "/queues/%2F/multi-consumer-queue1"), + http_delete(Config, "/queues/%2F/multi-consumer-queue1", ?NO_CONTENT), + + % assert some basic data is there + [C1, C2] = maps:get(consumer_details, Res), + % channel details proplist must not be empty + #{} = maps:get(channel_details, C1), + #{} = maps:get(channel_details, C2), + % check one of the augmented properties + 0 = maps:get(prefetch_count, C1), + 0 = maps:get(prefetch_count, C2), + Q = maps:get(name, Res), + + amqp_channel:close(Chan), + amqp_channel:close(Chan2), + rabbit_ct_client_helpers:close_connection(Conn), + + ok. + +queue_consumer_cancelled(Config) -> + {ok, Chan} = amqp_connection:open_channel(?config(conn, Config)), + _ = queue_declare(Chan, <<"some-queue">>), + _ = wait_for_queue(Config, "/queues/%2F/some-queue"), + + Tag = consume(Chan, <<"some-queue">>), + + #'basic.cancel_ok'{} = + amqp_channel:call(Chan, #'basic.cancel'{consumer_tag = Tag}), + force_stats(), + Res = http_get(Config, "/queues/%2F/some-queue"), + + amqp_channel:close(Chan), + + % assert there are no consumer details + [] = maps:get(consumer_details, Res), + <<"some-queue">> = maps:get(name, Res), + http_delete(Config, "/queues/%2F/some-queue", ?NO_CONTENT), + ok. + +queue_consumer_channel_closed(Config) -> + {ok, Chan} = amqp_connection:open_channel(?config(conn, Config)), + _ = queue_declare(Chan, <<"some-queue">>), + _ = wait_for_queue(Config, "/queues/%2F/some-queue"), + + consume(Chan, <<"some-queue">>), + force_stats(), % ensure channel stats have been written + + amqp_channel:close(Chan), + force_stats(), + + Res = http_get(Config, "/queues/%2F/some-queue"), + % assert there are no consumer details + [] = maps:get(consumer_details, Res), + <<"some-queue">> = maps:get(name, Res), + + http_delete(Config, "/queues/%2F/some-queue", ?NO_CONTENT), + ok. + +queue(Config) -> + http_put(Config, "/queues/%2F/some-queue", none, [?CREATED, ?NO_CONTENT]), + _ = wait_for_queue(Config, "/queues/%2F/some-queue"), + + {ok, Chan} = amqp_connection:open_channel(?config(conn, Config)), + {ok, Chan2} = amqp_connection:open_channel(?config(conn, Config)), + + publish(Chan, <<"some-queue">>), + basic_get(Chan, <<"some-queue">>), + publish(Chan2, <<"some-queue">>), + basic_get(Chan2, <<"some-queue">>), + force_stats(), + timer:sleep(5100), + Res = http_get(Config, "/queues/%2F/some-queue"), + % assert single queue is returned + [#{} | _] = maps:get(deliveries, Res), + + amqp_channel:close(Chan), + amqp_channel:close(Chan2), + http_delete(Config, "/queues/%2F/some-queue", ?NO_CONTENT), + + ok. + +queues_single(Config) -> + http_put(Config, "/queues/%2F/some-queue", none, [?CREATED, ?NO_CONTENT]), + _ = wait_for_queue(Config, "/queues/%2F/some-queue"), + + force_stats(), + Res = http_get(Config, "/queues/%2F"), + http_delete(Config, "/queues/%2F/some-queue", ?NO_CONTENT), + + % assert at least one queue is returned + ?assert(length(Res) >= 1), + + ok. + +queues_multiple(Config) -> + {ok, Chan} = amqp_connection:open_channel(?config(conn, Config)), + _ = queue_declare(Chan, <<"some-queue">>), + _ = queue_declare(Chan, <<"some-other-queue">>), + _ = wait_for_queue(Config, "/queues/%2F/some-queue"), + _ = wait_for_queue(Config, "/queues/%2F/some-other-queue"), + + force_stats(), + timer:sleep(5100), + + Res = http_get(Config, "/queues/%2F"), + [Q1, Q2 | _] = Res, + + % assert some basic data is present + http_delete(Config, "/queues/%2F/some-queue", ?NO_CONTENT), + http_delete(Config, "/queues/%2F/some-other-queue", ?NO_CONTENT), + + false = (maps:get(name, Q1) =:= maps:get(name, Q2)), + amqp_channel:close(Chan), + + ok. + +queues_removed(Config) -> + http_put(Config, "/queues/%2F/some-queue", none, [?CREATED, ?NO_CONTENT]), + force_stats(), + N = length(http_get(Config, "/queues/%2F")), + http_delete(Config, "/queues/%2F/some-queue", ?NO_CONTENT), + force_stats(), + ?assertEqual(N - 1, length(http_get(Config, "/queues/%2F"))), + ok. + +channels_multiple_on_different_nodes(Config) -> + Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 1), + {ok, Chan} = amqp_connection:open_channel(Conn), + _ = queue_declare(Chan, <<"some-queue">>), + _ = wait_for_queue(Config, "/queues/%2F/some-queue"), + + Conn2 = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 1), + {ok, Chan2} = amqp_connection:open_channel(Conn2), + consume(Chan, <<"some-queue">>), + + timer:sleep(5100), + force_stats(), + + Res = http_get(Config, "/channels"), + % assert two channels are present + [_,_] = Res, + + http_delete(Config, "/queues/%2F/some-queue", ?NO_CONTENT), + + amqp_channel:close(Chan), + amqp_channel:close(Chan2), + rabbit_ct_client_helpers:close_connection(Conn), + rabbit_ct_client_helpers:close_connection(Conn2), + + ok. + +channel_closed(Config) -> + {ok, Chan} = amqp_connection:open_channel(?config(conn, Config)), + _ = queue_declare(Chan, <<"some-queue">>), + _ = wait_for_queue(Config, "/queues/%2F/some-queue"), + + {ok, Chan2} = amqp_connection:open_channel(?config(conn, Config)), + force_stats(), + + consume(Chan2, <<"some-queue">>), + amqp_channel:close(Chan), + + timer:sleep(5100), + force_stats(), + + Res = http_get(Config, "/channels"), + % assert one channel is present + [_] = Res, + + http_delete(Config, "/queues/%2F/some-queue", ?NO_CONTENT), + + amqp_channel:close(Chan2), + + ok. + +channel(Config) -> + {ok, Chan} = amqp_connection:open_channel(?config(conn, Config)), + [{_, ChData}] = rabbit_ct_broker_helpers:rpc(Config, 0, ets, tab2list, [channel_created]), + + ChName = uri_string:recompose(#{path => binary_to_list(pget(name, ChData))}), + timer:sleep(5100), + force_stats(), + Res = http_get(Config, "/channels/" ++ ChName ), + % assert channel is non empty + #{} = Res, + + amqp_channel:close(Chan), + ok. + +channel_other_node(Config) -> + Q = <<"some-queue">>, + http_put(Config, "/queues/%2F/some-queue", none, [?CREATED, ?NO_CONTENT]), + Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 1), + {ok, Chan} = amqp_connection:open_channel(Conn), + [{_, ChData}] = rabbit_ct_broker_helpers:rpc(Config, 1, ets, tab2list, + [channel_created]), + ChName = uri_string:recompose(#{path => binary_to_list(pget(name, ChData))}), + consume(Chan, Q), + publish(Chan, Q), + + timer:sleep(5100), + force_stats(), + + Res = http_get(Config, "/channels/" ++ ChName ), + % assert channel is non empty + #{} = Res, + [#{}] = maps:get(deliveries, Res), + #{} = maps:get(connection_details, Res), + + http_delete(Config, "/queues/%2F/some-queue", ?NO_CONTENT), + amqp_connection:close(Conn), + + ok. + +channel_with_consumer_on_other_node(Config) -> + {ok, Chan} = amqp_connection:open_channel(?config(conn, Config)), + Q = <<"some-queue">>, + _ = queue_declare(Chan, Q), + _ = wait_for_queue(Config, "/queues/%2F/some-queue"), + + ChName = get_channel_name(Config, 0), + consume(Chan, Q), + publish(Chan, Q), + + timer:sleep(5100), + force_stats(), + + Res = http_get(Config, "/channels/" ++ ChName), + http_delete(Config, "/queues/%2F/some-queue", ?NO_CONTENT), + % assert channel is non empty + #{} = Res, + [#{}] = maps:get(consumer_details, Res), + + amqp_channel:close(Chan), + + ok. + +channel_with_consumer_on_one_node(Config) -> + {ok, Chan} = amqp_connection:open_channel(?config(conn, Config)), + Q = <<"some-queue">>, + _ = queue_declare(Chan, Q), + _ = wait_for_queue(Config, "/queues/%2F/some-queue"), + + ChName = get_channel_name(Config, 0), + consume(Chan, Q), + + timer:sleep(5100), + force_stats(), + + Res = http_get(Config, "/channels/" ++ ChName), + amqp_channel:close(Chan), + + http_delete(Config, "/queues/%2F/some-queue", ?NO_CONTENT), + % assert channel is non empty + #{} = Res, + [#{}] = maps:get(consumer_details, Res), + ok. + +consumers(Config) -> + Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 0), + {ok, Chan} = amqp_connection:open_channel(Conn), + _ = queue_declare(Chan, <<"some-queue">>), + _ = wait_for_queue(Config, "/queues/%2F/some-queue"), + + Conn2 = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 1), + {ok, Chan2} = amqp_connection:open_channel(Conn2), + consume(Chan, <<"some-queue">>), + consume(Chan2, <<"some-queue">>), + + timer:sleep(5100), + force_stats(), + Res = http_get(Config, "/consumers"), + + % assert there are two non-empty consumer records + [#{} = C1, #{} = C2] = Res, + #{} = maps:get(channel_details, C1), + #{} = maps:get(channel_details, C2), + + http_delete(Config, "/queues/%2F/some-queue", ?NO_CONTENT), + + amqp_channel:close(Chan), + rabbit_ct_client_helpers:close_connection(Conn), + rabbit_ct_client_helpers:close_connection(Conn2), + + ok. + + +connections(Config) -> + %% one connection is maintained by CT helpers + {ok, Chan} = amqp_connection:open_channel(?config(conn, Config)), + + Conn2 = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 0), + {ok, _Chan2} = amqp_connection:open_channel(Conn2), + + %% channel count needs a bit longer for 2nd chan + timer:sleep(5100), + force_stats(), + + Res = http_get(Config, "/connections"), + + % assert there are two non-empty connection records + [#{} = C1, #{} = C2] = Res, + 1 = maps:get(channels, C1), + 1 = maps:get(channels, C2), + + amqp_channel:close(Chan), + rabbit_ct_client_helpers:close_connection(Conn2), + + ok. + + +exchanges(Config) -> + {ok, _Chan0} = amqp_connection:open_channel(?config(conn, Config)), + Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 1), + {ok, Chan} = amqp_connection:open_channel(Conn), + QName = <<"exchanges-test">>, + XName = <<"some-exchange">>, + Q = queue_declare(Chan, QName), + exchange_declare(Chan, XName), + queue_bind(Chan, XName, Q, <<"some-key">>), + consume(Chan, QName), + publish_to(Chan, XName, <<"some-key">>), + + force_stats(), + Res = http_get(Config, "/exchanges"), + [X] = [X || X <- Res, maps:get(name, X) =:= XName], + + ?assertEqual(<<"direct">>, maps:get(type, X)), + + amqp_channel:close(Chan), + rabbit_ct_client_helpers:close_connection(Conn), + + ok. + + +exchange(Config) -> + {ok, _Chan0} = amqp_connection:open_channel(?config(conn, Config)), + Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 1), + {ok, Chan} = amqp_connection:open_channel(Conn), + QName = <<"exchanges-test">>, + XName = <<"some-other-exchange">>, + Q = queue_declare(Chan, QName), + exchange_declare(Chan, XName), + queue_bind(Chan, XName, Q, <<"some-key">>), + consume(Chan, QName), + publish_to(Chan, XName, <<"some-key">>), + + force_stats(), + force_stats(), + Res = http_get(Config, "/exchanges/%2F/some-other-exchange"), + + ?assertEqual(<<"direct">>, maps:get(type, Res)), + + amqp_channel:close(Chan), + rabbit_ct_client_helpers:close_connection(Conn), + + ok. + +vhosts(Config) -> + Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 0), + {ok, Chan} = amqp_connection:open_channel(Conn), + _ = queue_declare(Chan, <<"some-queue">>), + _ = wait_for_queue(Config, "/queues/%2F/some-queue"), + + Conn2 = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 1), + {ok, Chan2} = amqp_connection:open_channel(Conn2), + publish(Chan2, <<"some-queue">>), + timer:sleep(5100), % TODO force stat emission + force_stats(), + Res = http_get(Config, "/vhosts"), + + http_delete(Config, "/queues/%2F/some-queue", ?NO_CONTENT), + % default vhost + [#{} = Vhost] = Res, + % assert vhost has some message stats + #{} = maps:get(message_stats, Vhost), + + amqp_channel:close(Chan), + amqp_channel:close(Chan2), + rabbit_ct_client_helpers:close_connection(Conn), + + ok. + +nodes(Config) -> + Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 0), + {ok, Chan} = amqp_connection:open_channel(Conn), + _ = queue_declare(Chan, <<"some-queue">>), + _ = wait_for_queue(Config, "/queues/%2F/some-queue"), + + {ok, Chan2} = amqp_connection:open_channel(Conn), + publish(Chan2, <<"some-queue">>), + timer:sleep(5100), % TODO force stat emission + force_stats(), + Res = http_get(Config, "/nodes"), + http_delete(Config, "/queues/%2F/some-queue", ?NO_CONTENT), + + [#{} = N1 , #{} = N2] = Res, + ?assert(is_binary(maps:get(name, N1))), + ?assert(is_binary(maps:get(name, N2))), + [#{} | _] = maps:get(cluster_links, N1), + [#{} | _] = maps:get(cluster_links, N2), + + amqp_channel:close(Chan), + amqp_channel:close(Chan2), + rabbit_ct_client_helpers:close_connection(Conn), + + ok. + +overview(Config) -> + Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 0), + {ok, Chan} = amqp_connection:open_channel(Conn), + _ = queue_declare(Chan, <<"queue-n1">>), + _ = queue_declare(Chan, <<"queue-n2">>), + _ = wait_for_queue(Config, "/queues/%2F/queue-n1"), + _ = wait_for_queue(Config, "/queues/%2F/queue-n2"), + + Conn2 = rabbit_ct_client_helpers:open_unmanaged_connection(Config, 1), + {ok, Chan2} = amqp_connection:open_channel(Conn2), + publish(Chan, <<"queue-n1">>), + publish(Chan2, <<"queue-n2">>), + timer:sleep(5100), % TODO force stat emission + force_stats(), % channel count needs a bit longer for 2nd chan + Res = http_get(Config, "/overview"), + + http_delete(Config, "/queues/%2F/queue-n1", ?NO_CONTENT), + http_delete(Config, "/queues/%2F/queue-n2", ?NO_CONTENT), + % assert there are two non-empty connection records + ObjTots = maps:get(object_totals, Res), + ?assert(maps:get(connections, ObjTots) >= 2), + ?assert(maps:get(channels, ObjTots) >= 2), + #{} = QT = maps:get(queue_totals, Res), + ?assert(maps:get(messages_ready, QT) >= 2), + MS = maps:get(message_stats, Res), + ?assert(maps:get(publish, MS) >= 2), + ChurnRates = maps:get(churn_rates, Res), + ?assertEqual(maps:get(queue_declared, ChurnRates), 2), + ?assertEqual(maps:get(queue_created, ChurnRates), 2), + ?assertEqual(maps:get(queue_deleted, ChurnRates), 0), + ?assertEqual(maps:get(channel_created, ChurnRates), 2), + ?assertEqual(maps:get(channel_closed, ChurnRates), 0), + ?assertEqual(maps:get(connection_closed, ChurnRates), 0), + + amqp_channel:close(Chan), + amqp_channel:close(Chan2), + rabbit_ct_client_helpers:close_connection(Conn), + rabbit_ct_client_helpers:close_connection(Conn2), + + ok. + +disable_plugin(Config) -> + Node = get_node_config(Config, 0, nodename), + Status0 = rabbit_ct_broker_helpers:rpc(Config, Node, rabbit, status, []), + Listeners0 = proplists:get_value(listeners, Status0), + ?assert(lists:member(http, listener_protos(Listeners0))), + rabbit_ct_broker_helpers:disable_plugin(Config, Node, 'rabbitmq_web_dispatch'), + Status = rabbit_ct_broker_helpers:rpc(Config, Node, rabbit, status, []), + Listeners = proplists:get_value(listeners, Status), + ?assert(not lists:member(http, listener_protos(Listeners))), + rabbit_ct_broker_helpers:enable_plugin(Config, Node, 'rabbitmq_management'). + +%%---------------------------------------------------------------------------- +%% + +clear_all_table_data() -> + [ets:delete_all_objects(T) || {T, _} <- ?CORE_TABLES], + [ets:delete_all_objects(T) || {T, _} <- ?TABLES], + [gen_server:call(P, purge_cache) + || {_, P, _, _} <- supervisor:which_children(rabbit_mgmt_db_cache_sup)], + send_to_all_collectors(purge_old_stats). + +get_channel_name(Config, Node) -> + [{_, ChData}|_] = rabbit_ct_broker_helpers:rpc(Config, Node, ets, tab2list, + [channel_created]), + uri_string:recompose(#{path => binary_to_list(pget(name, ChData))}). + +consume(Channel, Queue) -> + #'basic.consume_ok'{consumer_tag = Tag} = + amqp_channel:call(Channel, #'basic.consume'{queue = Queue}), + Tag. + +publish(Channel, Key) -> + Payload = <<"foobar">>, + Publish = #'basic.publish'{routing_key = Key}, + amqp_channel:cast(Channel, Publish, #amqp_msg{payload = Payload}). + +basic_get(Channel, Queue) -> + Publish = #'basic.get'{queue = Queue}, + amqp_channel:call(Channel, Publish). + +publish_to(Channel, Exchange, Key) -> + Payload = <<"foobar">>, + Publish = #'basic.publish'{routing_key = Key, + exchange = Exchange}, + amqp_channel:cast(Channel, Publish, #amqp_msg{payload = Payload}). + +exchange_declare(Chan, Name) -> + Declare = #'exchange.declare'{exchange = Name}, + #'exchange.declare_ok'{} = amqp_channel:call(Chan, Declare). + +queue_declare(Chan) -> + Declare = #'queue.declare'{}, + #'queue.declare_ok'{queue = Q} = amqp_channel:call(Chan, Declare), + Q. + +queue_declare(Chan, Name) -> + Declare = #'queue.declare'{queue = Name}, + #'queue.declare_ok'{queue = Q} = amqp_channel:call(Chan, Declare), + Q. + +queue_declare_durable(Chan, Name) -> + Declare = #'queue.declare'{queue = Name, durable = true, exclusive = false}, + #'queue.declare_ok'{queue = Q} = amqp_channel:call(Chan, Declare), + Q. + +queue_bind(Chan, Ex, Q, Key) -> + Binding = #'queue.bind'{queue = Q, + exchange = Ex, + routing_key = Key}, + #'queue.bind_ok'{} = amqp_channel:call(Chan, Binding). + +wait_for_mirrored_queue(Config, Path) -> + wait_for_queue(Config, Path, [slave_nodes, synchronised_slave_nodes]). + +wait_for_queue(Config, Path) -> + wait_for_queue(Config, Path, []). + +wait_for_queue(Config, Path, Keys) -> + wait_for_queue(Config, Path, Keys, 1000). + +wait_for_queue(_Config, Path, Keys, 0) -> + exit({timeout, {Path, Keys}}); + +wait_for_queue(Config, Path, Keys, Count) -> + Res = http_get(Config, Path), + case present(Keys, Res) of + false -> timer:sleep(10), + wait_for_queue(Config, Path, Keys, Count - 1); + true -> Res + end. + +present([], _Res) -> + true; +present(Keys, Res) -> + lists:all(fun (Key) -> + X = maps:get(Key, Res, undefined), + X =/= [] andalso X =/= undefined + end, Keys). + +extract_node(N) -> + list_to_atom(hd(string:tokens(binary_to_list(N), "@"))). + +%% debugging utilities + +trace_fun(Config, MFs) -> + Nodename1 = get_node_config(Config, 0, nodename), + Nodename2 = get_node_config(Config, 1, nodename), + dbg:tracer(process, {fun(A,_) -> + ct:pal(?LOW_IMPORTANCE, + "TRACE: ~p", [A]) + end, ok}), + dbg:n(Nodename1), + dbg:n(Nodename2), + dbg:p(all,c), + [ dbg:tpl(M, F, cx) || {M, F} <- MFs], + [ dbg:tpl(M, F, A, cx) || {M, F, A} <- MFs]. + +dump_table(Config, Table) -> + Data = rabbit_ct_broker_helpers:rpc(Config, 0, ets, tab2list, [Table]), + ct:pal(?LOW_IMPORTANCE, "Node 0: Dump of table ~p:~n~p~n", [Table, Data]), + Data0 = rabbit_ct_broker_helpers:rpc(Config, 1, ets, tab2list, [Table]), + ct:pal(?LOW_IMPORTANCE, "Node 1: Dump of table ~p:~n~p~n", [Table, Data0]). + +force_stats() -> + force_all(), + timer:sleep(2000). + +force_all() -> + [begin + {rabbit_mgmt_external_stats, N} ! emit_update, + timer:sleep(125) + end || N <- [node() | nodes()]], + send_to_all_collectors(collect_metrics). + +send_to_all_collectors(Msg) -> + [begin + [{rabbit_mgmt_metrics_collector:name(Table), N} ! Msg + || {Table, _} <- ?CORE_TABLES] + end || N <- [node() | nodes()]]. + +listener_protos(Listeners) -> + [listener_proto(L) || L <- Listeners]. + +listener_proto(#listener{protocol = Proto}) -> + Proto; +listener_proto(Proto) when is_atom(Proto) -> + Proto; +%% rabbit:status/0 used this formatting before rabbitmq/rabbitmq-cli#340 +listener_proto({Proto, _Port, _Interface}) -> + Proto. diff --git a/deps/rabbitmq_management/test/clustering_prop_SUITE.erl b/deps/rabbitmq_management/test/clustering_prop_SUITE.erl new file mode 100644 index 0000000000..98790745db --- /dev/null +++ b/deps/rabbitmq_management/test/clustering_prop_SUITE.erl @@ -0,0 +1,280 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2016-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(clustering_prop_SUITE). + +-include_lib("amqp_client/include/amqp_client.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("proper/include/proper.hrl"). +-include_lib("rabbit_common/include/rabbit_core_metrics.hrl"). +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_metrics.hrl"). +-include_lib("rabbitmq_ct_helpers/include/rabbit_mgmt_test.hrl"). + + +-import(rabbit_ct_broker_helpers, [get_node_config/3]). +-import(rabbit_mgmt_test_util, [http_get/2, http_get_from_node/3]). +-import(rabbit_misc, [pget/2]). + +-compile([export_all, nowarn_format]). + +-export_type([rmqnode/0, queues/0]). + +all() -> + [ + {group, non_parallel_tests} + ]. + +groups() -> + [{non_parallel_tests, [], [ + prop_connection_channel_counts_test + ]} + ]. + +%% ------------------------------------------------------------------- +%% Testsuite setup/teardown. +%% ------------------------------------------------------------------- + +merge_app_env(Config) -> + Config1 = rabbit_ct_helpers:merge_app_env(Config, + {rabbit, [ + {collect_statistics, fine}, + {collect_statistics_interval, 500} + ]}), + rabbit_ct_helpers:merge_app_env(Config1, + {rabbitmq_management, [ + {rates_mode, detailed}, + {sample_retention_policies, + %% List of {MaxAgeInSeconds, SampleEveryNSeconds} + [{global, [{605, 5}, {3660, 60}, {29400, 600}, {86400, 1800}]}, + {basic, [{605, 5}, {3600, 60}]}, + {detailed, [{605, 5}]}] }]}). + +init_per_suite(Config) -> + rabbit_ct_helpers:log_environment(), + inets:start(), + Config1 = rabbit_ct_helpers:set_config(Config, [ + {rmq_nodename_suffix, ?MODULE}, + {rmq_nodes_count, 3} + ]), + Config2 = merge_app_env(Config1), + rabbit_ct_helpers:run_setup_steps(Config2, + rabbit_ct_broker_helpers:setup_steps()). + +end_per_suite(Config) -> + rabbit_ct_helpers:run_teardown_steps(Config, + rabbit_ct_broker_helpers:teardown_steps()). + +init_per_group(_, Config) -> + Config. + +end_per_group(_, Config) -> + Config. + +init_per_testcase(multi_node_case1_test = Testcase, Config) -> + rabbit_ct_helpers:testcase_started(Config, Testcase); +init_per_testcase(Testcase, Config) -> + rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, clear_all_table_data, []), + rabbit_ct_broker_helpers:rpc(Config, 1, ?MODULE, clear_all_table_data, []), + rabbit_ct_broker_helpers:rpc(Config, 2, ?MODULE, clear_all_table_data, []), + rabbit_ct_helpers:testcase_started(Config, Testcase). + +end_per_testcase(Testcase, Config) -> + rabbit_ct_helpers:testcase_finished(Config, Testcase). + +%% ------------------------------------------------------------------- +%% Testcases. +%% ------------------------------------------------------------------- + +prop_connection_channel_counts_test(Config) -> + Fun = fun () -> prop_connection_channel_counts(Config) end, + rabbit_ct_proper_helpers:run_proper(Fun, [], 10). + +-type rmqnode() :: 0|1|2. +-type queues() :: qn1 | qn2 | qn3. + +prop_connection_channel_counts(Config) -> + ?FORALL(Ops, list(frequency([{6, {add_conn, rmqnode(), + list(chan)}}, + {3, rem_conn}, + {6, rem_chan}, + {1, force_stats}])), + begin + % ensure we begin with no connections + true = validate_counts(Config, []), + Cons = lists:foldl(fun (Op, Agg) -> + execute_op(Config, Op, Agg) + end, [], Ops), + force_stats(), + Res = validate_counts(Config, Cons), + cleanup(Cons), + force_stats(), + Res + end). + +validate_counts(Config, Conns) -> + Expected = length(Conns), + ChanCount = lists:sum([length(Chans) || {conn, _, Chans} <- Conns]), + C1 = length(http_get_from_node(Config, 0, "/connections")), + C2 = length(http_get_from_node(Config, 1, "/connections")), + C3 = length(http_get_from_node(Config, 2, "/connections")), + Ch1 = length(http_get_from_node(Config, 0, "/channels")), + Ch2 = length(http_get_from_node(Config, 1, "/channels")), + Ch3 = length(http_get_from_node(Config, 2, "/channels")), + [Expected, Expected, Expected, ChanCount, ChanCount, ChanCount] + =:= [C1, C2, C3, Ch1, Ch2, Ch3]. + + +cleanup(Conns) -> + [rabbit_ct_client_helpers:close_connection(Conn) + || {conn, Conn, _} <- Conns]. + +execute_op(Config, {add_conn, Node, Chans}, State) -> + Conn = rabbit_ct_client_helpers:open_unmanaged_connection(Config, Node), + Chans1 = [begin + {ok, Ch} = amqp_connection:open_channel(Conn), + Ch + end || _ <- Chans], + State ++ [{conn, Conn, Chans1}]; +execute_op(_Config, rem_chan, [{conn, Conn, [Ch | Chans]} | Rem]) -> + ok = amqp_channel:close(Ch), + Rem ++ [{conn, Conn, Chans}]; +execute_op(_Config, rem_chan, State) -> State; +execute_op(_Config, rem_conn, []) -> + []; +execute_op(_Config, rem_conn, [{conn, Conn, _Chans} | Rem]) -> + rabbit_ct_client_helpers:close_connection(Conn), + Rem; +execute_op(_Config, force_stats, State) -> + force_stats(), + State. + +%%---------------------------------------------------------------------------- +%% + +force_stats() -> + force_all(), + timer:sleep(5000). + +force_all() -> + [begin + {rabbit_mgmt_external_stats, N} ! emit_update, + timer:sleep(100), + [{rabbit_mgmt_metrics_collector:name(Table), N} ! collect_metrics + || {Table, _} <- ?CORE_TABLES] + end + || N <- [node() | nodes()]]. + +clear_all_table_data() -> + [ets:delete_all_objects(T) || {T, _} <- ?CORE_TABLES], + [ets:delete_all_objects(T) || {T, _} <- ?TABLES], + [gen_server:call(P, purge_cache) + || {_, P, _, _} <- supervisor:which_children(rabbit_mgmt_db_cache_sup)]. + +get_channel_name(Config, Node) -> + [{_, ChData}|_] = rabbit_ct_broker_helpers:rpc(Config, Node, ets, tab2list, + [channel_created]), + uri_string:recompose(#{path => binary_to_list(pget(name, ChData))}). + +consume(Channel, Queue) -> + #'basic.consume_ok'{consumer_tag = Tag} = + amqp_channel:call(Channel, #'basic.consume'{queue = Queue}), + Tag. + +publish(Channel, Key) -> + Payload = <<"foobar">>, + Publish = #'basic.publish'{routing_key = Key}, + amqp_channel:cast(Channel, Publish, #amqp_msg{payload = Payload}). + +basic_get(Channel, Queue) -> + Publish = #'basic.get'{queue = Queue}, + amqp_channel:call(Channel, Publish). + +publish_to(Channel, Exchange, Key) -> + Payload = <<"foobar">>, + Publish = #'basic.publish'{routing_key = Key, + exchange = Exchange}, + amqp_channel:cast(Channel, Publish, #amqp_msg{payload = Payload}). + +exchange_declare(Chan, Name) -> + Declare = #'exchange.declare'{exchange = Name}, + #'exchange.declare_ok'{} = amqp_channel:call(Chan, Declare). + +queue_declare(Chan) -> + Declare = #'queue.declare'{}, + #'queue.declare_ok'{queue = Q} = amqp_channel:call(Chan, Declare), + Q. + +queue_declare(Chan, Name) -> + Declare = #'queue.declare'{queue = Name}, + #'queue.declare_ok'{queue = Q} = amqp_channel:call(Chan, Declare), + Q. + +queue_bind(Chan, Ex, Q, Key) -> + Binding = #'queue.bind'{queue = Q, + exchange = Ex, + routing_key = Key}, + #'queue.bind_ok'{} = amqp_channel:call(Chan, Binding). + +wait_for(Config, Path) -> + wait_for(Config, Path, [slave_nodes, synchronised_slave_nodes]). + +wait_for(Config, Path, Keys) -> + wait_for(Config, Path, Keys, 1000). + +wait_for(_Config, Path, Keys, 0) -> + exit({timeout, {Path, Keys}}); + +wait_for(Config, Path, Keys, Count) -> + Res = http_get(Config, Path), + case present(Keys, Res) of + false -> timer:sleep(10), + wait_for(Config, Path, Keys, Count - 1); + true -> Res + end. + +present(Keys, Res) -> + lists:all(fun (Key) -> + X = pget(Key, Res), + X =/= [] andalso X =/= undefined + end, Keys). + +assert_single_node(Exp, Act) -> + ?assertEqual(1, length(Act)), + assert_node(Exp, hd(Act)). + +assert_nodes(Exp, Act0) -> + Act = [extract_node(A) || A <- Act0], + ?assertEqual(length(Exp), length(Act)), + [?assert(lists:member(E, Act)) || E <- Exp]. + +assert_node(Exp, Act) -> + ?assertEqual(Exp, list_to_atom(binary_to_list(Act))). + +extract_node(N) -> + list_to_atom(hd(string:tokens(binary_to_list(N), "@"))). + +%% debugging utilities + +trace_fun(Config, MFs) -> + Nodename1 = get_node_config(Config, 0, nodename), + Nodename2 = get_node_config(Config, 1, nodename), + dbg:tracer(process, {fun(A,_) -> + ct:pal(?LOW_IMPORTANCE, + "TRACE: ~p", [A]) + end, ok}), + dbg:n(Nodename1), + dbg:n(Nodename2), + dbg:p(all,c), + [ dbg:tpl(M, F, cx) || {M, F} <- MFs], + [ dbg:tpl(M, F, A, cx) || {M, F, A} <- MFs]. + +dump_table(Config, Table) -> + Data = rabbit_ct_broker_helpers:rpc(Config, 0, ets, tab2list, [Table]), + ct:pal(?LOW_IMPORTANCE, "Node 0: Dump of table ~p:~n~p~n", [Table, Data]), + Data0 = rabbit_ct_broker_helpers:rpc(Config, 1, ets, tab2list, [Table]), + ct:pal(?LOW_IMPORTANCE, "Node 1: Dump of table ~p:~n~p~n", [Table, Data0]). + diff --git a/deps/rabbitmq_management/test/config_schema_SUITE.erl b/deps/rabbitmq_management/test/config_schema_SUITE.erl new file mode 100644 index 0000000000..e40337c60a --- /dev/null +++ b/deps/rabbitmq_management/test/config_schema_SUITE.erl @@ -0,0 +1,55 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2016-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(config_schema_SUITE). + +-compile(export_all). + +all() -> + [ + run_snippets + ]. + +%% ------------------------------------------------------------------- +%% Testsuite setup/teardown. +%% ------------------------------------------------------------------- + +init_per_suite(Config) -> + rabbit_ct_helpers:log_environment(), + Config1 = rabbit_ct_helpers:run_setup_steps(Config), + rabbit_ct_config_schema:init_schemas(rabbitmq_management, Config1). + + +end_per_suite(Config) -> + rabbit_ct_helpers:run_teardown_steps(Config). + +init_per_testcase(Testcase, Config) -> + rabbit_ct_helpers:testcase_started(Config, Testcase), + Config1 = rabbit_ct_helpers:set_config(Config, [ + {rmq_nodename_suffix, Testcase} + ]), + rabbit_ct_helpers:run_steps(Config1, + rabbit_ct_broker_helpers:setup_steps() ++ + rabbit_ct_client_helpers:setup_steps()). + +end_per_testcase(Testcase, Config) -> + Config1 = rabbit_ct_helpers:run_steps(Config, + rabbit_ct_client_helpers:teardown_steps() ++ + rabbit_ct_broker_helpers:teardown_steps()), + rabbit_ct_helpers:testcase_finished(Config1, Testcase). + +%% ------------------------------------------------------------------- +%% Testcases. +%% ------------------------------------------------------------------- + +run_snippets(Config) -> + ok = rabbit_ct_broker_helpers:rpc(Config, 0, + ?MODULE, run_snippets1, [Config]). + +run_snippets1(Config) -> + rabbit_ct_config_schema:run_snippets(Config). + diff --git a/deps/rabbitmq_management/test/config_schema_SUITE_data/certs/cacert.pem b/deps/rabbitmq_management/test/config_schema_SUITE_data/certs/cacert.pem new file mode 100644 index 0000000000..eaf6b67806 --- /dev/null +++ b/deps/rabbitmq_management/test/config_schema_SUITE_data/certs/cacert.pem @@ -0,0 +1 @@ +I'm not a certificate diff --git a/deps/rabbitmq_management/test/config_schema_SUITE_data/certs/cert.pem b/deps/rabbitmq_management/test/config_schema_SUITE_data/certs/cert.pem new file mode 100644 index 0000000000..eaf6b67806 --- /dev/null +++ b/deps/rabbitmq_management/test/config_schema_SUITE_data/certs/cert.pem @@ -0,0 +1 @@ +I'm not a certificate diff --git a/deps/rabbitmq_management/test/config_schema_SUITE_data/certs/key.pem b/deps/rabbitmq_management/test/config_schema_SUITE_data/certs/key.pem new file mode 100644 index 0000000000..eaf6b67806 --- /dev/null +++ b/deps/rabbitmq_management/test/config_schema_SUITE_data/certs/key.pem @@ -0,0 +1 @@ +I'm not a certificate diff --git a/deps/rabbitmq_management/test/config_schema_SUITE_data/rabbit-mgmt/access.log b/deps/rabbitmq_management/test/config_schema_SUITE_data/rabbit-mgmt/access.log new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/deps/rabbitmq_management/test/config_schema_SUITE_data/rabbit-mgmt/access.log diff --git a/deps/rabbitmq_management/test/config_schema_SUITE_data/rabbitmq_management.snippets b/deps/rabbitmq_management/test/config_schema_SUITE_data/rabbitmq_management.snippets new file mode 100644 index 0000000000..aa7b8b12c7 --- /dev/null +++ b/deps/rabbitmq_management/test/config_schema_SUITE_data/rabbitmq_management.snippets @@ -0,0 +1,525 @@ +[{http_log, + "listeners.tcp.default = 5672 + collect_statistics_interval = 10000 + management.http_log_dir = test/config_schema_SUITE_data/rabbit-mgmt + management.rates_mode = basic", + [{rabbit,[{tcp_listeners,[5672]},{collect_statistics_interval,10000}]}, + {rabbitmq_management, + [{http_log_dir,"test/config_schema_SUITE_data/rabbit-mgmt"}, + {rates_mode,basic}]}], + [rabbitmq_management]}, + + %% + %% TCP listener + %% + + {tcp_listener_port_only, + "management.tcp.port = 15674", + [{rabbitmq_management,[ + {tcp_config,[ + {port,15674} + ]} + ]}], + [rabbitmq_management]}, + + {tcp_listener_interface_port, + "management.tcp.ip = 192.168.1.2 + management.tcp.port = 15674", + [{rabbitmq_management,[ + {tcp_config,[ + {ip, "192.168.1.2"}, + {port,15674} + ]} + ]}], + [rabbitmq_management]}, + + {tcp_listener_server_opts_compress, + "management.tcp.compress = true", + [ + {rabbitmq_management, [ + {tcp_config, [{cowboy_opts, [{compress, true}]}]} + ]} + ], [rabbitmq_management] + }, + + {tcp_listener_server_opts_compress_and_idle_timeout, + "management.tcp.compress = true + management.tcp.idle_timeout = 123", + [ + {rabbitmq_management, [ + {tcp_config, [{cowboy_opts, [{compress, true}, + {idle_timeout, 123}]}]} + ]} + ], [rabbitmq_management] + }, + + {tcp_listener_server_opts_compress_and_multiple_timeouts, + "management.tcp.compress = true + management.tcp.idle_timeout = 123 + management.tcp.inactivity_timeout = 456 + management.tcp.request_timeout = 789", + [ + {rabbitmq_management, [ + {tcp_config, [{cowboy_opts, [{compress, true}, + {idle_timeout, 123}, + {inactivity_timeout, 456}, + {request_timeout, 789}]}]} + ]} + ], [rabbitmq_management] + }, + + {tcp_listener_server_opts_multiple_timeouts_only, + "management.tcp.idle_timeout = 123 + management.tcp.inactivity_timeout = 456 + management.tcp.request_timeout = 789", + [ + {rabbitmq_management, [ + {tcp_config, [{cowboy_opts, [{idle_timeout, 123}, + {inactivity_timeout, 456}, + {request_timeout, 789}]}]} + ]} + ], [rabbitmq_management] + }, + + {tcp_listener_server_opts_shutdown_timeout, + "management.tcp.shutdown_timeout = 7000", + [ + {rabbitmq_management, [ + {tcp_config, [{cowboy_opts, [{shutdown_timeout, 7000}]}]} + ]} + ], [rabbitmq_management] + }, + + {tcp_listener_server_opts_max_keepalive, + "management.tcp.max_keepalive = 120", + [ + {rabbitmq_management, [ + {tcp_config, [{cowboy_opts, [{max_keepalive, 120}]}]} + ]} + ], [rabbitmq_management] + }, + + + %% + %% TLS listener + %% + + {tls_listener_port_only, + "management.ssl.port = 15671", + [{rabbitmq_management,[ + {ssl_config,[ + {port,15671} + ]} + ]}], + [rabbitmq_management]}, + + {tls_listener_interface_port, + "management.ssl.ip = 192.168.1.2 + management.ssl.port = 15671", + [{rabbitmq_management,[ + {ssl_config,[ + {ip, "192.168.1.2"}, + {port,15671} + ]} + ]}], + [rabbitmq_management]}, + + {tls_listener, + "management.ssl.ip = 192.168.1.2 + management.ssl.port = 15671 + management.ssl.cacertfile = test/config_schema_SUITE_data/certs/cacert.pem + management.ssl.certfile = test/config_schema_SUITE_data/certs/cert.pem + management.ssl.keyfile = test/config_schema_SUITE_data/certs/key.pem + management.ssl.verify = verify_none + management.ssl.fail_if_no_peer_cert = false", + [{rabbitmq_management,[ + {ssl_config,[ + {ip, "192.168.1.2"}, + {port,15671}, + {cacertfile,"test/config_schema_SUITE_data/certs/cacert.pem"}, + {certfile,"test/config_schema_SUITE_data/certs/cert.pem"}, + {keyfile,"test/config_schema_SUITE_data/certs/key.pem"}, + {verify, verify_none}, + {fail_if_no_peer_cert, false} + ]} + ]}], + [rabbitmq_management]}, + + {tls_listener_cipher_suites, + "management.ssl.ip = 192.168.1.2 + management.ssl.port = 15671 + management.ssl.cacertfile = test/config_schema_SUITE_data/certs/cacert.pem + management.ssl.certfile = test/config_schema_SUITE_data/certs/cert.pem + management.ssl.keyfile = test/config_schema_SUITE_data/certs/key.pem + + management.ssl.honor_cipher_order = true + management.ssl.honor_ecc_order = true + management.ssl.client_renegotiation = false + management.ssl.secure_renegotiate = true + + management.ssl.verify = verify_peer + management.ssl.fail_if_no_peer_cert = false + + management.ssl.versions.1 = tlsv1.2 + management.ssl.versions.2 = tlsv1.1 + + management.ssl.ciphers.1 = ECDHE-ECDSA-AES256-GCM-SHA384 + management.ssl.ciphers.2 = ECDHE-RSA-AES256-GCM-SHA384 + management.ssl.ciphers.3 = ECDHE-ECDSA-AES256-SHA384 + management.ssl.ciphers.4 = ECDHE-RSA-AES256-SHA384 + management.ssl.ciphers.5 = ECDH-ECDSA-AES256-GCM-SHA384 + management.ssl.ciphers.6 = ECDH-RSA-AES256-GCM-SHA384 + management.ssl.ciphers.7 = ECDH-ECDSA-AES256-SHA384 + management.ssl.ciphers.8 = ECDH-RSA-AES256-SHA384 + management.ssl.ciphers.9 = DHE-RSA-AES256-GCM-SHA384", + [{rabbitmq_management,[ + {ssl_config,[ + {ip, "192.168.1.2"}, + {port,15671}, + {cacertfile,"test/config_schema_SUITE_data/certs/cacert.pem"}, + {certfile,"test/config_schema_SUITE_data/certs/cert.pem"}, + {keyfile,"test/config_schema_SUITE_data/certs/key.pem"}, + + {verify, verify_peer}, + {fail_if_no_peer_cert, false}, + + {honor_cipher_order, true}, + {honor_ecc_order, true}, + {client_renegotiation, false}, + {secure_renegotiate, true}, + + {versions,['tlsv1.2','tlsv1.1']}, + {ciphers, [ + "ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-AES256-SHA384", + "ECDHE-RSA-AES256-SHA384", + "ECDH-ECDSA-AES256-GCM-SHA384", + "ECDH-RSA-AES256-GCM-SHA384", + "ECDH-ECDSA-AES256-SHA384", + "ECDH-RSA-AES256-SHA384", + "DHE-RSA-AES256-GCM-SHA384" + ]} + ]} + ]}], + [rabbitmq_management]}, + + {tls_listener_server_opts_compress, + "management.ssl.compress = true", + [ + {rabbitmq_management, [ + {ssl_config, [{cowboy_opts, [{compress, true}]}]} + ]} + ], [rabbitmq_management] + }, + + {tls_listener_server_opts_compress_and_idle_timeout, + "management.ssl.compress = true + management.ssl.idle_timeout = 123", + [ + {rabbitmq_management, [ + {ssl_config, [{cowboy_opts, [{compress, true}, + {idle_timeout, 123}]}]} + ]} + ], [rabbitmq_management] + }, + + {tls_listener_server_opts_compress_and_multiple_timeouts, + "management.ssl.compress = true + management.ssl.idle_timeout = 123 + management.ssl.inactivity_timeout = 456 + management.ssl.request_timeout = 789", + [ + {rabbitmq_management, [ + {ssl_config, [{cowboy_opts, [{compress, true}, + {idle_timeout, 123}, + {inactivity_timeout, 456}, + {request_timeout, 789}]}]} + ]} + ], [rabbitmq_management] + }, + + {tls_listener_server_opts_multiple_timeouts_only, + "management.ssl.idle_timeout = 123 + management.ssl.inactivity_timeout = 456 + management.ssl.request_timeout = 789", + [ + {rabbitmq_management, [ + {ssl_config, [{cowboy_opts, [{idle_timeout, 123}, + {inactivity_timeout, 456}, + {request_timeout, 789}]}]} + ]} + ], [rabbitmq_management] + }, + + {tls_listener_server_opts_shutdown_timeout, + "management.ssl.shutdown_timeout = 7000", + [ + {rabbitmq_management, [ + {ssl_config, [{cowboy_opts, [{shutdown_timeout, 7000}]}]} + ]} + ], [rabbitmq_management] + }, + + {tls_listener_server_opts_max_keepalive, + "management.ssl.max_keepalive = 120", + [ + {rabbitmq_management, [ + {ssl_config, [{cowboy_opts, [{max_keepalive, 120}]}]} + ]} + ], [rabbitmq_management] + }, + + %% + %% Retention Policies + %% + + {retention_policies, + "management.sample_retention_policies.global.minute = 5 + management.sample_retention_policies.global.hour = 60 + management.sample_retention_policies.global.day = 1200 + + management.sample_retention_policies.basic.minute = 5 + management.sample_retention_policies.basic.hour = 60 + + management.sample_retention_policies.detailed.10 = 5", + [{rabbitmq_management, + [{sample_retention_policies, + [{global,[{60,5},{3600,60},{86400,1200}]}, + {basic,[{60,5},{3600,60}]}, + {detailed,[{10,5}]}]}]}], + [rabbitmq_management]}, + + {path_prefix, + "management.path_prefix = /a/prefix", + [ + {rabbitmq_management, [ + {path_prefix, "/a/prefix"} + ]} + ], [rabbitmq_management] + }, + + {login_session_timeout, + "management.login_session_timeout = 30", + [ + {rabbitmq_management, [ + {login_session_timeout, 30} + ]} + ], [rabbitmq_management] + }, + + %% + %% Inter-node query result caching + %% + + {db_cache_multiplier, + "management.db_cache_multiplier = 7", + [ + {rabbitmq_management, [ + {management_db_cache_multiplier, 7} + ]} + ], [rabbitmq_management] + }, + + %% + %% CORS + %% + + {cors_settings, + "management.cors.allow_origins.1 = https://origin1.org + management.cors.allow_origins.2 = https://origin2.org + management.cors.max_age = 3600", + [ + {rabbitmq_management, [ + {cors_allow_origins, ["https://origin1.org", "https://origin2.org"]}, + {cors_max_age, 3600} + ]} + ], [rabbitmq_management] + }, + + {cors_wildcard, + "management.cors.allow_origins.1 = * + management.cors.max_age = 3600", + [ + {rabbitmq_management, [ + {cors_allow_origins, ["*"]}, + {cors_max_age, 3600} + ]} + ], [rabbitmq_management] + }, + + + %% + %% CSP + %% + + {csp_policy_case1, + "management.csp.policy = default-src 'self'", + [ + {rabbitmq_management, [ + {content_security_policy, "default-src 'self'"} + ]} + ], [rabbitmq_management] + }, + + {csp_policy_case2, + "management.csp.policy = default-src https://onlinebanking.examplebank.com", + [ + {rabbitmq_management, [ + {content_security_policy, "default-src https://onlinebanking.examplebank.com"} + ]} + ], [rabbitmq_management] + }, + + {csp_policy_case3, + "management.csp.policy = default-src 'self' *.mailsite.com; img-src *", + [ + {rabbitmq_management, [ + {content_security_policy, "default-src 'self' *.mailsite.com; img-src *"} + ]} + ], [rabbitmq_management] + }, + + %% + %% HSTS + %% + + {hsts_policy_case1, + "management.hsts.policy = max-age=31536000; includeSubDomains", + [ + {rabbitmq_management, [ + {strict_transport_security, "max-age=31536000; includeSubDomains"} + ]} + ], [rabbitmq_management] + }, + + {csp_and_hsts_combined, + "management.csp.policy = default-src 'self' *.mailsite.com; img-src * + management.hsts.policy = max-age=31536000; includeSubDomains", + [ + {rabbitmq_management, [ + {content_security_policy, "default-src 'self' *.mailsite.com; img-src *"}, + {strict_transport_security, "max-age=31536000; includeSubDomains"} + ]} + ], [rabbitmq_management] + }, + + + %% + %% Legacy listener configuration + %% + + {legacy_tcp_listener, + "management.listener.port = 12345", + [{rabbitmq_management,[{listener,[{port,12345}]}]}], + [rabbitmq_management]}, + + {legacy_ssl_listener, + "management.listener.port = 15671 + management.listener.ssl = true + management.listener.ssl_opts.cacertfile = test/config_schema_SUITE_data/certs/cacert.pem + management.listener.ssl_opts.certfile = test/config_schema_SUITE_data/certs/cert.pem + management.listener.ssl_opts.keyfile = test/config_schema_SUITE_data/certs/key.pem", + [{rabbitmq_management, + [{listener, + [{port,15671}, + {ssl,true}, + {ssl_opts, + [{cacertfile, + "test/config_schema_SUITE_data/certs/cacert.pem"}, + {certfile,"test/config_schema_SUITE_data/certs/cert.pem"}, + {keyfile, + "test/config_schema_SUITE_data/certs/key.pem"}]}]}]}], + [rabbitmq_management]}, + + {legacy_tcp_listener_ip, + "management.listener.port = 15672 + management.listener.ip = 127.0.0.1", + [{rabbitmq_management,[{listener,[{port,15672},{ip,"127.0.0.1"}]}]}], + [rabbitmq_management]}, + {legacy_ssl_listener_port, + "management.listener.port = 15672 + management.listener.ssl = true + + management.listener.ssl_opts.cacertfile = test/config_schema_SUITE_data/certs/cacert.pem + management.listener.ssl_opts.certfile = test/config_schema_SUITE_data/certs/cert.pem + management.listener.ssl_opts.keyfile = test/config_schema_SUITE_data/certs/key.pem", + [{rabbitmq_management, + [{listener, + [{port,15672}, + {ssl,true}, + {ssl_opts, + [{cacertfile, + "test/config_schema_SUITE_data/certs/cacert.pem"}, + {certfile,"test/config_schema_SUITE_data/certs/cert.pem"}, + {keyfile, + "test/config_schema_SUITE_data/certs/key.pem"}]}]}]}], + [rabbitmq_management]}, + + {legacy_server_opts_compress, + "management.listener.server.compress = true", + [ + {rabbitmq_management, [ + {listener, [{cowboy_opts, [{compress, true}]}]} + ]} + ], [rabbitmq_management] + }, + + {legacy_server_opts_compress_and_idle_timeout, + "management.listener.server.compress = true + management.listener.server.idle_timeout = 123", + [ + {rabbitmq_management, [ + {listener, [{cowboy_opts, [{compress, true}, + {idle_timeout, 123}]}]} + ]} + ], [rabbitmq_management] + }, + + {legacy_server_opts_compress_and_multiple_timeouts, + "management.listener.server.compress = true + management.listener.server.idle_timeout = 123 + management.listener.server.inactivity_timeout = 456 + management.listener.server.request_timeout = 789", + [ + {rabbitmq_management, [ + {listener, [{cowboy_opts, [{compress, true}, + {idle_timeout, 123}, + {inactivity_timeout, 456}, + {request_timeout, 789}]}]} + ]} + ], [rabbitmq_management] + }, + + {legacy_server_opts_multiple_timeouts_only, + "management.listener.server.idle_timeout = 123 + management.listener.server.inactivity_timeout = 456 + management.listener.server.request_timeout = 789", + [ + {rabbitmq_management, [ + {listener, [{cowboy_opts, [{idle_timeout, 123}, + {inactivity_timeout, 456}, + {request_timeout, 789}]}]} + ]} + ], [rabbitmq_management] + }, + + {legacy_server_opts_shutdown_timeout, + "management.listener.server.shutdown_timeout = 7000", + [ + {rabbitmq_management, [ + {listener, [{cowboy_opts, [{shutdown_timeout, 7000}]}]} + ]} + ], [rabbitmq_management] + }, + + {legacy_server_opts_max_keepalive, + "management.listener.server.max_keepalive = 120", + [ + {rabbitmq_management, [ + {listener, [{cowboy_opts, [{max_keepalive, 120}]}]} + ]} + ], [rabbitmq_management] + } + +]. diff --git a/deps/rabbitmq_management/test/listener_config_SUITE.erl b/deps/rabbitmq_management/test/listener_config_SUITE.erl new file mode 100644 index 0000000000..46d65d2be3 --- /dev/null +++ b/deps/rabbitmq_management/test/listener_config_SUITE.erl @@ -0,0 +1,135 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2016-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(listener_config_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-compile(export_all). + +all() -> + [ + {group, non_parallel_tests} + ]. + +groups() -> + [{non_parallel_tests, [], [ + no_config_defaults, + tcp_config_only, + ssl_config_only, + + multiple_listeners + ]}]. + +init_per_suite(Config) -> + application:load(rabbitmq_management), + Config. + +end_per_suite(Config) -> + Config. + +init_per_testcase(_, Config) -> + application:unset_env(rabbitmq_management, listener), + application:unset_env(rabbitmq_management, tcp_config), + application:unset_env(rabbitmq_management, ssl_config), + Config. + +end_per_testcase(_, Config) -> + application:unset_env(rabbitmq_management, listener), + application:unset_env(rabbitmq_management, tcp_config), + application:unset_env(rabbitmq_management, ssl_config), + Config. + +%% +%% Test Cases +%% + +no_config_defaults(_Config) -> + ?assertEqual([ + [ + {cowboy_opts,[ + {sendfile, false} + ]}, + {port, 15672}] + ], rabbit_mgmt_app:get_listeners_config()). + + +tcp_config_only(_Config) -> + application:set_env(rabbitmq_management, tcp_config, [ + {port, 999}, + {cowboy_opts, [ + {idle_timeout, 10000} + ]} + ]), + + Expected = [ + {cowboy_opts,[ + {idle_timeout, 10000}, + {sendfile, false} + ]}, + {port, 999} + ], + ?assertEqual(lists:usort(Expected), get_single_listener_config()). + +ssl_config_only(_Config) -> + application:set_env(rabbitmq_management, ssl_config, [ + {port, 999}, + {idle_timeout, 10000} + ]), + + Expected = [ + {cowboy_opts,[ + {sendfile,false} + ]}, + {port, 999}, + {ssl, true}, + {ssl_opts, [ + {port, 999}, + {idle_timeout, 10000} + ]} + ], + ?assertEqual(lists:usort(Expected), get_single_listener_config()). + +multiple_listeners(_Config) -> + application:set_env(rabbitmq_management, tcp_config, [ + {port, 998}, + {cowboy_opts, [ + {idle_timeout, 10000} + ]} + ]), + application:set_env(rabbitmq_management, ssl_config, [ + {port, 999}, + {idle_timeout, 10000} + ]), + Expected = [ + [ + {cowboy_opts, [ + {idle_timeout, 10000}, + {sendfile, false} + ]}, + {port,998} + ], + + [ + {cowboy_opts,[ + {sendfile, false} + ]}, + {port, 999}, + {ssl, true}, + {ssl_opts, [ + {port, 999}, + {idle_timeout, 10000} + ]} + ] + ], + ?assertEqual(lists:usort(Expected), rabbit_mgmt_app:get_listeners_config()). + + +get_single_listener_config() -> + [Config] = rabbit_mgmt_app:get_listeners_config(), + lists:usort(Config). diff --git a/deps/rabbitmq_management/test/rabbit_mgmt_http_SUITE.erl b/deps/rabbitmq_management/test/rabbit_mgmt_http_SUITE.erl new file mode 100644 index 0000000000..85ae582503 --- /dev/null +++ b/deps/rabbitmq_management/test/rabbit_mgmt_http_SUITE.erl @@ -0,0 +1,3545 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2016-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_http_SUITE). + +-include_lib("amqp_client/include/amqp_client.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("rabbitmq_ct_helpers/include/rabbit_mgmt_test.hrl"). + +-import(rabbit_ct_client_helpers, [close_connection/1, close_channel/1, + open_unmanaged_connection/1]). +-import(rabbit_mgmt_test_util, [assert_list/2, assert_item/2, test_item/2, + assert_keys/2, assert_no_keys/2, + http_get/2, http_get/3, http_get/5, + http_get_no_auth/3, + http_get_no_map/2, + http_put/4, http_put/6, + http_post/4, http_post/6, + http_upload_raw/8, + http_delete/3, http_delete/5, + http_put_raw/4, http_post_accept_json/4, + req/4, auth_header/2, + assert_permanent_redirect/3, + uri_base_from/2, format_for_upload/1, + amqp_port/1, req/6]). + +-import(rabbit_misc, [pget/2]). + +-define(COLLECT_INTERVAL, 1000). +-define(PATH_PREFIX, "/custom-prefix"). + +-compile(export_all). + +all() -> + [ + {group, all_tests_with_prefix}, + {group, all_tests_without_prefix}, + {group, user_limits_ff} + ]. + +groups() -> + [ + {all_tests_with_prefix, [], all_tests()}, + {all_tests_without_prefix, [], all_tests()}, + {user_limits_ff, [], [ + user_limits_list_test, + user_limit_set_test + ]} + ]. + +all_tests() -> [ + cli_redirect_test, + api_redirect_test, + stats_redirect_test, + overview_test, + auth_test, + cluster_name_test, + nodes_test, + memory_test, + ets_tables_memory_test, + vhosts_test, + vhosts_description_test, + vhosts_trace_test, + users_test, + users_legacy_administrator_test, + adding_a_user_with_password_test, + adding_a_user_with_password_hash_test, + adding_a_user_with_permissions_in_single_operation_test, + adding_a_user_without_tags_fails_test, + adding_a_user_without_password_or_hash_test, + adding_a_user_with_both_password_and_hash_fails_test, + updating_a_user_without_password_or_hash_clears_password_test, + user_credential_validation_accept_everything_succeeds_test, + user_credential_validation_min_length_succeeds_test, + user_credential_validation_min_length_fails_test, + updating_tags_of_a_passwordless_user_test, + permissions_validation_test, + permissions_list_test, + permissions_test, + connections_test, + multiple_invalid_connections_test, + exchanges_test, + queues_test, + quorum_queues_test, + queues_well_formed_json_test, + bindings_test, + bindings_post_test, + bindings_null_routing_key_test, + bindings_e2e_test, + permissions_administrator_test, + permissions_vhost_test, + permissions_amqp_test, + permissions_connection_channel_consumer_test, + consumers_cq_test, + consumers_qq_test, + definitions_test, + definitions_vhost_test, + definitions_password_test, + definitions_remove_things_test, + definitions_server_named_queue_test, + definitions_with_charset_test, + long_definitions_test, + long_definitions_multipart_test, + aliveness_test, + arguments_test, + arguments_table_test, + queue_purge_test, + queue_actions_test, + exclusive_consumer_test, + exclusive_queue_test, + connections_channels_pagination_test, + exchanges_pagination_test, + exchanges_pagination_permissions_test, + queue_pagination_test, + queue_pagination_columns_test, + queues_pagination_permissions_test, + samples_range_test, + sorting_test, + format_output_test, + columns_test, + get_test, + get_encoding_test, + get_fail_test, + publish_test, + publish_large_message_test, + publish_accept_json_test, + publish_fail_test, + publish_base64_test, + publish_unrouted_test, + if_empty_unused_test, + parameters_test, + global_parameters_test, + policy_test, + policy_permissions_test, + issue67_test, + extensions_test, + cors_test, + vhost_limits_list_test, + vhost_limit_set_test, + rates_test, + single_active_consumer_cq_test, + single_active_consumer_qq_test, + oauth_test, + disable_basic_auth_test, + login_test, + csp_headers_test, + auth_attempts_test +]. + +%% ------------------------------------------------------------------- +%% Testsuite setup/teardown. +%% ------------------------------------------------------------------- +merge_app_env(Config) -> + Config1 = rabbit_ct_helpers:merge_app_env(Config, + {rabbit, [ + {collect_statistics_interval, ?COLLECT_INTERVAL} + ]}), + rabbit_ct_helpers:merge_app_env(Config1, + {rabbitmq_management, [ + {sample_retention_policies, + [{global, [{605, 1}]}, + {basic, [{605, 1}]}, + {detailed, [{10, 1}]}] + }]}). + +start_broker(Config) -> + Setup0 = rabbit_ct_broker_helpers:setup_steps(), + Setup1 = rabbit_ct_client_helpers:setup_steps(), + Steps = Setup0 ++ Setup1, + rabbit_ct_helpers:run_setup_steps(Config, Steps). + +finish_init(Group, Config) -> + rabbit_ct_helpers:log_environment(), + inets:start(), + NodeConf = [{rmq_nodename_suffix, Group}], + Config1 = rabbit_ct_helpers:set_config(Config, NodeConf), + merge_app_env(Config1). + +enable_feature_flag_or_skip(FFName, Group, Config0) -> + Config1 = finish_init(Group, Config0), + Config2 = start_broker(Config1), + Nodes = rabbit_ct_broker_helpers:get_node_configs( + Config2, nodename), + Ret = rabbit_ct_broker_helpers:rpc( + Config2, 0, + rabbit_feature_flags, + is_supported_remotely, + [Nodes, [FFName], 60000]), + case Ret of + true -> + ok = rabbit_ct_broker_helpers:rpc( + Config2, 0, rabbit_feature_flags, enable, [FFName]), + Config2; + false -> + end_per_group(Group, Config2), + {skip, rabbit_misc:format("Feature flag '~s' is not supported", [FFName])} + end. + +init_per_group(all_tests_with_prefix=Group, Config0) -> + PathConfig = {rabbitmq_management, [{path_prefix, ?PATH_PREFIX}]}, + Config1 = rabbit_ct_helpers:merge_app_env(Config0, PathConfig), + Config2 = finish_init(Group, Config1), + Config3 = start_broker(Config2), + Nodes = rabbit_ct_broker_helpers:get_node_configs( + Config3, nodename), + Ret = rabbit_ct_broker_helpers:rpc( + Config3, 0, + rabbit_feature_flags, + is_supported_remotely, + [Nodes, [quorum_queue], 60000]), + case Ret of + true -> + ok = rabbit_ct_broker_helpers:rpc( + Config3, 0, rabbit_feature_flags, enable, [quorum_queue]), + Config3; + false -> + end_per_group(Group, Config3), + {skip, "Quorum queues are unsupported"} + end; +init_per_group(user_limits_ff = Group, Config0) -> + enable_feature_flag_or_skip(user_limits, Group, Config0); +init_per_group(Group, Config0) -> + enable_feature_flag_or_skip(quorum_queue, Group, Config0). + +end_per_group(_, Config) -> + inets:stop(), + Teardown0 = rabbit_ct_client_helpers:teardown_steps(), + Teardown1 = rabbit_ct_broker_helpers:teardown_steps(), + Steps = Teardown0 ++ Teardown1, + rabbit_ct_helpers:run_teardown_steps(Config, Steps). + +init_per_testcase(Testcase = permissions_vhost_test, Config) -> + rabbit_ct_broker_helpers:delete_vhost(Config, <<"myvhost">>), + rabbit_ct_broker_helpers:delete_vhost(Config, <<"myvhost1">>), + rabbit_ct_broker_helpers:delete_vhost(Config, <<"myvhost2">>), + rabbit_ct_helpers:testcase_started(Config, Testcase); + +init_per_testcase(Testcase, Config) -> + rabbit_ct_broker_helpers:close_all_connections(Config, 0, <<"rabbit_mgmt_SUITE:init_per_testcase">>), + rabbit_ct_helpers:testcase_started(Config, Testcase). + +end_per_testcase(Testcase, Config) -> + rabbit_ct_broker_helpers:close_all_connections(Config, 0, <<"rabbit_mgmt_SUITE:end_per_testcase">>), + rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_management, disable_basic_auth, false]), + Config1 = end_per_testcase0(Testcase, Config), + rabbit_ct_helpers:testcase_finished(Config1, Testcase). + +end_per_testcase0(T, Config) + when T =:= long_definitions_test; T =:= long_definitions_multipart_test -> + Vhosts = long_definitions_vhosts(T), + [rabbit_ct_broker_helpers:delete_vhost(Config, Name) + || #{name := Name} <- Vhosts], + Config; +end_per_testcase0(queues_test, Config) -> + rabbit_ct_broker_helpers:delete_vhost(Config, <<"downvhost">>), + Config; +end_per_testcase0(vhost_limits_list_test, Config) -> + rabbit_ct_broker_helpers:delete_vhost(Config, <<"limit_test_vhost_1">>), + rabbit_ct_broker_helpers:delete_vhost(Config, <<"limit_test_vhost_2">>), + rabbit_ct_broker_helpers:delete_user(Config, <<"limit_test_vhost_1_user">>), + rabbit_ct_broker_helpers:delete_user(Config, <<"limit_test_vhost_2_user">>), + rabbit_ct_broker_helpers:delete_user(Config, <<"no_vhost_user">>), + Config; +end_per_testcase0(vhost_limit_set_test, Config) -> + rabbit_ct_broker_helpers:delete_vhost(Config, <<"limit_test_vhost_1">>), + rabbit_ct_broker_helpers:delete_user(Config, <<"limit_test_vhost_1_user">>), + Config; +end_per_testcase0(user_limits_list_test, Config) -> + rabbit_ct_broker_helpers:delete_vhost(Config, <<"limit_test_vhost_1">>), + rabbit_ct_broker_helpers:delete_user(Config, <<"limit_test_user_1_user">>), + rabbit_ct_broker_helpers:delete_user(Config, <<"limit_test_user_2_user">>), + rabbit_ct_broker_helpers:delete_user(Config, <<"no_vhost_user">>), + Config; +end_per_testcase0(user_limit_set_test, Config) -> + rabbit_ct_broker_helpers:delete_vhost(Config, <<"limit_test_vhost_1">>), + rabbit_ct_broker_helpers:delete_user(Config, <<"limit_test_user_1_user">>), + rabbit_ct_broker_helpers:delete_user(Config, <<"limit_test_vhost_1_user">>), + Config; +end_per_testcase0(permissions_vhost_test, Config) -> + rabbit_ct_broker_helpers:delete_vhost(Config, <<"myvhost1">>), + rabbit_ct_broker_helpers:delete_vhost(Config, <<"myvhost2">>), + rabbit_ct_broker_helpers:delete_user(Config, <<"myuser1">>), + rabbit_ct_broker_helpers:delete_user(Config, <<"myuser2">>), + Config; +end_per_testcase0(_, Config) -> Config. + +%% ------------------------------------------------------------------- +%% Testcases. +%% ------------------------------------------------------------------- + +overview_test(Config) -> + %% Rather crude, but this req doesn't say much and at least this means it + %% didn't blow up. + true = 0 < length(maps:get(listeners, http_get(Config, "/overview"))), + http_put(Config, "/users/myuser", [{password, <<"myuser">>}, + {tags, <<"management">>}], {group, '2xx'}), + http_get(Config, "/overview", "myuser", "myuser", ?OK), + http_delete(Config, "/users/myuser", {group, '2xx'}), + passed. + +cluster_name_test(Config) -> + http_put(Config, "/users/myuser", [{password, <<"myuser">>}, + {tags, <<"management">>}], {group, '2xx'}), + http_put(Config, "/cluster-name", [{name, "foo"}], "myuser", "myuser", ?NOT_AUTHORISED), + http_put(Config, "/cluster-name", [{name, "foo"}], {group, '2xx'}), + #{name := <<"foo">>} = http_get(Config, "/cluster-name", "myuser", "myuser", ?OK), + http_delete(Config, "/users/myuser", {group, '2xx'}), + passed. + +nodes_test(Config) -> + http_put(Config, "/users/user", [{password, <<"user">>}, + {tags, <<"management">>}], {group, '2xx'}), + http_put(Config, "/users/monitor", [{password, <<"monitor">>}, + {tags, <<"monitoring">>}], {group, '2xx'}), + DiscNode = #{type => <<"disc">>, running => true}, + assert_list([DiscNode], http_get(Config, "/nodes")), + assert_list([DiscNode], http_get(Config, "/nodes", "monitor", "monitor", ?OK)), + http_get(Config, "/nodes", "user", "user", ?NOT_AUTHORISED), + [Node] = http_get(Config, "/nodes"), + Path = "/nodes/" ++ binary_to_list(maps:get(name, Node)), + assert_item(DiscNode, http_get(Config, Path, ?OK)), + assert_item(DiscNode, http_get(Config, Path, "monitor", "monitor", ?OK)), + http_get(Config, Path, "user", "user", ?NOT_AUTHORISED), + http_delete(Config, "/users/user", {group, '2xx'}), + http_delete(Config, "/users/monitor", {group, '2xx'}), + passed. + +memory_test(Config) -> + [Node] = http_get(Config, "/nodes"), + Path = "/nodes/" ++ binary_to_list(maps:get(name, Node)) ++ "/memory", + Result = http_get(Config, Path, ?OK), + assert_keys([memory], Result), + Keys = [total, connection_readers, connection_writers, connection_channels, + connection_other, queue_procs, queue_slave_procs, plugins, + other_proc, mnesia, mgmt_db, msg_index, other_ets, binary, code, + atom, other_system, allocated_unused, reserved_unallocated], + assert_keys(Keys, maps:get(memory, Result)), + http_get(Config, "/nodes/nonode/memory", ?NOT_FOUND), + %% Relative memory as a percentage of the total + Result1 = http_get(Config, Path ++ "/relative", ?OK), + assert_keys([memory], Result1), + Breakdown = maps:get(memory, Result1), + assert_keys(Keys, Breakdown), + + assert_item(#{total => 100}, Breakdown), + %% allocated_unused and reserved_unallocated + %% make this test pretty unpredictable + assert_percentage(Breakdown, 20), + http_get(Config, "/nodes/nonode/memory/relative", ?NOT_FOUND), + passed. + +ets_tables_memory_test(Config) -> + [Node] = http_get(Config, "/nodes"), + Path = "/nodes/" ++ binary_to_list(maps:get(name, Node)) ++ "/memory/ets", + Result = http_get(Config, Path, ?OK), + assert_keys([ets_tables_memory], Result), + NonMgmtKeys = [rabbit_vhost,rabbit_user_permission], + Keys = [queue_stats, vhost_stats_coarse_conn_stats, + connection_created_stats, channel_process_stats, consumer_stats, + queue_msg_rates], + assert_keys(Keys ++ NonMgmtKeys, maps:get(ets_tables_memory, Result)), + http_get(Config, "/nodes/nonode/memory/ets", ?NOT_FOUND), + %% Relative memory as a percentage of the total + ResultRelative = http_get(Config, Path ++ "/relative", ?OK), + assert_keys([ets_tables_memory], ResultRelative), + Breakdown = maps:get(ets_tables_memory, ResultRelative), + assert_keys(Keys, Breakdown), + assert_item(#{total => 100}, Breakdown), + assert_percentage(Breakdown), + http_get(Config, "/nodes/nonode/memory/ets/relative", ?NOT_FOUND), + + ResultMgmt = http_get(Config, Path ++ "/management", ?OK), + assert_keys([ets_tables_memory], ResultMgmt), + assert_keys(Keys, maps:get(ets_tables_memory, ResultMgmt)), + assert_no_keys(NonMgmtKeys, maps:get(ets_tables_memory, ResultMgmt)), + + ResultMgmtRelative = http_get(Config, Path ++ "/management/relative", ?OK), + assert_keys([ets_tables_memory], ResultMgmtRelative), + assert_keys(Keys, maps:get(ets_tables_memory, ResultMgmtRelative)), + assert_no_keys(NonMgmtKeys, maps:get(ets_tables_memory, ResultMgmtRelative)), + assert_item(#{total => 100}, maps:get(ets_tables_memory, ResultMgmtRelative)), + assert_percentage(maps:get(ets_tables_memory, ResultMgmtRelative)), + + ResultUnknownFilter = http_get(Config, Path ++ "/blahblah", ?OK), + #{ets_tables_memory := <<"no_tables">>} = ResultUnknownFilter, + passed. + +assert_percentage(Breakdown0) -> + assert_percentage(Breakdown0, 0). + +assert_percentage(Breakdown0, ExtraMargin) -> + Breakdown = maps:to_list(Breakdown0), + Total = lists:sum([P || {K, P} <- Breakdown, K =/= total]), + AcceptableMargin = (length(Breakdown) - 1) + ExtraMargin, + %% Rounding up and down can lose some digits. Never more than the number + %% of items in the breakdown. + case ((Total =< 100 + AcceptableMargin) andalso (Total >= 100 - AcceptableMargin)) of + false -> + throw({bad_percentage, Total, Breakdown}); + true -> + ok + end. + +auth_test(Config) -> + http_put(Config, "/users/user", [{password, <<"user">>}, + {tags, <<"">>}], {group, '2xx'}), + test_auth(Config, ?NOT_AUTHORISED, []), + test_auth(Config, ?NOT_AUTHORISED, [auth_header("user", "user")]), + test_auth(Config, ?NOT_AUTHORISED, [auth_header("guest", "gust")]), + test_auth(Config, ?OK, [auth_header("guest", "guest")]), + http_delete(Config, "/users/user", {group, '2xx'}), + passed. + +%% This test is rather over-verbose as we're trying to test understanding of +%% Webmachine +vhosts_test(Config) -> + assert_list([#{name => <<"/">>}], http_get(Config, "/vhosts")), + %% Create a new one + http_put(Config, "/vhosts/myvhost", none, {group, '2xx'}), + %% PUT should be idempotent + http_put(Config, "/vhosts/myvhost", none, {group, '2xx'}), + %% Check it's there + assert_list([#{name => <<"/">>}, #{name => <<"myvhost">>}], + http_get(Config, "/vhosts")), + %% Check individually + assert_item(#{name => <<"/">>}, http_get(Config, "/vhosts/%2F", ?OK)), + assert_item(#{name => <<"myvhost">>},http_get(Config, "/vhosts/myvhost")), + + %% Crash it + rabbit_ct_broker_helpers:force_vhost_failure(Config, <<"myvhost">>), + [NodeData] = http_get(Config, "/nodes"), + Node = binary_to_atom(maps:get(name, NodeData), utf8), + assert_item(#{name => <<"myvhost">>, cluster_state => #{Node => <<"stopped">>}}, + http_get(Config, "/vhosts/myvhost")), + + %% Restart it + http_post(Config, "/vhosts/myvhost/start/" ++ atom_to_list(Node), [], {group, '2xx'}), + assert_item(#{name => <<"myvhost">>, cluster_state => #{Node => <<"running">>}}, + http_get(Config, "/vhosts/myvhost")), + + %% Delete it + http_delete(Config, "/vhosts/myvhost", {group, '2xx'}), + %% It's not there + http_get(Config, "/vhosts/myvhost", ?NOT_FOUND), + http_delete(Config, "/vhosts/myvhost", ?NOT_FOUND), + + passed. + +vhosts_description_test(Config) -> + Ret = rabbit_ct_broker_helpers:enable_feature_flag( + Config, virtual_host_metadata), + + http_put(Config, "/vhosts/myvhost", [{description, <<"vhost description">>}, + {tags, <<"tag1,tag2">>}], {group, '2xx'}), + Expected = case Ret of + {skip, _} -> + #{name => <<"myvhost">>}; + _ -> + #{name => <<"myvhost">>, + metadata => #{ + description => <<"vhost description">>, + tags => [<<"tag1">>, <<"tag2">>] + }} + end, + assert_item(Expected, http_get(Config, "/vhosts/myvhost")), + + %% Delete it + http_delete(Config, "/vhosts/myvhost", {group, '2xx'}), + + passed. + +vhosts_trace_test(Config) -> + http_put(Config, "/vhosts/myvhost", none, {group, '2xx'}), + Disabled = #{name => <<"myvhost">>, tracing => false}, + Enabled = #{name => <<"myvhost">>, tracing => true}, + assert_item(Disabled, http_get(Config, "/vhosts/myvhost")), + http_put(Config, "/vhosts/myvhost", [{tracing, true}], {group, '2xx'}), + assert_item(Enabled, http_get(Config, "/vhosts/myvhost")), + http_put(Config, "/vhosts/myvhost", [{tracing, true}], {group, '2xx'}), + assert_item(Enabled, http_get(Config, "/vhosts/myvhost")), + http_put(Config, "/vhosts/myvhost", [{tracing, false}], {group, '2xx'}), + assert_item(Disabled, http_get(Config, "/vhosts/myvhost")), + http_delete(Config, "/vhosts/myvhost", {group, '2xx'}), + + passed. + +users_test(Config) -> + assert_item(#{name => <<"guest">>, tags => <<"administrator">>}, + http_get(Config, "/whoami")), + rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_management, login_session_timeout, 100]), + assert_item(#{name => <<"guest">>, + tags => <<"administrator">>, + login_session_timeout => 100}, + http_get(Config, "/whoami")), + http_get(Config, "/users/myuser", ?NOT_FOUND), + http_put_raw(Config, "/users/myuser", "Something not JSON", ?BAD_REQUEST), + http_put(Config, "/users/myuser", [{flim, <<"flam">>}], ?BAD_REQUEST), + http_put(Config, "/users/myuser", [{tags, <<"management">>}, + {password, <<"myuser">>}], + {group, '2xx'}), + http_put(Config, "/users/myuser", [{password_hash, <<"not_hash">>}], ?BAD_REQUEST), + http_put(Config, "/users/myuser", [{password_hash, + <<"IECV6PZI/Invh0DL187KFpkO5Jc=">>}, + {tags, <<"management">>}], {group, '2xx'}), + assert_item(#{name => <<"myuser">>, tags => <<"management">>, + password_hash => <<"IECV6PZI/Invh0DL187KFpkO5Jc=">>, + hashing_algorithm => <<"rabbit_password_hashing_sha256">>}, + http_get(Config, "/users/myuser")), + + http_put(Config, "/users/myuser", [{password_hash, + <<"IECV6PZI/Invh0DL187KFpkO5Jc=">>}, + {hashing_algorithm, <<"rabbit_password_hashing_md5">>}, + {tags, <<"management">>}], {group, '2xx'}), + assert_item(#{name => <<"myuser">>, tags => <<"management">>, + password_hash => <<"IECV6PZI/Invh0DL187KFpkO5Jc=">>, + hashing_algorithm => <<"rabbit_password_hashing_md5">>}, + http_get(Config, "/users/myuser")), + http_put(Config, "/users/myuser", [{password, <<"password">>}, + {tags, <<"administrator, foo">>}], {group, '2xx'}), + assert_item(#{name => <<"myuser">>, tags => <<"administrator,foo">>}, + http_get(Config, "/users/myuser")), + assert_list(lists:sort([#{name => <<"myuser">>, tags => <<"administrator,foo">>}, + #{name => <<"guest">>, tags => <<"administrator">>}]), + lists:sort(http_get(Config, "/users"))), + test_auth(Config, ?OK, [auth_header("myuser", "password")]), + http_delete(Config, "/users/myuser", {group, '2xx'}), + test_auth(Config, ?NOT_AUTHORISED, [auth_header("myuser", "password")]), + http_get(Config, "/users/myuser", ?NOT_FOUND), + passed. + +without_permissions_users_test(Config) -> + assert_item(#{name => <<"guest">>, tags => <<"administrator">>}, + http_get(Config, "/whoami")), + http_put(Config, "/users/myuser", [{password_hash, + <<"IECV6PZI/Invh0DL187KFpkO5Jc=">>}, + {tags, <<"management">>}], {group, '2xx'}), + Perms = [{configure, <<".*">>}, {write, <<".*">>}, {read, <<".*">>}], + http_put(Config, "/permissions/%2F/myuser", Perms, {group, '2xx'}), + http_put(Config, "/users/myuserwithoutpermissions", [{password_hash, + <<"IECV6PZI/Invh0DL187KFpkO5Jc=">>}, + {tags, <<"management">>}], {group, '2xx'}), + assert_list([#{name => <<"myuserwithoutpermissions">>, tags => <<"management">>, + hashing_algorithm => <<"rabbit_password_hashing_sha256">>, + password_hash => <<"IECV6PZI/Invh0DL187KFpkO5Jc=">>}], + http_get(Config, "/users/without-permissions")), + http_delete(Config, "/users/myuser", {group, '2xx'}), + http_delete(Config, "/users/myuserwithoutpermissions", {group, '2xx'}), + passed. + +users_bulk_delete_test(Config) -> + assert_item(#{name => <<"guest">>, tags => <<"administrator">>}, + http_get(Config, "/whoami")), + http_put(Config, "/users/myuser1", [{tags, <<"management">>}, {password, <<"myuser">>}], + {group, '2xx'}), + http_put(Config, "/users/myuser2", [{tags, <<"management">>}, {password, <<"myuser">>}], + {group, '2xx'}), + http_put(Config, "/users/myuser3", [{tags, <<"management">>}, {password, <<"myuser">>}], + {group, '2xx'}), + http_get(Config, "/users/myuser1", {group, '2xx'}), + http_get(Config, "/users/myuser2", {group, '2xx'}), + http_get(Config, "/users/myuser3", {group, '2xx'}), + + http_post_json(Config, "/users/bulk-delete", + "{\"users\": [\"myuser1\", \"myuser2\"]}", {group, '2xx'}), + http_get(Config, "/users/myuser1", ?NOT_FOUND), + http_get(Config, "/users/myuser2", ?NOT_FOUND), + http_get(Config, "/users/myuser3", {group, '2xx'}), + http_post_json(Config, "/users/bulk-delete", "{\"users\": [\"myuser3\"]}", + {group, '2xx'}), + http_get(Config, "/users/myuser3", ?NOT_FOUND), + passed. + +users_legacy_administrator_test(Config) -> + http_put(Config, "/users/myuser1", [{administrator, <<"true">>}, + {password, <<"myuser1">>}], + {group, '2xx'}), + http_put(Config, "/users/myuser2", [{administrator, <<"false">>}, + {password, <<"myuser2">>}], + {group, '2xx'}), + assert_item(#{name => <<"myuser1">>, tags => <<"administrator">>}, + http_get(Config, "/users/myuser1")), + assert_item(#{name => <<"myuser2">>, tags => <<"">>}, + http_get(Config, "/users/myuser2")), + http_delete(Config, "/users/myuser1", {group, '2xx'}), + http_delete(Config, "/users/myuser2", {group, '2xx'}), + passed. + +adding_a_user_with_password_test(Config) -> + http_put(Config, "/users/user10", [{tags, <<"management">>}, + {password, <<"password">>}], + [?CREATED, ?NO_CONTENT]), + http_delete(Config, "/users/user10", ?NO_CONTENT). +adding_a_user_with_password_hash_test(Config) -> + http_put(Config, "/users/user11", [{tags, <<"management">>}, + %% SHA-256 of "secret" + {password_hash, <<"2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b">>}], + [?CREATED, ?NO_CONTENT]), + http_delete(Config, "/users/user11", ?NO_CONTENT). + +adding_a_user_with_permissions_in_single_operation_test(Config) -> + QArgs = #{}, + PermArgs = #{configure => <<".*">>, + write => <<".*">>, + read => <<".*">>}, + http_delete(Config, "/vhosts/vhost42", [?NO_CONTENT, ?NOT_FOUND]), + http_delete(Config, "/vhosts/vhost43", [?NO_CONTENT, ?NOT_FOUND]), + http_delete(Config, "/users/user-preconfigured-perms", [?NO_CONTENT, ?NOT_FOUND]), + + http_put(Config, "/vhosts/vhost42", none, [?CREATED, ?NO_CONTENT]), + http_put(Config, "/vhosts/vhost43", none, [?CREATED, ?NO_CONTENT]), + + http_put(Config, "/users/user-preconfigured-perms", [{password, <<"user-preconfigured-perms">>}, + {tags, <<"management">>}, + {permissions, [ + {<<"vhost42">>, PermArgs}, + {<<"vhost43">>, PermArgs} + ]}], + [?CREATED, ?NO_CONTENT]), + assert_list([#{tracing => false, name => <<"vhost42">>}, + #{tracing => false, name => <<"vhost43">>}], + http_get(Config, "/vhosts", "user-preconfigured-perms", "user-preconfigured-perms", ?OK)), + http_put(Config, "/queues/vhost42/myqueue", QArgs, + "user-preconfigured-perms", "user-preconfigured-perms", [?CREATED, ?NO_CONTENT]), + http_put(Config, "/queues/vhost43/myqueue", QArgs, + "user-preconfigured-perms", "user-preconfigured-perms", [?CREATED, ?NO_CONTENT]), + Test1 = + fun(Path) -> + http_get(Config, Path, "user-preconfigured-perms", "user-preconfigured-perms", ?OK) + end, + Test2 = + fun(Path1, Path2) -> + http_get(Config, Path1 ++ "/vhost42/" ++ Path2, "user-preconfigured-perms", "user-preconfigured-perms", + ?OK), + http_get(Config, Path1 ++ "/vhost43/" ++ Path2, "user-preconfigured-perms", "user-preconfigured-perms", + ?OK) + end, + Test1("/exchanges"), + Test2("/exchanges", ""), + Test2("/exchanges", "amq.direct"), + Test1("/queues"), + Test2("/queues", ""), + Test2("/queues", "myqueue"), + Test1("/bindings"), + Test2("/bindings", ""), + Test2("/queues", "myqueue/bindings"), + Test2("/exchanges", "amq.default/bindings/source"), + Test2("/exchanges", "amq.default/bindings/destination"), + Test2("/bindings", "e/amq.default/q/myqueue"), + Test2("/bindings", "e/amq.default/q/myqueue/myqueue"), + http_delete(Config, "/vhosts/vhost42", ?NO_CONTENT), + http_delete(Config, "/vhosts/vhost43", ?NO_CONTENT), + http_delete(Config, "/users/user-preconfigured-perms", ?NO_CONTENT), + passed. + +adding_a_user_without_tags_fails_test(Config) -> + http_put(Config, "/users/no-tags", [{password, <<"password">>}], ?BAD_REQUEST). + +%% creating a passwordless user makes sense when x509x certificates or another +%% "external" authentication mechanism or backend is used. +%% See rabbitmq/rabbitmq-management#383. +adding_a_user_without_password_or_hash_test(Config) -> + http_put(Config, "/users/no-pwd", [{tags, <<"management">>}], [?CREATED, ?NO_CONTENT]), + http_put(Config, "/users/no-pwd", [{tags, <<"management">>}], [?CREATED, ?NO_CONTENT]), + http_delete(Config, "/users/no-pwd", ?NO_CONTENT). + +adding_a_user_with_both_password_and_hash_fails_test(Config) -> + http_put(Config, "/users/myuser", [{password, <<"password">>}, + {password_hash, <<"password_hash">>}], + ?BAD_REQUEST), + http_put(Config, "/users/myuser", [{tags, <<"management">>}, + {password, <<"password">>}, + {password_hash, <<"password_hash">>}], ?BAD_REQUEST). + +updating_a_user_without_password_or_hash_clears_password_test(Config) -> + http_put(Config, "/users/myuser", [{tags, <<"management">>}, + {password, <<"myuser">>}], [?CREATED, ?NO_CONTENT]), + %% in this case providing no password or password_hash will + %% clear users' credentials + http_put(Config, "/users/myuser", [{tags, <<"management">>}], [?CREATED, ?NO_CONTENT]), + assert_item(#{name => <<"myuser">>, + tags => <<"management">>, + password_hash => <<>>, + hashing_algorithm => <<"rabbit_password_hashing_sha256">>}, + http_get(Config, "/users/myuser")), + http_delete(Config, "/users/myuser", ?NO_CONTENT). + +-define(NON_GUEST_USERNAME, <<"abc">>). + +user_credential_validation_accept_everything_succeeds_test(Config) -> + rabbit_ct_broker_helpers:delete_user(Config, ?NON_GUEST_USERNAME), + rabbit_ct_broker_helpers:switch_credential_validator(Config, accept_everything), + http_put(Config, "/users/abc", [{password, <<"password">>}, + {tags, <<"management">>}], {group, '2xx'}), + rabbit_ct_broker_helpers:delete_user(Config, ?NON_GUEST_USERNAME). + +user_credential_validation_min_length_succeeds_test(Config) -> + rabbit_ct_broker_helpers:delete_user(Config, ?NON_GUEST_USERNAME), + rabbit_ct_broker_helpers:switch_credential_validator(Config, min_length, 5), + http_put(Config, "/users/abc", [{password, <<"password">>}, + {tags, <<"management">>}], {group, '2xx'}), + rabbit_ct_broker_helpers:delete_user(Config, ?NON_GUEST_USERNAME), + rabbit_ct_broker_helpers:switch_credential_validator(Config, accept_everything). + +user_credential_validation_min_length_fails_test(Config) -> + rabbit_ct_broker_helpers:delete_user(Config, ?NON_GUEST_USERNAME), + rabbit_ct_broker_helpers:switch_credential_validator(Config, min_length, 5), + http_put(Config, "/users/abc", [{password, <<"_">>}, + {tags, <<"management">>}], ?BAD_REQUEST), + rabbit_ct_broker_helpers:switch_credential_validator(Config, accept_everything). + +updating_tags_of_a_passwordless_user_test(Config) -> + rabbit_ct_broker_helpers:delete_user(Config, ?NON_GUEST_USERNAME), + http_put(Config, "/users/abc", [{tags, <<"management">>}, + {password, <<"myuser">>}], [?CREATED, ?NO_CONTENT]), + + %% clear user's password + http_put(Config, "/users/abc", [{tags, <<"management">>}], [?CREATED, ?NO_CONTENT]), + assert_item(#{name => ?NON_GUEST_USERNAME, + tags => <<"management">>, + password_hash => <<>>, + hashing_algorithm => <<"rabbit_password_hashing_sha256">>}, + http_get(Config, "/users/abc")), + + http_put(Config, "/users/abc", [{tags, <<"impersonator">>}], [?CREATED, ?NO_CONTENT]), + assert_item(#{name => ?NON_GUEST_USERNAME, + tags => <<"impersonator">>, + password_hash => <<>>, + hashing_algorithm => <<"rabbit_password_hashing_sha256">>}, + http_get(Config, "/users/abc")), + + http_put(Config, "/users/abc", [{tags, <<"">>}], [?CREATED, ?NO_CONTENT]), + assert_item(#{name => ?NON_GUEST_USERNAME, + tags => <<"">>, + password_hash => <<>>, + hashing_algorithm => <<"rabbit_password_hashing_sha256">>}, + http_get(Config, "/users/abc")), + + http_delete(Config, "/users/abc", ?NO_CONTENT). + +permissions_validation_test(Config) -> + Good = [{configure, <<".*">>}, {write, <<".*">>}, {read, <<".*">>}], + http_put(Config, "/permissions/wrong/guest", Good, ?BAD_REQUEST), + http_put(Config, "/permissions/%2F/wrong", Good, ?BAD_REQUEST), + http_put(Config, "/permissions/%2F/guest", + [{configure, <<"[">>}, {write, <<".*">>}, {read, <<".*">>}], + ?BAD_REQUEST), + http_put(Config, "/permissions/%2F/guest", Good, {group, '2xx'}), + passed. + +permissions_list_test(Config) -> + AllPerms = http_get(Config, "/permissions"), + GuestInDefaultVHost = #{user => <<"guest">>, + vhost => <<"/">>, + configure => <<".*">>, + write => <<".*">>, + read => <<".*">>}, + + ?assert(lists:member(GuestInDefaultVHost, AllPerms)), + + http_put(Config, "/users/myuser1", [{password, <<"">>}, {tags, <<"administrator">>}], + {group, '2xx'}), + http_put(Config, "/users/myuser2", [{password, <<"">>}, {tags, <<"administrator">>}], + {group, '2xx'}), + http_put(Config, "/vhosts/myvhost1", none, {group, '2xx'}), + http_put(Config, "/vhosts/myvhost2", none, {group, '2xx'}), + + Perms = [{configure, <<"foo">>}, {write, <<"foo">>}, {read, <<"foo">>}], + http_put(Config, "/permissions/myvhost1/myuser1", Perms, {group, '2xx'}), + http_put(Config, "/permissions/myvhost2/myuser1", Perms, {group, '2xx'}), + http_put(Config, "/permissions/myvhost1/myuser2", Perms, {group, '2xx'}), + + %% The user that creates the vhosts gets permission automatically + %% See https://github.com/rabbitmq/rabbitmq-management/issues/444 + ?assertEqual(6, length(http_get(Config, "/permissions"))), + ?assertEqual(2, length(http_get(Config, "/users/myuser1/permissions"))), + ?assertEqual(1, length(http_get(Config, "/users/myuser2/permissions"))), + + http_get(Config, "/users/notmyuser/permissions", ?NOT_FOUND), + http_get(Config, "/vhosts/notmyvhost/permissions", ?NOT_FOUND), + + http_delete(Config, "/users/myuser1", {group, '2xx'}), + http_delete(Config, "/users/myuser2", {group, '2xx'}), + http_delete(Config, "/vhosts/myvhost1", {group, '2xx'}), + http_delete(Config, "/vhosts/myvhost2", {group, '2xx'}), + passed. + +permissions_test(Config) -> + http_put(Config, "/users/myuser", [{password, <<"myuser">>}, {tags, <<"administrator">>}], + {group, '2xx'}), + http_put(Config, "/vhosts/myvhost", none, {group, '2xx'}), + + http_put(Config, "/permissions/myvhost/myuser", + [{configure, <<"foo">>}, {write, <<"foo">>}, {read, <<"foo">>}], + {group, '2xx'}), + + Permission = #{user => <<"myuser">>, + vhost => <<"myvhost">>, + configure => <<"foo">>, + write => <<"foo">>, + read => <<"foo">>}, + %% The user that creates the vhosts gets permission automatically + %% See https://github.com/rabbitmq/rabbitmq-management/issues/444 + PermissionOwner = #{user => <<"guest">>, + vhost => <<"/">>, + configure => <<".*">>, + write => <<".*">>, + read => <<".*">>}, + Default = #{user => <<"guest">>, + vhost => <<"/">>, + configure => <<".*">>, + write => <<".*">>, + read => <<".*">>}, + Permission = http_get(Config, "/permissions/myvhost/myuser"), + assert_list(lists:sort([Permission, PermissionOwner, Default]), + lists:sort(http_get(Config, "/permissions"))), + assert_list([Permission], http_get(Config, "/users/myuser/permissions")), + http_delete(Config, "/permissions/myvhost/myuser", {group, '2xx'}), + http_get(Config, "/permissions/myvhost/myuser", ?NOT_FOUND), + + http_delete(Config, "/users/myuser", {group, '2xx'}), + http_delete(Config, "/vhosts/myvhost", {group, '2xx'}), + passed. + +topic_permissions_list_test(Config) -> + http_put(Config, "/users/myuser1", [{password, <<"">>}, {tags, <<"administrator">>}], + {group, '2xx'}), + http_put(Config, "/users/myuser2", [{password, <<"">>}, {tags, <<"administrator">>}], + {group, '2xx'}), + http_put(Config, "/vhosts/myvhost1", none, {group, '2xx'}), + http_put(Config, "/vhosts/myvhost2", none, {group, '2xx'}), + + TopicPerms = [{exchange, <<"amq.topic">>}, {write, <<"^a">>}, {read, <<"^b">>}], + http_put(Config, "/topic-permissions/myvhost1/myuser1", TopicPerms, {group, '2xx'}), + http_put(Config, "/topic-permissions/myvhost2/myuser1", TopicPerms, {group, '2xx'}), + http_put(Config, "/topic-permissions/myvhost1/myuser2", TopicPerms, {group, '2xx'}), + + TopicPerms2 = [{exchange, <<"amq.direct">>}, {write, <<"^a">>}, {read, <<"^b">>}], + http_put(Config, "/topic-permissions/myvhost1/myuser1", TopicPerms2, {group, '2xx'}), + + 4 = length(http_get(Config, "/topic-permissions")), + 3 = length(http_get(Config, "/users/myuser1/topic-permissions")), + 1 = length(http_get(Config, "/users/myuser2/topic-permissions")), + 3 = length(http_get(Config, "/vhosts/myvhost1/topic-permissions")), + 1 = length(http_get(Config, "/vhosts/myvhost2/topic-permissions")), + + http_get(Config, "/users/notmyuser/topic-permissions", ?NOT_FOUND), + http_get(Config, "/vhosts/notmyvhost/topic-permissions", ?NOT_FOUND), + + %% Delete permissions for a single vhost-user-exchange combination + http_delete(Config, "/topic-permissions/myvhost1/myuser1/amq.direct", {group, '2xx'}), + 3 = length(http_get(Config, "/topic-permissions")), + + http_delete(Config, "/users/myuser1", {group, '2xx'}), + http_delete(Config, "/users/myuser2", {group, '2xx'}), + http_delete(Config, "/vhosts/myvhost1", {group, '2xx'}), + http_delete(Config, "/vhosts/myvhost2", {group, '2xx'}), + passed. + +topic_permissions_test(Config) -> + http_put(Config, "/users/myuser1", [{password, <<"">>}, {tags, <<"administrator">>}], + {group, '2xx'}), + http_put(Config, "/users/myuser2", [{password, <<"">>}, {tags, <<"administrator">>}], + {group, '2xx'}), + http_put(Config, "/vhosts/myvhost1", none, {group, '2xx'}), + http_put(Config, "/vhosts/myvhost2", none, {group, '2xx'}), + + TopicPerms = [{exchange, <<"amq.topic">>}, {write, <<"^a">>}, {read, <<"^b">>}], + http_put(Config, "/topic-permissions/myvhost1/myuser1", TopicPerms, {group, '2xx'}), + http_put(Config, "/topic-permissions/myvhost2/myuser1", TopicPerms, {group, '2xx'}), + http_put(Config, "/topic-permissions/myvhost1/myuser2", TopicPerms, {group, '2xx'}), + + 3 = length(http_get(Config, "/topic-permissions")), + 1 = length(http_get(Config, "/topic-permissions/myvhost1/myuser1")), + 1 = length(http_get(Config, "/topic-permissions/myvhost2/myuser1")), + 1 = length(http_get(Config, "/topic-permissions/myvhost1/myuser2")), + + http_get(Config, "/topic-permissions/myvhost1/notmyuser", ?NOT_FOUND), + http_get(Config, "/topic-permissions/notmyvhost/myuser2", ?NOT_FOUND), + + http_delete(Config, "/users/myuser1", {group, '2xx'}), + http_delete(Config, "/users/myuser2", {group, '2xx'}), + http_delete(Config, "/vhosts/myvhost1", {group, '2xx'}), + http_delete(Config, "/vhosts/myvhost2", {group, '2xx'}), + passed. + +connections_test(Config) -> + {Conn, _Ch} = open_connection_and_channel(Config), + LocalPort = local_port(Conn), + Path = binary_to_list( + rabbit_mgmt_format:print( + "/connections/127.0.0.1%3A~w%20-%3E%20127.0.0.1%3A~w", + [LocalPort, amqp_port(Config)])), + timer:sleep(1500), + Connection = http_get(Config, Path, ?OK), + ?assert(maps:is_key(recv_oct, Connection)), + ?assert(maps:is_key(garbage_collection, Connection)), + ?assert(maps:is_key(send_oct_details, Connection)), + ?assert(maps:is_key(reductions, Connection)), + http_delete(Config, Path, {group, '2xx'}), + %% TODO rabbit_reader:shutdown/2 returns before the connection is + %% closed. It may not be worth fixing. + Fun = fun() -> + try + http_get(Config, Path, ?NOT_FOUND), + true + catch + _:_ -> + false + end + end, + wait_until(Fun, 60), + close_connection(Conn), + passed. + +multiple_invalid_connections_test(Config) -> + Count = 100, + spawn_invalid(Config, Count), + Page0 = http_get(Config, "/connections?page=1&page_size=100", ?OK), + wait_for_answers(Count), + Page1 = http_get(Config, "/connections?page=1&page_size=100", ?OK), + ?assertEqual(0, maps:get(total_count, Page0)), + ?assertEqual(0, maps:get(total_count, Page1)), + passed. + +test_auth(Config, Code, Headers) -> + {ok, {{_, Code, _}, _, _}} = req(Config, get, "/overview", Headers), + passed. + +exchanges_test(Config) -> + %% Can list exchanges + http_get(Config, "/exchanges", {group, '2xx'}), + %% Can pass booleans or strings + Good = [{type, <<"direct">>}, {durable, <<"true">>}], + http_put(Config, "/vhosts/myvhost", none, {group, '2xx'}), + http_get(Config, "/exchanges/myvhost/foo", ?NOT_FOUND), + http_put(Config, "/exchanges/myvhost/foo", Good, {group, '2xx'}), + http_put(Config, "/exchanges/myvhost/foo", Good, {group, '2xx'}), + http_get(Config, "/exchanges/%2F/foo", ?NOT_FOUND), + assert_item(#{name => <<"foo">>, + vhost => <<"myvhost">>, + type => <<"direct">>, + durable => true, + auto_delete => false, + internal => false, + arguments => #{}}, + http_get(Config, "/exchanges/myvhost/foo")), + http_put(Config, "/exchanges/badvhost/bar", Good, ?NOT_FOUND), + http_put(Config, "/exchanges/myvhost/bar", [{type, <<"bad_exchange_type">>}], + ?BAD_REQUEST), + http_put(Config, "/exchanges/myvhost/bar", [{type, <<"direct">>}, + {durable, <<"troo">>}], + ?BAD_REQUEST), + http_put(Config, "/exchanges/myvhost/foo", [{type, <<"direct">>}], + ?BAD_REQUEST), + + http_delete(Config, "/exchanges/myvhost/foo", {group, '2xx'}), + http_delete(Config, "/exchanges/myvhost/foo", ?NOT_FOUND), + + http_delete(Config, "/vhosts/myvhost", {group, '2xx'}), + http_get(Config, "/exchanges/badvhost", ?NOT_FOUND), + passed. + +queues_test(Config) -> + Good = [{durable, true}], + http_get(Config, "/queues/%2F/foo", ?NOT_FOUND), + http_put(Config, "/queues/%2F/foo", Good, {group, '2xx'}), + http_put(Config, "/queues/%2F/foo", Good, {group, '2xx'}), + http_get(Config, "/queues/%2F/foo", ?OK), + + rabbit_ct_broker_helpers:add_vhost(Config, <<"downvhost">>), + rabbit_ct_broker_helpers:set_full_permissions(Config, <<"downvhost">>), + http_put(Config, "/queues/downvhost/foo", Good, {group, '2xx'}), + http_put(Config, "/queues/downvhost/bar", Good, {group, '2xx'}), + + rabbit_ct_broker_helpers:force_vhost_failure(Config, <<"downvhost">>), + %% The vhost is down + Node = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename), + DownVHost = #{name => <<"downvhost">>, tracing => false, cluster_state => #{Node => <<"stopped">>}}, + assert_item(DownVHost, http_get(Config, "/vhosts/downvhost")), + + DownQueues = http_get(Config, "/queues/downvhost"), + DownQueue = http_get(Config, "/queues/downvhost/foo"), + + assert_list([#{name => <<"bar">>, + vhost => <<"downvhost">>, + state => <<"stopped">>, + durable => true, + auto_delete => false, + exclusive => false, + arguments => #{}}, + #{name => <<"foo">>, + vhost => <<"downvhost">>, + state => <<"stopped">>, + durable => true, + auto_delete => false, + exclusive => false, + arguments => #{}}], DownQueues), + assert_item(#{name => <<"foo">>, + vhost => <<"downvhost">>, + state => <<"stopped">>, + durable => true, + auto_delete => false, + exclusive => false, + arguments => #{}}, DownQueue), + + http_put(Config, "/queues/badvhost/bar", Good, ?NOT_FOUND), + http_put(Config, "/queues/%2F/bar", + [{durable, <<"troo">>}], + ?BAD_REQUEST), + http_put(Config, "/queues/%2F/foo", + [{durable, false}], + ?BAD_REQUEST), + + http_put(Config, "/queues/%2F/baz", Good, {group, '2xx'}), + Queues = http_get(Config, "/queues/%2F"), + Queue = http_get(Config, "/queues/%2F/foo"), + assert_list([#{name => <<"baz">>, + vhost => <<"/">>, + durable => true, + auto_delete => false, + exclusive => false, + arguments => #{}}, + #{name => <<"foo">>, + vhost => <<"/">>, + durable => true, + auto_delete => false, + exclusive => false, + arguments => #{}}], Queues), + assert_item(#{name => <<"foo">>, + vhost => <<"/">>, + durable => true, + auto_delete => false, + exclusive => false, + arguments => #{}}, Queue), + + http_delete(Config, "/queues/%2F/foo", {group, '2xx'}), + http_delete(Config, "/queues/%2F/baz", {group, '2xx'}), + http_delete(Config, "/queues/%2F/foo", ?NOT_FOUND), + http_get(Config, "/queues/badvhost", ?NOT_FOUND), + + http_delete(Config, "/queues/downvhost/foo", {group, '2xx'}), + http_delete(Config, "/queues/downvhost/bar", {group, '2xx'}), + passed. + +quorum_queues_test(Config) -> + %% Test in a loop that no metrics are left behing after deleting a queue + quorum_queues_test_loop(Config, 5). + +quorum_queues_test_loop(_Config, 0) -> + passed; +quorum_queues_test_loop(Config, N) -> + Good = [{durable, true}, {arguments, [{'x-queue-type', 'quorum'}]}], + http_get(Config, "/queues/%2f/qq", ?NOT_FOUND), + http_put(Config, "/queues/%2f/qq", Good, {group, '2xx'}), + + {Conn, Ch} = open_connection_and_channel(Config), + Publish = fun() -> + amqp_channel:call( + Ch, #'basic.publish'{exchange = <<"">>, + routing_key = <<"qq">>}, + #amqp_msg{payload = <<"message">>}) + end, + Publish(), + Publish(), + wait_until(fun() -> + Num = maps:get(messages, http_get(Config, "/queues/%2f/qq?lengths_age=60&lengths_incr=5&msg_rates_age=60&msg_rates_incr=5&data_rates_age=60&data_rates_incr=5"), undefined), + ct:pal("wait_until got ~w", [N]), + 2 == Num + end, 100), + + http_delete(Config, "/queues/%2f/qq", {group, '2xx'}), + http_put(Config, "/queues/%2f/qq", Good, {group, '2xx'}), + + wait_until(fun() -> + 0 == maps:get(messages, http_get(Config, "/queues/%2f/qq?lengths_age=60&lengths_incr=5&msg_rates_age=60&msg_rates_incr=5&data_rates_age=60&data_rates_incr=5"), undefined) + end, 100), + + http_delete(Config, "/queues/%2f/qq", {group, '2xx'}), + close_connection(Conn), + quorum_queues_test_loop(Config, N-1). + +queues_well_formed_json_test(Config) -> + %% TODO This test should be extended to the whole API + Good = [{durable, true}], + http_put(Config, "/queues/%2F/foo", Good, {group, '2xx'}), + http_put(Config, "/queues/%2F/baz", Good, {group, '2xx'}), + + Queues = http_get_no_map(Config, "/queues/%2F"), + %% Ensure keys are unique + [begin + Sorted = lists:sort(Q), + Sorted = lists:usort(Q) + end || Q <- Queues], + + http_delete(Config, "/queues/%2F/foo", {group, '2xx'}), + http_delete(Config, "/queues/%2F/baz", {group, '2xx'}), + passed. + +bindings_test(Config) -> + XArgs = [{type, <<"direct">>}], + QArgs = #{}, + http_put(Config, "/exchanges/%2F/myexchange", XArgs, {group, '2xx'}), + http_put(Config, "/queues/%2F/myqueue", QArgs, {group, '2xx'}), + BArgs = [{routing_key, <<"routing">>}, {arguments, []}], + http_post(Config, "/bindings/%2F/e/myexchange/q/myqueue", BArgs, {group, '2xx'}), + http_get(Config, "/bindings/%2F/e/myexchange/q/myqueue/routing", ?OK), + http_get(Config, "/bindings/%2F/e/myexchange/q/myqueue/rooting", ?NOT_FOUND), + Binding = + #{source => <<"myexchange">>, + vhost => <<"/">>, + destination => <<"myqueue">>, + destination_type => <<"queue">>, + routing_key => <<"routing">>, + arguments => #{}, + properties_key => <<"routing">>}, + DBinding = + #{source => <<"">>, + vhost => <<"/">>, + destination => <<"myqueue">>, + destination_type => <<"queue">>, + routing_key => <<"myqueue">>, + arguments => #{}, + properties_key => <<"myqueue">>}, + Binding = http_get(Config, "/bindings/%2F/e/myexchange/q/myqueue/routing"), + assert_list([Binding], + http_get(Config, "/bindings/%2F/e/myexchange/q/myqueue")), + assert_list([DBinding, Binding], + http_get(Config, "/queues/%2F/myqueue/bindings")), + assert_list([Binding], + http_get(Config, "/exchanges/%2F/myexchange/bindings/source")), + http_delete(Config, "/bindings/%2F/e/myexchange/q/myqueue/routing", {group, '2xx'}), + http_delete(Config, "/bindings/%2F/e/myexchange/q/myqueue/routing", ?NOT_FOUND), + http_delete(Config, "/exchanges/%2F/myexchange", {group, '2xx'}), + http_delete(Config, "/queues/%2F/myqueue", {group, '2xx'}), + http_get(Config, "/bindings/badvhost", ?NOT_FOUND), + http_get(Config, "/bindings/badvhost/myqueue/myexchange/routing", ?NOT_FOUND), + http_get(Config, "/bindings/%2F/e/myexchange/q/myqueue/routing", ?NOT_FOUND), + passed. + +bindings_post_test(Config) -> + XArgs = [{type, <<"direct">>}], + QArgs = #{}, + BArgs = [{routing_key, <<"routing">>}, {arguments, [{foo, <<"bar">>}]}], + http_put(Config, "/exchanges/%2F/myexchange", XArgs, {group, '2xx'}), + http_put(Config, "/queues/%2F/myqueue", QArgs, {group, '2xx'}), + http_post(Config, "/bindings/%2F/e/myexchange/q/badqueue", BArgs, ?NOT_FOUND), + http_post(Config, "/bindings/%2F/e/badexchange/q/myqueue", BArgs, ?NOT_FOUND), + + Headers1 = http_post(Config, "/bindings/%2F/e/myexchange/q/myqueue", #{}, {group, '2xx'}), + Want0 = "myqueue/\~", + ?assertEqual(Want0, pget("location", Headers1)), + + Headers2 = http_post(Config, "/bindings/%2F/e/myexchange/q/myqueue", BArgs, {group, '2xx'}), + %% Args hash is calculated from a table, generated from args. + Hash = table_hash([{<<"foo">>,longstr,<<"bar">>}]), + PropertiesKey = "routing\~" ++ Hash, + + Want1 = "myqueue/" ++ PropertiesKey, + ?assertEqual(Want1, pget("location", Headers2)), + + PropertiesKeyBin = list_to_binary(PropertiesKey), + Want2 = #{source => <<"myexchange">>, + vhost => <<"/">>, + destination => <<"myqueue">>, + destination_type => <<"queue">>, + routing_key => <<"routing">>, + arguments => #{foo => <<"bar">>}, + properties_key => PropertiesKeyBin}, + URI = "/bindings/%2F/e/myexchange/q/myqueue/" ++ PropertiesKey, + ?assertEqual(Want2, http_get(Config, URI, ?OK)), + + http_get(Config, URI ++ "x", ?NOT_FOUND), + http_delete(Config, URI, {group, '2xx'}), + http_delete(Config, "/exchanges/%2F/myexchange", {group, '2xx'}), + http_delete(Config, "/queues/%2F/myqueue", {group, '2xx'}), + passed. + +bindings_null_routing_key_test(Config) -> + http_delete(Config, "/exchanges/%2F/myexchange", {one_of, [201, 404]}), + XArgs = [{type, <<"direct">>}], + QArgs = #{}, + BArgs = [{routing_key, null}, {arguments, #{}}], + http_put(Config, "/exchanges/%2F/myexchange", XArgs, {group, '2xx'}), + http_put(Config, "/queues/%2F/myqueue", QArgs, {group, '2xx'}), + http_post(Config, "/bindings/%2F/e/myexchange/q/badqueue", BArgs, ?NOT_FOUND), + http_post(Config, "/bindings/%2F/e/badexchange/q/myqueue", BArgs, ?NOT_FOUND), + + Headers1 = http_post(Config, "/bindings/%2F/e/myexchange/q/myqueue", #{}, {group, '2xx'}), + Want0 = "myqueue/\~", + ?assertEqual(Want0, pget("location", Headers1)), + + Headers2 = http_post(Config, "/bindings/%2F/e/myexchange/q/myqueue", BArgs, {group, '2xx'}), + %% Args hash is calculated from a table, generated from args. + Hash = table_hash([]), + PropertiesKey = "null\~" ++ Hash, + + ?assertEqual("myqueue/null", pget("location", Headers2)), + Want1 = #{arguments => #{}, + destination => <<"myqueue">>, + destination_type => <<"queue">>, + properties_key => <<"null">>, + routing_key => null, + source => <<"myexchange">>, + vhost => <<"/">>}, + URI = "/bindings/%2F/e/myexchange/q/myqueue/" ++ PropertiesKey, + ?assertEqual(Want1, http_get(Config, URI, ?OK)), + + http_get(Config, URI ++ "x", ?NOT_FOUND), + http_delete(Config, URI, {group, '2xx'}), + http_delete(Config, "/exchanges/%2F/myexchange", {group, '2xx'}), + http_delete(Config, "/queues/%2F/myqueue", {group, '2xx'}), + passed. + +bindings_e2e_test(Config) -> + BArgs = [{routing_key, <<"routing">>}, {arguments, []}], + http_post(Config, "/bindings/%2F/e/amq.direct/e/badexchange", BArgs, ?NOT_FOUND), + http_post(Config, "/bindings/%2F/e/badexchange/e/amq.fanout", BArgs, ?NOT_FOUND), + Headers = http_post(Config, "/bindings/%2F/e/amq.direct/e/amq.fanout", BArgs, {group, '2xx'}), + "amq.fanout/routing" = pget("location", Headers), + #{source := <<"amq.direct">>, + vhost := <<"/">>, + destination := <<"amq.fanout">>, + destination_type := <<"exchange">>, + routing_key := <<"routing">>, + arguments := #{}, + properties_key := <<"routing">>} = + http_get(Config, "/bindings/%2F/e/amq.direct/e/amq.fanout/routing", ?OK), + http_delete(Config, "/bindings/%2F/e/amq.direct/e/amq.fanout/routing", {group, '2xx'}), + http_post(Config, "/bindings/%2F/e/amq.direct/e/amq.headers", BArgs, {group, '2xx'}), + Binding = + #{source => <<"amq.direct">>, + vhost => <<"/">>, + destination => <<"amq.headers">>, + destination_type => <<"exchange">>, + routing_key => <<"routing">>, + arguments => #{}, + properties_key => <<"routing">>}, + Binding = http_get(Config, "/bindings/%2F/e/amq.direct/e/amq.headers/routing"), + assert_list([Binding], + http_get(Config, "/bindings/%2F/e/amq.direct/e/amq.headers")), + assert_list([Binding], + http_get(Config, "/exchanges/%2F/amq.direct/bindings/source")), + assert_list([Binding], + http_get(Config, "/exchanges/%2F/amq.headers/bindings/destination")), + http_delete(Config, "/bindings/%2F/e/amq.direct/e/amq.headers/routing", {group, '2xx'}), + http_get(Config, "/bindings/%2F/e/amq.direct/e/amq.headers/rooting", ?NOT_FOUND), + passed. + +permissions_administrator_test(Config) -> + http_put(Config, "/users/isadmin", [{password, <<"isadmin">>}, + {tags, <<"administrator">>}], {group, '2xx'}), + http_put(Config, "/users/notadmin", [{password, <<"notadmin">>}, + {tags, <<"administrator">>}], {group, '2xx'}), + http_put(Config, "/users/notadmin", [{password, <<"notadmin">>}, + {tags, <<"management">>}], {group, '2xx'}), + Test = + fun(Path) -> + http_get(Config, Path, "notadmin", "notadmin", ?NOT_AUTHORISED), + http_get(Config, Path, "isadmin", "isadmin", ?OK), + http_get(Config, Path, "guest", "guest", ?OK) + end, + Test("/vhosts/%2F"), + Test("/vhosts/%2F/permissions"), + Test("/users"), + Test("/users/guest"), + Test("/users/guest/permissions"), + Test("/permissions"), + Test("/permissions/%2F/guest"), + http_delete(Config, "/users/notadmin", {group, '2xx'}), + http_delete(Config, "/users/isadmin", {group, '2xx'}), + passed. + +permissions_vhost_test(Config) -> + QArgs = #{}, + PermArgs = [{configure, <<".*">>}, {write, <<".*">>}, {read, <<".*">>}], + http_put(Config, "/users/myadmin", [{password, <<"myadmin">>}, + {tags, <<"administrator">>}], {group, '2xx'}), + http_put(Config, "/users/myuser", [{password, <<"myuser">>}, + {tags, <<"management">>}], {group, '2xx'}), + http_put(Config, "/vhosts/myvhost1", none, {group, '2xx'}), + http_put(Config, "/vhosts/myvhost2", none, {group, '2xx'}), + http_put(Config, "/permissions/myvhost1/myuser", PermArgs, {group, '2xx'}), + http_put(Config, "/permissions/myvhost1/guest", PermArgs, {group, '2xx'}), + http_put(Config, "/permissions/myvhost2/guest", PermArgs, {group, '2xx'}), + assert_list([#{name => <<"/">>}, + #{name => <<"myvhost1">>}, + #{name => <<"myvhost2">>}], http_get(Config, "/vhosts", ?OK)), + assert_list([#{name => <<"myvhost1">>}], + http_get(Config, "/vhosts", "myuser", "myuser", ?OK)), + http_put(Config, "/queues/myvhost1/myqueue", QArgs, {group, '2xx'}), + http_put(Config, "/queues/myvhost2/myqueue", QArgs, {group, '2xx'}), + Test1 = + fun(Path) -> + Results = http_get(Config, Path, "myuser", "myuser", ?OK), + [case maps:get(vhost, Result) of + <<"myvhost2">> -> + throw({got_result_from_vhost2_in, Path, Result}); + _ -> + ok + end || Result <- Results] + end, + Test2 = + fun(Path1, Path2) -> + http_get(Config, Path1 ++ "/myvhost1/" ++ Path2, "myuser", "myuser", + ?OK), + http_get(Config, Path1 ++ "/myvhost2/" ++ Path2, "myuser", "myuser", + ?NOT_AUTHORISED) + end, + Test3 = + fun(Path1) -> + http_get(Config, Path1 ++ "/myvhost1/", "myadmin", "myadmin", + ?OK) + end, + Test1("/exchanges"), + Test2("/exchanges", ""), + Test2("/exchanges", "amq.direct"), + Test3("/exchanges"), + Test1("/queues"), + Test2("/queues", ""), + Test3("/queues"), + Test2("/queues", "myqueue"), + Test1("/bindings"), + Test2("/bindings", ""), + Test3("/bindings"), + Test2("/queues", "myqueue/bindings"), + Test2("/exchanges", "amq.default/bindings/source"), + Test2("/exchanges", "amq.default/bindings/destination"), + Test2("/bindings", "e/amq.default/q/myqueue"), + Test2("/bindings", "e/amq.default/q/myqueue/myqueue"), + http_delete(Config, "/vhosts/myvhost1", {group, '2xx'}), + http_delete(Config, "/vhosts/myvhost2", {group, '2xx'}), + http_delete(Config, "/users/myuser", {group, '2xx'}), + http_delete(Config, "/users/myadmin", {group, '2xx'}), + passed. + +permissions_amqp_test(Config) -> + %% Just test that it works at all, not that it works in all possible cases. + QArgs = #{}, + PermArgs = [{configure, <<"foo.*">>}, {write, <<"foo.*">>}, + {read, <<"foo.*">>}], + http_put(Config, "/users/myuser", [{password, <<"myuser">>}, + {tags, <<"management">>}], {group, '2xx'}), + http_put(Config, "/permissions/%2F/myuser", PermArgs, {group, '2xx'}), + http_put(Config, "/queues/%2F/bar-queue", QArgs, "myuser", "myuser", + ?NOT_AUTHORISED), + http_put(Config, "/queues/%2F/bar-queue", QArgs, "nonexistent", "nonexistent", + ?NOT_AUTHORISED), + http_delete(Config, "/users/myuser", {group, '2xx'}), + passed. + +%% Opens a new connection and a channel on it. +%% The channel is not managed by rabbit_ct_client_helpers and +%% should be explicitly closed by the caller. +open_connection_and_channel(Config) -> + Conn = rabbit_ct_client_helpers:open_connection(Config, 0), + {ok, Ch} = amqp_connection:open_channel(Conn), + {Conn, Ch}. + +get_conn(Config, Username, Password) -> + Port = amqp_port(Config), + {ok, Conn} = amqp_connection:start(#amqp_params_network{ + port = Port, + username = list_to_binary(Username), + password = list_to_binary(Password)}), + LocalPort = local_port(Conn), + ConnPath = rabbit_misc:format( + "/connections/127.0.0.1%3A~w%20-%3E%20127.0.0.1%3A~w", + [LocalPort, Port]), + ChPath = rabbit_misc:format( + "/channels/127.0.0.1%3A~w%20-%3E%20127.0.0.1%3A~w%20(1)", + [LocalPort, Port]), + ConnChPath = rabbit_misc:format( + "/connections/127.0.0.1%3A~w%20-%3E%20127.0.0.1%3A~w/channels", + [LocalPort, Port]), + {Conn, ConnPath, ChPath, ConnChPath}. + +permissions_connection_channel_consumer_test(Config) -> + PermArgs = [{configure, <<".*">>}, {write, <<".*">>}, {read, <<".*">>}], + http_put(Config, "/users/user", [{password, <<"user">>}, + {tags, <<"management">>}], {group, '2xx'}), + http_put(Config, "/permissions/%2F/user", PermArgs, {group, '2xx'}), + http_put(Config, "/users/monitor", [{password, <<"monitor">>}, + {tags, <<"monitoring">>}], {group, '2xx'}), + http_put(Config, "/permissions/%2F/monitor", PermArgs, {group, '2xx'}), + http_put(Config, "/queues/%2F/test", #{}, {group, '2xx'}), + + {Conn1, UserConn, UserCh, UserConnCh} = get_conn(Config, "user", "user"), + {Conn2, MonConn, MonCh, MonConnCh} = get_conn(Config, "monitor", "monitor"), + {Conn3, AdmConn, AdmCh, AdmConnCh} = get_conn(Config, "guest", "guest"), + {ok, Ch1} = amqp_connection:open_channel(Conn1), + {ok, Ch2} = amqp_connection:open_channel(Conn2), + {ok, Ch3} = amqp_connection:open_channel(Conn3), + [amqp_channel:subscribe( + Ch, #'basic.consume'{queue = <<"test">>}, self()) || + Ch <- [Ch1, Ch2, Ch3]], + timer:sleep(1500), + AssertLength = fun (Path, User, Len) -> + Res = http_get(Config, Path, User, User, ?OK), + ?assertEqual(Len, length(Res)) + end, + [begin + AssertLength(P, "user", 1), + AssertLength(P, "monitor", 3), + AssertLength(P, "guest", 3) + end || P <- ["/connections", "/channels", "/consumers", "/consumers/%2F"]], + + AssertRead = fun(Path, UserStatus) -> + http_get(Config, Path, "user", "user", UserStatus), + http_get(Config, Path, "monitor", "monitor", ?OK), + http_get(Config, Path, ?OK) + end, + AssertRead(UserConn, ?OK), + AssertRead(MonConn, ?NOT_AUTHORISED), + AssertRead(AdmConn, ?NOT_AUTHORISED), + AssertRead(UserCh, ?OK), + AssertRead(MonCh, ?NOT_AUTHORISED), + AssertRead(AdmCh, ?NOT_AUTHORISED), + AssertRead(UserConnCh, ?OK), + AssertRead(MonConnCh, ?NOT_AUTHORISED), + AssertRead(AdmConnCh, ?NOT_AUTHORISED), + + AssertClose = fun(Path, User, Status) -> + http_delete(Config, Path, User, User, Status) + end, + AssertClose(UserConn, "monitor", ?NOT_AUTHORISED), + AssertClose(MonConn, "user", ?NOT_AUTHORISED), + AssertClose(AdmConn, "guest", {group, '2xx'}), + AssertClose(MonConn, "guest", {group, '2xx'}), + AssertClose(UserConn, "user", {group, '2xx'}), + + http_delete(Config, "/users/user", {group, '2xx'}), + http_delete(Config, "/users/monitor", {group, '2xx'}), + http_get(Config, "/connections/foo", ?NOT_FOUND), + http_get(Config, "/channels/foo", ?NOT_FOUND), + http_delete(Config, "/queues/%2F/test", {group, '2xx'}), + passed. + +consumers_cq_test(Config) -> + consumers_test(Config, [{'x-queue-type', <<"classic">>}]). + +consumers_qq_test(Config) -> + consumers_test(Config, [{'x-queue-type', <<"quorum">>}]). + +consumers_test(Config, Args) -> + QArgs = [{auto_delete, false}, {durable, true}, + {arguments, Args}], + http_put(Config, "/queues/%2F/test", QArgs, {group, '2xx'}), + {Conn, _ConnPath, _ChPath, _ConnChPath} = get_conn(Config, "guest", "guest"), + {ok, Ch} = amqp_connection:open_channel(Conn), + amqp_channel:subscribe( + Ch, #'basic.consume'{queue = <<"test">>, + no_ack = false, + consumer_tag = <<"my-ctag">> }, self()), + timer:sleep(1500), + assert_list([#{exclusive => false, + ack_required => true, + active => true, + activity_status => <<"up">>, + consumer_tag => <<"my-ctag">>}], http_get(Config, "/consumers")), + amqp_connection:close(Conn), + http_delete(Config, "/queues/%2F/test", {group, '2xx'}), + passed. + +single_active_consumer_cq_test(Config) -> + single_active_consumer(Config, + "/queues/%2F/single-active-consumer-cq", + <<"single-active-consumer-cq">>, + [{'x-queue-type', <<"classic">>}]). + +single_active_consumer_qq_test(Config) -> + single_active_consumer(Config, + "/queues/%2F/single-active-consumer-qq", + <<"single-active-consumer-qq">>, + [{'x-queue-type', <<"quorum">>}]). + +single_active_consumer(Config, Url, QName, Args) -> + QArgs = [{auto_delete, false}, {durable, true}, + {arguments, [{'x-single-active-consumer', true}] ++ Args}], + http_put(Config, Url, QArgs, {group, '2xx'}), + {Conn, _ConnPath, _ChPath, _ConnChPath} = get_conn(Config, "guest", "guest"), + {ok, Ch} = amqp_connection:open_channel(Conn), + amqp_channel:subscribe( + Ch, #'basic.consume'{queue = QName, + no_ack = true, + consumer_tag = <<"1">> }, self()), + {ok, Ch2} = amqp_connection:open_channel(Conn), + amqp_channel:subscribe( + Ch2, #'basic.consume'{queue = QName, + no_ack = true, + consumer_tag = <<"2">> }, self()), + timer:sleep(1500), + assert_list([#{exclusive => false, + ack_required => false, + active => true, + activity_status => <<"single_active">>, + consumer_tag => <<"1">>}, + #{exclusive => false, + ack_required => false, + active => false, + activity_status => <<"waiting">>, + consumer_tag => <<"2">>}], http_get(Config, "/consumers")), + amqp_channel:close(Ch), + timer:sleep(1500), + assert_list([#{exclusive => false, + ack_required => false, + active => true, + activity_status => <<"single_active">>, + consumer_tag => <<"2">>}], http_get(Config, "/consumers")), + amqp_connection:close(Conn), + http_delete(Config, Url, {group, '2xx'}), + passed. + +defs(Config, Key, URI, CreateMethod, Args) -> + defs(Config, Key, URI, CreateMethod, Args, + fun(URI2) -> http_delete(Config, URI2, {group, '2xx'}) end). + +defs_v(Config, Key, URI, CreateMethod, Args) -> + Rep1 = fun (S, S2) -> re:replace(S, "<vhost>", S2, [{return, list}]) end, + ReplaceVHostInArgs = fun(M, V2) -> maps:map(fun(vhost, _) -> V2; + (_, V1) -> V1 end, M) end, + + %% Test against default vhost + defs(Config, Key, Rep1(URI, "%2F"), CreateMethod, ReplaceVHostInArgs(Args, <<"/">>)), + + %% Test against new vhost + http_put(Config, "/vhosts/test", none, {group, '2xx'}), + PermArgs = [{configure, <<".*">>}, {write, <<".*">>}, {read, <<".*">>}], + http_put(Config, "/permissions/test/guest", PermArgs, {group, '2xx'}), + DeleteFun0 = fun(URI2) -> + http_delete(Config, URI2, {group, '2xx'}) + end, + DeleteFun1 = fun(_) -> + http_delete(Config, "/vhosts/test", {group, '2xx'}) + end, + defs(Config, Key, Rep1(URI, "test"), + CreateMethod, ReplaceVHostInArgs(Args, <<"test">>), + DeleteFun0, DeleteFun1). + +create(Config, CreateMethod, URI, Args) -> + case CreateMethod of + put -> http_put(Config, URI, Args, {group, '2xx'}), + URI; + put_update -> http_put(Config, URI, Args, {group, '2xx'}), + URI; + post -> Headers = http_post(Config, URI, Args, {group, '2xx'}), + rabbit_web_dispatch_util:unrelativise( + URI, pget("location", Headers)) + end. + +defs(Config, Key, URI, CreateMethod, Args, DeleteFun) -> + defs(Config, Key, URI, CreateMethod, Args, DeleteFun, DeleteFun). + +defs(Config, Key, URI, CreateMethod, Args, DeleteFun0, DeleteFun1) -> + %% Create the item + URI2 = create(Config, CreateMethod, URI, Args), + %% Make sure it ends up in definitions + Definitions = http_get(Config, "/definitions", ?OK), + true = lists:any(fun(I) -> test_item(Args, I) end, maps:get(Key, Definitions)), + + %% Delete it + DeleteFun0(URI2), + + %% Post the definitions back, it should get recreated in correct form + http_post(Config, "/definitions", Definitions, {group, '2xx'}), + assert_item(Args, http_get(Config, URI2, ?OK)), + + %% And delete it again + DeleteFun1(URI2), + + passed. + +register_parameters_and_policy_validator(Config) -> + rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_mgmt_runtime_parameters_util, register, []), + rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_mgmt_runtime_parameters_util, register_policy_validator, []). + +unregister_parameters_and_policy_validator(Config) -> + rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_mgmt_runtime_parameters_util, unregister_policy_validator, []), + rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_mgmt_runtime_parameters_util, unregister, []). + +definitions_test(Config) -> + register_parameters_and_policy_validator(Config), + + defs_v(Config, queues, "/queues/<vhost>/my-queue", put, + #{name => <<"my-queue">>, + durable => true}), + defs_v(Config, exchanges, "/exchanges/<vhost>/my-exchange", put, + #{name => <<"my-exchange">>, + type => <<"direct">>}), + defs_v(Config, bindings, "/bindings/<vhost>/e/amq.direct/e/amq.fanout", post, + #{routing_key => <<"routing">>, arguments => #{}}), + defs_v(Config, policies, "/policies/<vhost>/my-policy", put, + #{vhost => vhost, + name => <<"my-policy">>, + pattern => <<".*">>, + definition => #{testpos => [1, 2, 3]}, + priority => 1}), + defs_v(Config, parameters, "/parameters/test/<vhost>/good", put, + #{vhost => vhost, + component => <<"test">>, + name => <<"good">>, + value => <<"ignore">>}), + defs(Config, global_parameters, "/global-parameters/good", put, + #{name => <<"good">>, + value => #{a => <<"b">>}}), + defs(Config, users, "/users/myuser", put, + #{name => <<"myuser">>, + password_hash => <<"WAbU0ZIcvjTpxM3Q3SbJhEAM2tQ=">>, + hashing_algorithm => <<"rabbit_password_hashing_sha256">>, + tags => <<"management">>}), + defs(Config, vhosts, "/vhosts/myvhost", put, + #{name => <<"myvhost">>}), + defs(Config, permissions, "/permissions/%2F/guest", put, + #{user => <<"guest">>, + vhost => <<"/">>, + configure => <<"c">>, + write => <<"w">>, + read => <<"r">>}), + defs(Config, topic_permissions, "/topic-permissions/%2F/guest", put, + #{user => <<"guest">>, + vhost => <<"/">>, + exchange => <<"amq.topic">>, + write => <<"^a">>, + read => <<"^b">>}), + + %% We just messed with guest's permissions + http_put(Config, "/permissions/%2F/guest", + #{configure => <<".*">>, + write => <<".*">>, + read => <<".*">>}, {group, '2xx'}), + BrokenConfig = + #{users => [], + vhosts => [], + permissions => [], + queues => [], + exchanges => [#{name => <<"not.direct">>, + vhost => <<"/">>, + type => <<"definitely not direct">>, + durable => true, + auto_delete => false, + arguments => []} + ], + bindings => []}, + http_post(Config, "/definitions", BrokenConfig, ?BAD_REQUEST), + + unregister_parameters_and_policy_validator(Config), + passed. + +long_definitions_test(Config) -> + %% Vhosts take time to start. Generate a bunch of them + Vhosts = long_definitions_vhosts(long_definitions_test), + LongDefs = + #{users => [], + vhosts => Vhosts, + permissions => [], + queues => [], + exchanges => [], + bindings => []}, + http_post(Config, "/definitions", LongDefs, {group, '2xx'}), + passed. + +long_definitions_multipart_test(Config) -> + %% Vhosts take time to start. Generate a bunch of them + Vhosts = long_definitions_vhosts(long_definitions_multipart_test), + LongDefs = + #{users => [], + vhosts => Vhosts, + permissions => [], + queues => [], + exchanges => [], + bindings => []}, + Data = binary_to_list(format_for_upload(LongDefs)), + CodeExp = {group, '2xx'}, + Boundary = "------------long_definitions_multipart_test", + Body = format_multipart_filedata(Boundary, [{file, "file", Data}]), + ContentType = lists:concat(["multipart/form-data; boundary=", Boundary]), + MoreHeaders = [{"content-type", ContentType}, {"content-length", integer_to_list(length(Body))}], + http_upload_raw(Config, post, "/definitions", Body, "guest", "guest", CodeExp, MoreHeaders), + passed. + +long_definitions_vhosts(long_definitions_test) -> + [#{name => <<"long_definitions_test-", (integer_to_binary(N))/binary>>} || + N <- lists:seq(1, 120)]; +long_definitions_vhosts(long_definitions_multipart_test) -> + Bin = list_to_binary(lists:flatten(lists:duplicate(524288, "X"))), + [#{name => <<"long_definitions_test-", Bin/binary, (integer_to_binary(N))/binary>>} || + N <- lists:seq(1, 16)]. + +defs_vhost(Config, Key, URI, CreateMethod, Args) -> + Rep1 = fun (S, S2) -> re:replace(S, "<vhost>", S2, [{return, list}]) end, + ReplaceVHostInArgs = fun(M, V2) -> maps:map(fun(vhost, _) -> V2; + (_, V1) -> V1 end, M) end, + + %% Create test vhost + http_put(Config, "/vhosts/test", none, {group, '2xx'}), + PermArgs = [{configure, <<".*">>}, {write, <<".*">>}, {read, <<".*">>}], + http_put(Config, "/permissions/test/guest", PermArgs, {group, '2xx'}), + + %% Test against default vhost + defs_vhost(Config, Key, URI, Rep1, "%2F", "test", CreateMethod, + ReplaceVHostInArgs(Args, <<"/">>), ReplaceVHostInArgs(Args, <<"test">>), + fun(URI2) -> http_delete(Config, URI2, {group, '2xx'}) end), + + %% Test against test vhost + defs_vhost(Config, Key, URI, Rep1, "test", "%2F", CreateMethod, + ReplaceVHostInArgs(Args, <<"test">>), ReplaceVHostInArgs(Args, <<"/">>), + fun(URI2) -> http_delete(Config, URI2, {group, '2xx'}) end), + + %% Remove test vhost + http_delete(Config, "/vhosts/test", {group, '2xx'}). + + +defs_vhost(Config, Key, URI0, Rep1, VHost1, VHost2, CreateMethod, Args1, Args2, + DeleteFun) -> + %% Create the item + URI2 = create(Config, CreateMethod, Rep1(URI0, VHost1), Args1), + %% Make sure it ends up in definitions + Definitions = http_get(Config, "/definitions/" ++ VHost1, ?OK), + true = lists:any(fun(I) -> test_item(Args1, I) end, maps:get(Key, Definitions)), + + %% Make sure it is not in the other vhost + Definitions0 = http_get(Config, "/definitions/" ++ VHost2, ?OK), + false = lists:any(fun(I) -> test_item(Args2, I) end, maps:get(Key, Definitions0)), + + %% Post the definitions back + http_post(Config, "/definitions/" ++ VHost2, Definitions, {group, '2xx'}), + + %% Make sure it is now in the other vhost + Definitions1 = http_get(Config, "/definitions/" ++ VHost2, ?OK), + true = lists:any(fun(I) -> test_item(Args2, I) end, maps:get(Key, Definitions1)), + + %% Delete it + DeleteFun(URI2), + URI3 = create(Config, CreateMethod, Rep1(URI0, VHost2), Args2), + DeleteFun(URI3), + passed. + +definitions_vhost_test(Config) -> + %% Ensures that definitions can be exported/imported from a single virtual + %% host to another + + register_parameters_and_policy_validator(Config), + + defs_vhost(Config, queues, "/queues/<vhost>/my-queue", put, + #{name => <<"my-queue">>, + durable => true}), + defs_vhost(Config, exchanges, "/exchanges/<vhost>/my-exchange", put, + #{name => <<"my-exchange">>, + type => <<"direct">>}), + defs_vhost(Config, bindings, "/bindings/<vhost>/e/amq.direct/e/amq.fanout", post, + #{routing_key => <<"routing">>, arguments => #{}}), + defs_vhost(Config, policies, "/policies/<vhost>/my-policy", put, + #{vhost => vhost, + name => <<"my-policy">>, + pattern => <<".*">>, + definition => #{testpos => [1, 2, 3]}, + priority => 1}), + + defs_vhost(Config, parameters, "/parameters/vhost-limits/<vhost>/limits", put, + #{vhost => vhost, + name => <<"limits">>, + component => <<"vhost-limits">>, + value => #{ 'max-connections' => 100 }}), + Upload = + #{queues => [], + exchanges => [], + policies => [], + parameters => [], + bindings => []}, + http_post(Config, "/definitions/othervhost", Upload, ?NOT_FOUND), + + unregister_parameters_and_policy_validator(Config), + passed. + +definitions_password_test(Config) -> + % Import definitions from 3.5.x + Config35 = #{rabbit_version => <<"3.5.4">>, + users => [#{name => <<"myuser">>, + password_hash => <<"WAbU0ZIcvjTpxM3Q3SbJhEAM2tQ=">>, + tags => <<"management">>} + ]}, + Expected35 = #{name => <<"myuser">>, + password_hash => <<"WAbU0ZIcvjTpxM3Q3SbJhEAM2tQ=">>, + hashing_algorithm => <<"rabbit_password_hashing_md5">>, + tags => <<"management">>}, + http_post(Config, "/definitions", Config35, {group, '2xx'}), + Definitions35 = http_get(Config, "/definitions", ?OK), + ct:pal("Definitions35: ~p", [Definitions35]), + Users35 = maps:get(users, Definitions35), + true = lists:any(fun(I) -> test_item(Expected35, I) end, Users35), + + %% Import definitions from from 3.6.0 + Config36 = #{rabbit_version => <<"3.6.0">>, + users => [#{name => <<"myuser">>, + password_hash => <<"WAbU0ZIcvjTpxM3Q3SbJhEAM2tQ=">>, + tags => <<"management">>} + ]}, + Expected36 = #{name => <<"myuser">>, + password_hash => <<"WAbU0ZIcvjTpxM3Q3SbJhEAM2tQ=">>, + hashing_algorithm => <<"rabbit_password_hashing_sha256">>, + tags => <<"management">>}, + http_post(Config, "/definitions", Config36, {group, '2xx'}), + + Definitions36 = http_get(Config, "/definitions", ?OK), + Users36 = maps:get(users, Definitions36), + true = lists:any(fun(I) -> test_item(Expected36, I) end, Users36), + + %% No hashing_algorithm provided + ConfigDefault = #{rabbit_version => <<"3.6.1">>, + users => [#{name => <<"myuser">>, + password_hash => <<"WAbU0ZIcvjTpxM3Q3SbJhEAM2tQ=">>, + tags => <<"management">>} + ]}, + rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, [rabbit, + password_hashing_module, + rabbit_password_hashing_sha512]), + + ExpectedDefault = #{name => <<"myuser">>, + password_hash => <<"WAbU0ZIcvjTpxM3Q3SbJhEAM2tQ=">>, + hashing_algorithm => <<"rabbit_password_hashing_sha512">>, + tags => <<"management">>}, + http_post(Config, "/definitions", ConfigDefault, {group, '2xx'}), + + DefinitionsDefault = http_get(Config, "/definitions", ?OK), + UsersDefault = maps:get(users, DefinitionsDefault), + + true = lists:any(fun(I) -> test_item(ExpectedDefault, I) end, UsersDefault), + passed. + +definitions_remove_things_test(Config) -> + {Conn, Ch} = open_connection_and_channel(Config), + amqp_channel:call(Ch, #'queue.declare'{ queue = <<"my-exclusive">>, + exclusive = true }), + http_get(Config, "/queues/%2F/my-exclusive", ?OK), + Definitions = http_get(Config, "/definitions", ?OK), + [] = maps:get(queues, Definitions), + [] = maps:get(exchanges, Definitions), + [] = maps:get(bindings, Definitions), + amqp_channel:close(Ch), + close_connection(Conn), + passed. + +definitions_server_named_queue_test(Config) -> + {Conn, Ch} = open_connection_and_channel(Config), + %% declares a durable server-named queue for the sake of exporting the definition + #'queue.declare_ok'{queue = QName} = + amqp_channel:call(Ch, #'queue.declare'{queue = <<"">>, durable = true}), + Path = "/queues/%2F/" ++ rabbit_http_util:quote_plus(QName), + http_get(Config, Path, ?OK), + Definitions = http_get(Config, "/definitions", ?OK), + close_channel(Ch), + close_connection(Conn), + http_delete(Config, Path, {group, '2xx'}), + http_get(Config, Path, ?NOT_FOUND), + http_post(Config, "/definitions", Definitions, {group, '2xx'}), + %% amq.* entities are not imported + http_get(Config, Path, ?NOT_FOUND), + passed. + +definitions_with_charset_test(Config) -> + Path = "/definitions", + Body0 = http_get(Config, Path, ?OK), + Headers = [auth_header("guest", "guest")], + Url = uri_base_from(Config, 0) ++ Path, + Body1 = format_for_upload(Body0), + Request = {Url, Headers, "application/json; charset=utf-8", Body1}, + {ok, {{_, ?NO_CONTENT, _}, _, []}} = httpc:request(post, Request, ?HTTPC_OPTS, []), + passed. + +aliveness_test(Config) -> + #{status := <<"ok">>} = http_get(Config, "/aliveness-test/%2F", ?OK), + http_get(Config, "/aliveness-test/foo", ?NOT_FOUND), + http_delete(Config, "/queues/%2F/aliveness-test", {group, '2xx'}), + passed. + +arguments_test(Config) -> + XArgs = [{type, <<"headers">>}, + {arguments, [{'alternate-exchange', <<"amq.direct">>}]}], + QArgs = [{arguments, [{'x-expires', 1800000}]}], + BArgs = [{routing_key, <<"">>}, + {arguments, [{'x-match', <<"all">>}, + {foo, <<"bar">>}]}], + http_delete(Config, "/exchanges/%2F/myexchange", {one_of, [201, 404]}), + http_put(Config, "/exchanges/%2F/myexchange", XArgs, {group, '2xx'}), + http_put(Config, "/queues/%2F/arguments_test", QArgs, {group, '2xx'}), + http_post(Config, "/bindings/%2F/e/myexchange/q/arguments_test", BArgs, {group, '2xx'}), + Definitions = http_get(Config, "/definitions", ?OK), + http_delete(Config, "/exchanges/%2F/myexchange", {group, '2xx'}), + http_delete(Config, "/queues/%2F/arguments_test", {group, '2xx'}), + http_post(Config, "/definitions", Definitions, {group, '2xx'}), + #{'alternate-exchange' := <<"amq.direct">>} = + maps:get(arguments, http_get(Config, "/exchanges/%2F/myexchange", ?OK)), + #{'x-expires' := 1800000} = + maps:get(arguments, http_get(Config, "/queues/%2F/arguments_test", ?OK)), + + ArgsTable = [{<<"foo">>,longstr,<<"bar">>}, {<<"x-match">>, longstr, <<"all">>}], + Hash = table_hash(ArgsTable), + PropertiesKey = [$~] ++ Hash, + + assert_item( + #{'x-match' => <<"all">>, foo => <<"bar">>}, + maps:get(arguments, + http_get(Config, "/bindings/%2F/e/myexchange/q/arguments_test/" ++ + PropertiesKey, ?OK)) + ), + http_delete(Config, "/exchanges/%2F/myexchange", {group, '2xx'}), + http_delete(Config, "/queues/%2F/arguments_test", {group, '2xx'}), + passed. + +table_hash(Table) -> + binary_to_list(rabbit_mgmt_format:args_hash(Table)). + +arguments_table_test(Config) -> + Args = #{'upstreams' => [<<"amqp://localhost/%2F/upstream1">>, + <<"amqp://localhost/%2F/upstream2">>]}, + XArgs = #{type => <<"headers">>, + arguments => Args}, + http_delete(Config, "/exchanges/%2F/myexchange", {one_of, [201, 404]}), + http_put(Config, "/exchanges/%2F/myexchange", XArgs, {group, '2xx'}), + Definitions = http_get(Config, "/definitions", ?OK), + http_delete(Config, "/exchanges/%2F/myexchange", {group, '2xx'}), + http_post(Config, "/definitions", Definitions, {group, '2xx'}), + Args = maps:get(arguments, http_get(Config, "/exchanges/%2F/myexchange", ?OK)), + http_delete(Config, "/exchanges/%2F/myexchange", {group, '2xx'}), + passed. + +queue_purge_test(Config) -> + QArgs = #{}, + http_put(Config, "/queues/%2F/myqueue", QArgs, {group, '2xx'}), + {Conn, Ch} = open_connection_and_channel(Config), + Publish = fun() -> + amqp_channel:call( + Ch, #'basic.publish'{exchange = <<"">>, + routing_key = <<"myqueue">>}, + #amqp_msg{payload = <<"message">>}) + end, + Publish(), + Publish(), + amqp_channel:call( + Ch, #'queue.declare'{queue = <<"exclusive">>, exclusive = true}), + {#'basic.get_ok'{}, _} = + amqp_channel:call(Ch, #'basic.get'{queue = <<"myqueue">>}), + http_delete(Config, "/queues/%2F/myqueue/contents", {group, '2xx'}), + http_delete(Config, "/queues/%2F/badqueue/contents", ?NOT_FOUND), + http_delete(Config, "/queues/%2F/exclusive/contents", ?BAD_REQUEST), + http_delete(Config, "/queues/%2F/exclusive", ?BAD_REQUEST), + #'basic.get_empty'{} = + amqp_channel:call(Ch, #'basic.get'{queue = <<"myqueue">>}), + close_channel(Ch), + close_connection(Conn), + http_delete(Config, "/queues/%2F/myqueue", {group, '2xx'}), + passed. + +queue_actions_test(Config) -> + http_put(Config, "/queues/%2F/q", #{}, {group, '2xx'}), + http_post(Config, "/queues/%2F/q/actions", [{action, sync}], {group, '2xx'}), + http_post(Config, "/queues/%2F/q/actions", [{action, cancel_sync}], {group, '2xx'}), + http_post(Config, "/queues/%2F/q/actions", [{action, change_colour}], ?BAD_REQUEST), + http_delete(Config, "/queues/%2F/q", {group, '2xx'}), + passed. + +exclusive_consumer_test(Config) -> + {Conn, Ch} = open_connection_and_channel(Config), + #'queue.declare_ok'{ queue = QName } = + amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), + amqp_channel:subscribe(Ch, #'basic.consume'{queue = QName, + exclusive = true}, self()), + timer:sleep(1500), %% Sadly we need to sleep to let the stats update + http_get(Config, "/queues/%2F/"), %% Just check we don't blow up + close_channel(Ch), + close_connection(Conn), + passed. + + +exclusive_queue_test(Config) -> + {Conn, Ch} = open_connection_and_channel(Config), + #'queue.declare_ok'{ queue = QName } = + amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), + timer:sleep(1500), %% Sadly we need to sleep to let the stats update + Path = "/queues/%2F/" ++ rabbit_http_util:quote_plus(QName), + Queue = http_get(Config, Path), + assert_item(#{name => QName, + vhost => <<"/">>, + durable => false, + auto_delete => false, + exclusive => true, + arguments => #{}}, Queue), + amqp_channel:close(Ch), + close_connection(Conn), + passed. + +connections_channels_pagination_test(Config) -> + %% this test uses "unmanaged" (by Common Test helpers) connections to avoid + %% connection caching + Conn = open_unmanaged_connection(Config), + {ok, Ch} = amqp_connection:open_channel(Conn), + Conn1 = open_unmanaged_connection(Config), + {ok, Ch1} = amqp_connection:open_channel(Conn1), + Conn2 = open_unmanaged_connection(Config), + {ok, Ch2} = amqp_connection:open_channel(Conn2), + + %% for stats to update + timer:sleep(1500), + PageOfTwo = http_get(Config, "/connections?page=1&page_size=2", ?OK), + ?assertEqual(3, maps:get(total_count, PageOfTwo)), + ?assertEqual(3, maps:get(filtered_count, PageOfTwo)), + ?assertEqual(2, maps:get(item_count, PageOfTwo)), + ?assertEqual(1, maps:get(page, PageOfTwo)), + ?assertEqual(2, maps:get(page_size, PageOfTwo)), + ?assertEqual(2, maps:get(page_count, PageOfTwo)), + + + TwoOfTwo = http_get(Config, "/channels?page=2&page_size=2", ?OK), + ?assertEqual(3, maps:get(total_count, TwoOfTwo)), + ?assertEqual(3, maps:get(filtered_count, TwoOfTwo)), + ?assertEqual(1, maps:get(item_count, TwoOfTwo)), + ?assertEqual(2, maps:get(page, TwoOfTwo)), + ?assertEqual(2, maps:get(page_size, TwoOfTwo)), + ?assertEqual(2, maps:get(page_count, TwoOfTwo)), + + amqp_channel:close(Ch), + amqp_connection:close(Conn), + amqp_channel:close(Ch1), + amqp_connection:close(Conn1), + amqp_channel:close(Ch2), + amqp_connection:close(Conn2), + + passed. + +exchanges_pagination_test(Config) -> + QArgs = #{}, + PermArgs = [{configure, <<".*">>}, {write, <<".*">>}, {read, <<".*">>}], + http_put(Config, "/vhosts/vh1", none, {group, '2xx'}), + http_put(Config, "/permissions/vh1/guest", PermArgs, {group, '2xx'}), + http_get(Config, "/exchanges/vh1?page=1&page_size=2", ?OK), + http_put(Config, "/exchanges/%2F/test0", QArgs, {group, '2xx'}), + http_put(Config, "/exchanges/vh1/test1", QArgs, {group, '2xx'}), + http_put(Config, "/exchanges/%2F/test2_reg", QArgs, {group, '2xx'}), + http_put(Config, "/exchanges/vh1/reg_test3", QArgs, {group, '2xx'}), + + %% for stats to update + timer:sleep(1500), + + Total = length(rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_exchange, list_names, [])), + + PageOfTwo = http_get(Config, "/exchanges?page=1&page_size=2", ?OK), + ?assertEqual(Total, maps:get(total_count, PageOfTwo)), + ?assertEqual(Total, maps:get(filtered_count, PageOfTwo)), + ?assertEqual(2, maps:get(item_count, PageOfTwo)), + ?assertEqual(1, maps:get(page, PageOfTwo)), + ?assertEqual(2, maps:get(page_size, PageOfTwo)), + ?assertEqual(round(Total / 2), maps:get(page_count, PageOfTwo)), + assert_list([#{name => <<"">>, vhost => <<"/">>}, + #{name => <<"amq.direct">>, vhost => <<"/">>} + ], maps:get(items, PageOfTwo)), + + ByName = http_get(Config, "/exchanges?page=1&page_size=2&name=reg", ?OK), + ?assertEqual(Total, maps:get(total_count, ByName)), + ?assertEqual(2, maps:get(filtered_count, ByName)), + ?assertEqual(2, maps:get(item_count, ByName)), + ?assertEqual(1, maps:get(page, ByName)), + ?assertEqual(2, maps:get(page_size, ByName)), + ?assertEqual(1, maps:get(page_count, ByName)), + assert_list([#{name => <<"test2_reg">>, vhost => <<"/">>}, + #{name => <<"reg_test3">>, vhost => <<"vh1">>} + ], maps:get(items, ByName)), + + + RegExByName = http_get(Config, + "/exchanges?page=1&page_size=2&name=%5E(?=%5Ereg)&use_regex=true", + ?OK), + ?assertEqual(Total, maps:get(total_count, RegExByName)), + ?assertEqual(1, maps:get(filtered_count, RegExByName)), + ?assertEqual(1, maps:get(item_count, RegExByName)), + ?assertEqual(1, maps:get(page, RegExByName)), + ?assertEqual(2, maps:get(page_size, RegExByName)), + ?assertEqual(1, maps:get(page_count, RegExByName)), + assert_list([#{name => <<"reg_test3">>, vhost => <<"vh1">>} + ], maps:get(items, RegExByName)), + + + http_get(Config, "/exchanges?page=1000", ?BAD_REQUEST), + http_get(Config, "/exchanges?page=-1", ?BAD_REQUEST), + http_get(Config, "/exchanges?page=not_an_integer_value", ?BAD_REQUEST), + http_get(Config, "/exchanges?page=1&page_size=not_an_integer_value", ?BAD_REQUEST), + http_get(Config, "/exchanges?page=1&page_size=501", ?BAD_REQUEST), %% max 500 allowed + http_get(Config, "/exchanges?page=-1&page_size=-2", ?BAD_REQUEST), + http_delete(Config, "/exchanges/%2F/test0", {group, '2xx'}), + http_delete(Config, "/exchanges/vh1/test1", {group, '2xx'}), + http_delete(Config, "/exchanges/%2F/test2_reg", {group, '2xx'}), + http_delete(Config, "/exchanges/vh1/reg_test3", {group, '2xx'}), + http_delete(Config, "/vhosts/vh1", {group, '2xx'}), + passed. + +exchanges_pagination_permissions_test(Config) -> + http_put(Config, "/users/admin", [{password, <<"admin">>}, + {tags, <<"administrator">>}], {group, '2xx'}), + http_put(Config, "/users/non-admin", [{password, <<"non-admin">>}, + {tags, <<"management">>}], {group, '2xx'}), + Perms = [{configure, <<".*">>}, + {write, <<".*">>}, + {read, <<".*">>}], + http_put(Config, "/vhosts/vh1", none, {group, '2xx'}), + http_put(Config, "/permissions/vh1/non-admin", Perms, {group, '2xx'}), + http_put(Config, "/permissions/%2F/admin", Perms, {group, '2xx'}), + http_put(Config, "/permissions/vh1/admin", Perms, {group, '2xx'}), + QArgs = #{}, + http_put(Config, "/exchanges/%2F/test0", QArgs, "admin", "admin", {group, '2xx'}), + http_put(Config, "/exchanges/vh1/test1", QArgs, "non-admin", "non-admin", {group, '2xx'}), + + %% for stats to update + timer:sleep(1500), + + FirstPage = http_get(Config, "/exchanges?page=1&name=test1", "non-admin", "non-admin", ?OK), + + ?assertEqual(8, maps:get(total_count, FirstPage)), + ?assertEqual(1, maps:get(item_count, FirstPage)), + ?assertEqual(1, maps:get(page, FirstPage)), + ?assertEqual(100, maps:get(page_size, FirstPage)), + ?assertEqual(1, maps:get(page_count, FirstPage)), + assert_list([#{name => <<"test1">>, vhost => <<"vh1">>} + ], maps:get(items, FirstPage)), + http_delete(Config, "/exchanges/%2F/test0", {group, '2xx'}), + http_delete(Config, "/exchanges/vh1/test1", {group, '2xx'}), + http_delete(Config, "/users/admin", {group, '2xx'}), + http_delete(Config, "/users/non-admin", {group, '2xx'}), + passed. + + + +queue_pagination_test(Config) -> + QArgs = #{}, + PermArgs = [{configure, <<".*">>}, {write, <<".*">>}, {read, <<".*">>}], + http_put(Config, "/vhosts/vh1", none, {group, '2xx'}), + http_put(Config, "/permissions/vh1/guest", PermArgs, {group, '2xx'}), + + http_get(Config, "/queues/vh1?page=1&page_size=2", ?OK), + + http_put(Config, "/queues/%2F/test0", QArgs, {group, '2xx'}), + http_put(Config, "/queues/vh1/test1", QArgs, {group, '2xx'}), + http_put(Config, "/queues/%2F/test2_reg", QArgs, {group, '2xx'}), + http_put(Config, "/queues/vh1/reg_test3", QArgs, {group, '2xx'}), + + %% for stats to update + timer:sleep(1500), + + Total = length(rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_amqqueue, list_names, [])), + + PageOfTwo = http_get(Config, "/queues?page=1&page_size=2", ?OK), + ?assertEqual(Total, maps:get(total_count, PageOfTwo)), + ?assertEqual(Total, maps:get(filtered_count, PageOfTwo)), + ?assertEqual(2, maps:get(item_count, PageOfTwo)), + ?assertEqual(1, maps:get(page, PageOfTwo)), + ?assertEqual(2, maps:get(page_size, PageOfTwo)), + ?assertEqual(2, maps:get(page_count, PageOfTwo)), + assert_list([#{name => <<"test0">>, vhost => <<"/">>}, + #{name => <<"test2_reg">>, vhost => <<"/">>} + ], maps:get(items, PageOfTwo)), + + SortedByName = http_get(Config, "/queues?sort=name&page=1&page_size=2", ?OK), + ?assertEqual(Total, maps:get(total_count, SortedByName)), + ?assertEqual(Total, maps:get(filtered_count, SortedByName)), + ?assertEqual(2, maps:get(item_count, SortedByName)), + ?assertEqual(1, maps:get(page, SortedByName)), + ?assertEqual(2, maps:get(page_size, SortedByName)), + ?assertEqual(2, maps:get(page_count, SortedByName)), + assert_list([#{name => <<"reg_test3">>, vhost => <<"vh1">>}, + #{name => <<"test0">>, vhost => <<"/">>} + ], maps:get(items, SortedByName)), + + + FirstPage = http_get(Config, "/queues?page=1", ?OK), + ?assertEqual(Total, maps:get(total_count, FirstPage)), + ?assertEqual(Total, maps:get(filtered_count, FirstPage)), + ?assertEqual(4, maps:get(item_count, FirstPage)), + ?assertEqual(1, maps:get(page, FirstPage)), + ?assertEqual(100, maps:get(page_size, FirstPage)), + ?assertEqual(1, maps:get(page_count, FirstPage)), + assert_list([#{name => <<"test0">>, vhost => <<"/">>}, + #{name => <<"test1">>, vhost => <<"vh1">>}, + #{name => <<"test2_reg">>, vhost => <<"/">>}, + #{name => <<"reg_test3">>, vhost =><<"vh1">>} + ], maps:get(items, FirstPage)), + + + ReverseSortedByName = http_get(Config, + "/queues?page=2&page_size=2&sort=name&sort_reverse=true", + ?OK), + ?assertEqual(Total, maps:get(total_count, ReverseSortedByName)), + ?assertEqual(Total, maps:get(filtered_count, ReverseSortedByName)), + ?assertEqual(2, maps:get(item_count, ReverseSortedByName)), + ?assertEqual(2, maps:get(page, ReverseSortedByName)), + ?assertEqual(2, maps:get(page_size, ReverseSortedByName)), + ?assertEqual(2, maps:get(page_count, ReverseSortedByName)), + assert_list([#{name => <<"test0">>, vhost => <<"/">>}, + #{name => <<"reg_test3">>, vhost => <<"vh1">>} + ], maps:get(items, ReverseSortedByName)), + + + ByName = http_get(Config, "/queues?page=1&page_size=2&name=reg", ?OK), + ?assertEqual(Total, maps:get(total_count, ByName)), + ?assertEqual(2, maps:get(filtered_count, ByName)), + ?assertEqual(2, maps:get(item_count, ByName)), + ?assertEqual(1, maps:get(page, ByName)), + ?assertEqual(2, maps:get(page_size, ByName)), + ?assertEqual(1, maps:get(page_count, ByName)), + assert_list([#{name => <<"test2_reg">>, vhost => <<"/">>}, + #{name => <<"reg_test3">>, vhost => <<"vh1">>} + ], maps:get(items, ByName)), + + RegExByName = http_get(Config, + "/queues?page=1&page_size=2&name=%5E(?=%5Ereg)&use_regex=true", + ?OK), + ?assertEqual(Total, maps:get(total_count, RegExByName)), + ?assertEqual(1, maps:get(filtered_count, RegExByName)), + ?assertEqual(1, maps:get(item_count, RegExByName)), + ?assertEqual(1, maps:get(page, RegExByName)), + ?assertEqual(2, maps:get(page_size, RegExByName)), + ?assertEqual(1, maps:get(page_count, RegExByName)), + assert_list([#{name => <<"reg_test3">>, vhost => <<"vh1">>} + ], maps:get(items, RegExByName)), + + + http_get(Config, "/queues?page=1000", ?BAD_REQUEST), + http_get(Config, "/queues?page=-1", ?BAD_REQUEST), + http_get(Config, "/queues?page=not_an_integer_value", ?BAD_REQUEST), + http_get(Config, "/queues?page=1&page_size=not_an_integer_value", ?BAD_REQUEST), + http_get(Config, "/queues?page=1&page_size=501", ?BAD_REQUEST), %% max 500 allowed + http_get(Config, "/queues?page=-1&page_size=-2", ?BAD_REQUEST), + http_delete(Config, "/queues/%2F/test0", {group, '2xx'}), + http_delete(Config, "/queues/vh1/test1", {group, '2xx'}), + http_delete(Config, "/queues/%2F/test2_reg", {group, '2xx'}), + http_delete(Config, "/queues/vh1/reg_test3", {group, '2xx'}), + http_delete(Config, "/vhosts/vh1", {group, '2xx'}), + passed. + +queue_pagination_columns_test(Config) -> + QArgs = #{}, + PermArgs = [{configure, <<".*">>}, {write, <<".*">>}, {read, <<".*">>}], + http_put(Config, "/vhosts/vh1", none, [?CREATED, ?NO_CONTENT]), + http_put(Config, "/permissions/vh1/guest", PermArgs, [?CREATED, ?NO_CONTENT]), + + http_get(Config, "/queues/vh1?columns=name&page=1&page_size=2", ?OK), + http_put(Config, "/queues/%2F/queue_a", QArgs, {group, '2xx'}), + http_put(Config, "/queues/vh1/queue_b", QArgs, {group, '2xx'}), + http_put(Config, "/queues/%2F/queue_c", QArgs, {group, '2xx'}), + http_put(Config, "/queues/vh1/queue_d", QArgs, {group, '2xx'}), + PageOfTwo = http_get(Config, "/queues?columns=name&page=1&page_size=2", ?OK), + ?assertEqual(4, maps:get(total_count, PageOfTwo)), + ?assertEqual(4, maps:get(filtered_count, PageOfTwo)), + ?assertEqual(2, maps:get(item_count, PageOfTwo)), + ?assertEqual(1, maps:get(page, PageOfTwo)), + ?assertEqual(2, maps:get(page_size, PageOfTwo)), + ?assertEqual(2, maps:get(page_count, PageOfTwo)), + assert_list([#{name => <<"queue_a">>}, + #{name => <<"queue_c">>} + ], maps:get(items, PageOfTwo)), + + ColumnNameVhost = http_get(Config, "/queues/vh1?columns=name&page=1&page_size=2", ?OK), + ?assertEqual(2, maps:get(total_count, ColumnNameVhost)), + ?assertEqual(2, maps:get(filtered_count, ColumnNameVhost)), + ?assertEqual(2, maps:get(item_count, ColumnNameVhost)), + ?assertEqual(1, maps:get(page, ColumnNameVhost)), + ?assertEqual(2, maps:get(page_size, ColumnNameVhost)), + ?assertEqual(1, maps:get(page_count, ColumnNameVhost)), + assert_list([#{name => <<"queue_b">>}, + #{name => <<"queue_d">>} + ], maps:get(items, ColumnNameVhost)), + + ColumnsNameVhost = http_get(Config, "/queues?columns=name,vhost&page=2&page_size=2", ?OK), + ?assertEqual(4, maps:get(total_count, ColumnsNameVhost)), + ?assertEqual(4, maps:get(filtered_count, ColumnsNameVhost)), + ?assertEqual(2, maps:get(item_count, ColumnsNameVhost)), + ?assertEqual(2, maps:get(page, ColumnsNameVhost)), + ?assertEqual(2, maps:get(page_size, ColumnsNameVhost)), + ?assertEqual(2, maps:get(page_count, ColumnsNameVhost)), + assert_list([ + #{name => <<"queue_b">>, + vhost => <<"vh1">>}, + #{name => <<"queue_d">>, + vhost => <<"vh1">>} + ], maps:get(items, ColumnsNameVhost)), + + + http_delete(Config, "/queues/%2F/queue_a", {group, '2xx'}), + http_delete(Config, "/queues/vh1/queue_b", {group, '2xx'}), + http_delete(Config, "/queues/%2F/queue_c", {group, '2xx'}), + http_delete(Config, "/queues/vh1/queue_d", {group, '2xx'}), + http_delete(Config, "/vhosts/vh1", {group, '2xx'}), + passed. + +queues_pagination_permissions_test(Config) -> + http_put(Config, "/users/non-admin", [{password, <<"non-admin">>}, + {tags, <<"management">>}], {group, '2xx'}), + http_put(Config, "/users/admin", [{password, <<"admin">>}, + {tags, <<"administrator">>}], {group, '2xx'}), + Perms = [{configure, <<".*">>}, + {write, <<".*">>}, + {read, <<".*">>}], + http_put(Config, "/vhosts/vh1", none, {group, '2xx'}), + http_put(Config, "/permissions/vh1/non-admin", Perms, {group, '2xx'}), + http_put(Config, "/permissions/%2F/admin", Perms, {group, '2xx'}), + http_put(Config, "/permissions/vh1/admin", Perms, {group, '2xx'}), + QArgs = #{}, + http_put(Config, "/queues/%2F/test0", QArgs, {group, '2xx'}), + http_put(Config, "/queues/vh1/test1", QArgs, "non-admin","non-admin", {group, '2xx'}), + FirstPage = http_get(Config, "/queues?page=1", "non-admin", "non-admin", ?OK), + ?assertEqual(1, maps:get(total_count, FirstPage)), + ?assertEqual(1, maps:get(item_count, FirstPage)), + ?assertEqual(1, maps:get(page, FirstPage)), + ?assertEqual(100, maps:get(page_size, FirstPage)), + ?assertEqual(1, maps:get(page_count, FirstPage)), + assert_list([#{name => <<"test1">>, vhost => <<"vh1">>} + ], maps:get(items, FirstPage)), + + FirstPageAdm = http_get(Config, "/queues?page=1", "admin", "admin", ?OK), + ?assertEqual(2, maps:get(total_count, FirstPageAdm)), + ?assertEqual(2, maps:get(item_count, FirstPageAdm)), + ?assertEqual(1, maps:get(page, FirstPageAdm)), + ?assertEqual(100, maps:get(page_size, FirstPageAdm)), + ?assertEqual(1, maps:get(page_count, FirstPageAdm)), + assert_list([#{name => <<"test1">>, vhost => <<"vh1">>}, + #{name => <<"test0">>, vhost => <<"/">>} + ], maps:get(items, FirstPageAdm)), + + http_delete(Config, "/queues/%2F/test0", {group, '2xx'}), + http_delete(Config, "/queues/vh1/test1","admin","admin", {group, '2xx'}), + http_delete(Config, "/users/admin", {group, '2xx'}), + http_delete(Config, "/users/non-admin", {group, '2xx'}), + passed. + +samples_range_test(Config) -> + {Conn, Ch} = open_connection_and_channel(Config), + + %% Channels + timer:sleep(2000), + [ConnInfo | _] = http_get(Config, "/channels?lengths_age=60&lengths_incr=1", ?OK), + http_get(Config, "/channels?lengths_age=6000&lengths_incr=1", ?BAD_REQUEST), + + ConnDetails = maps:get(connection_details, ConnInfo), + ConnName0 = maps:get(name, ConnDetails), + ConnName = uri_string:recompose(#{path => binary_to_list(ConnName0)}), + ChanName = ConnName ++ uri_string:recompose(#{path => " (1)"}), + + http_get(Config, "/channels/" ++ ChanName ++ "?lengths_age=60&lengths_incr=1", ?OK), + http_get(Config, "/channels/" ++ ChanName ++ "?lengths_age=6000&lengths_incr=1", ?BAD_REQUEST), + + http_get(Config, "/vhosts/%2F/channels?lengths_age=60&lengths_incr=1", ?OK), + http_get(Config, "/vhosts/%2F/channels?lengths_age=6000&lengths_incr=1", ?BAD_REQUEST), + + %% Connections. + + http_get(Config, "/connections?lengths_age=60&lengths_incr=1", ?OK), + http_get(Config, "/connections?lengths_age=6000&lengths_incr=1", ?BAD_REQUEST), + + http_get(Config, "/connections/" ++ ConnName ++ "?lengths_age=60&lengths_incr=1", ?OK), + http_get(Config, "/connections/" ++ ConnName ++ "?lengths_age=6000&lengths_incr=1", ?BAD_REQUEST), + + http_get(Config, "/connections/" ++ ConnName ++ "/channels?lengths_age=60&lengths_incr=1", ?OK), + http_get(Config, "/connections/" ++ ConnName ++ "/channels?lengths_age=6000&lengths_incr=1", ?BAD_REQUEST), + + http_get(Config, "/vhosts/%2F/connections?lengths_age=60&lengths_incr=1", ?OK), + http_get(Config, "/vhosts/%2F/connections?lengths_age=6000&lengths_incr=1", ?BAD_REQUEST), + + amqp_channel:close(Ch), + amqp_connection:close(Conn), + + %% Exchanges + + http_get(Config, "/exchanges?lengths_age=60&lengths_incr=1", ?OK), + http_get(Config, "/exchanges?lengths_age=6000&lengths_incr=1", ?BAD_REQUEST), + + http_get(Config, "/exchanges/%2F/amq.direct?lengths_age=60&lengths_incr=1", ?OK), + http_get(Config, "/exchanges/%2F/amq.direct?lengths_age=6000&lengths_incr=1", ?BAD_REQUEST), + + %% Nodes + http_get(Config, "/nodes?lengths_age=60&lengths_incr=1", ?OK), + http_get(Config, "/nodes?lengths_age=6000&lengths_incr=1", ?BAD_REQUEST), + + %% Overview + http_get(Config, "/overview?lengths_age=60&lengths_incr=1", ?OK), + http_get(Config, "/overview?lengths_age=6000&lengths_incr=1", ?BAD_REQUEST), + + %% Queues + http_put(Config, "/queues/%2F/test-001", #{}, {group, '2xx'}), + timer:sleep(2000), + + http_get(Config, "/queues/%2F?lengths_age=60&lengths_incr=1", ?OK), + http_get(Config, "/queues/%2F?lengths_age=6000&lengths_incr=1", ?BAD_REQUEST), + http_get(Config, "/queues/%2F/test-001?lengths_age=60&lengths_incr=1", ?OK), + http_get(Config, "/queues/%2F/test-001?lengths_age=6000&lengths_incr=1", ?BAD_REQUEST), + + http_delete(Config, "/queues/%2F/test-001", {group, '2xx'}), + + %% Vhosts + http_put(Config, "/vhosts/vh1", none, {group, '2xx'}), + timer:sleep(2000), + + http_get(Config, "/vhosts?lengths_age=60&lengths_incr=1", ?OK), + http_get(Config, "/vhosts?lengths_age=6000&lengths_incr=1", ?BAD_REQUEST), + http_get(Config, "/vhosts/vh1?lengths_age=60&lengths_incr=1", ?OK), + http_get(Config, "/vhosts/vh1?lengths_age=6000&lengths_incr=1", ?BAD_REQUEST), + + http_delete(Config, "/vhosts/vh1", {group, '2xx'}), + + passed. + +sorting_test(Config) -> + QArgs = #{}, + PermArgs = [{configure, <<".*">>}, {write, <<".*">>}, {read, <<".*">>}], + http_put(Config, "/vhosts/vh19", none, {group, '2xx'}), + http_put(Config, "/permissions/vh19/guest", PermArgs, {group, '2xx'}), + http_put(Config, "/queues/%2F/test0", QArgs, {group, '2xx'}), + http_put(Config, "/queues/vh19/test1", QArgs, {group, '2xx'}), + http_put(Config, "/queues/%2F/test2", QArgs, {group, '2xx'}), + http_put(Config, "/queues/vh19/test3", QArgs, {group, '2xx'}), + timer:sleep(2000), + assert_list([#{name => <<"test0">>}, + #{name => <<"test2">>}, + #{name => <<"test1">>}, + #{name => <<"test3">>}], http_get(Config, "/queues", ?OK)), + assert_list([#{name => <<"test0">>}, + #{name => <<"test1">>}, + #{name => <<"test2">>}, + #{name => <<"test3">>}], http_get(Config, "/queues?sort=name", ?OK)), + assert_list([#{name => <<"test0">>}, + #{name => <<"test2">>}, + #{name => <<"test1">>}, + #{name => <<"test3">>}], http_get(Config, "/queues?sort=vhost", ?OK)), + assert_list([#{name => <<"test3">>}, + #{name => <<"test1">>}, + #{name => <<"test2">>}, + #{name => <<"test0">>}], http_get(Config, "/queues?sort_reverse=true", ?OK)), + assert_list([#{name => <<"test3">>}, + #{name => <<"test2">>}, + #{name => <<"test1">>}, + #{name => <<"test0">>}], http_get(Config, "/queues?sort=name&sort_reverse=true", ?OK)), + assert_list([#{name => <<"test3">>}, + #{name => <<"test1">>}, + #{name => <<"test2">>}, + #{name => <<"test0">>}], http_get(Config, "/queues?sort=vhost&sort_reverse=true", ?OK)), + %% Rather poor but at least test it doesn't blow up with dots + http_get(Config, "/queues?sort=owner_pid_details.name", ?OK), + http_delete(Config, "/queues/%2F/test0", {group, '2xx'}), + http_delete(Config, "/queues/vh19/test1", {group, '2xx'}), + http_delete(Config, "/queues/%2F/test2", {group, '2xx'}), + http_delete(Config, "/queues/vh19/test3", {group, '2xx'}), + http_delete(Config, "/vhosts/vh19", {group, '2xx'}), + passed. + +format_output_test(Config) -> + QArgs = #{}, + PermArgs = [{configure, <<".*">>}, {write, <<".*">>}, {read, <<".*">>}], + http_put(Config, "/vhosts/vh129", none, {group, '2xx'}), + http_put(Config, "/permissions/vh129/guest", PermArgs, {group, '2xx'}), + http_put(Config, "/queues/%2F/test0", QArgs, {group, '2xx'}), + timer:sleep(2000), + assert_list([#{name => <<"test0">>, + consumer_utilisation => null, + exclusive_consumer_tag => null, + recoverable_slaves => null}], http_get(Config, "/queues", ?OK)), + http_delete(Config, "/queues/%2F/test0", {group, '2xx'}), + http_delete(Config, "/vhosts/vh129", {group, '2xx'}), + passed. + +columns_test(Config) -> + Path = "/queues/%2F/columns.test", + TTL = 30000, + http_delete(Config, Path, [{group, '2xx'}, 404]), + http_put(Config, Path, [{arguments, [{<<"x-message-ttl">>, TTL}]}], + {group, '2xx'}), + Item = #{arguments => #{'x-message-ttl' => TTL}, name => <<"columns.test">>}, + timer:sleep(2000), + [Item] = http_get(Config, "/queues?columns=arguments.x-message-ttl,name", ?OK), + Item = http_get(Config, "/queues/%2F/columns.test?columns=arguments.x-message-ttl,name", ?OK), + http_delete(Config, Path, {group, '2xx'}), + passed. + +get_test(Config) -> + %% Real world example... + Headers = [{<<"x-forwarding">>, array, + [{table, + [{<<"uri">>, longstr, + <<"amqp://localhost/%2F/upstream">>}]}]}], + http_put(Config, "/queues/%2F/myqueue", #{}, {group, '2xx'}), + {Conn, Ch} = open_connection_and_channel(Config), + #'confirm.select_ok'{} = amqp_channel:call(Ch, #'confirm.select'{}), + Publish = fun (Payload) -> + amqp_channel:cast( + Ch, #'basic.publish'{exchange = <<>>, + routing_key = <<"myqueue">>}, + #amqp_msg{props = #'P_basic'{headers = Headers}, + payload = Payload}), + amqp_channel:wait_for_confirms_or_die(Ch, 5) + end, + Publish(<<"1aaa">>), + Publish(<<"2aaa">>), + Publish(<<"3aaa">>), + [Msg] = http_post(Config, "/queues/%2F/myqueue/get", [{ackmode, ack_requeue_false}, + {count, 1}, + {encoding, auto}, + {truncate, 1}], ?OK), + false = maps:get(redelivered, Msg), + <<>> = maps:get(exchange, Msg), + <<"myqueue">> = maps:get(routing_key, Msg), + <<"1">> = maps:get(payload, Msg), + #{'x-forwarding' := + [#{uri := <<"amqp://localhost/%2F/upstream">>}]} = + maps:get(headers, maps:get(properties, Msg)), + + [M2, M3] = http_post(Config, "/queues/%2F/myqueue/get", [{ackmode, ack_requeue_true}, + {count, 5}, + {encoding, auto}], ?OK), + <<"2aaa">> = maps:get(payload, M2), + <<"3aaa">> = maps:get(payload, M3), + 2 = length(http_post(Config, "/queues/%2F/myqueue/get", [{ackmode, ack_requeue_false}, + {count, 5}, + {encoding, auto}], ?OK)), + Publish(<<"4aaa">>), + Publish(<<"5aaa">>), + [M4, M5] = http_post(Config, "/queues/%2F/myqueue/get", + [{ackmode, reject_requeue_true}, + {count, 5}, + {encoding, auto}], ?OK), + + <<"4aaa">> = maps:get(payload, M4), + <<"5aaa">> = maps:get(payload, M5), + 2 = length(http_post(Config, "/queues/%2F/myqueue/get", + [{ackmode, ack_requeue_false}, + {count, 5}, + {encoding, auto}], ?OK)), + + [] = http_post(Config, "/queues/%2F/myqueue/get", [{ackmode, ack_requeue_false}, + {count, 5}, + {encoding, auto}], ?OK), + http_delete(Config, "/queues/%2F/myqueue", {group, '2xx'}), + amqp_channel:close(Ch), + close_connection(Conn), + + passed. + +get_encoding_test(Config) -> + Utf8Text = <<"Loïc was here!"/utf8>>, + Utf8Payload = base64:encode(Utf8Text), + BinPayload = base64:encode(<<0:64, 16#ff, 16#fd, 0:64>>), + Utf8Msg = msg(<<"get_encoding_test">>, #{}, Utf8Payload, <<"base64">>), + BinMsg = msg(<<"get_encoding_test">>, #{}, BinPayload, <<"base64">>), + http_put(Config, "/queues/%2F/get_encoding_test", #{}, {group, '2xx'}), + http_post(Config, "/exchanges/%2F/amq.default/publish", Utf8Msg, ?OK), + http_post(Config, "/exchanges/%2F/amq.default/publish", BinMsg, ?OK), + timer:sleep(250), + [RecvUtf8Msg1, RecvBinMsg1] = http_post(Config, "/queues/%2F/get_encoding_test/get", + [{ackmode, ack_requeue_false}, + {count, 2}, + {encoding, auto}], ?OK), + %% Utf-8 payload must be returned as a utf-8 string when auto encoding is used. + ?assertEqual(<<"string">>, maps:get(payload_encoding, RecvUtf8Msg1)), + ?assertEqual(Utf8Text, maps:get(payload, RecvUtf8Msg1)), + %% Binary payload must be base64-encoded when auto is used. + ?assertEqual(<<"base64">>, maps:get(payload_encoding, RecvBinMsg1)), + ?assertEqual(BinPayload, maps:get(payload, RecvBinMsg1)), + %% Good. Now try forcing the base64 encoding. + http_post(Config, "/exchanges/%2F/amq.default/publish", Utf8Msg, ?OK), + http_post(Config, "/exchanges/%2F/amq.default/publish", BinMsg, ?OK), + [RecvUtf8Msg2, RecvBinMsg2] = http_post(Config, "/queues/%2F/get_encoding_test/get", + [{ackmode, ack_requeue_false}, + {count, 2}, + {encoding, base64}], ?OK), + %% All payloads must be base64-encoded when base64 encoding is used. + ?assertEqual(<<"base64">>, maps:get(payload_encoding, RecvUtf8Msg2)), + ?assertEqual(Utf8Payload, maps:get(payload, RecvUtf8Msg2)), + ?assertEqual(<<"base64">>, maps:get(payload_encoding, RecvBinMsg2)), + ?assertEqual(BinPayload, maps:get(payload, RecvBinMsg2)), + http_delete(Config, "/queues/%2F/get_encoding_test", {group, '2xx'}), + passed. + +get_fail_test(Config) -> + http_put(Config, "/users/myuser", [{password, <<"password">>}, + {tags, <<"management">>}], {group, '2xx'}), + http_put(Config, "/queues/%2F/myqueue", #{}, {group, '2xx'}), + http_post(Config, "/queues/%2F/myqueue/get", + [{ackmode, ack_requeue_false}, + {count, 1}, + {encoding, auto}], "myuser", "password", ?NOT_AUTHORISED), + http_delete(Config, "/queues/%2F/myqueue", {group, '2xx'}), + http_delete(Config, "/users/myuser", {group, '2xx'}), + passed. + + +-define(LARGE_BODY_BYTES, 25000000). + +publish_test(Config) -> + Headers = #{'x-forwarding' => [#{uri => <<"amqp://localhost/%2F/upstream">>}]}, + Msg = msg(<<"publish_test">>, Headers, <<"Hello world">>), + http_put(Config, "/queues/%2F/publish_test", #{}, {group, '2xx'}), + ?assertEqual(#{routed => true}, + http_post(Config, "/exchanges/%2F/amq.default/publish", Msg, ?OK)), + [Msg2] = http_post(Config, "/queues/%2F/publish_test/get", [{ackmode, ack_requeue_false}, + {count, 1}, + {encoding, auto}], ?OK), + assert_item(Msg, Msg2), + http_post(Config, "/exchanges/%2F/amq.default/publish", Msg2, ?OK), + [Msg3] = http_post(Config, "/queues/%2F/publish_test/get", [{ackmode, ack_requeue_false}, + {count, 1}, + {encoding, auto}], ?OK), + assert_item(Msg, Msg3), + http_delete(Config, "/queues/%2F/publish_test", {group, '2xx'}), + passed. + +publish_large_message_test(Config) -> + Headers = #{'x-forwarding' => [#{uri => <<"amqp://localhost/%2F/upstream">>}]}, + Body = binary:copy(<<"a">>, ?LARGE_BODY_BYTES), + Msg = msg(<<"publish_accept_json_test">>, Headers, Body), + http_put(Config, "/queues/%2F/publish_accept_json_test", #{}, {group, '2xx'}), + ?assertEqual(#{routed => true}, + http_post_accept_json(Config, "/exchanges/%2F/amq.default/publish", + Msg, ?OK)), + + [Msg2] = http_post_accept_json(Config, "/queues/%2F/publish_accept_json_test/get", + [{ackmode, ack_requeue_false}, + {count, 1}, + {encoding, auto}], ?OK), + assert_item(Msg, Msg2), + http_post_accept_json(Config, "/exchanges/%2F/amq.default/publish", Msg2, ?OK), + [Msg3] = http_post_accept_json(Config, "/queues/%2F/publish_accept_json_test/get", + [{ackmode, ack_requeue_false}, + {count, 1}, + {encoding, auto}], ?OK), + assert_item(Msg, Msg3), + http_delete(Config, "/queues/%2F/publish_accept_json_test", {group, '2xx'}), + passed. + +publish_accept_json_test(Config) -> + Headers = #{'x-forwarding' => [#{uri => <<"amqp://localhost/%2F/upstream">>}]}, + Msg = msg(<<"publish_accept_json_test">>, Headers, <<"Hello world">>), + http_put(Config, "/queues/%2F/publish_accept_json_test", #{}, {group, '2xx'}), + ?assertEqual(#{routed => true}, + http_post_accept_json(Config, "/exchanges/%2F/amq.default/publish", + Msg, ?OK)), + + [Msg2] = http_post_accept_json(Config, "/queues/%2F/publish_accept_json_test/get", + [{ackmode, ack_requeue_false}, + {count, 1}, + {encoding, auto}], ?OK), + assert_item(Msg, Msg2), + http_post_accept_json(Config, "/exchanges/%2F/amq.default/publish", Msg2, ?OK), + [Msg3] = http_post_accept_json(Config, "/queues/%2F/publish_accept_json_test/get", + [{ackmode, ack_requeue_false}, + {count, 1}, + {encoding, auto}], ?OK), + assert_item(Msg, Msg3), + http_delete(Config, "/queues/%2F/publish_accept_json_test", {group, '2xx'}), + passed. + +publish_fail_test(Config) -> + Msg = msg(<<"publish_fail_test">>, [], <<"Hello world">>), + http_put(Config, "/queues/%2F/publish_fail_test", #{}, {group, '2xx'}), + http_put(Config, "/users/myuser", [{password, <<"password">>}, + {tags, <<"management">>}], {group, '2xx'}), + http_post(Config, "/exchanges/%2F/amq.default/publish", Msg, "myuser", "password", + ?NOT_AUTHORISED), + Msg2 = [{exchange, <<"">>}, + {routing_key, <<"publish_fail_test">>}, + {properties, [{user_id, <<"foo">>}]}, + {payload, <<"Hello world">>}, + {payload_encoding, <<"string">>}], + http_post(Config, "/exchanges/%2F/amq.default/publish", Msg2, ?BAD_REQUEST), + Msg3 = [{exchange, <<"">>}, + {routing_key, <<"publish_fail_test">>}, + {properties, []}, + {payload, [<<"not a string">>]}, + {payload_encoding, <<"string">>}], + http_post(Config, "/exchanges/%2F/amq.default/publish", Msg3, ?BAD_REQUEST), + MsgTemplate = [{exchange, <<"">>}, + {routing_key, <<"publish_fail_test">>}, + {payload, <<"Hello world">>}, + {payload_encoding, <<"string">>}], + [http_post(Config, "/exchanges/%2F/amq.default/publish", + [{properties, [BadProp]} | MsgTemplate], ?BAD_REQUEST) + || BadProp <- [{priority, <<"really high">>}, + {timestamp, <<"recently">>}, + {expiration, 1234}]], + http_delete(Config, "/queues/%2F/publish_fail_test", {group, '2xx'}), + http_delete(Config, "/users/myuser", {group, '2xx'}), + passed. + +publish_base64_test(Config) -> + %% "abcd" + %% @todo Note that we used to accept [] instead of {struct, []} when we shouldn't have. + %% This is a breaking change and probably needs to be documented. + Msg = msg(<<"publish_base64_test">>, #{}, <<"YWJjZA==">>, <<"base64">>), + BadMsg1 = msg(<<"publish_base64_test">>, #{}, <<"flibble">>, <<"base64">>), + BadMsg2 = msg(<<"publish_base64_test">>, #{}, <<"YWJjZA==">>, <<"base99">>), + http_put(Config, "/queues/%2F/publish_base64_test", #{}, {group, '2xx'}), + http_post(Config, "/exchanges/%2F/amq.default/publish", Msg, ?OK), + http_post(Config, "/exchanges/%2F/amq.default/publish", BadMsg1, ?BAD_REQUEST), + http_post(Config, "/exchanges/%2F/amq.default/publish", BadMsg2, ?BAD_REQUEST), + timer:sleep(250), + [Msg2] = http_post(Config, "/queues/%2F/publish_base64_test/get", [{ackmode, ack_requeue_false}, + {count, 1}, + {encoding, auto}], ?OK), + ?assertEqual(<<"abcd">>, maps:get(payload, Msg2)), + http_delete(Config, "/queues/%2F/publish_base64_test", {group, '2xx'}), + passed. + +publish_unrouted_test(Config) -> + Msg = msg(<<"hmmm">>, #{}, <<"Hello world">>), + ?assertEqual(#{routed => false}, + http_post(Config, "/exchanges/%2F/amq.default/publish", Msg, ?OK)). + +if_empty_unused_test(Config) -> + http_put(Config, "/exchanges/%2F/test", #{}, {group, '2xx'}), + http_put(Config, "/queues/%2F/test", #{}, {group, '2xx'}), + http_post(Config, "/bindings/%2F/e/test/q/test", #{}, {group, '2xx'}), + http_post(Config, "/exchanges/%2F/amq.default/publish", + msg(<<"test">>, #{}, <<"Hello world">>), ?OK), + http_delete(Config, "/queues/%2F/test?if-empty=true", ?BAD_REQUEST), + http_delete(Config, "/exchanges/%2F/test?if-unused=true", ?BAD_REQUEST), + http_delete(Config, "/queues/%2F/test/contents", {group, '2xx'}), + + {Conn, _ConnPath, _ChPath, _ConnChPath} = get_conn(Config, "guest", "guest"), + {ok, Ch} = amqp_connection:open_channel(Conn), + amqp_channel:subscribe(Ch, #'basic.consume'{queue = <<"test">> }, self()), + http_delete(Config, "/queues/%2F/test?if-unused=true", ?BAD_REQUEST), + amqp_connection:close(Conn), + + http_delete(Config, "/queues/%2F/test?if-empty=true", {group, '2xx'}), + http_delete(Config, "/exchanges/%2F/test?if-unused=true", {group, '2xx'}), + passed. + +parameters_test(Config) -> + register_parameters_and_policy_validator(Config), + + http_put(Config, "/parameters/test/%2F/good", [{value, <<"ignore">>}], {group, '2xx'}), + http_put(Config, "/parameters/test/%2F/maybe", [{value, <<"good">>}], {group, '2xx'}), + http_put(Config, "/parameters/test/%2F/maybe", [{value, <<"bad">>}], ?BAD_REQUEST), + http_put(Config, "/parameters/test/%2F/bad", [{value, <<"good">>}], ?BAD_REQUEST), + http_put(Config, "/parameters/test/um/good", [{value, <<"ignore">>}], ?NOT_FOUND), + + Good = #{vhost => <<"/">>, + component => <<"test">>, + name => <<"good">>, + value => <<"ignore">>}, + Maybe = #{vhost => <<"/">>, + component => <<"test">>, + name => <<"maybe">>, + value => <<"good">>}, + List = [Good, Maybe], + + assert_list(List, http_get(Config, "/parameters")), + assert_list(List, http_get(Config, "/parameters/test")), + assert_list(List, http_get(Config, "/parameters/test/%2F")), + assert_list([], http_get(Config, "/parameters/oops")), + http_get(Config, "/parameters/test/oops", ?NOT_FOUND), + + assert_item(Good, http_get(Config, "/parameters/test/%2F/good", ?OK)), + assert_item(Maybe, http_get(Config, "/parameters/test/%2F/maybe", ?OK)), + + http_delete(Config, "/parameters/test/%2F/good", {group, '2xx'}), + http_delete(Config, "/parameters/test/%2F/maybe", {group, '2xx'}), + http_delete(Config, "/parameters/test/%2F/bad", ?NOT_FOUND), + + 0 = length(http_get(Config, "/parameters")), + 0 = length(http_get(Config, "/parameters/test")), + 0 = length(http_get(Config, "/parameters/test/%2F")), + unregister_parameters_and_policy_validator(Config), + passed. + +global_parameters_test(Config) -> + InitialParameters = http_get(Config, "/global-parameters"), + http_put(Config, "/global-parameters/good", [{value, [{a, <<"b">>}]}], {group, '2xx'}), + http_put(Config, "/global-parameters/maybe", [{value,[{c, <<"d">>}]}], {group, '2xx'}), + + Good = #{name => <<"good">>, + value => #{a => <<"b">>}}, + Maybe = #{name => <<"maybe">>, + value => #{c => <<"d">>}}, + List = InitialParameters ++ [Good, Maybe], + + assert_list(List, http_get(Config, "/global-parameters")), + http_get(Config, "/global-parameters/oops", ?NOT_FOUND), + + assert_item(Good, http_get(Config, "/global-parameters/good", ?OK)), + assert_item(Maybe, http_get(Config, "/global-parameters/maybe", ?OK)), + + http_delete(Config, "/global-parameters/good", {group, '2xx'}), + http_delete(Config, "/global-parameters/maybe", {group, '2xx'}), + http_delete(Config, "/global-parameters/bad", ?NOT_FOUND), + + InitialCount = length(InitialParameters), + InitialCount = length(http_get(Config, "/global-parameters")), + passed. + +policy_test(Config) -> + register_parameters_and_policy_validator(Config), + PolicyPos = #{vhost => <<"/">>, + name => <<"policy_pos">>, + pattern => <<".*">>, + definition => #{testpos => [1,2,3]}, + priority => 10}, + PolicyEven = #{vhost => <<"/">>, + name => <<"policy_even">>, + pattern => <<".*">>, + definition => #{testeven => [1,2,3,4]}, + priority => 10}, + http_put(Config, + "/policies/%2F/policy_pos", + maps:remove(key, PolicyPos), + {group, '2xx'}), + http_put(Config, + "/policies/%2F/policy_even", + maps:remove(key, PolicyEven), + {group, '2xx'}), + assert_item(PolicyPos, http_get(Config, "/policies/%2F/policy_pos", ?OK)), + assert_item(PolicyEven, http_get(Config, "/policies/%2F/policy_even", ?OK)), + List = [PolicyPos, PolicyEven], + assert_list(List, http_get(Config, "/policies", ?OK)), + assert_list(List, http_get(Config, "/policies/%2F", ?OK)), + + http_delete(Config, "/policies/%2F/policy_pos", {group, '2xx'}), + http_delete(Config, "/policies/%2F/policy_even", {group, '2xx'}), + 0 = length(http_get(Config, "/policies")), + 0 = length(http_get(Config, "/policies/%2F")), + unregister_parameters_and_policy_validator(Config), + passed. + +policy_permissions_test(Config) -> + register_parameters_and_policy_validator(Config), + + http_put(Config, "/users/admin", [{password, <<"admin">>}, + {tags, <<"administrator">>}], {group, '2xx'}), + http_put(Config, "/users/mon", [{password, <<"mon">>}, + {tags, <<"monitoring">>}], {group, '2xx'}), + http_put(Config, "/users/policy", [{password, <<"policy">>}, + {tags, <<"policymaker">>}], {group, '2xx'}), + http_put(Config, "/users/mgmt", [{password, <<"mgmt">>}, + {tags, <<"management">>}], {group, '2xx'}), + Perms = [{configure, <<".*">>}, + {write, <<".*">>}, + {read, <<".*">>}], + http_put(Config, "/vhosts/v", none, {group, '2xx'}), + http_put(Config, "/permissions/v/admin", Perms, {group, '2xx'}), + http_put(Config, "/permissions/v/mon", Perms, {group, '2xx'}), + http_put(Config, "/permissions/v/policy", Perms, {group, '2xx'}), + http_put(Config, "/permissions/v/mgmt", Perms, {group, '2xx'}), + + Policy = [{pattern, <<".*">>}, + {definition, [{<<"ha-mode">>, <<"all">>}]}], + Param = [{value, <<"">>}], + + http_put(Config, "/policies/%2F/HA", Policy, {group, '2xx'}), + http_put(Config, "/parameters/test/%2F/good", Param, {group, '2xx'}), + + Pos = fun (U) -> + http_put(Config, "/policies/v/HA", Policy, U, U, {group, '2xx'}), + http_put(Config, "/parameters/test/v/good", Param, U, U, {group, '2xx'}), + http_get(Config, "/policies", U, U, {group, '2xx'}), + http_get(Config, "/parameters/test", U, U, {group, '2xx'}), + http_get(Config, "/parameters", U, U, {group, '2xx'}), + http_get(Config, "/policies/v", U, U, {group, '2xx'}), + http_get(Config, "/parameters/test/v", U, U, {group, '2xx'}), + http_get(Config, "/policies/v/HA", U, U, {group, '2xx'}), + http_get(Config, "/parameters/test/v/good", U, U, {group, '2xx'}) + end, + Neg = fun (U) -> + http_put(Config, "/policies/v/HA", Policy, U, U, ?NOT_AUTHORISED), + http_put(Config, + "/parameters/test/v/good", Param, U, U, ?NOT_AUTHORISED), + http_put(Config, + "/parameters/test/v/admin", Param, U, U, ?NOT_AUTHORISED), + %% Policies are read-only for management and monitoring. + http_get(Config, "/policies", U, U, ?OK), + http_get(Config, "/policies/v", U, U, ?OK), + http_get(Config, "/parameters", U, U, ?NOT_AUTHORISED), + http_get(Config, "/parameters/test", U, U, ?NOT_AUTHORISED), + http_get(Config, "/parameters/test/v", U, U, ?NOT_AUTHORISED), + http_get(Config, "/policies/v/HA", U, U, ?NOT_AUTHORISED), + http_get(Config, "/parameters/test/v/good", U, U, ?NOT_AUTHORISED) + end, + AlwaysNeg = + fun (U) -> + http_put(Config, "/policies/%2F/HA", Policy, U, U, ?NOT_AUTHORISED), + http_put(Config, "/parameters/test/%2F/good", Param, U, U, ?NOT_AUTHORISED), + http_get(Config, "/policies/%2F/HA", U, U, ?NOT_AUTHORISED), + http_get(Config, "/parameters/test/%2F/good", U, U, ?NOT_AUTHORISED) + end, + AdminPos = + fun (U) -> + http_put(Config, "/policies/%2F/HA", Policy, U, U, {group, '2xx'}), + http_put(Config, "/parameters/test/%2F/good", Param, U, U, {group, '2xx'}), + http_get(Config, "/policies/%2F/HA", U, U, {group, '2xx'}), + http_get(Config, "/parameters/test/%2F/good", U, U, {group, '2xx'}) + end, + + [Neg(U) || U <- ["mon", "mgmt"]], + [Pos(U) || U <- ["admin", "policy"]], + [AlwaysNeg(U) || U <- ["mon", "mgmt", "policy"]], + [AdminPos(U) || U <- ["admin"]], + + %% This one is deliberately different between admin and policymaker. + http_put(Config, "/parameters/test/v/admin", Param, "admin", "admin", {group, '2xx'}), + http_put(Config, "/parameters/test/v/admin", Param, "policy", "policy", + ?BAD_REQUEST), + + http_delete(Config, "/vhosts/v", {group, '2xx'}), + http_delete(Config, "/users/admin", {group, '2xx'}), + http_delete(Config, "/users/mon", {group, '2xx'}), + http_delete(Config, "/users/policy", {group, '2xx'}), + http_delete(Config, "/users/mgmt", {group, '2xx'}), + http_delete(Config, "/policies/%2F/HA", {group, '2xx'}), + + unregister_parameters_and_policy_validator(Config), + passed. + +issue67_test(Config)-> + {ok, {{_, 401, _}, Headers, _}} = req(Config, get, "/queues", + [auth_header("user_no_access", "password_no_access")]), + ?assertEqual("application/json", + proplists:get_value("content-type",Headers)), + passed. + +extensions_test(Config) -> + [#{javascript := <<"dispatcher.js">>}] = http_get(Config, "/extensions", ?OK), + passed. + +cors_test(Config) -> + %% With CORS disabled. No header should be received. + R = req(Config, get, "/overview", [auth_header("guest", "guest")]), + io:format("CORS test R: ~p~n", [R]), + {ok, {_, HdNoCORS, _}} = R, + io:format("CORS test HdNoCORS: ~p~n", [HdNoCORS]), + false = lists:keymember("access-control-allow-origin", 1, HdNoCORS), + %% The Vary header should include "Origin" regardless of CORS configuration. + {_, "accept, accept-encoding, origin"} = lists:keyfind("vary", 1, HdNoCORS), + %% Enable CORS. + rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, [rabbitmq_management, cors_allow_origins, ["https://rabbitmq.com"]]), + %% We should only receive allow-origin and allow-credentials from GET. + {ok, {_, HdGetCORS, _}} = req(Config, get, "/overview", + [{"origin", "https://rabbitmq.com"}, auth_header("guest", "guest")]), + true = lists:keymember("access-control-allow-origin", 1, HdGetCORS), + true = lists:keymember("access-control-allow-credentials", 1, HdGetCORS), + false = lists:keymember("access-control-expose-headers", 1, HdGetCORS), + false = lists:keymember("access-control-max-age", 1, HdGetCORS), + false = lists:keymember("access-control-allow-methods", 1, HdGetCORS), + false = lists:keymember("access-control-allow-headers", 1, HdGetCORS), + %% We should receive allow-origin, allow-credentials and allow-methods from OPTIONS. + {ok, {{_, 200, _}, HdOptionsCORS, _}} = req(Config, options, "/overview", + [{"origin", "https://rabbitmq.com"}]), + true = lists:keymember("access-control-allow-origin", 1, HdOptionsCORS), + true = lists:keymember("access-control-allow-credentials", 1, HdOptionsCORS), + false = lists:keymember("access-control-expose-headers", 1, HdOptionsCORS), + true = lists:keymember("access-control-max-age", 1, HdOptionsCORS), + true = lists:keymember("access-control-allow-methods", 1, HdOptionsCORS), + false = lists:keymember("access-control-allow-headers", 1, HdOptionsCORS), + %% We should receive allow-headers when request-headers is sent. + {ok, {_, HdAllowHeadersCORS, _}} = req(Config, options, "/overview", + [{"origin", "https://rabbitmq.com"}, + auth_header("guest", "guest"), + {"access-control-request-headers", "x-piggy-bank"}]), + {_, "x-piggy-bank"} = lists:keyfind("access-control-allow-headers", 1, HdAllowHeadersCORS), + %% Disable preflight request caching. + rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, [rabbitmq_management, cors_max_age, undefined]), + %% We shouldn't receive max-age anymore. + {ok, {_, HdNoMaxAgeCORS, _}} = req(Config, options, "/overview", + [{"origin", "https://rabbitmq.com"}, auth_header("guest", "guest")]), + false = lists:keymember("access-control-max-age", 1, HdNoMaxAgeCORS), + + %% Check OPTIONS method in all paths + check_cors_all_endpoints(Config), + %% Disable CORS again. + rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, [rabbitmq_management, cors_allow_origins, []]), + passed. + +check_cors_all_endpoints(Config) -> + Endpoints = get_all_http_endpoints(), + + [begin + ct:pal("Options for ~p~n", [EP]), + {ok, {{_, 200, _}, _, _}} = req(Config, options, EP, [{"origin", "https://rabbitmq.com"}]) + end + || EP <- Endpoints]. + +get_all_http_endpoints() -> + [ Path || {Path, _, _} <- rabbit_mgmt_dispatcher:dispatcher() ]. + +vhost_limits_list_test(Config) -> + [] = http_get(Config, "/vhost-limits", ?OK), + + http_get(Config, "/vhost-limits/limit_test_vhost_1", ?NOT_FOUND), + rabbit_ct_broker_helpers:add_vhost(Config, <<"limit_test_vhost_1">>), + + [] = http_get(Config, "/vhost-limits/limit_test_vhost_1", ?OK), + http_get(Config, "/vhost-limits/limit_test_vhost_2", ?NOT_FOUND), + + rabbit_ct_broker_helpers:add_vhost(Config, <<"limit_test_vhost_2">>), + + [] = http_get(Config, "/vhost-limits/limit_test_vhost_2", ?OK), + + Limits1 = [#{vhost => <<"limit_test_vhost_1">>, + value => #{'max-connections' => 100, 'max-queues' => 100}}], + Limits2 = [#{vhost => <<"limit_test_vhost_2">>, + value => #{'max-connections' => 200}}], + + Expected = Limits1 ++ Limits2, + + lists:map( + fun(#{vhost := VHost, value := Val}) -> + Param = [ {atom_to_binary(K, utf8),V} || {K,V} <- maps:to_list(Val) ], + ok = rabbit_ct_broker_helpers:set_parameter(Config, 0, VHost, <<"vhost-limits">>, <<"limits">>, Param) + end, + Expected), + + Expected = http_get(Config, "/vhost-limits", ?OK), + Limits1 = http_get(Config, "/vhost-limits/limit_test_vhost_1", ?OK), + Limits2 = http_get(Config, "/vhost-limits/limit_test_vhost_2", ?OK), + + NoVhostUser = <<"no_vhost_user">>, + rabbit_ct_broker_helpers:add_user(Config, NoVhostUser), + rabbit_ct_broker_helpers:set_user_tags(Config, 0, NoVhostUser, [management]), + [] = http_get(Config, "/vhost-limits", NoVhostUser, NoVhostUser, ?OK), + http_get(Config, "/vhost-limits/limit_test_vhost_1", NoVhostUser, NoVhostUser, ?NOT_AUTHORISED), + http_get(Config, "/vhost-limits/limit_test_vhost_2", NoVhostUser, NoVhostUser, ?NOT_AUTHORISED), + + Vhost1User = <<"limit_test_vhost_1_user">>, + rabbit_ct_broker_helpers:add_user(Config, Vhost1User), + rabbit_ct_broker_helpers:set_user_tags(Config, 0, Vhost1User, [management]), + rabbit_ct_broker_helpers:set_full_permissions(Config, Vhost1User, <<"limit_test_vhost_1">>), + Limits1 = http_get(Config, "/vhost-limits", Vhost1User, Vhost1User, ?OK), + Limits1 = http_get(Config, "/vhost-limits/limit_test_vhost_1", Vhost1User, Vhost1User, ?OK), + http_get(Config, "/vhost-limits/limit_test_vhost_2", Vhost1User, Vhost1User, ?NOT_AUTHORISED), + + Vhost2User = <<"limit_test_vhost_2_user">>, + rabbit_ct_broker_helpers:add_user(Config, Vhost2User), + rabbit_ct_broker_helpers:set_user_tags(Config, 0, Vhost2User, [management]), + rabbit_ct_broker_helpers:set_full_permissions(Config, Vhost2User, <<"limit_test_vhost_2">>), + Limits2 = http_get(Config, "/vhost-limits", Vhost2User, Vhost2User, ?OK), + http_get(Config, "/vhost-limits/limit_test_vhost_1", Vhost2User, Vhost2User, ?NOT_AUTHORISED), + Limits2 = http_get(Config, "/vhost-limits/limit_test_vhost_2", Vhost2User, Vhost2User, ?OK). + +vhost_limit_set_test(Config) -> + [] = http_get(Config, "/vhost-limits", ?OK), + rabbit_ct_broker_helpers:add_vhost(Config, <<"limit_test_vhost_1">>), + [] = http_get(Config, "/vhost-limits/limit_test_vhost_1", ?OK), + + %% Set a limit + http_put(Config, "/vhost-limits/limit_test_vhost_1/max-queues", [{value, 100}], ?NO_CONTENT), + + + Limits_Queues = [#{vhost => <<"limit_test_vhost_1">>, + value => #{'max-queues' => 100}}], + + Limits_Queues = http_get(Config, "/vhost-limits/limit_test_vhost_1", ?OK), + + %% Set another limit + http_put(Config, "/vhost-limits/limit_test_vhost_1/max-connections", [{value, 200}], ?NO_CONTENT), + + Limits_Queues_Connections = [#{vhost => <<"limit_test_vhost_1">>, + value => #{'max-connections' => 200, 'max-queues' => 100}}], + + Limits_Queues_Connections = http_get(Config, "/vhost-limits/limit_test_vhost_1", ?OK), + + Limits1 = [#{vhost => <<"limit_test_vhost_1">>, + value => #{'max-connections' => 200, 'max-queues' => 100}}], + Limits1 = http_get(Config, "/vhost-limits", ?OK), + + %% Update a limit + http_put(Config, "/vhost-limits/limit_test_vhost_1/max-connections", [{value, 1000}], ?NO_CONTENT), + Limits2 = [#{vhost => <<"limit_test_vhost_1">>, + value => #{'max-connections' => 1000, 'max-queues' => 100}}], + Limits2 = http_get(Config, "/vhost-limits/limit_test_vhost_1", ?OK), + + + Vhost1User = <<"limit_test_vhost_1_user">>, + rabbit_ct_broker_helpers:add_user(Config, Vhost1User), + rabbit_ct_broker_helpers:set_user_tags(Config, 0, Vhost1User, [management]), + rabbit_ct_broker_helpers:set_full_permissions(Config, Vhost1User, <<"limit_test_vhost_1">>), + + Limits3 = [#{vhost => <<"limit_test_vhost_1">>, + value => #{'max-connections' => 1000, + 'max-queues' => 100}}], + Limits3 = http_get(Config, "/vhost-limits/limit_test_vhost_1", Vhost1User, Vhost1User, ?OK), + + %% Only admin can update limits + http_put(Config, "/vhost-limits/limit_test_vhost_1/max-connections", [{value, 300}], ?NO_CONTENT), + + %% Clear a limit + http_delete(Config, "/vhost-limits/limit_test_vhost_1/max-connections", ?NO_CONTENT), + Limits4 = [#{vhost => <<"limit_test_vhost_1">>, + value => #{'max-queues' => 100}}], + Limits4 = http_get(Config, "/vhost-limits/limit_test_vhost_1", ?OK), + + %% Only admin can clear limits + http_delete(Config, "/vhost-limits/limit_test_vhost_1/max-queues", Vhost1User, Vhost1User, ?NOT_AUTHORISED), + + %% Unknown limit error + http_put(Config, "/vhost-limits/limit_test_vhost_1/max-channels", [{value, 200}], ?BAD_REQUEST). + +user_limits_list_test(Config) -> + ?assertEqual([], http_get(Config, "/user-limits", ?OK)), + + Vhost1 = <<"limit_test_vhost_1">>, + rabbit_ct_broker_helpers:add_vhost(Config, <<"limit_test_vhost_1">>), + + http_get(Config, "/user-limits/limit_test_user_1", ?NOT_FOUND), + + User1 = <<"limit_test_user_1">>, + rabbit_ct_broker_helpers:add_user(Config, User1), + rabbit_ct_broker_helpers:set_user_tags(Config, 0, User1, [management]), + rabbit_ct_broker_helpers:set_full_permissions(Config, User1, Vhost1), + + ?assertEqual([], http_get(Config, "/user-limits/limit_test_user_1", ?OK)), + http_get(Config, "/user-limits/limit_test_user_2", ?NOT_FOUND), + + User2 = <<"limit_test_user_2">>, + rabbit_ct_broker_helpers:add_user(Config, User2), + rabbit_ct_broker_helpers:set_user_tags(Config, 0, User2, [management]), + rabbit_ct_broker_helpers:set_full_permissions(Config, User2, Vhost1), + + ?assertEqual([], http_get(Config, "/user-limits/limit_test_user_2", ?OK)), + + Limits1 = [ + #{ + user => User1, + value => #{ + 'max-connections' => 100, + 'max-channels' => 100 + } + }], + Limits2 = [ + #{ + user => User2, + value => #{ + 'max-connections' => 200 + } + }], + + Expected = Limits1 ++ Limits2, + + lists:map( + fun(#{user := Username, value := Val}) -> + rabbit_ct_broker_helpers:set_user_limits(Config, 0, Username, Val) + end, + Expected), + + rabbit_ct_helpers:await_condition( + fun() -> + Expected =:= http_get(Config, "/user-limits", ?OK) + end), + Limits1 = http_get(Config, "/user-limits/limit_test_user_1", ?OK), + Limits2 = http_get(Config, "/user-limits/limit_test_user_2", ?OK), + + %% Clear limits and assert + rabbit_ct_broker_helpers:clear_user_limits(Config, 0, User1, + <<"max-connections">>), + + Limits3 = [#{user => User1, value => #{'max-channels' => 100}}], + ?assertEqual(Limits3, http_get(Config, "/user-limits/limit_test_user_1", ?OK)), + + rabbit_ct_broker_helpers:clear_user_limits(Config, 0, User1, + <<"max-channels">>), + ?assertEqual([], http_get(Config, "/user-limits/limit_test_user_1", ?OK)), + + rabbit_ct_broker_helpers:clear_user_limits(Config, 0, <<"limit_test_user_2">>, + <<"all">>), + ?assertEqual([], http_get(Config, "/user-limits/limit_test_user_2", ?OK)), + + %% Limit user with no vhost + NoVhostUser = <<"no_vhost_user">>, + rabbit_ct_broker_helpers:add_user(Config, NoVhostUser), + rabbit_ct_broker_helpers:set_user_tags(Config, 0, NoVhostUser, [management]), + + Limits4 = #{ + user => NoVhostUser, + value => #{ + 'max-connections' => 150, + 'max-channels' => 150 + } + }, + rabbit_ct_broker_helpers:set_user_limits(Config, 0, NoVhostUser, maps:get(value, Limits4)), + + ?assertEqual([Limits4], http_get(Config, "/user-limits/no_vhost_user", ?OK)). + +user_limit_set_test(Config) -> + ?assertEqual([], http_get(Config, "/user-limits", ?OK)), + + User1 = <<"limit_test_user_1">>, + rabbit_ct_broker_helpers:add_user(Config, User1), + + ?assertEqual([], http_get(Config, "/user-limits/limit_test_user_1", ?OK)), + + %% Set a user limit + http_put(Config, "/user-limits/limit_test_user_1/max-channels", [{value, 100}], ?NO_CONTENT), + + MaxChannelsLimit = [#{user => User1, value => #{'max-channels' => 100}}], + ?assertEqual(MaxChannelsLimit, http_get(Config, "/user-limits/limit_test_user_1", ?OK)), + + %% Set another user limit + http_put(Config, "/user-limits/limit_test_user_1/max-connections", [{value, 200}], ?NO_CONTENT), + + MaxConnectionsAndChannelsLimit = [ + #{ + user => User1, + value => #{ + 'max-connections' => 200, + 'max-channels' => 100 + } + } + ], + ?assertEqual(MaxConnectionsAndChannelsLimit, http_get(Config, "/user-limits/limit_test_user_1", ?OK)), + + Limits1 = [ + #{ + user => User1, + value => #{ + 'max-connections' => 200, + 'max-channels' => 100 + } + }], + ?assertEqual(Limits1, http_get(Config, "/user-limits", ?OK)), + + %% Update a user limit + http_put(Config, "/user-limits/limit_test_user_1/max-connections", [{value, 1000}], ?NO_CONTENT), + Limits2 = [ + #{ + user => User1, + value => #{ + 'max-connections' => 1000, + 'max-channels' => 100 + } + }], + ?assertEqual(Limits2, http_get(Config, "/user-limits/limit_test_user_1", ?OK)), + + Vhost1 = <<"limit_test_vhost_1">>, + rabbit_ct_broker_helpers:add_vhost(Config, Vhost1), + + Vhost1User = <<"limit_test_vhost_1_user">>, + rabbit_ct_broker_helpers:add_user(Config, Vhost1User), + rabbit_ct_broker_helpers:set_user_tags(Config, 0, Vhost1User, [management]), + rabbit_ct_broker_helpers:set_full_permissions(Config, Vhost1User, Vhost1), + + Limits3 = [ + #{ + user => User1, + value => #{ + 'max-connections' => 1000, + 'max-channels' => 100 + } + }], + ?assertEqual(Limits3, http_get(Config, "/user-limits/limit_test_user_1", Vhost1User, Vhost1User, ?OK)), + + %% Clear a limit + http_delete(Config, "/user-limits/limit_test_user_1/max-connections", ?NO_CONTENT), + Limits4 = [#{user => User1, value => #{'max-channels' => 100}}], + ?assertEqual(Limits4, http_get(Config, "/user-limits/limit_test_user_1", ?OK)), + + %% Only admin can clear limits + http_delete(Config, "/user-limits/limit_test_user_1/max-channels", Vhost1User, Vhost1User, ?NOT_AUTHORISED), + + %% Unknown limit error + http_put(Config, "/user-limits/limit_test_user_1/max-unknown", [{value, 200}], ?BAD_REQUEST). + +rates_test(Config) -> + http_put(Config, "/queues/%2F/myqueue", none, {group, '2xx'}), + {Conn, Ch} = open_connection_and_channel(Config), + Pid = spawn_link(fun() -> publish(Ch) end), + + Condition = fun() -> + Overview = http_get(Config, "/overview"), + MsgStats = maps:get(message_stats, Overview), + QueueTotals = maps:get(queue_totals, Overview), + + maps:get(messages_ready, QueueTotals) > 0 andalso + maps:get(messages, QueueTotals) > 0 andalso + maps:get(publish, MsgStats) > 0 andalso + maps:get(rate, maps:get(publish_details, MsgStats)) > 0 andalso + maps:get(rate, maps:get(messages_ready_details, QueueTotals)) > 0 andalso + maps:get(rate, maps:get(messages_details, QueueTotals)) > 0 + end, + rabbit_ct_helpers:await_condition(Condition, 60000), + Pid ! stop_publish, + close_channel(Ch), + close_connection(Conn), + http_delete(Config, "/queues/%2F/myqueue", ?NO_CONTENT), + passed. + +cli_redirect_test(Config) -> + assert_permanent_redirect(Config, "cli", "/cli/index.html"), + passed. + +api_redirect_test(Config) -> + assert_permanent_redirect(Config, "api", "/api/index.html"), + passed. + +stats_redirect_test(Config) -> + assert_permanent_redirect(Config, "doc/stats.html", "/api/index.html"), + passed. + +oauth_test(Config) -> + Map1 = http_get(Config, "/auth", ?OK), + %% Defaults + ?assertEqual(false, maps:get(enable_uaa, Map1)), + ?assertEqual(<<>>, maps:get(uaa_client_id, Map1)), + ?assertEqual(<<>>, maps:get(uaa_location, Map1)), + %% Misconfiguration + rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_management, enable_uaa, true]), + Map2 = http_get(Config, "/auth", ?OK), + ?assertEqual(false, maps:get(enable_uaa, Map2)), + ?assertEqual(<<>>, maps:get(uaa_client_id, Map2)), + ?assertEqual(<<>>, maps:get(uaa_location, Map2)), + %% Valid config + rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_management, uaa_client_id, "rabbit_user"]), + rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_management, uaa_location, "http://localhost:8080/uaa"]), + Map3 = http_get(Config, "/auth", ?OK), + ?assertEqual(true, maps:get(enable_uaa, Map3)), + ?assertEqual(<<"rabbit_user">>, maps:get(uaa_client_id, Map3)), + ?assertEqual(<<"http://localhost:8080/uaa">>, maps:get(uaa_location, Map3)), + %% cleanup + rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_management, enable_uaa]). + +login_test(Config) -> + http_put(Config, "/users/myuser", [{password, <<"myuser">>}, + {tags, <<"management">>}], {group, '2xx'}), + %% Let's do a post without any other form of authorization + {ok, {{_, CodeAct, _}, Headers, _}} = + req(Config, 0, post, "/login", + [{"content-type", "application/x-www-form-urlencoded"}], + <<"username=myuser&password=myuser">>), + ?assertEqual(200, CodeAct), + + %% Extract the authorization header + [Cookie, _Version] = binary:split(list_to_binary(proplists:get_value("set-cookie", Headers)), + <<";">>, [global]), + [_, Auth] = binary:split(Cookie, <<"=">>, []), + + %% Request the overview with the auth obtained + {ok, {{_, CodeAct1, _}, _, _}} = + req(Config, get, "/overview", [{"Authorization", "Basic " ++ binary_to_list(Auth)}]), + ?assertEqual(200, CodeAct1), + + %% Let's request a login with an unknown user + {ok, {{_, CodeAct2, _}, Headers2, _}} = + req(Config, 0, post, "/login", + [{"content-type", "application/x-www-form-urlencoded"}], + <<"username=misteryusernumber1&password=myuser">>), + ?assertEqual(401, CodeAct2), + ?assert(not proplists:is_defined("set-cookie", Headers2)), + + http_delete(Config, "/users/myuser", {group, '2xx'}), + passed. + +csp_headers_test(Config) -> + AuthHeader = auth_header("guest", "guest"), + Headers = [{"origin", "https://rabbitmq.com"}, AuthHeader], + {ok, {_, HdGetCsp0, _}} = req(Config, get, "/whoami", Headers), + ?assert(lists:keymember("content-security-policy", 1, HdGetCsp0)), + {ok, {_, HdGetCsp1, _}} = req(Config, get_static, "/index.html", Headers), + ?assert(lists:keymember("content-security-policy", 1, HdGetCsp1)). + +disable_basic_auth_test(Config) -> + rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_management, disable_basic_auth, true]), + http_get(Config, "/overview", ?NOT_AUTHORISED), + + %% Ensure that a request without auth header does not return a basic auth prompt + OverviewResponseHeaders = http_get_no_auth(Config, "/overview", ?NOT_AUTHORISED), + ?assertEqual(false, lists:keymember("www-authenticate", 1, OverviewResponseHeaders)), + + http_get(Config, "/nodes", ?NOT_AUTHORISED), + http_get(Config, "/vhosts", ?NOT_AUTHORISED), + http_get(Config, "/vhost-limits", ?NOT_AUTHORISED), + http_put(Config, "/queues/%2F/myqueue", none, ?NOT_AUTHORISED), + Policy = [{pattern, <<".*">>}, + {definition, [{<<"ha-mode">>, <<"all">>}]}], + http_put(Config, "/policies/%2F/HA", Policy, ?NOT_AUTHORISED), + http_delete(Config, "/queues/%2F/myqueue", ?NOT_AUTHORISED), + http_get(Config, "/definitions", ?NOT_AUTHORISED), + http_post(Config, "/definitions", [], ?NOT_AUTHORISED), + rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_management, disable_basic_auth, 50]), + %% Defaults to 'false' when config is invalid + http_get(Config, "/overview", ?OK). + +auth_attempts_test(Config) -> + rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_core_metrics, reset_auth_attempt_metrics, []), + {Conn, _Ch} = open_connection_and_channel(Config), + close_connection(Conn), + [NodeData] = http_get(Config, "/nodes"), + Node = binary_to_atom(maps:get(name, NodeData), utf8), + Map = http_get(Config, "/auth/attempts/" ++ atom_to_list(Node), ?OK), + Http = get_auth_attempts(<<"http">>, Map), + Amqp091 = get_auth_attempts(<<"amqp091">>, Map), + ?assertEqual(false, maps:is_key(remote_address, Amqp091)), + ?assertEqual(false, maps:is_key(username, Amqp091)), + ?assertEqual(1, maps:get(auth_attempts, Amqp091)), + ?assertEqual(1, maps:get(auth_attempts_succeeded, Amqp091)), + ?assertEqual(0, maps:get(auth_attempts_failed, Amqp091)), + ?assertEqual(false, maps:is_key(remote_address, Http)), + ?assertEqual(false, maps:is_key(username, Http)), + ?assertEqual(2, maps:get(auth_attempts, Http)), + ?assertEqual(2, maps:get(auth_attempts_succeeded, Http)), + ?assertEqual(0, maps:get(auth_attempts_failed, Http)), + + rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbit, track_auth_attempt_source, true]), + {Conn2, _Ch2} = open_connection_and_channel(Config), + close_connection(Conn2), + Map2 = http_get(Config, "/auth/attempts/" ++ atom_to_list(Node) ++ "/source", ?OK), + Map3 = http_get(Config, "/auth/attempts/" ++ atom_to_list(Node), ?OK), + Http2 = get_auth_attempts(<<"http">>, Map2), + Http3 = get_auth_attempts(<<"http">>, Map3), + Amqp091_2 = get_auth_attempts(<<"amqp091">>, Map2), + Amqp091_3 = get_auth_attempts(<<"amqp091">>, Map3), + ?assertEqual(<<"127.0.0.1">>, maps:get(remote_address, Http2)), + ?assertEqual(<<"guest">>, maps:get(username, Http2)), + ?assertEqual(1, maps:get(auth_attempts, Http2)), + ?assertEqual(1, maps:get(auth_attempts_succeeded, Http2)), + ?assertEqual(0, maps:get(auth_attempts_failed, Http2)), + + ?assertEqual(false, maps:is_key(remote_address, Http3)), + ?assertEqual(false, maps:is_key(username, Http3)), + ?assertEqual(4, maps:get(auth_attempts, Http3)), + ?assertEqual(4, maps:get(auth_attempts_succeeded, Http3)), + ?assertEqual(0, maps:get(auth_attempts_failed, Http3)), + + ?assertEqual(true, <<>> =/= maps:get(remote_address, Amqp091_2)), + ?assertEqual(<<"guest">>, maps:get(username, Amqp091_2)), + ?assertEqual(1, maps:get(auth_attempts, Amqp091_2)), + ?assertEqual(1, maps:get(auth_attempts_succeeded, Amqp091_2)), + ?assertEqual(0, maps:get(auth_attempts_failed, Amqp091_2)), + + ?assertEqual(false, maps:is_key(remote_address, Amqp091_3)), + ?assertEqual(false, maps:is_key(username, Amqp091_3)), + ?assertEqual(2, maps:get(auth_attempts, Amqp091_3)), + ?assertEqual(2, maps:get(auth_attempts_succeeded, Amqp091_3)), + ?assertEqual(0, maps:get(auth_attempts_failed, Amqp091_3)), + + passed. + +%% ------------------------------------------------------------------- +%% Helpers. +%% ------------------------------------------------------------------- + +msg(Key, Headers, Body) -> + msg(Key, Headers, Body, <<"string">>). + +msg(Key, Headers, Body, Enc) -> + #{exchange => <<"">>, + routing_key => Key, + properties => #{delivery_mode => 2, + headers => Headers}, + payload => Body, + payload_encoding => Enc}. + +local_port(Conn) -> + [{sock, Sock}] = amqp_connection:info(Conn, [sock]), + {ok, Port} = inet:port(Sock), + Port. + +spawn_invalid(_Config, 0) -> + ok; +spawn_invalid(Config, N) -> + Self = self(), + spawn(fun() -> + timer:sleep(rand:uniform(250)), + {ok, Sock} = gen_tcp:connect("localhost", amqp_port(Config), [list]), + ok = gen_tcp:send(Sock, "Some Data"), + receive_msg(Self) + end), + spawn_invalid(Config, N-1). + +receive_msg(Self) -> + receive + {tcp, _, [$A, $M, $Q, $P | _]} -> + Self ! done + after + 60000 -> + Self ! no_reply + end. + +wait_for_answers(0) -> + ok; +wait_for_answers(N) -> + receive + done -> + wait_for_answers(N-1); + no_reply -> + throw(no_reply) + end. + +publish(Ch) -> + amqp_channel:call(Ch, #'basic.publish'{exchange = <<"">>, + routing_key = <<"myqueue">>}, + #amqp_msg{payload = <<"message">>}), + receive + stop_publish -> + ok + after 20 -> + publish(Ch) + end. + +wait_until(_Fun, 0) -> + ?assert(wait_failed); +wait_until(Fun, N) -> + case Fun() of + true -> + timer:sleep(1500); + false -> + timer:sleep(?COLLECT_INTERVAL + 100), + wait_until(Fun, N - 1) + end. + +http_post_json(Config, Path, Body, Assertion) -> + http_upload_raw(Config, post, Path, Body, "guest", "guest", + Assertion, [{"content-type", "application/json"}]). + +%% @doc encode fields and file for HTTP post multipart/form-data. +%% @reference Inspired by <a href="http://code.activestate.com/recipes/146306/">Python implementation</a>. +format_multipart_filedata(Boundary, Files) -> + FileParts = lists:map(fun({FieldName, FileName, FileContent}) -> + [lists:concat(["--", Boundary]), + lists:concat(["content-disposition: form-data; name=\"", atom_to_list(FieldName), "\"; filename=\"", FileName, "\""]), + lists:concat(["content-type: ", "application/octet-stream"]), + "", + FileContent] + end, Files), + FileParts2 = lists:append(FileParts), + EndingParts = [lists:concat(["--", Boundary, "--"]), ""], + Parts = lists:append([FileParts2, EndingParts]), + string:join(Parts, "\r\n"). + +get_auth_attempts(Protocol, Map) -> + [A] = lists:filter(fun(#{protocol := P}) -> + P == Protocol + end, Map), + A. diff --git a/deps/rabbitmq_management/test/rabbit_mgmt_http_health_checks_SUITE.erl b/deps/rabbitmq_management/test/rabbit_mgmt_http_health_checks_SUITE.erl new file mode 100644 index 0000000000..8ef17ed29c --- /dev/null +++ b/deps/rabbitmq_management/test/rabbit_mgmt_http_health_checks_SUITE.erl @@ -0,0 +1,399 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2016-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_http_health_checks_SUITE). + +-include("rabbit_mgmt.hrl"). +-include_lib("amqp_client/include/amqp_client.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("rabbitmq_ct_helpers/include/rabbit_mgmt_test.hrl"). + +-import(rabbit_mgmt_test_util, [http_get/3, + req/4, + auth_header/2]). + +-define(COLLECT_INTERVAL, 1000). +-define(PATH_PREFIX, "/custom-prefix"). + +-compile(export_all). + +all() -> + [ + {group, all_tests}, + {group, single_node} + ]. + +groups() -> + [ + {all_tests, [], all_tests()}, + {single_node, [], [ + alarms_test, + local_alarms_test, + is_quorum_critical_single_node_test, + is_mirror_sync_critical_single_node_test]} + ]. + +all_tests() -> [ + health_checks_test, + is_quorum_critical_test, + is_mirror_sync_critical_test, + virtual_hosts_test, + protocol_listener_test, + port_listener_test, + certificate_expiration_test + ]. + +%% ------------------------------------------------------------------- +%% Testsuite setup/teardown. +%% ------------------------------------------------------------------- + +init_per_group(Group, Config0) -> + PathConfig = {rabbitmq_management, [{path_prefix, ?PATH_PREFIX}]}, + Config1 = rabbit_ct_helpers:merge_app_env(Config0, PathConfig), + rabbit_ct_helpers:log_environment(), + inets:start(), + ClusterSize = case Group of + all_tests -> 3; + single_node -> 1 + end, + NodeConf = [{rmq_nodename_suffix, Group}, + {rmq_nodes_count, ClusterSize}, + {tcp_ports_base}], + Config2 = rabbit_ct_helpers:set_config(Config1, NodeConf), + Ret = rabbit_ct_helpers:run_setup_steps( + Config2, + rabbit_ct_broker_helpers:setup_steps() ++ + rabbit_ct_client_helpers:setup_steps()), + case Ret of + {skip, _} -> + Ret; + Config3 -> + EnableFF = rabbit_ct_broker_helpers:enable_feature_flag( + Config3, quorum_queue), + case EnableFF of + ok -> + Config3; + Skip -> + end_per_group(Group, Config3), + Skip + end + end. + +end_per_group(_, Config) -> + inets:stop(), + Teardown0 = rabbit_ct_client_helpers:teardown_steps(), + Teardown1 = rabbit_ct_broker_helpers:teardown_steps(), + Steps = Teardown0 ++ Teardown1, + rabbit_ct_helpers:run_teardown_steps(Config, Steps). + +init_per_testcase(Testcase, Config) -> + rabbit_ct_helpers:testcase_started(Config, Testcase). + +end_per_testcase(is_quorum_critical_test = Testcase, Config) -> + [_, Server2, Server3] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename), + _ = rabbit_ct_broker_helpers:start_node(Config, Server2), + _ = rabbit_ct_broker_helpers:start_node(Config, Server3), + rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, delete_queues, []), + rabbit_ct_helpers:testcase_finished(Config, Testcase); +end_per_testcase(is_mirror_sync_critical_test = Testcase, Config) -> + [_, Server2, Server3] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename), + _ = rabbit_ct_broker_helpers:start_node(Config, Server2), + _ = rabbit_ct_broker_helpers:start_node(Config, Server3), + ok = rabbit_ct_broker_helpers:clear_policy(Config, 0, <<"ha">>), + rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, delete_queues, []), + rabbit_ct_helpers:testcase_finished(Config, Testcase); +end_per_testcase(Testcase, Config) -> + rabbit_ct_helpers:testcase_finished(Config, Testcase). + +%% ------------------------------------------------------------------- +%% Testcases. +%% ------------------------------------------------------------------- + +health_checks_test(Config) -> + Port = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_mgmt), + http_get(Config, "/health/checks/certificate-expiration/1/days", ?OK), + http_get(Config, io_lib:format("/health/checks/port-listener/~p", [Port]), ?OK), + http_get(Config, "/health/checks/protocol-listener/http", ?OK), + http_get(Config, "/health/checks/virtual-hosts", ?OK), + http_get(Config, "/health/checks/node-is-mirror-sync-critical", ?OK), + http_get(Config, "/health/checks/node-is-quorum-critical", ?OK), + passed. + +alarms_test(Config) -> + Server = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename), + rabbit_ct_broker_helpers:clear_all_alarms(Config, Server), + + EndpointPath = "/health/checks/alarms", + Check0 = http_get(Config, EndpointPath, ?OK), + ?assertEqual(<<"ok">>, maps:get(status, Check0)), + + ok = rabbit_ct_broker_helpers:set_alarm(Config, Server, memory), + rabbit_ct_helpers:await_condition( + fun() -> rabbit_ct_broker_helpers:get_alarms(Config, Server) =/= [] end + ), + + Body = http_get_failed(Config, EndpointPath), + ?assertEqual(<<"failed">>, maps:get(<<"status">>, Body)), + ?assert(is_list(maps:get(<<"alarms">>, Body))), + + rabbit_ct_broker_helpers:clear_all_alarms(Config, Server), + rabbit_ct_helpers:await_condition( + fun() -> rabbit_ct_broker_helpers:get_alarms(Config, Server) =:= [] end + ), + ct:pal("Alarms: ~p", [rabbit_ct_broker_helpers:get_alarms(Config, Server)]), + + passed. + +local_alarms_test(Config) -> + Server = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename), + rabbit_ct_broker_helpers:clear_all_alarms(Config, Server), + + EndpointPath = "/health/checks/local-alarms", + Check0 = http_get(Config, EndpointPath, ?OK), + ?assertEqual(<<"ok">>, maps:get(status, Check0)), + + ok = rabbit_ct_broker_helpers:set_alarm(Config, Server, file_descriptor_limit), + rabbit_ct_helpers:await_condition( + fun() -> rabbit_ct_broker_helpers:get_alarms(Config, Server) =/= [] end + ), + + Body = http_get_failed(Config, EndpointPath), + ?assertEqual(<<"failed">>, maps:get(<<"status">>, Body)), + ?assert(is_list(maps:get(<<"alarms">>, Body))), + + rabbit_ct_broker_helpers:clear_all_alarms(Config, Server), + rabbit_ct_helpers:await_condition( + fun() -> rabbit_ct_broker_helpers:get_local_alarms(Config, Server) =:= [] end + ), + + passed. + + +is_quorum_critical_single_node_test(Config) -> + Check0 = http_get(Config, "/health/checks/node-is-quorum-critical", ?OK), + ?assertEqual(<<"single node cluster">>, maps:get(reason, Check0)), + ?assertEqual(<<"ok">>, maps:get(status, Check0)), + + Server = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename), + Ch = rabbit_ct_client_helpers:open_channel(Config, Server), + Args = [{<<"x-queue-type">>, longstr, <<"quorum">>}], + QName = <<"is_quorum_critical_single_node_test">>, + ?assertEqual({'queue.declare_ok', QName, 0, 0}, + amqp_channel:call(Ch, #'queue.declare'{queue = QName, + durable = true, + auto_delete = false, + arguments = Args})), + Check1 = http_get(Config, "/health/checks/node-is-quorum-critical", ?OK), + ?assertEqual(<<"single node cluster">>, maps:get(reason, Check1)), + + passed. + +is_quorum_critical_test(Config) -> + Check0 = http_get(Config, "/health/checks/node-is-quorum-critical", ?OK), + ?assertEqual(false, maps:is_key(reason, Check0)), + ?assertEqual(<<"ok">>, maps:get(status, Check0)), + + [Server1, Server2, Server3] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename), + Ch = rabbit_ct_client_helpers:open_channel(Config, Server1), + Args = [{<<"x-queue-type">>, longstr, <<"quorum">>}], + QName = <<"is_quorum_critical_test">>, + ?assertEqual({'queue.declare_ok', QName, 0, 0}, + amqp_channel:call(Ch, #'queue.declare'{queue = QName, + durable = true, + auto_delete = false, + arguments = Args})), + Check1 = http_get(Config, "/health/checks/node-is-quorum-critical", ?OK), + ?assertEqual(false, maps:is_key(reason, Check1)), + + ok = rabbit_ct_broker_helpers:stop_node(Config, Server2), + ok = rabbit_ct_broker_helpers:stop_node(Config, Server3), + + Body = http_get_failed(Config, "/health/checks/node-is-quorum-critical"), + ?assertEqual(<<"failed">>, maps:get(<<"status">>, Body)), + ?assertEqual(true, maps:is_key(<<"reason">>, Body)), + [Queue] = maps:get(<<"queues">>, Body), + ?assertEqual(QName, maps:get(<<"name">>, Queue)), + + passed. + +is_mirror_sync_critical_single_node_test(Config) -> + Check0 = http_get(Config, "/health/checks/node-is-mirror-sync-critical", ?OK), + ?assertEqual(<<"single node cluster">>, maps:get(reason, Check0)), + ?assertEqual(<<"ok">>, maps:get(status, Check0)), + + ok = rabbit_ct_broker_helpers:set_policy( + Config, 0, <<"ha">>, <<"is_mirror_sync.*">>, <<"queues">>, + [{<<"ha-mode">>, <<"all">>}]), + Server = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename), + Ch = rabbit_ct_client_helpers:open_channel(Config, Server), + QName = <<"is_mirror_sync_critical_single_node_test">>, + ?assertEqual({'queue.declare_ok', QName, 0, 0}, + amqp_channel:call(Ch, #'queue.declare'{queue = QName, + durable = true, + auto_delete = false, + arguments = []})), + Check1 = http_get(Config, "/health/checks/node-is-mirror-sync-critical", ?OK), + ?assertEqual(<<"single node cluster">>, maps:get(reason, Check1)), + + passed. + +is_mirror_sync_critical_test(Config) -> + Path = "/health/checks/node-is-mirror-sync-critical", + Check0 = http_get(Config, Path, ?OK), + ?assertEqual(false, maps:is_key(reason, Check0)), + ?assertEqual(<<"ok">>, maps:get(status, Check0)), + + ok = rabbit_ct_broker_helpers:set_policy( + Config, 0, <<"ha">>, <<"is_mirror_sync.*">>, <<"queues">>, + [{<<"ha-mode">>, <<"all">>}]), + [Server1, Server2, Server3] = rabbit_ct_broker_helpers:get_node_configs(Config, nodename), + Ch = rabbit_ct_client_helpers:open_channel(Config, Server1), + QName = <<"is_mirror_sync_critical_test">>, + ?assertEqual({'queue.declare_ok', QName, 0, 0}, + amqp_channel:call(Ch, #'queue.declare'{queue = QName, + durable = true, + auto_delete = false, + arguments = []})), + rabbit_ct_helpers:await_condition( + fun() -> + {ok, {{_, Code, _}, _, _}} = req(Config, get, Path, [auth_header("guest", "guest")]), + Code == ?OK + end), + Check1 = http_get(Config, Path, ?OK), + ?assertEqual(false, maps:is_key(reason, Check1)), + + ok = rabbit_ct_broker_helpers:stop_node(Config, Server2), + ok = rabbit_ct_broker_helpers:stop_node(Config, Server3), + + Body = http_get_failed(Config, Path), + ?assertEqual(<<"failed">>, maps:get(<<"status">>, Body)), + ?assertEqual(true, maps:is_key(<<"reason">>, Body)), + [Queue] = maps:get(<<"queues">>, Body), + ?assertEqual(QName, maps:get(<<"name">>, Queue)), + + passed. + +virtual_hosts_test(Config) -> + VHost1 = <<"vhost1">>, + VHost2 = <<"vhost2">>, + add_vhost(Config, VHost1), + add_vhost(Config, VHost2), + + Path = "/health/checks/virtual-hosts", + Check0 = http_get(Config, Path, ?OK), + ?assertEqual(<<"ok">>, maps:get(status, Check0)), + + rabbit_ct_broker_helpers:force_vhost_failure(Config, VHost1), + + Body1 = http_get_failed(Config, Path), + ?assertEqual(<<"failed">>, maps:get(<<"status">>, Body1)), + ?assertEqual(true, maps:is_key(<<"reason">>, Body1)), + ?assertEqual([VHost1], maps:get(<<"virtual-hosts">>, Body1)), + + rabbit_ct_broker_helpers:force_vhost_failure(Config, VHost2), + + Body2 = http_get_failed(Config, Path), + ?assertEqual(<<"failed">>, maps:get(<<"status">>, Body2)), + ?assertEqual(true, maps:is_key(<<"reason">>, Body2)), + VHosts = lists:sort([VHost1, VHost2]), + ?assertEqual(VHosts, lists:sort(maps:get(<<"virtual-hosts">>, Body2))), + + rabbit_ct_broker_helpers:delete_vhost(Config, VHost1), + rabbit_ct_broker_helpers:delete_vhost(Config, VHost2), + http_get(Config, Path, ?OK), + + passed. + +protocol_listener_test(Config) -> + Check0 = http_get(Config, "/health/checks/protocol-listener/http", ?OK), + ?assertEqual(<<"ok">>, maps:get(status, Check0)), + + http_get(Config, "/health/checks/protocol-listener/amqp", ?OK), + http_get(Config, "/health/checks/protocol-listener/amqp0.9.1", ?OK), + http_get(Config, "/health/checks/protocol-listener/amqp0-9-1", ?OK), + + Body0 = http_get_failed(Config, "/health/checks/protocol-listener/mqtt"), + ?assertEqual(<<"failed">>, maps:get(<<"status">>, Body0)), + ?assertEqual(true, maps:is_key(<<"reason">>, Body0)), + ?assertEqual(<<"mqtt">>, maps:get(<<"missing">>, Body0)), + ?assert(lists:member(<<"http">>, maps:get(<<"protocols">>, Body0))), + ?assert(lists:member(<<"clustering">>, maps:get(<<"protocols">>, Body0))), + ?assert(lists:member(<<"amqp">>, maps:get(<<"protocols">>, Body0))), + + http_get_failed(Config, "/health/checks/protocol-listener/doe"), + http_get_failed(Config, "/health/checks/protocol-listener/mqtts"), + http_get_failed(Config, "/health/checks/protocol-listener/stomp"), + http_get_failed(Config, "/health/checks/protocol-listener/stomp1.0"), + + passed. + +port_listener_test(Config) -> + AMQP = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp), + MGMT = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_mgmt), + MQTT = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_mqtt), + + Path = fun(Port) -> + lists:flatten(io_lib:format("/health/checks/port-listener/~p", [Port])) + end, + + Check0 = http_get(Config, Path(AMQP), ?OK), + ?assertEqual(<<"ok">>, maps:get(status, Check0)), + + Check1 = http_get(Config, Path(MGMT), ?OK), + ?assertEqual(<<"ok">>, maps:get(status, Check1)), + + http_get(Config, "/health/checks/port-listener/bananas", ?BAD_REQUEST), + + Body0 = http_get_failed(Config, Path(MQTT)), + ?assertEqual(<<"failed">>, maps:get(<<"status">>, Body0)), + ?assertEqual(true, maps:is_key(<<"reason">>, Body0)), + ?assertEqual(MQTT, maps:get(<<"missing">>, Body0)), + ?assert(lists:member(AMQP, maps:get(<<"ports">>, Body0))), + ?assert(lists:member(MGMT, maps:get(<<"ports">>, Body0))), + + passed. + +certificate_expiration_test(Config) -> + Check0 = http_get(Config, "/health/checks/certificate-expiration/1/weeks", ?OK), + ?assertEqual(<<"ok">>, maps:get(status, Check0)), + + http_get(Config, "/health/checks/certificate-expiration/1/days", ?OK), + http_get(Config, "/health/checks/certificate-expiration/1/months", ?OK), + + http_get(Config, "/health/checks/certificate-expiration/two/weeks", ?BAD_REQUEST), + http_get(Config, "/health/checks/certificate-expiration/2/week", ?BAD_REQUEST), + http_get(Config, "/health/checks/certificate-expiration/2/doe", ?BAD_REQUEST), + + Body0 = http_get_failed(Config, "/health/checks/certificate-expiration/10/years"), + ?assertEqual(<<"failed">>, maps:get(<<"status">>, Body0)), + ?assertEqual(true, maps:is_key(<<"reason">>, Body0)), + [Expired] = maps:get(<<"expired">>, Body0), + ?assertEqual(<<"amqp/ssl">>, maps:get(<<"protocol">>, Expired)), + AMQP_TLS = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_amqp_tls), + ?assertEqual(AMQP_TLS, maps:get(<<"port">>, Expired)), + Node = atom_to_binary(rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename), utf8), + ?assertEqual(Node, maps:get(<<"node">>, Expired)), + ?assertEqual(true, maps:is_key(<<"cacertfile">>, Expired)), + ?assertEqual(true, maps:is_key(<<"certfile">>, Expired)), + ?assertEqual(true, maps:is_key(<<"certfile_expires_on">>, Expired)), + ?assertEqual(true, maps:is_key(<<"interface">>, Expired)), + + passed. + +http_get_failed(Config, Path) -> + {ok, {{_, Code, _}, _, ResBody}} = req(Config, get, Path, [auth_header("guest", "guest")]), + ?assertEqual(Code, ?HEALTH_CHECK_FAILURE_STATUS), + rabbit_json:decode(rabbit_data_coercion:to_binary(ResBody)). + +delete_queues() -> + [rabbit_amqqueue:delete(Q, false, false, <<"dummy">>) + || Q <- rabbit_amqqueue:list()]. + +add_vhost(Config, VHost) -> + rabbit_ct_broker_helpers:add_vhost(Config, VHost), + rabbit_ct_broker_helpers:set_full_permissions(Config, <<"guest">>, VHost). diff --git a/deps/rabbitmq_management/test/rabbit_mgmt_only_http_SUITE.erl b/deps/rabbitmq_management/test/rabbit_mgmt_only_http_SUITE.erl new file mode 100644 index 0000000000..38bb2bac1a --- /dev/null +++ b/deps/rabbitmq_management/test/rabbit_mgmt_only_http_SUITE.erl @@ -0,0 +1,1716 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2016-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_only_http_SUITE). + +-include_lib("amqp_client/include/amqp_client.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("rabbitmq_ct_helpers/include/rabbit_mgmt_test.hrl"). + +-import(rabbit_ct_client_helpers, [close_connection/1, close_channel/1, + open_unmanaged_connection/1]). +-import(rabbit_mgmt_test_util, [assert_list/2, assert_item/2, test_item/2, + assert_keys/2, assert_no_keys/2, + http_get/2, http_get/3, http_get/5, + http_get_no_map/2, + http_put/4, http_put/6, + http_post/4, http_post/6, + http_upload_raw/8, + http_delete/3, http_delete/5, + http_put_raw/4, http_post_accept_json/4, + req/4, auth_header/2, + assert_permanent_redirect/3, + uri_base_from/2, format_for_upload/1, + amqp_port/1]). + +-import(rabbit_misc, [pget/2]). + +-define(COLLECT_INTERVAL, 1000). +-define(PATH_PREFIX, "/custom-prefix"). + +-compile(export_all). + +all() -> + [ + {group, all_tests_with_prefix}, + {group, all_tests_without_prefix}, + {group, stats_disabled_on_request} + ]. + +groups() -> + [ + {all_tests_with_prefix, [], all_tests()}, + {all_tests_without_prefix, [], all_tests()}, + {stats_disabled_on_request, [], [disable_with_disable_stats_parameter_test]}, + {invalid_config, [], [invalid_config_test]} + ]. + +all_tests() -> [ + overview_test, + nodes_test, + vhosts_test, + connections_test, + exchanges_test, + queues_test, + mirrored_queues_test, + quorum_queues_test, + queues_well_formed_json_test, + permissions_vhost_test, + permissions_connection_channel_consumer_test, + consumers_cq_test, + consumers_qq_test, + arguments_test, + queue_actions_test, + exclusive_queue_test, + connections_channels_pagination_test, + exchanges_pagination_test, + exchanges_pagination_permissions_test, + queue_pagination_test, + queue_pagination_columns_test, + queues_pagination_permissions_test, + samples_range_test, + sorting_test, + columns_test, + if_empty_unused_test, + queues_enable_totals_test, + double_encoded_json_test + ]. + +%% ------------------------------------------------------------------- +%% Testsuite setup/teardown. +%% ------------------------------------------------------------------- +merge_app_env(Config, DisableStats) -> + Config1 = rabbit_ct_helpers:merge_app_env(Config, + {rabbit, [ + {collect_statistics_interval, ?COLLECT_INTERVAL} + ]}), + rabbit_ct_helpers:merge_app_env(Config1, + {rabbitmq_management, [ + {disable_management_stats, DisableStats}, + {sample_retention_policies, + [{global, [{605, 1}]}, + {basic, [{605, 1}]}, + {detailed, [{10, 1}]}] + }]}). + +start_broker(Config) -> + Setup0 = rabbit_ct_broker_helpers:setup_steps(), + Setup1 = rabbit_ct_client_helpers:setup_steps(), + Steps = Setup0 ++ Setup1, + rabbit_ct_helpers:run_setup_steps(Config, Steps). + +finish_init(Group, Config) when Group == all_tests_with_prefix -> + finish_init(Group, Config, true); +finish_init(Group, Config) when Group == all_tests_without_prefix -> + finish_init(Group, Config, true); +finish_init(Group, Config) when Group == stats_disabled_on_request -> + finish_init(Group, Config, false); +finish_init(Group, Config) when Group == invalid_config_test -> + finish_init(Group, Config, true). + +finish_init(Group, Config, DisableStats) -> + rabbit_ct_helpers:log_environment(), + inets:start(), + NodeConf = [{rmq_nodename_suffix, Group}], + Config1 = rabbit_ct_helpers:set_config(Config, NodeConf), + merge_app_env(Config1, DisableStats). + +init_per_group(all_tests_with_prefix=Group, Config0) -> + PathConfig = {rabbitmq_management, [{path_prefix, ?PATH_PREFIX}]}, + Config1 = rabbit_ct_helpers:merge_app_env(Config0, PathConfig), + Config2 = finish_init(Group, Config1), + Config3 = start_broker(Config2), + Nodes = rabbit_ct_broker_helpers:get_node_configs( + Config3, nodename), + Ret = rabbit_ct_broker_helpers:rpc( + Config3, 0, + rabbit_feature_flags, + is_supported_remotely, + [Nodes, [quorum_queue], 60000]), + case Ret of + true -> + ok = rabbit_ct_broker_helpers:rpc( + Config3, 0, rabbit_feature_flags, enable, [quorum_queue]), + Config3; + false -> + end_per_group(Group, Config3), + {skip, "Quorum queues are unsupported"} + end; +init_per_group(Group, Config0) -> + Config1 = finish_init(Group, Config0), + Config2 = start_broker(Config1), + Nodes = rabbit_ct_broker_helpers:get_node_configs( + Config2, nodename), + Ret = rabbit_ct_broker_helpers:rpc( + Config2, 0, + rabbit_feature_flags, + is_supported_remotely, + [Nodes, [quorum_queue], 60000]), + case Ret of + true -> + ok = rabbit_ct_broker_helpers:rpc( + Config2, 0, rabbit_feature_flags, enable, [quorum_queue]), + Config2; + false -> + end_per_group(Group, Config2), + {skip, "Quorum queues are unsupported"} + end. + +end_per_group(_, Config) -> + inets:stop(), + Teardown0 = rabbit_ct_client_helpers:teardown_steps(), + Teardown1 = rabbit_ct_broker_helpers:teardown_steps(), + Steps = Teardown0 ++ Teardown1, + rabbit_ct_helpers:run_teardown_steps(Config, Steps). + +init_per_testcase(Testcase = permissions_vhost_test, Config) -> + rabbit_ct_broker_helpers:delete_vhost(Config, <<"myvhost">>), + rabbit_ct_broker_helpers:delete_vhost(Config, <<"myvhost1">>), + rabbit_ct_broker_helpers:delete_vhost(Config, <<"myvhost2">>), + rabbit_ct_helpers:testcase_started(Config, Testcase); + +init_per_testcase(Testcase, Config) -> + rabbit_ct_broker_helpers:close_all_connections(Config, 0, <<"rabbit_mgmt_only_http_SUITE:init_per_testcase">>), + rabbit_ct_helpers:testcase_started(Config, Testcase). + +end_per_testcase(Testcase, Config) -> + rabbit_ct_broker_helpers:close_all_connections(Config, 0, <<"rabbit_mgmt_only_http_SUITE:end_per_testcase">>), + Config1 = end_per_testcase0(Testcase, Config), + rabbit_ct_helpers:testcase_finished(Config1, Testcase). + +end_per_testcase0(Testcase = queues_enable_totals_test, Config) -> + rabbit_ct_broker_helpers:rpc(Config, 0, application, unset_env, + [rabbitmq_management, enable_queue_totals]), + rabbit_ct_helpers:testcase_finished(Config, Testcase); +end_per_testcase0(Testcase = queues_test, Config) -> + rabbit_ct_broker_helpers:delete_vhost(Config, <<"downvhost">>), + rabbit_ct_helpers:testcase_finished(Config, Testcase); +end_per_testcase0(Testcase = permissions_vhost_test, Config) -> + rabbit_ct_broker_helpers:delete_vhost(Config, <<"myvhost1">>), + rabbit_ct_broker_helpers:delete_vhost(Config, <<"myvhost2">>), + rabbit_ct_broker_helpers:delete_user(Config, <<"myuser1">>), + rabbit_ct_broker_helpers:delete_user(Config, <<"myuser2">>), + rabbit_ct_helpers:testcase_finished(Config, Testcase); +end_per_testcase0(_, Config) -> + Config. + +%% ------------------------------------------------------------------- +%% Testcases. +%% ------------------------------------------------------------------- + +overview_test(Config) -> + Overview = http_get(Config, "/overview"), + ?assert(maps:is_key(node, Overview)), + ?assert(maps:is_key(object_totals, Overview)), + ?assert(not maps:is_key(queue_totals, Overview)), + ?assert(not maps:is_key(churn_rates, Overview)), + ?assert(not maps:is_key(message_stats, Overview)), + http_put(Config, "/users/myuser", [{password, <<"myuser">>}, + {tags, <<"management">>}], {group, '2xx'}), + OverviewU = http_get(Config, "/overview", "myuser", "myuser", ?OK), + ?assert(maps:is_key(node, OverviewU)), + ?assert(maps:is_key(object_totals, OverviewU)), + ?assert(not maps:is_key(queue_totals, OverviewU)), + ?assert(not maps:is_key(churn_rates, OverviewU)), + ?assert(not maps:is_key(message_stats, OverviewU)), + http_delete(Config, "/users/myuser", {group, '2xx'}), + passed. + +nodes_test(Config) -> + http_put(Config, "/users/user", [{password, <<"user">>}, + {tags, <<"management">>}], {group, '2xx'}), + http_put(Config, "/users/monitor", [{password, <<"monitor">>}, + {tags, <<"monitoring">>}], {group, '2xx'}), + DiscNode = #{type => <<"disc">>, running => true}, + assert_list([DiscNode], http_get(Config, "/nodes")), + assert_list([DiscNode], http_get(Config, "/nodes", "monitor", "monitor", ?OK)), + http_get(Config, "/nodes", "user", "user", ?NOT_AUTHORISED), + [Node] = http_get(Config, "/nodes"), + Path = "/nodes/" ++ binary_to_list(maps:get(name, Node)), + NodeReply = http_get(Config, Path, ?OK), + assert_item(DiscNode, NodeReply), + ?assert(not maps:is_key(channel_closed, NodeReply)), + ?assert(not maps:is_key(disk_free, NodeReply)), + ?assert(not maps:is_key(fd_used, NodeReply)), + ?assert(not maps:is_key(io_file_handle_open_attempt_avg_time, NodeReply)), + ?assert(maps:is_key(name, NodeReply)), + assert_item(DiscNode, http_get(Config, Path, "monitor", "monitor", ?OK)), + http_get(Config, Path, "user", "user", ?NOT_AUTHORISED), + http_delete(Config, "/users/user", {group, '2xx'}), + http_delete(Config, "/users/monitor", {group, '2xx'}), + passed. + +%% This test is rather over-verbose as we're trying to test understanding of +%% Webmachine +vhosts_test(Config) -> + assert_list([#{name => <<"/">>}], http_get(Config, "/vhosts")), + %% Create a new one + http_put(Config, "/vhosts/myvhost", none, {group, '2xx'}), + %% PUT should be idempotent + http_put(Config, "/vhosts/myvhost", none, {group, '2xx'}), + %% Check it's there + [GetFirst | _] = GetAll = http_get(Config, "/vhosts"), + assert_list([#{name => <<"/">>}, #{name => <<"myvhost">>}], GetAll), + ?assert(not maps:is_key(message_stats, GetFirst)), + ?assert(not maps:is_key(messages_ready_details, GetFirst)), + ?assert(not maps:is_key(recv_oct, GetFirst)), + ?assert(maps:is_key(cluster_state, GetFirst)), + + %% Check individually + Get = http_get(Config, "/vhosts/%2F", ?OK), + assert_item(#{name => <<"/">>}, Get), + assert_item(#{name => <<"myvhost">>},http_get(Config, "/vhosts/myvhost")), + ?assert(not maps:is_key(message_stats, Get)), + ?assert(not maps:is_key(messages_ready_details, Get)), + ?assert(not maps:is_key(recv_oct, Get)), + ?assert(maps:is_key(cluster_state, Get)), + + %% Crash it + rabbit_ct_broker_helpers:force_vhost_failure(Config, <<"myvhost">>), + [NodeData] = http_get(Config, "/nodes"), + Node = binary_to_atom(maps:get(name, NodeData), utf8), + assert_item(#{name => <<"myvhost">>, cluster_state => #{Node => <<"stopped">>}}, + http_get(Config, "/vhosts/myvhost")), + + %% Restart it + http_post(Config, "/vhosts/myvhost/start/" ++ atom_to_list(Node), [], {group, '2xx'}), + assert_item(#{name => <<"myvhost">>, cluster_state => #{Node => <<"running">>}}, + http_get(Config, "/vhosts/myvhost")), + + %% Delete it + http_delete(Config, "/vhosts/myvhost", {group, '2xx'}), + %% It's not there + http_get(Config, "/vhosts/myvhost", ?NOT_FOUND), + http_delete(Config, "/vhosts/myvhost", ?NOT_FOUND), + + passed. + +connections_test(Config) -> + {Conn, _Ch} = open_connection_and_channel(Config), + LocalPort = local_port(Conn), + Path = binary_to_list( + rabbit_mgmt_format:print( + "/connections/127.0.0.1%3A~w%20-%3E%20127.0.0.1%3A~w", + [LocalPort, amqp_port(Config)])), + timer:sleep(1500), + Connection = http_get(Config, Path, ?OK), + ?assertEqual(1, maps:size(Connection)), + ?assert(maps:is_key(name, Connection)), + ?assert(not maps:is_key(recv_oct_details, Connection)), + http_delete(Config, Path, {group, '2xx'}), + %% TODO rabbit_reader:shutdown/2 returns before the connection is + %% closed. It may not be worth fixing. + Fun = fun() -> + try + http_get(Config, Path, ?NOT_FOUND), + true + catch + _:_ -> + false + end + end, + wait_until(Fun, 60), + close_connection(Conn), + passed. + +exchanges_test(Config) -> + %% Can list exchanges + http_get(Config, "/exchanges", {group, '2xx'}), + %% Can pass booleans or strings + Good = [{type, <<"direct">>}, {durable, <<"true">>}], + http_put(Config, "/vhosts/myvhost", none, {group, '2xx'}), + http_get(Config, "/exchanges/myvhost/foo", ?NOT_FOUND), + http_put(Config, "/exchanges/myvhost/foo", Good, {group, '2xx'}), + http_put(Config, "/exchanges/myvhost/foo", Good, {group, '2xx'}), + http_get(Config, "/exchanges/%2F/foo", ?NOT_FOUND), + Exchange = http_get(Config, "/exchanges/myvhost/foo"), + assert_item(#{name => <<"foo">>, + vhost => <<"myvhost">>, + type => <<"direct">>, + durable => true, + auto_delete => false, + internal => false, + arguments => #{}}, + Exchange), + ?assert(not maps:is_key(message_stats, Exchange)), + http_put(Config, "/exchanges/badvhost/bar", Good, ?NOT_FOUND), + http_put(Config, "/exchanges/myvhost/bar", [{type, <<"bad_exchange_type">>}], + ?BAD_REQUEST), + http_put(Config, "/exchanges/myvhost/bar", [{type, <<"direct">>}, + {durable, <<"troo">>}], + ?BAD_REQUEST), + http_put(Config, "/exchanges/myvhost/foo", [{type, <<"direct">>}], + ?BAD_REQUEST), + + http_delete(Config, "/exchanges/myvhost/foo", {group, '2xx'}), + http_delete(Config, "/exchanges/myvhost/foo", ?NOT_FOUND), + + http_delete(Config, "/vhosts/myvhost", {group, '2xx'}), + http_get(Config, "/exchanges/badvhost", ?NOT_FOUND), + passed. + +queues_test(Config) -> + Good = [{durable, true}], + GoodQQ = [{durable, true}, {arguments, [{'x-queue-type', 'quorum'}]}], + http_get(Config, "/queues/%2F/foo", ?NOT_FOUND), + http_put(Config, "/queues/%2F/foo", GoodQQ, {group, '2xx'}), + http_put(Config, "/queues/%2F/foo", GoodQQ, {group, '2xx'}), + + rabbit_ct_broker_helpers:add_vhost(Config, <<"downvhost">>), + rabbit_ct_broker_helpers:set_full_permissions(Config, <<"downvhost">>), + http_put(Config, "/queues/downvhost/foo", Good, {group, '2xx'}), + http_put(Config, "/queues/downvhost/bar", Good, {group, '2xx'}), + + rabbit_ct_broker_helpers:force_vhost_failure(Config, <<"downvhost">>), + %% The vhost is down + Node = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename), + DownVHost = #{name => <<"downvhost">>, tracing => false, cluster_state => #{Node => <<"stopped">>}}, + assert_item(DownVHost, http_get(Config, "/vhosts/downvhost")), + + DownQueues = http_get(Config, "/queues/downvhost"), + DownQueue = http_get(Config, "/queues/downvhost/foo"), + + assert_list([#{name => <<"bar">>, + vhost => <<"downvhost">>, + state => <<"stopped">>, + durable => true, + auto_delete => false, + exclusive => false, + arguments => #{}}, + #{name => <<"foo">>, + vhost => <<"downvhost">>, + state => <<"stopped">>, + durable => true, + auto_delete => false, + exclusive => false, + arguments => #{}}], DownQueues), + assert_item(#{name => <<"foo">>, + vhost => <<"downvhost">>, + state => <<"stopped">>, + durable => true, + auto_delete => false, + exclusive => false, + arguments => #{}}, DownQueue), + + http_put(Config, "/queues/badvhost/bar", Good, ?NOT_FOUND), + http_put(Config, "/queues/%2F/bar", + [{durable, <<"troo">>}], + ?BAD_REQUEST), + http_put(Config, "/queues/%2F/foo", + [{durable, false}], + ?BAD_REQUEST), + + Policy = [{pattern, <<"baz">>}, + {definition, [{<<"ha-mode">>, <<"all">>}]}], + http_put(Config, "/policies/%2F/HA", Policy, {group, '2xx'}), + http_put(Config, "/queues/%2F/baz", Good, {group, '2xx'}), + Queues = http_get(Config, "/queues/%2F"), + Queue = http_get(Config, "/queues/%2F/foo"), + + NodeBin = atom_to_binary(Node, utf8), + assert_list([#{name => <<"baz">>, + vhost => <<"/">>, + durable => true, + auto_delete => false, + exclusive => false, + arguments => #{}, + node => NodeBin, + slave_nodes => [], + synchronised_slave_nodes => []}, + #{name => <<"foo">>, + vhost => <<"/">>, + durable => true, + auto_delete => false, + exclusive => false, + arguments => #{'x-queue-type' => <<"quorum">>}, + leader => NodeBin, + members => [NodeBin]}], Queues), + assert_item(#{name => <<"foo">>, + vhost => <<"/">>, + durable => true, + auto_delete => false, + exclusive => false, + arguments => #{'x-queue-type' => <<"quorum">>}, + leader => NodeBin, + members => [NodeBin]}, Queue), + + ?assert(not maps:is_key(messages, Queue)), + ?assert(not maps:is_key(message_stats, Queue)), + ?assert(not maps:is_key(messages_details, Queue)), + ?assert(not maps:is_key(reductions_details, Queue)), + ?assertEqual(NodeBin, maps:get(leader, Queue)), + ?assertEqual([NodeBin], maps:get(members, Queue)), + ?assertEqual([NodeBin], maps:get(online, Queue)), + + http_delete(Config, "/queues/%2F/foo", {group, '2xx'}), + http_delete(Config, "/queues/%2F/baz", {group, '2xx'}), + http_delete(Config, "/queues/%2F/foo", ?NOT_FOUND), + http_get(Config, "/queues/badvhost", ?NOT_FOUND), + + http_delete(Config, "/queues/downvhost/foo", {group, '2xx'}), + http_delete(Config, "/queues/downvhost/bar", {group, '2xx'}), + passed. + +queues_enable_totals_test(Config) -> + rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_management, enable_queue_totals, true]), + + Good = [{durable, true}], + GoodQQ = [{durable, true}, {arguments, [{'x-queue-type', 'quorum'}]}], + http_get(Config, "/queues/%2F/foo", ?NOT_FOUND), + http_put(Config, "/queues/%2F/foo", GoodQQ, {group, '2xx'}), + + Policy = [{pattern, <<"baz">>}, + {definition, [{<<"ha-mode">>, <<"all">>}]}], + http_put(Config, "/policies/%2F/HA", Policy, {group, '2xx'}), + http_put(Config, "/queues/%2F/baz", Good, {group, '2xx'}), + + {Conn, Ch} = open_connection_and_channel(Config), + Publish = fun(Q) -> + amqp_channel:call( + Ch, #'basic.publish'{exchange = <<"">>, + routing_key = Q}, + #amqp_msg{payload = <<"message">>}) + end, + Publish(<<"baz">>), + Publish(<<"foo">>), + Publish(<<"foo">>), + + Fun = fun() -> + length(rabbit_ct_broker_helpers:rpc(Config, 0, ets, tab2list, + [queue_coarse_metrics])) == 2 + end, + wait_until(Fun, 60), + + Queues = http_get(Config, "/queues/%2F"), + Queue = http_get(Config, "/queues/%2F/foo"), + + Node = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename), + NodeBin = atom_to_binary(Node, utf8), + assert_list([#{name => <<"baz">>, + vhost => <<"/">>, + durable => true, + auto_delete => false, + exclusive => false, + arguments => #{}, + node => NodeBin, + slave_nodes => [], + messages => 1, + messages_ready => 1, + messages_unacknowledged => 0, + synchronised_slave_nodes => []}, + #{name => <<"foo">>, + vhost => <<"/">>, + durable => true, + auto_delete => false, + exclusive => null, + arguments => #{'x-queue-type' => <<"quorum">>}, + leader => NodeBin, + messages => 2, + messages_ready => 2, + messages_unacknowledged => 0, + members => [NodeBin]}], Queues), + assert_item(#{name => <<"foo">>, + vhost => <<"/">>, + durable => true, + auto_delete => false, + exclusive => false, + arguments => #{'x-queue-type' => <<"quorum">>}, + leader => NodeBin, + members => [NodeBin]}, Queue), + + ?assert(not maps:is_key(messages, Queue)), + ?assert(not maps:is_key(messages_ready, Queue)), + ?assert(not maps:is_key(messages_unacknowledged, Queue)), + ?assert(not maps:is_key(message_stats, Queue)), + ?assert(not maps:is_key(messages_details, Queue)), + ?assert(not maps:is_key(reductions_details, Queue)), + + http_delete(Config, "/queues/%2F/foo", {group, '2xx'}), + http_delete(Config, "/queues/%2F/baz", {group, '2xx'}), + close_connection(Conn), + + passed. + +mirrored_queues_test(Config) -> + Policy = [{pattern, <<".*">>}, + {definition, [{<<"ha-mode">>, <<"all">>}]}], + http_put(Config, "/policies/%2F/HA", Policy, {group, '2xx'}), + + Good = [{durable, true}, {arguments, []}], + http_get(Config, "/queues/%2f/ha", ?NOT_FOUND), + http_put(Config, "/queues/%2f/ha", Good, {group, '2xx'}), + + {Conn, Ch} = open_connection_and_channel(Config), + Publish = fun() -> + amqp_channel:call( + Ch, #'basic.publish'{exchange = <<"">>, + routing_key = <<"ha">>}, + #amqp_msg{payload = <<"message">>}) + end, + Publish(), + Publish(), + + Queue = http_get(Config, "/queues/%2f/ha?lengths_age=60&lengths_incr=5&msg_rates_age=60&msg_rates_incr=5&data_rates_age=60&data_rates_incr=5"), + + %% It's really only one node, but the only thing that matters in this test is to verify the + %% key exists + Nodes = lists:sort(rabbit_ct_broker_helpers:get_node_configs(Config, nodename)), + + ?assert(not maps:is_key(messages, Queue)), + ?assert(not maps:is_key(messages_details, Queue)), + ?assert(not maps:is_key(reductions_details, Queue)), + ?assert(true, lists:member(maps:get(node, Queue), Nodes)), + ?assertEqual([], get_nodes(slave_nodes, Queue)), + ?assertEqual([], get_nodes(synchronised_slave_nodes, Queue)), + + http_delete(Config, "/queues/%2f/ha", {group, '2xx'}), + close_connection(Conn). + +quorum_queues_test(Config) -> + Good = [{durable, true}, {arguments, [{'x-queue-type', 'quorum'}]}], + http_get(Config, "/queues/%2f/qq", ?NOT_FOUND), + http_put(Config, "/queues/%2f/qq", Good, {group, '2xx'}), + + {Conn, Ch} = open_connection_and_channel(Config), + Publish = fun() -> + amqp_channel:call( + Ch, #'basic.publish'{exchange = <<"">>, + routing_key = <<"qq">>}, + #amqp_msg{payload = <<"message">>}) + end, + Publish(), + Publish(), + + Queue = http_get(Config, "/queues/%2f/qq?lengths_age=60&lengths_incr=5&msg_rates_age=60&msg_rates_incr=5&data_rates_age=60&data_rates_incr=5"), + + %% It's really only one node, but the only thing that matters in this test is to verify the + %% key exists + Nodes = lists:sort(rabbit_ct_broker_helpers:get_node_configs(Config, nodename)), + + ?assert(not maps:is_key(messages, Queue)), + ?assert(not maps:is_key(messages_details, Queue)), + ?assert(not maps:is_key(reductions_details, Queue)), + ?assert(true, lists:member(maps:get(leader, Queue, undefined), Nodes)), + ?assertEqual(Nodes, get_nodes(members, Queue)), + ?assertEqual(Nodes, get_nodes(online, Queue)), + + http_delete(Config, "/queues/%2f/qq", {group, '2xx'}), + close_connection(Conn). + +get_nodes(Tag, Queue) -> + lists:sort([binary_to_atom(B, utf8) || B <- maps:get(Tag, Queue)]). + +queues_well_formed_json_test(Config) -> + %% TODO This test should be extended to the whole API + Good = [{durable, true}], + http_put(Config, "/queues/%2F/foo", Good, {group, '2xx'}), + http_put(Config, "/queues/%2F/baz", Good, {group, '2xx'}), + + Queues = http_get_no_map(Config, "/queues/%2F"), + %% Ensure keys are unique + [begin + Sorted = lists:sort(Q), + Sorted = lists:usort(Q) + end || Q <- Queues], + + http_delete(Config, "/queues/%2F/foo", {group, '2xx'}), + http_delete(Config, "/queues/%2F/baz", {group, '2xx'}), + passed. + +permissions_vhost_test(Config) -> + QArgs = #{}, + PermArgs = [{configure, <<".*">>}, {write, <<".*">>}, {read, <<".*">>}], + http_put(Config, "/users/myadmin", [{password, <<"myadmin">>}, + {tags, <<"administrator">>}], {group, '2xx'}), + http_put(Config, "/users/myuser", [{password, <<"myuser">>}, + {tags, <<"management">>}], {group, '2xx'}), + http_put(Config, "/vhosts/myvhost1", none, {group, '2xx'}), + http_put(Config, "/vhosts/myvhost2", none, {group, '2xx'}), + http_put(Config, "/permissions/myvhost1/myuser", PermArgs, {group, '2xx'}), + http_put(Config, "/permissions/myvhost1/guest", PermArgs, {group, '2xx'}), + http_put(Config, "/permissions/myvhost2/guest", PermArgs, {group, '2xx'}), + assert_list([#{name => <<"/">>}, + #{name => <<"myvhost1">>}, + #{name => <<"myvhost2">>}], http_get(Config, "/vhosts", ?OK)), + assert_list([#{name => <<"myvhost1">>}], + http_get(Config, "/vhosts", "myuser", "myuser", ?OK)), + http_put(Config, "/queues/myvhost1/myqueue", QArgs, {group, '2xx'}), + http_put(Config, "/queues/myvhost2/myqueue", QArgs, {group, '2xx'}), + Test1 = + fun(Path) -> + Results = http_get(Config, Path, "myuser", "myuser", ?OK), + [case maps:get(vhost, Result) of + <<"myvhost2">> -> + throw({got_result_from_vhost2_in, Path, Result}); + _ -> + ok + end || Result <- Results] + end, + Test2 = + fun(Path1, Path2) -> + http_get(Config, Path1 ++ "/myvhost1/" ++ Path2, "myuser", "myuser", + ?OK), + http_get(Config, Path1 ++ "/myvhost2/" ++ Path2, "myuser", "myuser", + ?NOT_AUTHORISED) + end, + Test3 = + fun(Path1) -> + http_get(Config, Path1 ++ "/myvhost1/", "myadmin", "myadmin", + ?OK) + end, + Test1("/exchanges"), + Test2("/exchanges", ""), + Test2("/exchanges", "amq.direct"), + Test3("/exchanges"), + Test1("/queues"), + Test2("/queues", ""), + Test3("/queues"), + Test2("/queues", "myqueue"), + Test1("/bindings"), + Test2("/bindings", ""), + Test3("/bindings"), + Test2("/queues", "myqueue/bindings"), + Test2("/exchanges", "amq.default/bindings/source"), + Test2("/exchanges", "amq.default/bindings/destination"), + Test2("/bindings", "e/amq.default/q/myqueue"), + Test2("/bindings", "e/amq.default/q/myqueue/myqueue"), + http_delete(Config, "/vhosts/myvhost1", {group, '2xx'}), + http_delete(Config, "/vhosts/myvhost2", {group, '2xx'}), + http_delete(Config, "/users/myuser", {group, '2xx'}), + http_delete(Config, "/users/myadmin", {group, '2xx'}), + passed. + +%% Opens a new connection and a channel on it. +%% The channel is not managed by rabbit_ct_client_helpers and +%% should be explicitly closed by the caller. +open_connection_and_channel(Config) -> + Conn = rabbit_ct_client_helpers:open_connection(Config, 0), + {ok, Ch} = amqp_connection:open_channel(Conn), + {Conn, Ch}. + +get_conn(Config, Username, Password) -> + Port = amqp_port(Config), + {ok, Conn} = amqp_connection:start(#amqp_params_network{ + port = Port, + username = list_to_binary(Username), + password = list_to_binary(Password)}), + LocalPort = local_port(Conn), + ConnPath = rabbit_misc:format( + "/connections/127.0.0.1%3A~w%20-%3E%20127.0.0.1%3A~w", + [LocalPort, Port]), + ChPath = rabbit_misc:format( + "/channels/127.0.0.1%3A~w%20-%3E%20127.0.0.1%3A~w%20(1)", + [LocalPort, Port]), + ConnChPath = rabbit_misc:format( + "/connections/127.0.0.1%3A~w%20-%3E%20127.0.0.1%3A~w/channels", + [LocalPort, Port]), + {Conn, ConnPath, ChPath, ConnChPath}. + +permissions_connection_channel_consumer_test(Config) -> + PermArgs = [{configure, <<".*">>}, {write, <<".*">>}, {read, <<".*">>}], + http_put(Config, "/users/user", [{password, <<"user">>}, + {tags, <<"management">>}], {group, '2xx'}), + http_put(Config, "/permissions/%2F/user", PermArgs, {group, '2xx'}), + http_put(Config, "/users/monitor", [{password, <<"monitor">>}, + {tags, <<"monitoring">>}], {group, '2xx'}), + http_put(Config, "/permissions/%2F/monitor", PermArgs, {group, '2xx'}), + http_put(Config, "/queues/%2F/test", #{}, {group, '2xx'}), + + {Conn1, UserConn, UserCh, UserConnCh} = get_conn(Config, "user", "user"), + {Conn2, MonConn, MonCh, MonConnCh} = get_conn(Config, "monitor", "monitor"), + {Conn3, AdmConn, AdmCh, AdmConnCh} = get_conn(Config, "guest", "guest"), + {ok, Ch1} = amqp_connection:open_channel(Conn1), + {ok, Ch2} = amqp_connection:open_channel(Conn2), + {ok, Ch3} = amqp_connection:open_channel(Conn3), + [amqp_channel:subscribe( + Ch, #'basic.consume'{queue = <<"test">>}, self()) || + Ch <- [Ch1, Ch2, Ch3]], + timer:sleep(1500), + AssertLength = fun (Path, User, Len) -> + Res = http_get(Config, Path, User, User, ?OK), + rabbit_ct_helpers:await_condition( + fun () -> + Len =:= length(Res) + end) + end, + AssertDisabled = fun(Path) -> + http_get(Config, Path, "user", "user", ?BAD_REQUEST), + http_get(Config, Path, "monitor", "monitor", ?BAD_REQUEST), + http_get(Config, Path, "guest", "guest", ?BAD_REQUEST), + http_get(Config, Path, ?BAD_REQUEST) + end, + + AssertLength("/connections", "user", 1), + AssertLength("/connections", "monitor", 3), + AssertLength("/connections", "guest", 3), + + AssertDisabled("/channels"), + AssertDisabled("/consumers"), + AssertDisabled("/consumers/%2F"), + + AssertRead = fun(Path, UserStatus) -> + http_get(Config, Path, "user", "user", UserStatus), + http_get(Config, Path, "monitor", "monitor", ?OK), + http_get(Config, Path, ?OK) + end, + + AssertRead(UserConn, ?OK), + AssertRead(MonConn, ?NOT_AUTHORISED), + AssertRead(AdmConn, ?NOT_AUTHORISED), + AssertDisabled(UserCh), + AssertDisabled(MonCh), + AssertDisabled(AdmCh), + AssertRead(UserConnCh, ?OK), + AssertRead(MonConnCh, ?NOT_AUTHORISED), + AssertRead(AdmConnCh, ?NOT_AUTHORISED), + + AssertClose = fun(Path, User, Status) -> + http_delete(Config, Path, User, User, Status) + end, + AssertClose(UserConn, "monitor", ?NOT_AUTHORISED), + AssertClose(MonConn, "user", ?NOT_AUTHORISED), + AssertClose(AdmConn, "guest", {group, '2xx'}), + AssertClose(MonConn, "guest", {group, '2xx'}), + AssertClose(UserConn, "user", {group, '2xx'}), + + http_delete(Config, "/users/user", {group, '2xx'}), + http_delete(Config, "/users/monitor", {group, '2xx'}), + http_get(Config, "/connections/foo", ?NOT_FOUND), + http_get(Config, "/channels/foo", ?BAD_REQUEST), + http_delete(Config, "/queues/%2F/test", {group, '2xx'}), + passed. + +consumers_cq_test(Config) -> + consumers_test(Config, [{'x-queue-type', <<"classic">>}]). + +consumers_qq_test(Config) -> + consumers_test(Config, [{'x-queue-type', <<"quorum">>}]). + +consumers_test(Config, Args) -> + http_delete(Config, "/queues/%2F/test", [?NO_CONTENT, ?NOT_FOUND]), + QArgs = [{auto_delete, false}, {durable, true}, + {arguments, Args}], + http_put(Config, "/queues/%2F/test", QArgs, {group, '2xx'}), + {Conn, _ConnPath, _ChPath, _ConnChPath} = get_conn(Config, "guest", "guest"), + {ok, Ch} = amqp_connection:open_channel(Conn), + amqp_channel:subscribe( + Ch, #'basic.consume'{queue = <<"test">>, + no_ack = false, + consumer_tag = <<"my-ctag">> }, self()), + timer:sleep(1500), + + http_get(Config, "/consumers", ?BAD_REQUEST), + + amqp_connection:close(Conn), + http_delete(Config, "/queues/%2F/test", {group, '2xx'}), + passed. + +defs(Config, Key, URI, CreateMethod, Args) -> + defs(Config, Key, URI, CreateMethod, Args, + fun(URI2) -> http_delete(Config, URI2, {group, '2xx'}) end). + +defs_v(Config, Key, URI, CreateMethod, Args) -> + Rep1 = fun (S, S2) -> re:replace(S, "<vhost>", S2, [{return, list}]) end, + ReplaceVHostInArgs = fun(M, V2) -> maps:map(fun(vhost, _) -> V2; + (_, V1) -> V1 end, M) end, + + %% Test against default vhost + defs(Config, Key, Rep1(URI, "%2F"), CreateMethod, ReplaceVHostInArgs(Args, <<"/">>)), + + %% Test against new vhost + http_put(Config, "/vhosts/test", none, {group, '2xx'}), + PermArgs = [{configure, <<".*">>}, {write, <<".*">>}, {read, <<".*">>}], + http_put(Config, "/permissions/test/guest", PermArgs, {group, '2xx'}), + DeleteFun0 = fun(URI2) -> + http_delete(Config, URI2, {group, '2xx'}) + end, + DeleteFun1 = fun(_) -> + http_delete(Config, "/vhosts/test", {group, '2xx'}) + end, + defs(Config, Key, Rep1(URI, "test"), + CreateMethod, ReplaceVHostInArgs(Args, <<"test">>), + DeleteFun0, DeleteFun1). + +create(Config, CreateMethod, URI, Args) -> + case CreateMethod of + put -> http_put(Config, URI, Args, {group, '2xx'}), + URI; + put_update -> http_put(Config, URI, Args, {group, '2xx'}), + URI; + post -> Headers = http_post(Config, URI, Args, {group, '2xx'}), + rabbit_web_dispatch_util:unrelativise( + URI, pget("location", Headers)) + end. + +defs(Config, Key, URI, CreateMethod, Args, DeleteFun) -> + defs(Config, Key, URI, CreateMethod, Args, DeleteFun, DeleteFun). + +defs(Config, Key, URI, CreateMethod, Args, DeleteFun0, DeleteFun1) -> + %% Create the item + URI2 = create(Config, CreateMethod, URI, Args), + %% Make sure it ends up in definitions + Definitions = http_get(Config, "/definitions", ?OK), + true = lists:any(fun(I) -> test_item(Args, I) end, maps:get(Key, Definitions)), + + %% Delete it + DeleteFun0(URI2), + + %% Post the definitions back, it should get recreated in correct form + http_post(Config, "/definitions", Definitions, {group, '2xx'}), + assert_item(Args, http_get(Config, URI2, ?OK)), + + %% And delete it again + DeleteFun1(URI2), + + passed. + +register_parameters_and_policy_validator(Config) -> + rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_mgmt_runtime_parameters_util, register, []), + rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_mgmt_runtime_parameters_util, register_policy_validator, []). + +unregister_parameters_and_policy_validator(Config) -> + rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_mgmt_runtime_parameters_util, unregister_policy_validator, []), + rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_mgmt_runtime_parameters_util, unregister, []). + +arguments_test(Config) -> + XArgs = [{type, <<"headers">>}, + {arguments, [{'alternate-exchange', <<"amq.direct">>}]}], + QArgs = [{arguments, [{'x-expires', 1800000}]}], + BArgs = [{routing_key, <<"">>}, + {arguments, [{'x-match', <<"all">>}, + {foo, <<"bar">>}]}], + http_put(Config, "/exchanges/%2F/myexchange", XArgs, {group, '2xx'}), + http_put(Config, "/queues/%2F/arguments_test", QArgs, {group, '2xx'}), + http_post(Config, "/bindings/%2F/e/myexchange/q/arguments_test", BArgs, {group, '2xx'}), + Definitions = http_get(Config, "/definitions", ?OK), + http_delete(Config, "/exchanges/%2F/myexchange", {group, '2xx'}), + http_delete(Config, "/queues/%2F/arguments_test", {group, '2xx'}), + http_post(Config, "/definitions", Definitions, {group, '2xx'}), + #{'alternate-exchange' := <<"amq.direct">>} = + maps:get(arguments, http_get(Config, "/exchanges/%2F/myexchange", ?OK)), + #{'x-expires' := 1800000} = + maps:get(arguments, http_get(Config, "/queues/%2F/arguments_test", ?OK)), + + ArgsTable = [{<<"foo">>,longstr,<<"bar">>}, {<<"x-match">>, longstr, <<"all">>}], + Hash = table_hash(ArgsTable), + PropertiesKey = [$~] ++ Hash, + + assert_item( + #{'x-match' => <<"all">>, foo => <<"bar">>}, + maps:get(arguments, + http_get(Config, "/bindings/%2F/e/myexchange/q/arguments_test/" ++ + PropertiesKey, ?OK)) + ), + http_delete(Config, "/exchanges/%2F/myexchange", {group, '2xx'}), + http_delete(Config, "/queues/%2F/arguments_test", {group, '2xx'}), + passed. + +table_hash(Table) -> + binary_to_list(rabbit_mgmt_format:args_hash(Table)). + +queue_actions_test(Config) -> + http_put(Config, "/queues/%2F/q", #{}, {group, '2xx'}), + http_post(Config, "/queues/%2F/q/actions", [{action, sync}], {group, '2xx'}), + http_post(Config, "/queues/%2F/q/actions", [{action, cancel_sync}], {group, '2xx'}), + http_post(Config, "/queues/%2F/q/actions", [{action, change_colour}], ?BAD_REQUEST), + http_delete(Config, "/queues/%2F/q", {group, '2xx'}), + passed. + +double_encoded_json_test(Config) -> + Payload = rabbit_json:encode(rabbit_json:encode(#{<<"durable">> => true, <<"auto_delete">> => false})), + %% double-encoded JSON response is a 4xx, e.g. a Bad Request, and not a 500 + http_put_raw(Config, "/queues/%2F/q", Payload, {group, '4xx'}), + passed. + +exclusive_queue_test(Config) -> + {Conn, Ch} = open_connection_and_channel(Config), + #'queue.declare_ok'{queue = QName} = + amqp_channel:call(Ch, #'queue.declare'{exclusive = true}), + timer:sleep(2000), + Path = "/queues/%2F/" ++ rabbit_http_util:quote_plus(QName), + Queue = http_get(Config, Path), + assert_item(#{name => QName, + vhost => <<"/">>, + durable => false, + auto_delete => false, + exclusive => true, + arguments => #{}}, Queue), + amqp_channel:close(Ch), + close_connection(Conn), + passed. + +connections_channels_pagination_test(Config) -> + %% this test uses "unmanaged" (by Common Test helpers) connections to avoid + %% connection caching + Conn = open_unmanaged_connection(Config), + {ok, Ch} = amqp_connection:open_channel(Conn), + Conn1 = open_unmanaged_connection(Config), + {ok, Ch1} = amqp_connection:open_channel(Conn1), + Conn2 = open_unmanaged_connection(Config), + {ok, Ch2} = amqp_connection:open_channel(Conn2), + + rabbit_ct_helpers:await_condition( + fun() -> + PageOfTwo = http_get(Config, "/connections?page=1&page_size=2", ?OK), + 3 == maps:get(total_count, PageOfTwo) andalso + 3 == maps:get(filtered_count, PageOfTwo) andalso + 2 == maps:get(item_count, PageOfTwo) andalso + 1 == maps:get(page, PageOfTwo) andalso + 2 == maps:get(page_size, PageOfTwo) andalso + 2 == maps:get(page_count, PageOfTwo) andalso + lists:all(fun(C) -> + not maps:is_key(recv_oct_details, C) + end, maps:get(items, PageOfTwo)) + end), + + http_get(Config, "/channels?page=2&page_size=2", ?BAD_REQUEST), + + amqp_channel:close(Ch), + amqp_connection:close(Conn), + amqp_channel:close(Ch1), + amqp_connection:close(Conn1), + amqp_channel:close(Ch2), + amqp_connection:close(Conn2), + + passed. + +exchanges_pagination_test(Config) -> + QArgs = #{}, + PermArgs = [{configure, <<".*">>}, {write, <<".*">>}, {read, <<".*">>}], + http_put(Config, "/vhosts/vh1", none, {group, '2xx'}), + http_put(Config, "/permissions/vh1/guest", PermArgs, {group, '2xx'}), + http_get(Config, "/exchanges/vh1?page=1&page_size=2", ?OK), + http_put(Config, "/exchanges/%2F/test0", QArgs, {group, '2xx'}), + http_put(Config, "/exchanges/vh1/test1", QArgs, {group, '2xx'}), + http_put(Config, "/exchanges/%2F/test2_reg", QArgs, {group, '2xx'}), + http_put(Config, "/exchanges/vh1/reg_test3", QArgs, {group, '2xx'}), + + %% for stats to update + timer:sleep(1500), + + Total = length(rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_exchange, list_names, [])), + + PageOfTwo = http_get(Config, "/exchanges?page=1&page_size=2", ?OK), + ?assertEqual(Total, maps:get(total_count, PageOfTwo)), + ?assertEqual(Total, maps:get(filtered_count, PageOfTwo)), + ?assertEqual(2, maps:get(item_count, PageOfTwo)), + ?assertEqual(1, maps:get(page, PageOfTwo)), + ?assertEqual(2, maps:get(page_size, PageOfTwo)), + ?assertEqual(round(Total / 2), maps:get(page_count, PageOfTwo)), + Items1 = maps:get(items, PageOfTwo), + ?assert(lists:all(fun(E) -> + not maps:is_key(message_stats, E) + end, Items1)), + assert_list([#{name => <<"">>, vhost => <<"/">>}, + #{name => <<"amq.direct">>, vhost => <<"/">>} + ], Items1), + + ByName = http_get(Config, "/exchanges?page=1&page_size=2&name=reg", ?OK), + ?assertEqual(Total, maps:get(total_count, ByName)), + ?assertEqual(2, maps:get(filtered_count, ByName)), + ?assertEqual(2, maps:get(item_count, ByName)), + ?assertEqual(1, maps:get(page, ByName)), + ?assertEqual(2, maps:get(page_size, ByName)), + ?assertEqual(1, maps:get(page_count, ByName)), + Items2 = maps:get(items, ByName), + ?assert(lists:all(fun(E) -> + not maps:is_key(message_stats, E) + end, Items2)), + assert_list([#{name => <<"test2_reg">>, vhost => <<"/">>}, + #{name => <<"reg_test3">>, vhost => <<"vh1">>} + ], Items2), + + RegExByName = http_get(Config, + "/exchanges?page=1&page_size=2&name=%5E(?=%5Ereg)&use_regex=true", + ?OK), + ?assertEqual(Total, maps:get(total_count, RegExByName)), + ?assertEqual(1, maps:get(filtered_count, RegExByName)), + ?assertEqual(1, maps:get(item_count, RegExByName)), + ?assertEqual(1, maps:get(page, RegExByName)), + ?assertEqual(2, maps:get(page_size, RegExByName)), + ?assertEqual(1, maps:get(page_count, RegExByName)), + Items3 = maps:get(items, RegExByName), + ?assert(lists:all(fun(E) -> + not maps:is_key(message_stats, E) + end, Items3)), + assert_list([#{name => <<"reg_test3">>, vhost => <<"vh1">>} + ], Items3), + + + http_get(Config, "/exchanges?page=1000", ?BAD_REQUEST), + http_get(Config, "/exchanges?page=-1", ?BAD_REQUEST), + http_get(Config, "/exchanges?page=not_an_integer_value", ?BAD_REQUEST), + http_get(Config, "/exchanges?page=1&page_size=not_an_integer_value", ?BAD_REQUEST), + http_get(Config, "/exchanges?page=1&page_size=501", ?BAD_REQUEST), %% max 500 allowed + http_get(Config, "/exchanges?page=-1&page_size=-2", ?BAD_REQUEST), + http_delete(Config, "/exchanges/%2F/test0", {group, '2xx'}), + http_delete(Config, "/exchanges/vh1/test1", {group, '2xx'}), + http_delete(Config, "/exchanges/%2F/test2_reg", {group, '2xx'}), + http_delete(Config, "/exchanges/vh1/reg_test3", {group, '2xx'}), + http_delete(Config, "/vhosts/vh1", {group, '2xx'}), + passed. + +exchanges_pagination_permissions_test(Config) -> + http_put(Config, "/users/admin", [{password, <<"admin">>}, + {tags, <<"administrator">>}], {group, '2xx'}), + http_put(Config, "/users/non-admin", [{password, <<"non-admin">>}, + {tags, <<"management">>}], {group, '2xx'}), + Perms = [{configure, <<".*">>}, + {write, <<".*">>}, + {read, <<".*">>}], + http_put(Config, "/vhosts/vh1", none, {group, '2xx'}), + http_put(Config, "/permissions/vh1/non-admin", Perms, {group, '2xx'}), + http_put(Config, "/permissions/%2F/admin", Perms, {group, '2xx'}), + http_put(Config, "/permissions/vh1/admin", Perms, {group, '2xx'}), + QArgs = #{}, + http_put(Config, "/exchanges/%2F/test0", QArgs, "admin", "admin", {group, '2xx'}), + http_put(Config, "/exchanges/vh1/test1", QArgs, "non-admin", "non-admin", {group, '2xx'}), + + %% for stats to update + timer:sleep(1500), + + FirstPage = http_get(Config, "/exchanges?page=1&name=test1", "non-admin", "non-admin", ?OK), + + ?assertEqual(8, maps:get(total_count, FirstPage)), + ?assertEqual(1, maps:get(item_count, FirstPage)), + ?assertEqual(1, maps:get(page, FirstPage)), + ?assertEqual(100, maps:get(page_size, FirstPage)), + ?assertEqual(1, maps:get(page_count, FirstPage)), + Items = maps:get(items, FirstPage), + ?assert(lists:all(fun(C) -> + not maps:is_key(message_stats, C) + end, Items)), + assert_list([#{name => <<"test1">>, vhost => <<"vh1">>} + ], Items), + http_delete(Config, "/exchanges/%2F/test0", {group, '2xx'}), + http_delete(Config, "/exchanges/vh1/test1", {group, '2xx'}), + http_delete(Config, "/users/admin", {group, '2xx'}), + http_delete(Config, "/users/non-admin", {group, '2xx'}), + passed. + + + +queue_pagination_test(Config) -> + QArgs = #{}, + PermArgs = [{configure, <<".*">>}, {write, <<".*">>}, {read, <<".*">>}], + http_put(Config, "/vhosts/vh1", none, {group, '2xx'}), + http_put(Config, "/permissions/vh1/guest", PermArgs, {group, '2xx'}), + + http_get(Config, "/queues/vh1?page=1&page_size=2", ?OK), + + http_put(Config, "/queues/%2F/test0", QArgs, {group, '2xx'}), + http_put(Config, "/queues/vh1/test1", QArgs, {group, '2xx'}), + http_put(Config, "/queues/%2F/test2_reg", QArgs, {group, '2xx'}), + http_put(Config, "/queues/vh1/reg_test3", QArgs, {group, '2xx'}), + + %% for stats to update + timer:sleep(1500), + + Total = length(rabbit_ct_broker_helpers:rpc(Config, 0, rabbit_amqqueue, list_names, [])), + + PageOfTwo = http_get(Config, "/queues?page=1&page_size=2", ?OK), + ?assertEqual(Total, maps:get(total_count, PageOfTwo)), + ?assertEqual(Total, maps:get(filtered_count, PageOfTwo)), + ?assertEqual(2, maps:get(item_count, PageOfTwo)), + ?assertEqual(1, maps:get(page, PageOfTwo)), + ?assertEqual(2, maps:get(page_size, PageOfTwo)), + ?assertEqual(2, maps:get(page_count, PageOfTwo)), + Items1 = maps:get(items, PageOfTwo), + ?assert(lists:all(fun(C) -> + not maps:is_key(message_stats, C) + end, Items1)), + assert_list([#{name => <<"test0">>, vhost => <<"/">>}, + #{name => <<"test2_reg">>, vhost => <<"/">>} + ], Items1), + + SortedByName = http_get(Config, "/queues?sort=name&page=1&page_size=2", ?OK), + ?assertEqual(Total, maps:get(total_count, SortedByName)), + ?assertEqual(Total, maps:get(filtered_count, SortedByName)), + ?assertEqual(2, maps:get(item_count, SortedByName)), + ?assertEqual(1, maps:get(page, SortedByName)), + ?assertEqual(2, maps:get(page_size, SortedByName)), + ?assertEqual(2, maps:get(page_count, SortedByName)), + Items2 = maps:get(items, SortedByName), + ?assert(lists:all(fun(C) -> + not maps:is_key(message_stats, C) + end, Items2)), + assert_list([#{name => <<"reg_test3">>, vhost => <<"vh1">>}, + #{name => <<"test0">>, vhost => <<"/">>} + ], Items2), + + FirstPage = http_get(Config, "/queues?page=1", ?OK), + ?assertEqual(Total, maps:get(total_count, FirstPage)), + ?assertEqual(Total, maps:get(filtered_count, FirstPage)), + ?assertEqual(4, maps:get(item_count, FirstPage)), + ?assertEqual(1, maps:get(page, FirstPage)), + ?assertEqual(100, maps:get(page_size, FirstPage)), + ?assertEqual(1, maps:get(page_count, FirstPage)), + Items3 = maps:get(items, FirstPage), + ?assert(lists:all(fun(C) -> + not maps:is_key(message_stats, C) + end, Items3)), + assert_list([#{name => <<"test0">>, vhost => <<"/">>}, + #{name => <<"test1">>, vhost => <<"vh1">>}, + #{name => <<"test2_reg">>, vhost => <<"/">>}, + #{name => <<"reg_test3">>, vhost =><<"vh1">>} + ], Items3), + + ReverseSortedByName = http_get(Config, + "/queues?page=2&page_size=2&sort=name&sort_reverse=true", + ?OK), + ?assertEqual(Total, maps:get(total_count, ReverseSortedByName)), + ?assertEqual(Total, maps:get(filtered_count, ReverseSortedByName)), + ?assertEqual(2, maps:get(item_count, ReverseSortedByName)), + ?assertEqual(2, maps:get(page, ReverseSortedByName)), + ?assertEqual(2, maps:get(page_size, ReverseSortedByName)), + ?assertEqual(2, maps:get(page_count, ReverseSortedByName)), + Items4 = maps:get(items, ReverseSortedByName), + ?assert(lists:all(fun(C) -> + not maps:is_key(message_stats, C) + end, Items4)), + assert_list([#{name => <<"test0">>, vhost => <<"/">>}, + #{name => <<"reg_test3">>, vhost => <<"vh1">>} + ], Items4), + + ByName = http_get(Config, "/queues?page=1&page_size=2&name=reg", ?OK), + ?assertEqual(Total, maps:get(total_count, ByName)), + ?assertEqual(2, maps:get(filtered_count, ByName)), + ?assertEqual(2, maps:get(item_count, ByName)), + ?assertEqual(1, maps:get(page, ByName)), + ?assertEqual(2, maps:get(page_size, ByName)), + ?assertEqual(1, maps:get(page_count, ByName)), + Items5 = maps:get(items, ByName), + ?assert(lists:all(fun(C) -> + not maps:is_key(message_stats, C) + end, Items5)), + assert_list([#{name => <<"test2_reg">>, vhost => <<"/">>}, + #{name => <<"reg_test3">>, vhost => <<"vh1">>} + ], Items5), + + RegExByName = http_get(Config, + "/queues?page=1&page_size=2&name=%5E(?=%5Ereg)&use_regex=true", + ?OK), + ?assertEqual(Total, maps:get(total_count, RegExByName)), + ?assertEqual(1, maps:get(filtered_count, RegExByName)), + ?assertEqual(1, maps:get(item_count, RegExByName)), + ?assertEqual(1, maps:get(page, RegExByName)), + ?assertEqual(2, maps:get(page_size, RegExByName)), + ?assertEqual(1, maps:get(page_count, RegExByName)), + Items6 = maps:get(items, RegExByName), + ?assert(lists:all(fun(C) -> + not maps:is_key(message_stats, C) + end, Items6)), + assert_list([#{name => <<"reg_test3">>, vhost => <<"vh1">>} + ], Items6), + + http_get(Config, "/queues?page=1000", ?BAD_REQUEST), + http_get(Config, "/queues?page=-1", ?BAD_REQUEST), + http_get(Config, "/queues?page=not_an_integer_value", ?BAD_REQUEST), + http_get(Config, "/queues?page=1&page_size=not_an_integer_value", ?BAD_REQUEST), + http_get(Config, "/queues?page=1&page_size=501", ?BAD_REQUEST), %% max 500 allowed + http_get(Config, "/queues?page=-1&page_size=-2", ?BAD_REQUEST), + http_delete(Config, "/queues/%2F/test0", {group, '2xx'}), + http_delete(Config, "/queues/vh1/test1", {group, '2xx'}), + http_delete(Config, "/queues/%2F/test2_reg", {group, '2xx'}), + http_delete(Config, "/queues/vh1/reg_test3", {group, '2xx'}), + http_delete(Config, "/vhosts/vh1", {group, '2xx'}), + passed. + +queue_pagination_columns_test(Config) -> + QArgs = #{}, + PermArgs = [{configure, <<".*">>}, {write, <<".*">>}, {read, <<".*">>}], + http_put(Config, "/vhosts/vh1", none, [?CREATED, ?NO_CONTENT]), + http_put(Config, "/permissions/vh1/guest", PermArgs, [?CREATED, ?NO_CONTENT]), + + http_get(Config, "/queues/vh1?columns=name&page=1&page_size=2", ?OK), + http_put(Config, "/queues/%2F/queue_a", QArgs, {group, '2xx'}), + http_put(Config, "/queues/vh1/queue_b", QArgs, {group, '2xx'}), + http_put(Config, "/queues/%2F/queue_c", QArgs, {group, '2xx'}), + http_put(Config, "/queues/vh1/queue_d", QArgs, {group, '2xx'}), + PageOfTwo = http_get(Config, "/queues?columns=name&page=1&page_size=2", ?OK), + ?assertEqual(4, maps:get(total_count, PageOfTwo)), + ?assertEqual(4, maps:get(filtered_count, PageOfTwo)), + ?assertEqual(2, maps:get(item_count, PageOfTwo)), + ?assertEqual(1, maps:get(page, PageOfTwo)), + ?assertEqual(2, maps:get(page_size, PageOfTwo)), + ?assertEqual(2, maps:get(page_count, PageOfTwo)), + assert_list([#{name => <<"queue_a">>}, + #{name => <<"queue_c">>} + ], maps:get(items, PageOfTwo)), + + ColumnNameVhost = http_get(Config, "/queues/vh1?columns=name&page=1&page_size=2", ?OK), + ?assertEqual(2, maps:get(total_count, ColumnNameVhost)), + ?assertEqual(2, maps:get(filtered_count, ColumnNameVhost)), + ?assertEqual(2, maps:get(item_count, ColumnNameVhost)), + ?assertEqual(1, maps:get(page, ColumnNameVhost)), + ?assertEqual(2, maps:get(page_size, ColumnNameVhost)), + ?assertEqual(1, maps:get(page_count, ColumnNameVhost)), + assert_list([#{name => <<"queue_b">>}, + #{name => <<"queue_d">>} + ], maps:get(items, ColumnNameVhost)), + + ColumnsNameVhost = http_get(Config, "/queues?columns=name,vhost&page=2&page_size=2", ?OK), + ?assertEqual(4, maps:get(total_count, ColumnsNameVhost)), + ?assertEqual(4, maps:get(filtered_count, ColumnsNameVhost)), + ?assertEqual(2, maps:get(item_count, ColumnsNameVhost)), + ?assertEqual(2, maps:get(page, ColumnsNameVhost)), + ?assertEqual(2, maps:get(page_size, ColumnsNameVhost)), + ?assertEqual(2, maps:get(page_count, ColumnsNameVhost)), + assert_list([ + #{name => <<"queue_b">>, + vhost => <<"vh1">>}, + #{name => <<"queue_d">>, + vhost => <<"vh1">>} + ], maps:get(items, ColumnsNameVhost)), + + + http_delete(Config, "/queues/%2F/queue_a", {group, '2xx'}), + http_delete(Config, "/queues/vh1/queue_b", {group, '2xx'}), + http_delete(Config, "/queues/%2F/queue_c", {group, '2xx'}), + http_delete(Config, "/queues/vh1/queue_d", {group, '2xx'}), + http_delete(Config, "/vhosts/vh1", {group, '2xx'}), + passed. + +queues_pagination_permissions_test(Config) -> + http_delete(Config, "/vhosts/vh1", [?NO_CONTENT, ?NOT_FOUND]), + http_delete(Config, "/queues/%2F/test0", [?NO_CONTENT, ?NOT_FOUND]), + http_delete(Config, "/users/admin", [?NO_CONTENT, ?NOT_FOUND]), + http_delete(Config, "/users/non-admin", [?NO_CONTENT, ?NOT_FOUND]), + + http_put(Config, "/users/non-admin", [{password, <<"non-admin">>}, + {tags, <<"management">>}], {group, '2xx'}), + http_put(Config, "/users/admin", [{password, <<"admin">>}, + {tags, <<"administrator">>}], {group, '2xx'}), + Perms = [{configure, <<".*">>}, + {write, <<".*">>}, + {read, <<".*">>}], + http_put(Config, "/vhosts/vh1", none, {group, '2xx'}), + http_put(Config, "/permissions/vh1/non-admin", Perms, {group, '2xx'}), + http_put(Config, "/permissions/%2F/admin", Perms, {group, '2xx'}), + http_put(Config, "/permissions/vh1/admin", Perms, {group, '2xx'}), + QArgs = #{}, + http_put(Config, "/queues/%2F/test0", QArgs, {group, '2xx'}), + http_put(Config, "/queues/vh1/test1", QArgs, "non-admin","non-admin", {group, '2xx'}), + FirstPage = http_get(Config, "/queues?page=1", "non-admin", "non-admin", ?OK), + ?assertEqual(1, maps:get(total_count, FirstPage)), + ?assertEqual(1, maps:get(item_count, FirstPage)), + ?assertEqual(1, maps:get(page, FirstPage)), + ?assertEqual(100, maps:get(page_size, FirstPage)), + ?assertEqual(1, maps:get(page_count, FirstPage)), + assert_list([#{name => <<"test1">>, vhost => <<"vh1">>} + ], maps:get(items, FirstPage)), + + FirstPageAdm = http_get(Config, "/queues?page=1", "admin", "admin", ?OK), + ?assertEqual(2, maps:get(total_count, FirstPageAdm)), + ?assertEqual(2, maps:get(item_count, FirstPageAdm)), + ?assertEqual(1, maps:get(page, FirstPageAdm)), + ?assertEqual(100, maps:get(page_size, FirstPageAdm)), + ?assertEqual(1, maps:get(page_count, FirstPageAdm)), + assert_list([#{name => <<"test1">>, vhost => <<"vh1">>}, + #{name => <<"test0">>, vhost => <<"/">>} + ], maps:get(items, FirstPageAdm)), + + http_delete(Config, "/queues/%2F/test0", {group, '2xx'}), + http_delete(Config, "/queues/vh1/test1","admin","admin", {group, '2xx'}), + http_delete(Config, "/users/admin", {group, '2xx'}), + http_delete(Config, "/users/non-admin", {group, '2xx'}), + passed. + +samples_range_test(Config) -> + {Conn, Ch} = open_connection_and_channel(Config), + + %% Connections + rabbit_ct_helpers:await_condition( + fun() -> + 1 == length(http_get(Config, "/connections?lengths_age=60&lengths_incr=1", ?OK)) + end), + [Connection] = http_get(Config, "/connections?lengths_age=60&lengths_incr=1", ?OK), + ?assert(maps:is_key(name, Connection)), + + amqp_channel:close(Ch), + amqp_connection:close(Conn), + + %% Exchanges + [Exchange1 | _] = http_get(Config, "/exchanges?lengths_age=60&lengths_incr=1", ?OK), + ?assert(not maps:is_key(message_stats, Exchange1)), + Exchange2 = http_get(Config, "/exchanges/%2F/amq.direct?lengths_age=60&lengths_incr=1", ?OK), + ?assert(not maps:is_key(message_stats, Exchange2)), + + %% Nodes + [Node] = http_get(Config, "/nodes?lengths_age=60&lengths_incr=1", ?OK), + ?assert(not maps:is_key(channel_closed_details, Node)), + ?assert(not maps:is_key(channel_closed, Node)), + ?assert(not maps:is_key(disk_free, Node)), + ?assert(not maps:is_key(io_read_count, Node)), + + %% Overview + Overview = http_get(Config, "/overview?lengths_age=60&lengths_incr=1", ?OK), + ?assert(maps:is_key(node, Overview)), + ?assert(maps:is_key(object_totals, Overview)), + ?assert(not maps:is_key(queue_totals, Overview)), + ?assert(not maps:is_key(churn_rates, Overview)), + ?assert(not maps:is_key(message_stats, Overview)), + + %% Queues + http_put(Config, "/queues/%2F/test0", #{}, {group, '2xx'}), + + rabbit_ct_helpers:await_condition( + fun() -> + 1 == length(http_get(Config, "/queues/%2F?lengths_age=60&lengths_incr=1", ?OK)) + end), + + [Queue1] = http_get(Config, "/queues/%2F?lengths_age=60&lengths_incr=1", ?OK), + ?assert(not maps:is_key(message_stats, Queue1)), + ?assert(not maps:is_key(messages_details, Queue1)), + ?assert(not maps:is_key(reductions_details, Queue1)), + + Queue2 = http_get(Config, "/queues/%2F/test0?lengths_age=60&lengths_incr=1", ?OK), + ?assert(not maps:is_key(message_stats, Queue2)), + ?assert(not maps:is_key(messages_details, Queue2)), + ?assert(not maps:is_key(reductions_details, Queue2)), + + http_delete(Config, "/queues/%2F/test0", {group, '2xx'}), + + %% Vhosts + + http_put(Config, "/vhosts/vh1", none, {group, '2xx'}), + + rabbit_ct_helpers:await_condition( + fun() -> + length(http_get(Config, "/vhosts?lengths_age=60&lengths_incr=1", ?OK)) > 1 + end), + + [VHost | _] = http_get(Config, "/vhosts?lengths_age=60&lengths_incr=1", ?OK), + ?assert(not maps:is_key(message_stats, VHost)), + ?assert(not maps:is_key(messages_ready_details, VHost)), + ?assert(not maps:is_key(recv_oct, VHost)), + ?assert(maps:is_key(cluster_state, VHost)), + + http_delete(Config, "/vhosts/vh1", {group, '2xx'}), + + passed. + +disable_with_disable_stats_parameter_test(Config) -> + {Conn, Ch} = open_connection_and_channel(Config), + + %% Ensure we have some queue and exchange stats, needed later + http_put(Config, "/queues/%2F/test0", #{}, {group, '2xx'}), + timer:sleep(1500), + amqp_channel:call(Ch, #'basic.publish'{exchange = <<>>, + routing_key = <<"test0">>}, + #amqp_msg{payload = <<"message">>}), + + %% Channels. + + timer:sleep(1500), + %% Check first that stats are available + http_get(Config, "/channels", ?OK), + %% Now we can disable them + http_get(Config, "/channels?disable_stats=true", ?BAD_REQUEST), + + + %% Connections. + + %% Check first that stats are available + [ConnectionStats] = http_get(Config, "/connections", ?OK), + ?assert(maps:is_key(recv_oct_details, ConnectionStats)), + %% Now we can disable them + [Connection] = http_get(Config, "/connections?disable_stats=true", ?OK), + ?assert(maps:is_key(name, Connection)), + ?assert(not maps:is_key(recv_oct_details, Connection)), + + amqp_channel:close(Ch), + amqp_connection:close(Conn), + + %% Exchanges. + + %% Check first that stats are available + %% Exchange stats aren't published - even as 0 - until some messages have gone + %% through. At the end of this test we ensure that at least the default exchange + %% has something to show. + ExchangeStats = http_get(Config, "/exchanges", ?OK), + ?assert(lists:any(fun(E) -> + maps:is_key(message_stats, E) + end, ExchangeStats)), + %% Now we can disable them + Exchanges = http_get(Config, "/exchanges?disable_stats=true", ?OK), + ?assert(lists:all(fun(E) -> + not maps:is_key(message_stats, E) + end, Exchanges)), + Exchange = http_get(Config, "/exchanges/%2F/amq.direct?disable_stats=true&lengths_age=60&lengths_incr=1", ?OK), + ?assert(not maps:is_key(message_stats, Exchange)), + + %% Nodes. + + %% Check that stats are available + [NodeStats] = http_get(Config, "/nodes", ?OK), + ?assert(maps:is_key(channel_closed_details, NodeStats)), + %% Now we can disable them + [Node] = http_get(Config, "/nodes?disable_stats=true", ?OK), + ?assert(not maps:is_key(channel_closed_details, Node)), + ?assert(not maps:is_key(channel_closed, Node)), + ?assert(not maps:is_key(disk_free, Node)), + ?assert(not maps:is_key(io_read_count, Node)), + + + %% Overview. + + %% Check that stats are available + OverviewStats = http_get(Config, "/overview", ?OK), + ?assert(maps:is_key(message_stats, OverviewStats)), + %% Now we can disable them + Overview = http_get(Config, "/overview?disable_stats=true&lengths_age=60&lengths_incr=1", ?OK), + ?assert(not maps:is_key(queue_totals, Overview)), + ?assert(not maps:is_key(churn_rates, Overview)), + ?assert(not maps:is_key(message_stats, Overview)), + + %% Queues. + + %% Check that stats are available + [QueueStats] = http_get(Config, "/queues/%2F?lengths_age=60&lengths_incr=1", ?OK), + ?assert(maps:is_key(message_stats, QueueStats)), + %% Now we can disable them + [Queue] = http_get(Config, "/queues/%2F?disable_stats=true", ?OK), + ?assert(not maps:is_key(message_stats, Queue)), + ?assert(not maps:is_key(messages_details, Queue)), + ?assert(not maps:is_key(reductions_details, Queue)), + + http_delete(Config, "/queues/%2F/test0", {group, '2xx'}), + + %% Vhosts. + + %% Check that stats are available + VHostStats = http_get(Config, "/vhosts?lengths_age=60&lengths_incr=1", ?OK), + ?assert(lists:all(fun(E) -> + maps:is_key(message_stats, E) and + maps:is_key(messages_ready_details, E) + end, VHostStats)), + %% Now we can disable them + VHosts = http_get(Config, "/vhosts?disable_stats=true&lengths_age=60&lengths_incr=1", ?OK), + ?assert(lists:all(fun(E) -> + not maps:is_key(message_stats, E) and + not maps:is_key(messages_ready_details, E) + end, VHosts)), + + passed. + +sorting_test(Config) -> + QArgs = #{}, + PermArgs = [{configure, <<".*">>}, {write, <<".*">>}, {read, <<".*">>}], + http_put(Config, "/vhosts/vh1", none, {group, '2xx'}), + http_put(Config, "/permissions/vh1/guest", PermArgs, {group, '2xx'}), + http_put(Config, "/queues/%2F/test0", QArgs, {group, '2xx'}), + http_put(Config, "/queues/vh1/test1", QArgs, {group, '2xx'}), + http_put(Config, "/queues/%2F/test2", QArgs, {group, '2xx'}), + http_put(Config, "/queues/vh1/test3", QArgs, {group, '2xx'}), + List1 = http_get(Config, "/queues", ?OK), + assert_list([#{name => <<"test0">>}, + #{name => <<"test2">>}, + #{name => <<"test1">>}, + #{name => <<"test3">>}], List1), + ?assert(lists:all(fun(E) -> + not maps:is_key(message_stats, E) + end, List1)), + List2 = http_get(Config, "/queues?sort=name", ?OK), + assert_list([#{name => <<"test0">>}, + #{name => <<"test1">>}, + #{name => <<"test2">>}, + #{name => <<"test3">>}], List2), + ?assert(lists:all(fun(E) -> + not maps:is_key(message_stats, E) + end, List2)), + List3 = http_get(Config, "/queues?sort=vhost", ?OK), + assert_list([#{name => <<"test0">>}, + #{name => <<"test2">>}, + #{name => <<"test1">>}, + #{name => <<"test3">>}], List3), + ?assert(lists:all(fun(E) -> + not maps:is_key(message_stats, E) + end, List3)), + List4 = http_get(Config, "/queues?sort_reverse=true", ?OK), + assert_list([#{name => <<"test3">>}, + #{name => <<"test1">>}, + #{name => <<"test2">>}, + #{name => <<"test0">>}], List4), + ?assert(lists:all(fun(E) -> + not maps:is_key(message_stats, E) + end, List4)), + List5 = http_get(Config, "/queues?sort=name&sort_reverse=true", ?OK), + assert_list([#{name => <<"test3">>}, + #{name => <<"test2">>}, + #{name => <<"test1">>}, + #{name => <<"test0">>}], List5), + ?assert(lists:all(fun(E) -> + not maps:is_key(message_stats, E) + end, List5)), + List6 = http_get(Config, "/queues?sort=vhost&sort_reverse=true", ?OK), + assert_list([#{name => <<"test3">>}, + #{name => <<"test1">>}, + #{name => <<"test2">>}, + #{name => <<"test0">>}], List6), + ?assert(lists:all(fun(E) -> + not maps:is_key(message_stats, E) + end, List6)), + %% Rather poor but at least test it doesn't blow up with dots + http_get(Config, "/queues?sort=owner_pid_details.name", ?OK), + http_delete(Config, "/queues/%2F/test0", {group, '2xx'}), + http_delete(Config, "/queues/vh1/test1", {group, '2xx'}), + http_delete(Config, "/queues/%2F/test2", {group, '2xx'}), + http_delete(Config, "/queues/vh1/test3", {group, '2xx'}), + http_delete(Config, "/vhosts/vh1", {group, '2xx'}), + passed. + +columns_test(Config) -> + Path = "/queues/%2F/columns.test", + TTL = 30000, + http_delete(Config, Path, [{group, '2xx'}, 404]), + http_put(Config, Path, [{arguments, [{<<"x-message-ttl">>, TTL}]}], + {group, '2xx'}), + Item = #{arguments => #{'x-message-ttl' => TTL}, name => <<"columns.test">>}, + timer:sleep(2000), + [Item] = http_get(Config, "/queues?columns=arguments.x-message-ttl,name", ?OK), + Item = http_get(Config, "/queues/%2F/columns.test?columns=arguments.x-message-ttl,name", ?OK), + ?assert(not maps:is_key(message_stats, Item)), + ?assert(not maps:is_key(messages_details, Item)), + ?assert(not maps:is_key(reductions_details, Item)), + http_delete(Config, Path, {group, '2xx'}), + passed. + +if_empty_unused_test(Config) -> + http_put(Config, "/exchanges/%2F/test", #{}, {group, '2xx'}), + http_put(Config, "/queues/%2F/test", #{}, {group, '2xx'}), + http_post(Config, "/bindings/%2F/e/test/q/test", #{}, {group, '2xx'}), + http_post(Config, "/exchanges/%2F/amq.default/publish", + msg(<<"test">>, #{}, <<"Hello world">>), ?OK), + http_delete(Config, "/queues/%2F/test?if-empty=true", ?BAD_REQUEST), + http_delete(Config, "/exchanges/%2F/test?if-unused=true", ?BAD_REQUEST), + http_delete(Config, "/queues/%2F/test/contents", {group, '2xx'}), + + {Conn, _ConnPath, _ChPath, _ConnChPath} = get_conn(Config, "guest", "guest"), + {ok, Ch} = amqp_connection:open_channel(Conn), + amqp_channel:subscribe(Ch, #'basic.consume'{queue = <<"test">> }, self()), + http_delete(Config, "/queues/%2F/test?if-unused=true", ?BAD_REQUEST), + amqp_connection:close(Conn), + + http_delete(Config, "/queues/%2F/test?if-empty=true", {group, '2xx'}), + http_delete(Config, "/exchanges/%2F/test?if-unused=true", {group, '2xx'}), + passed. + +invalid_config_test(Config) -> + {Conn, _Ch} = open_connection_and_channel(Config), + + timer:sleep(1500), + + %% Check first that stats aren't available (configured on test setup) + http_get(Config, "/channels", ?BAD_REQUEST), + http_get(Config, "/connections", ?BAD_REQUEST), + http_get(Config, "/exchanges", ?BAD_REQUEST), + + %% Now we can set an invalid config, stats are still available (defaults to 'false') + rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_management, disable_management_stats, 50]), + http_get(Config, "/channels", ?OK), + http_get(Config, "/connections", ?OK), + http_get(Config, "/exchanges", ?OK), + + %% Set a valid config again + rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_management, disable_management_stats, true]), + http_get(Config, "/channels", ?BAD_REQUEST), + http_get(Config, "/connections", ?BAD_REQUEST), + http_get(Config, "/exchanges", ?BAD_REQUEST), + + %% Now we can set an invalid config in the agent, stats are still available + %% (defaults to 'false') + rabbit_ct_broker_helpers:rpc(Config, 0, application, set_env, + [rabbitmq_management_agent, disable_metrics_collector, "koala"]), + http_get(Config, "/channels", ?OK), + http_get(Config, "/connections", ?OK), + http_get(Config, "/exchanges", ?OK), + + amqp_connection:close(Conn), + + passed. + +%% ------------------------------------------------------------------- +%% Helpers. +%% ------------------------------------------------------------------- + +msg(Key, Headers, Body) -> + msg(Key, Headers, Body, <<"string">>). + +msg(Key, Headers, Body, Enc) -> + #{exchange => <<"">>, + routing_key => Key, + properties => #{delivery_mode => 2, + headers => Headers}, + payload => Body, + payload_encoding => Enc}. + +local_port(Conn) -> + [{sock, Sock}] = amqp_connection:info(Conn, [sock]), + {ok, Port} = inet:port(Sock), + Port. + +spawn_invalid(_Config, 0) -> + ok; +spawn_invalid(Config, N) -> + Self = self(), + spawn(fun() -> + timer:sleep(rand:uniform(250)), + {ok, Sock} = gen_tcp:connect("localhost", amqp_port(Config), [list]), + ok = gen_tcp:send(Sock, "Some Data"), + receive_msg(Self) + end), + spawn_invalid(Config, N-1). + +receive_msg(Self) -> + receive + {tcp, _, [$A, $M, $Q, $P | _]} -> + Self ! done + after + 60000 -> + Self ! no_reply + end. + +wait_for_answers(0) -> + ok; +wait_for_answers(N) -> + receive + done -> + wait_for_answers(N-1); + no_reply -> + throw(no_reply) + end. + +publish(Ch) -> + amqp_channel:call(Ch, #'basic.publish'{exchange = <<"">>, + routing_key = <<"myqueue">>}, + #amqp_msg{payload = <<"message">>}), + receive + stop_publish -> + ok + after 50 -> + publish(Ch) + end. + +wait_until(_Fun, 0) -> + ?assert(wait_failed); +wait_until(Fun, N) -> + case Fun() of + true -> + timer:sleep(1500); + false -> + timer:sleep(?COLLECT_INTERVAL + 100), + wait_until(Fun, N - 1) + end. + +http_post_json(Config, Path, Body, Assertion) -> + http_upload_raw(Config, post, Path, Body, "guest", "guest", + Assertion, [{"Content-Type", "application/json"}]). diff --git a/deps/rabbitmq_management/test/rabbit_mgmt_rabbitmqadmin_SUITE.erl b/deps/rabbitmq_management/test/rabbit_mgmt_rabbitmqadmin_SUITE.erl new file mode 100644 index 0000000000..7a192f225a --- /dev/null +++ b/deps/rabbitmq_management/test/rabbit_mgmt_rabbitmqadmin_SUITE.erl @@ -0,0 +1,512 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2016-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_rabbitmqadmin_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-compile(export_all). + +all() -> + [ {group, list_to_atom(Py)} || Py <- find_pythons() ]. + +groups() -> + Tests = [ + help, + host, + base_uri, + config_file, + user, + fmt_long, + fmt_kvp, + fmt_tsv, + fmt_table, + fmt_bash, + vhosts, + users, + permissions, + alt_vhost, + exchanges, + queues, + queues_unicode, + bindings, + policies, + operator_policies, + parameters, + publish, + ignore_vhost, + sort + ], + [ {list_to_atom(Py), [], Tests} || Py <- find_pythons() ]. + +%% ------------------------------------------------------------------- +%% Testsuite setup/teardown. +%% ------------------------------------------------------------------- + +init_per_suite(Config) -> + rabbit_ct_helpers:log_environment(), + inets:start(), + Config1 = rabbit_ct_helpers:set_config(Config, [ + {rmq_nodename_suffix, ?MODULE} + ]), + rabbit_ct_helpers:run_setup_steps(Config1, + rabbit_ct_broker_helpers:setup_steps() ++ + rabbit_ct_client_helpers:setup_steps() ++ + [fun (C) -> + rabbit_ct_helpers:set_config(C, + {rabbitmqadmin_path, + rabbitmqadmin(C)}) + end + ]). + +end_per_suite(Config) -> + ?assertNotEqual(os:getenv("HOME"), ?config(priv_dir, Config)), + rabbit_ct_helpers:run_teardown_steps(Config, + rabbit_ct_client_helpers:teardown_steps() ++ + rabbit_ct_broker_helpers:teardown_steps()). + +init_per_group(python2, Config) -> + rabbit_ct_helpers:set_config(Config, {python, "python2"}); +init_per_group(python3, Config) -> + rabbit_ct_helpers:set_config(Config, {python, "python3"}); +init_per_group(_, Config) -> + Config. + +end_per_group(_, Config) -> + Config. + +init_per_testcase(config_file, Config) -> + Home = os:getenv("HOME"), + os:putenv("HOME", ?config(priv_dir, Config)), + rabbit_ct_helpers:set_config(Config, {env_home, Home}); +init_per_testcase(Testcase, Config) -> + rabbit_ct_helpers:testcase_started(Config, Testcase). + +end_per_testcase(config_file, Config) -> + Home = rabbit_ct_helpers:get_config(Config, env_home), + os:putenv("HOME", Home); +end_per_testcase(Testcase, Config) -> + rabbit_ct_helpers:testcase_finished(Config, Testcase). + + +%% ------------------------------------------------------------------- +%% Testcases. +%% ------------------------------------------------------------------- + +help(Config) -> + {ok, _} = run(Config, ["--help"]), + {ok, _} = run(Config, ["help", "subcommands"]), + {ok, _} = run(Config, ["help", "config"]), + {error, _, _} = run(Config, ["help", "astronomy"]). + +host(Config) -> + {ok, _} = run(Config, ["show", "overview"]), + {ok, _} = run(Config, ["--host", "localhost", "show", "overview"]), + {error, _, _} = run(Config, ["--host", "some-host-that-does-not-exist", + "show", "overview"]). + +base_uri(Config) -> + {ok, _} = run(Config, ["--base-uri", "http://localhost", "list", "exchanges"]), + {ok, _} = run(Config, ["--base-uri", "http://localhost/", "list", "exchanges"]), + {ok, _} = run(Config, ["--base-uri", "http://localhost", "--vhost", "/", "list", "exchanges"]), + {ok, _} = run(Config, ["--base-uri", "http://localhost/", "--vhost", "/", "list", "exchanges"]), + {error, _, _} = run(Config, ["--base-uri", "https://some-host-that-does-not-exist:15672/", + "list", "exchanges"]), + {error, _, _} = run(Config, ["--base-uri", "http://localhost:15672/", "--vhost", "some-vhost-that-does-not-exist", + "list", "exchanges"]). + + +config_file(Config) -> + MgmtPort = integer_to_list(http_api_port(Config)), + {_DefConf, TestConf} = write_test_config(Config), + + %% try using a non-existent config file + ?assertMatch({error, _, _}, run(Config, ["--config", "/tmp/no-such-config-file", "show", "overview"])), + %% use a config file section with a reachable endpoint and correct credentials + ?assertMatch({ok, _}, run(Config, ["--config", TestConf, "--node", "reachable", "show", "overview"])), + + %% Default node in the config file uses an unreachable endpoint. Note that + %% the function that drives rabbitmqadmin will specify a --port and that will override + %% the config file value. + ?assertMatch({error, _, _}, run(Config, ["--config", TestConf, "show", "overview"])), + + %% overrides hostname and port using --base-uri + BaseURI = rabbit_misc:format("http://localhost:~s", [MgmtPort]), + ?assertMatch({ok, _}, run(Config, ["--config", TestConf, "--base-uri", BaseURI, "show", "overview"])), + + %% overrides --host and --port on the command line + ?assertMatch({ok, _}, run(Config, ["--config", TestConf, "--node", "default", "--host", "localhost", "--port", MgmtPort, "show", "overview"])), + + ?assertMatch({ok, _}, run(Config, ["show", "overview"])), + ?assertMatch({error, _, _}, run(Config, ["--node", "bad_credentials", "show", "overview"])), + %% overrides --username and --password on the command line with correct credentials + ?assertMatch({ok, _}, run(Config, ["--node", "bad_credentials", "--username", "guest", "--password", "guest", "show", "overview"])), + %% overrides --username and --password on the command line with incorrect credentials + ?assertMatch({error, _, _}, run(Config, ["--node", "bad_credentials", "--username", "gu3st", "--password", "guesTTTT", "show", "overview"])). + +user(Config) -> + ?assertMatch({ok, _}, run(Config, ["--user", "guest", "--password", "guest", "show", "overview"])), + ?assertMatch({error, _, _}, run(Config, ["--user", "no", "--password", "guest", "show", "overview"])), + ?assertMatch({error, _, _}, run(Config, ["--user", "guest", "--password", "no", "show", "overview"])). + +fmt_long(Config) -> + Out = multi_line_string([ + "", + "--------------------------------------------------------------------------------", + "", + " name: /", + "tracing: False", + "", + "--------------------------------------------------------------------------------", + "" ]), + ?assertEqual({ok, Out}, run(Config, ["--format", "long", "list", "vhosts", "name", "tracing"])). + +fmt_kvp(Config) -> + Out = multi_line_string(["name=\"/\" tracing=\"False\""]), + ?assertEqual({ok, Out}, run(Config, ["--format", "kvp", "list", "vhosts", "name", "tracing"])). + +fmt_tsv(Config) -> + Out = multi_line_string([ + "name\ttracing", + "/\tFalse" + ]), + ?assertEqual({ok, Out}, run(Config, ["--format", "tsv", "list", "vhosts", "name", "tracing"])). + +fmt_table(Config) -> + Out = multi_line_string([ + "+------+---------+", + "| name | tracing |", + "+------+---------+", + "| / | False |", + "+------+---------+" + ]), + ?assertEqual({ok, Out}, run(Config, ["list", "vhosts", "name", "tracing"])), + ?assertEqual({ok, Out}, run(Config, ["--format", "table", "list", + "vhosts", "name", "tracing"])). + +fmt_bash(Config) -> + {ok, "/\n"} = run(Config, ["--format", "bash", "list", + "vhosts", "name", "tracing"]). + +vhosts(Config) -> + {ok, ["/"]} = run_list(Config, l("vhosts")), + {ok, _} = run(Config, ["declare", "vhost", "name=foo"]), + {ok, ["/", "foo"]} = run_list(Config, l("vhosts")), + {ok, _} = run(Config, ["delete", "vhost", "name=foo"]), + {ok, ["/"]} = run_list(Config, l("vhosts")). + +users(Config) -> + {ok, ["guest"]} = run_list(Config, l("users")), + {error, _, _} = run(Config, ["declare", "user", "name=foo"]), + {ok, _} = run(Config, ["declare", "user", "name=foo", "password=pass", "tags="]), + + {ok, _} = run(Config, ["declare", "user", "name=with_password_hash1", "password_hash=MmJiODBkNTM3YjFkYTNlMzhiZDMwMzYxYWE4NTU2ODZiZGUwZWFjZDcxNjJmZWY2YTI1ZmU5N2JmNTI3YTI1Yg==", + "tags=management"]), + {ok, _} = run(Config, ["declare", "user", "name=with_password_hash2", + "hashing_algorithm=rabbit_password_hashing_sha256", "password_hash=MmJiODBkNTM3YjFkYTNlMzhiZDMwMzYxYWE4NTU2ODZiZGUwZWFjZDcxNjJmZWY2YTI1ZmU5N2JmNTI3YTI1Yg==", + "tags=management"]), + {ok, _} = run(Config, ["declare", "user", "name=with_password_hash3", + "hashing_algorithm=rabbit_password_hashing_sha512", "password_hash=YmQyYjFhYWY3ZWY0ZjA5YmU5ZjUyY2UyZDhkNTk5Njc0ZDgxYWE5ZDZhNDQyMTY5NmRjNGQ5M2RkMDYxOWQ2ODJjZTU2YjRkNjRhOWVmMDk3NzYxY2VkOTllMGY2NzI2NWI1Zjc2MDg1ZTViMGVlN2NhNDY5NmIyYWQ2ZmUyYjI=", + "tags=management"]), + + {error, _, _} = run(Config, ["declare", "user", "name=with_password_hash4", + "hashing_algorithm=rabbit_password_hashing_sha256", "password_hash=not-base64-encoded", + "tags=management"]), + + + {ok, ["foo", "guest", + "with_password_hash1", + "with_password_hash2", + "with_password_hash3"]} = run_list(Config, l("users")), + + {ok, _} = run(Config, ["delete", "user", "name=foo"]), + {ok, _} = run(Config, ["delete", "user", "name=with_password_hash1"]), + {ok, _} = run(Config, ["delete", "user", "name=with_password_hash2"]), + {ok, _} = run(Config, ["delete", "user", "name=with_password_hash3"]), + + {ok, ["guest"]} = run_list(Config, l("users")). + +permissions(Config) -> + {ok, _} = run(Config, ["declare", "vhost", "name=foo"]), + {ok, _} = run(Config, ["declare", "user", "name=bar", "password=pass", "tags="]), + %% The user that creates the vhosts gets permission automatically + %% See https://github.com/rabbitmq/rabbitmq-management/issues/444 + {ok, [["guest", "/"], + ["guest", "foo"]]} = run_table(Config, ["list", "permissions", + "user", "vhost"]), + {ok, _} = run(Config, ["declare", "permission", "user=bar", "vhost=foo", + "configure=.*", "write=.*", "read=.*"]), + {ok, [["guest", "/"], ["bar", "foo"], ["guest", "foo"]]} + = run_table(Config, ["list", "permissions", "user", "vhost"]), + {ok, _} = run(Config, ["delete", "user", "name=bar"]), + {ok, _} = run(Config, ["delete", "vhost", "name=foo"]). + +alt_vhost(Config) -> + {ok, _} = run(Config, ["declare", "vhost", "name=foo"]), + {ok, _} = run(Config, ["declare", "permission", "user=guest", "vhost=foo", + "configure=.*", "write=.*", "read=.*"]), + {ok, _} = run(Config, ["declare", "queue", "name=in_/"]), + {ok, _} = run(Config, ["--vhost", "foo", "declare", "queue", "name=in_foo"]), + {ok, [["/", "in_/"], ["foo", "in_foo"]]} = run_table(Config, ["list", "queues", + "vhost", "name"]), + {ok, _} = run(Config, ["--vhost", "foo", "delete", "queue", "name=in_foo"]), + {ok, _} = run(Config, ["delete", "queue", "name=in_/"]), + {ok, _} = run(Config, ["delete", "vhost", "name=foo"]). + +exchanges(Config) -> + {ok, _} = run(Config, ["declare", "exchange", "name=foo", "type=direct"]), + {ok, ["amq.direct", + "amq.fanout", + "amq.headers", + "amq.match", + "amq.rabbitmq.trace", + "amq.topic", + "foo"]} = run_list(Config, l("exchanges")), + {ok, _} = run(Config, ["delete", "exchange", "name=foo"]). + +queues(Config) -> + {ok, _} = run(Config, ["declare", "queue", "name=foo"]), + {ok, ["foo"]} = run_list(Config, l("queues")), + {ok, _} = run(Config, ["delete", "queue", "name=foo"]). + +queues_unicode(Config) -> + {ok, _} = run(Config, ["declare", "queue", "name=ööö"]), + %% 'ö' is encoded as 0xC3 0xB6 in UTF-8. We use a a list of + %% integers here because a binary literal would not work with Erlang + %% R16B03. + QUEUE_NAME = [195,182, 195,182, 195,182], + {ok, [QUEUE_NAME]} = run_list(Config, l("queues")), + {ok, _} = run(Config, ["delete", "queue", "name=ööö"]). + +bindings(Config) -> + {ok, _} = run(Config, ["declare", "queue", "name=foo"]), + {ok, _} = run(Config, ["declare", "binding", "source=amq.direct", + "destination=foo", "destination_type=queue", + "routing_key=test"]), + {ok, [["foo", "queue", "foo"], + ["amq.direct", "foo", "queue", "test"] + ]} = run_table(Config, + ["list", "bindings", + "source", "destination", + "destination_type", "routing_key"]), + {ok, _} = run(Config, ["delete", "queue", "name=foo"]). + +policies(Config) -> + {ok, _} = run(Config, ["declare", "policy", "name=ha", + "pattern=.*", "definition={\"ha-mode\":\"all\"}"]), + {ok, [["ha", "/", ".*", "{\"ha-mode\": \"all\"}"]]} = + run_table(Config, ["list", "policies", "name", + "vhost", "pattern", "definition"]), + {ok, _} = run(Config, ["delete", "policy", "name=ha"]). + +operator_policies(Config) -> + {ok, _} = run(Config, ["declare", "operator_policy", "name=len", + "pattern=.*", "definition={\"max-length\":100}"]), + {ok, [["len", "/", ".*", "{\"max-length\": 100}"]]} = + run_table(Config, ["list", "operator_policies", "name", + "vhost", "pattern", "definition"]), + {ok, _} = run(Config, ["delete", "operator_policy", "name=len"]). + +parameters(Config) -> + ok = rpc(Config, rabbit_mgmt_runtime_parameters_util, register, []), + {ok, _} = run(Config, ["declare", "parameter", "component=test", + "name=good", "value=123"]), + {ok, [["test", "good", "/", "123"]]} = run_table(Config, ["list", + "parameters", + "component", + "name", + "vhost", + "value"]), + {ok, _} = run(Config, ["delete", "parameter", "component=test", "name=good"]), + ok = rpc(Config, rabbit_mgmt_runtime_parameters_util, unregister, []). + +publish(Config) -> + {ok, _} = run(Config, ["declare", "queue", "name=test"]), + {ok, _} = run(Config, ["publish", "routing_key=test", "payload=test_1"]), + {ok, _} = run(Config, ["publish", "routing_key=test", "payload=test_2"]), + %% publish with stdin + %% TODO: this must support Python 3 as well + Py = find_python2(), + {ok, _} = rabbit_ct_helpers:exec([Py, "-c", + publish_with_stdin_python_program(Config, "test_3")], + []), + + M = exp_msg("test", 2, "False", "test_1"), + {ok, [M]} = run_table(Config, ["get", "queue=test", "ackmode=ack_requeue_false"]), + M2 = exp_msg("test", 1, "False", "test_2"), + {ok, [M2]} = run_table(Config, ["get", "queue=test", "ackmode=ack_requeue_true"]), + M3 = exp_msg("test", 1, "True", "test_2"), + {ok, [M3]} = run_table(Config, ["get", + "queue=test", + "ackmode=ack_requeue_false"]), + M4 = exp_msg("test", 0, "False", "test_3"), + {ok, [M4]} = run_table(Config, ["get", + "queue=test", + "ackmode=ack_requeue_false"]), + {ok, _} = run(Config, ["publish", "routing_key=test", "payload=test_4"]), + Fn = filename:join(?config(priv_dir, Config), "publish_test_4"), + + {ok, _} = run(Config, ["get", "queue=test", "ackmode=ack_requeue_false", "payload_file=" ++ Fn]), + {ok, <<"test_4">>} = file:read_file(Fn), + {ok, _} = run(Config, ["delete", "queue", "name=test"]). + +ignore_vhost(Config) -> + {ok, _} = run(Config, ["--vhost", "/", "show", "overview"]), + {ok, _} = run(Config, ["--vhost", "/", "list", "users"]), + {ok, _} = run(Config, ["--vhost", "/", "list", "vhosts"]), + {ok, _} = run(Config, ["--vhost", "/", "list", "nodes"]), + {ok, _} = run(Config, ["--vhost", "/", "list", "permissions"]), + {ok, _} = run(Config, ["--vhost", "/", "declare", "user", + "name=foo", "password=pass", "tags="]), + {ok, _} = run(Config, ["delete", "user", "name=foo"]). + +sort(Config) -> + {ok, _} = run(Config, ["declare", "queue", "name=foo"]), + {ok, _} = run(Config, ["declare", "binding", "source=amq.direct", + "destination=foo", "destination_type=queue", + "routing_key=bbb"]), + {ok, _} = run(Config, ["declare", "binding", "source=amq.topic", + "destination=foo", "destination_type=queue", + "routing_key=aaa"]), + {ok, [["foo"], + ["amq.direct", "bbb"], + ["amq.topic", "aaa"]]} = run_table(Config, ["--sort", "source", + "list", "bindings", + "source", "routing_key"]), + {ok, [["amq.topic", "aaa"], + ["amq.direct", "bbb"], + ["foo"]]} = run_table(Config, ["--sort", "routing_key", + "list", "bindings", "source", + "routing_key"]), + {ok, [["amq.topic", "aaa"], + ["amq.direct", "bbb"], + ["foo"]]} = run_table(Config, ["--sort", "source", + "--sort-reverse", "list", + "bindings", "source", + "routing_key"]), + {ok, _} = run(Config, ["delete", "queue", "name=foo"]). + +%% ------------------------------------------------------------------- +%% Utilities +%% ------------------------------------------------------------------- + +exp_msg(Key, Count, Redelivered, Payload) -> + % routing_key, message_count, + % payload, payload_bytes, + % payload_encoding, redelivered + [Key, integer_to_list(Count), + Payload, integer_to_list(length(Payload)), + "string", Redelivered]. + +rpc(Config, M, F, A) -> + rabbit_ct_broker_helpers:rpc(Config, 0, M, F, A). + +l(Thing) -> + ["list", Thing, "name"]. + +multi_line_string(Lines) -> + lists:flatten([string:join(Lines, io_lib:nl()), io_lib:nl()]). + +run_table(Config, Args) -> + {ok, Lines} = run_list(Config, Args), + Tokens = [string:tokens(L, "\t") || L <- Lines], + {ok, Tokens}. + +run_list(Config, Args) -> + A = ["-f", "tsv", "-q"], + case run(Config, A ++ Args) of + {ok, Out} -> {ok, string:tokens(Out, io_lib:nl())}; + Err -> Err + end. + +run(Config, Args) -> + Py = rabbit_ct_helpers:get_config(Config, python), + MgmtPort = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_mgmt), + RmqAdmin = rabbit_ct_helpers:get_config(Config, rabbitmqadmin_path), + rabbit_ct_helpers:exec([Py, + RmqAdmin, + "-P", + integer_to_list(MgmtPort)] ++ Args, + [drop_stdout]). + +rabbitmqadmin(Config) -> + filename:join([?config(current_srcdir, Config), "bin", "rabbitmqadmin"]). + +find_pythons() -> + Py2 = rabbit_ct_helpers:exec(["python2", "-V"]), + Py3 = rabbit_ct_helpers:exec(["python3", "-V"]), + case {Py2, Py3} of + {{ok, _}, {ok, _}} -> ["python2", "python3"]; + {{ok, _}, _} -> ["python2"]; + {_, {ok, _}} -> ["python3"]; + _ -> erlang:error("python not found") + end. + +find_python2() -> + Py2 = rabbit_ct_helpers:exec(["python2", "-V"]), + Py27 = rabbit_ct_helpers:exec(["python2.7", "-V"]), + case {Py2, Py27} of + {{ok, _}, {ok, _}} -> ["python2.7"]; + {{ok, _}, _} -> ["python2"]; + {_, {ok, _}} -> ["python2.7"]; + _ -> "python2" + end. + +publish_with_stdin_python_program(Config, In) -> + MgmtPort = rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_mgmt), + RmqAdmin = rabbit_ct_helpers:get_config(Config, rabbitmqadmin_path), + Py = find_python2(), + "import subprocess;" ++ + "proc = subprocess.Popen(['" ++ Py ++ "', '" ++ RmqAdmin ++ "', '-P', '" ++ integer_to_list(MgmtPort) ++ + "', 'publish', 'routing_key=test'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE);" ++ + "(stdout, stderr) = proc.communicate('" ++ In ++ "');" ++ + "exit(proc.returncode)". + +write_test_config(Config) -> + MgmtPort = integer_to_list(http_api_port(Config)), + PrivDir = ?config(priv_dir, Config), + DefaultConfig = [ + "[bad_credentials]", + "hostname = localhost", + "port =" ++ MgmtPort, + "username = gu/est", + "password = gu\\est", + "declare_vhost = /", + "vhost = /", + "", + "[bad_host]", + "hostname = non-existent.acme.com", + "port = " ++ MgmtPort, + "username = guest", + "password = guest" + ], + TestConfig = [ + "[reachable]", + "hostname = localhost", + "port = " ++ MgmtPort, + "username = guest", + "password = guest", + "declare_vhost = /", + "vhost = /", + "", + "[default]", + "hostname = non-existent.acme.com", + "port = 99799", + "username = guest", + "password = guest" + ], + DefaultConfig1 = [string:join(DefaultConfig, io_lib:nl()), io_lib:nl()], + TestConfig1 = [string:join(TestConfig, io_lib:nl()), io_lib:nl()], + FnDefault = filename:join(PrivDir, ".rabbitmqadmin.conf"), + FnTest = filename:join(PrivDir, "test-config"), + file:write_file(FnDefault, DefaultConfig1), + file:write_file(FnTest, TestConfig1), + {FnDefault, FnTest}. + +http_api_port(Config) -> + rabbit_ct_broker_helpers:get_node_config(Config, 0, tcp_port_mgmt). diff --git a/deps/rabbitmq_management/test/rabbit_mgmt_runtime_parameters_util.erl b/deps/rabbitmq_management/test/rabbit_mgmt_runtime_parameters_util.erl new file mode 100644 index 0000000000..0ac911b7c0 --- /dev/null +++ b/deps/rabbitmq_management/test/rabbit_mgmt_runtime_parameters_util.erl @@ -0,0 +1,63 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_runtime_parameters_util). +-behaviour(rabbit_runtime_parameter). +-behaviour(rabbit_policy_validator). + +-include_lib("rabbit_common/include/rabbit.hrl"). + +-export([validate/5, notify/5, notify_clear/4]). +-export([register/0, unregister/0]). +-export([validate_policy/1]). +-export([register_policy_validator/0, unregister_policy_validator/0]). + +%---------------------------------------------------------------------------- + +register() -> + rabbit_registry:register(runtime_parameter, <<"test">>, ?MODULE). + +unregister() -> + rabbit_registry:unregister(runtime_parameter, <<"test">>). + +validate(_, <<"test">>, <<"good">>, _Term, _User) -> ok; +validate(_, <<"test">>, <<"maybe">>, <<"good">>, _User) -> ok; +validate(_, <<"test">>, <<"admin">>, _Term, none) -> ok; +validate(_, <<"test">>, <<"admin">>, _Term, User) -> + case lists:member(administrator, User#user.tags) of + true -> ok; + false -> {error, "meh", []} + end; +validate(_, <<"test">>, _, _, _) -> {error, "meh", []}. + +notify(_, _, _, _, _) -> ok. +notify_clear(_, _, _, _) -> ok. + +%---------------------------------------------------------------------------- + +register_policy_validator() -> + rabbit_registry:register(policy_validator, <<"testeven">>, ?MODULE), + rabbit_registry:register(policy_validator, <<"testpos">>, ?MODULE). + +unregister_policy_validator() -> + rabbit_registry:unregister(policy_validator, <<"testeven">>), + rabbit_registry:unregister(policy_validator, <<"testpos">>). + +validate_policy([{<<"testeven">>, Terms}]) when is_list(Terms) -> + case length(Terms) rem 2 =:= 0 of + true -> ok; + false -> {error, "meh", []} + end; + +validate_policy([{<<"testpos">>, Terms}]) when is_list(Terms) -> + case lists:all(fun (N) -> is_integer(N) andalso N > 0 end, Terms) of + true -> ok; + false -> {error, "meh", []} + end; + +validate_policy(_) -> + {error, "meh", []}. diff --git a/deps/rabbitmq_management/test/rabbit_mgmt_stats_SUITE.erl b/deps/rabbitmq_management/test/rabbit_mgmt_stats_SUITE.erl new file mode 100644 index 0000000000..7ebb04da69 --- /dev/null +++ b/deps/rabbitmq_management/test/rabbit_mgmt_stats_SUITE.erl @@ -0,0 +1,458 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2016-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_stats_SUITE). + +-include_lib("proper/include/proper.hrl"). +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_metrics.hrl"). +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). + +-compile(export_all). +-compile({no_auto_import, [ceil/1]}). + +all() -> + [ + {group, parallel_tests} + ]. + +groups() -> + [ + {parallel_tests, [parallel], [ + format_rate_no_range_test, + format_zero_rate_no_range_test, + format_incremental_rate_no_range_test, + format_incremental_zero_rate_no_range_test, + format_total_no_range_test, + format_incremental_total_no_range_test, + format_rate_range_test, + format_incremental_rate_range_test, + format_incremental_zero_rate_range_test, + format_total_range_test, + format_incremental_total_range_test, + format_samples_range_test, + format_incremental_samples_range_test, + format_avg_rate_range_test, + format_incremental_avg_rate_range_test, + format_avg_range_test, + format_incremental_avg_range_test + ]} + ]. + +%% ------------------------------------------------------------------- +%% Testsuite setup/teardown. +%% ------------------------------------------------------------------- +init_per_suite(Config) -> + rabbit_ct_helpers:log_environment(), + Config. + +end_per_suite(Config) -> + Config. + +init_per_group(_, Config) -> + Config. + +end_per_group(_, _Config) -> + ok. + +init_per_testcase(_, Config) -> + Config. + +end_per_testcase(_, _Config) -> + ok. + +%% ------------------------------------------------------------------- +%% Generators. +%% ------------------------------------------------------------------- +elements_gen() -> + ?LET(Length, oneof([1, 2, 3, 7, 8, 20]), + ?LET(Elements, list(vector(Length, int())), + [erlang:list_to_tuple(E) || E <- Elements])). + +stats_tables() -> + [connection_stats_coarse_conn_stats, vhost_stats_coarse_conn_stats, + channel_stats_fine_stats, channel_exchange_stats_fine_stats, + channel_queue_stats_deliver_stats, vhost_stats_fine_stats, + queue_stats_deliver_stats, vhost_stats_deliver_stats, + channel_stats_deliver_stats, channel_process_stats, + queue_stats_publish, queue_exchange_stats_publish, + exchange_stats_publish_out, exchange_stats_publish_in, + queue_msg_stats, vhost_msg_stats, queue_process_stats, + node_coarse_stats, node_persister_stats, + node_node_coarse_stats, queue_msg_rates, vhost_msg_rates, + connection_churn_rates + ]. + +sample_size(large) -> + choose(15, 200); +sample_size(small) -> + choose(0, 1). + +sample_gen(_Table, 0) -> + []; +sample_gen(Table, 1) -> + ?LET(Stats, stats_gen(Table), [Stats || _ <- lists:seq(1, 5)]); +sample_gen(Table, N) -> + vector(N, stats_gen(Table)). + +content_gen(Size) -> + ?LET({Table, SampleSize}, {oneof(stats_tables()), sample_size(Size)}, + ?LET(Stats, sample_gen(Table, SampleSize), + {Table, Stats})). + +interval_gen() -> + %% Keep it at most 150ms, so the test runs in a reasonable time + choose(1, 150). + +stats_gen(Table) -> + ?LET(Vector, vector(length(?stats_per_table(Table)), choose(1, 100)), + list_to_tuple(Vector)). + +%% ------------------------------------------------------------------- +%% Testcases. +%% ------------------------------------------------------------------- + +%% Rates for 3 or more monotonically increasing samples will always be > 0 +format_rate_no_range_test(_Config) -> + Fun = fun() -> + prop_format(large, rate_check(fun(Rate) -> Rate > 0 end), + false, fun no_range/3) + end, + rabbit_ct_proper_helpers:run_proper(Fun, [], 100). + +prop_format(SampleSize, Check, Incremental, RangeFun) -> + ?FORALL( + {{Table, Data}, Interval}, {content_gen(SampleSize), interval_gen()}, + begin + {LastTS, Slide, Total, Samples} + = create_slide(Data, Interval, Incremental, SampleSize), + Range = RangeFun(Slide, LastTS, Interval), + SamplesFun = fun() -> [Slide] end, + InstantRateFun = fun() -> [Slide] end, + Results = rabbit_mgmt_stats:format_range(Range, LastTS, Table, 5000, + InstantRateFun, + SamplesFun), + ?WHENFAIL(io:format("Got: ~p~nSlide: ~p~nRange~p~n", [Results, Slide, Range]), + Check(Results, Total, Samples, Table)) + end). + +%% Rates for 1 or no samples will always be 0.0 as there aren't +%% enough datapoints to calculate the instant rate +format_zero_rate_no_range_test(_Config) -> + Fun = fun() -> + prop_format(small, rate_check(fun(Rate) -> Rate == 0.0 end), + false, fun no_range/3) + end, + rabbit_ct_proper_helpers:run_proper(Fun, [], 100). + +%% Rates for 3 or more monotonically increasing incremental samples will always be > 0 +format_incremental_rate_no_range_test(_Config) -> + Fun = fun() -> + prop_format(large, rate_check(fun(Rate) -> Rate > 0 end), + true, fun no_range/3) + end, + rabbit_ct_proper_helpers:run_proper(Fun, [], 100). + +%% Rates for 1 or no samples will always be 0.0 as there aren't +%% enough datapoints to calculate the instant rate +format_incremental_zero_rate_no_range_test(_Config) -> + Fun = fun() -> + prop_format(small, rate_check(fun(Rate) -> Rate == 0.0 end), + true, fun no_range/3) + end, + rabbit_ct_proper_helpers:run_proper(Fun, [], 100). + +%% Checking totals +format_total_no_range_test(_Config) -> + Fun = fun() -> + prop_format(large, fun check_total/4, false, fun no_range/3) + end, + rabbit_ct_proper_helpers:run_proper(Fun, [], 100). + +format_incremental_total_no_range_test(_Config) -> + Fun = fun() -> + prop_format(large, fun check_total/4, true, fun no_range/3) + end, + rabbit_ct_proper_helpers:run_proper(Fun, [], 100). + +%%--------------------- +%% Requests using range +%%--------------------- +format_rate_range_test(_Config) -> + %% Request a range bang on the middle, so we ensure no padding is applied + Fun = fun() -> + prop_format(large, rate_check(fun(Rate) -> Rate > 0 end), + false, fun full_range/3) + end, + rabbit_ct_proper_helpers:run_proper(Fun, [], 100). + +%% Rates for 3 or more monotonically increasing incremental samples will always be > 0 +format_incremental_rate_range_test(_Config) -> + %% Request a range bang on the middle, so we ensure no padding is applied + Fun = fun() -> + prop_format(large, rate_check(fun(Rate) -> Rate > 0 end), + true, fun full_range/3) + end, + rabbit_ct_proper_helpers:run_proper(Fun, [], 100). + +%% Rates for 1 or no samples will always be 0.0 as there aren't +%% enough datapoints to calculate the instant rate +format_incremental_zero_rate_range_test(_Config) -> + Fun = fun() -> + prop_format(small, rate_check(fun(Rate) -> Rate == 0.0 end), + true, fun full_range/3) + end, + rabbit_ct_proper_helpers:run_proper(Fun, [], 100). + +%% Checking totals +format_total_range_test(_Config) -> + Fun = fun() -> + prop_format(large, fun check_total/4, false, fun full_range_plus_interval/3) + end, + rabbit_ct_proper_helpers:run_proper(Fun, [], 100). + +format_incremental_total_range_test(_Config) -> + Fun = fun() -> + prop_format(large, fun check_total/4, true, fun full_range_plus_interval/3) + end, + rabbit_ct_proper_helpers:run_proper(Fun, [], 100). + +format_samples_range_test(_Config) -> + Fun = fun() -> + prop_format(large, fun check_samples/4, false, fun full_range/3) + end, + rabbit_ct_proper_helpers:run_proper(Fun, [], 100). + +format_incremental_samples_range_test(_Config) -> + Fun = fun() -> + prop_format(large, fun check_samples/4, true, fun full_range/3) + end, + rabbit_ct_proper_helpers:run_proper(Fun, [], 100). + +format_avg_rate_range_test(_Config) -> + Fun = fun() -> + prop_format(large, fun check_avg_rate/4, false, fun full_range/3) + end, + rabbit_ct_proper_helpers:run_proper(Fun, [], 100). + +format_incremental_avg_rate_range_test(_Config) -> + Fun = fun() -> + prop_format(large, fun check_avg_rate/4, true, fun full_range/3) + end, + rabbit_ct_proper_helpers:run_proper(Fun, [], 100). + +format_avg_range_test(_Config) -> + Fun = fun() -> + prop_format(large, fun check_avg/4, false, fun full_range/3) + end, + rabbit_ct_proper_helpers:run_proper(Fun, [], 100). + +format_incremental_avg_range_test(_Config) -> + Fun = fun() -> + prop_format(large, fun check_avg/4, true, fun full_range/3) + end, + rabbit_ct_proper_helpers:run_proper(Fun, [], 100). +%% ------------------------------------------------------------------- +%% Helpers +%% ------------------------------------------------------------------- +details(Table) -> + [list_to_atom(atom_to_list(S) ++ "_details") || S <- ?stats_per_table(Table)]. + +add(T1, undefined) -> + T1; +add(T1, T2) -> + list_to_tuple(lists:zipwith(fun(A, B) -> A + B end, tuple_to_list(T1), tuple_to_list(T2))). + +create_slide(Data, Interval, Incremental, SampleSize) -> + %% Use the samples as increments for data generation, + %% so we have always increasing counters + Now = 0, + Slide = exometer_slide:new(Now, 60 * 1000, + [{interval, Interval}, {incremental, Incremental}]), + Sleep = min_wait(Interval, Data), + lists:foldl( + fun(E, {TS0, Acc, Total, Samples}) -> + TS1 = TS0 + Sleep, + NewTotal = add(E, Total), + %% We use small sample sizes to keep a constant rate + Sample = create_sample(E, Incremental, SampleSize, NewTotal), + {TS1, exometer_slide:add_element(TS1, Sample, Acc), NewTotal, + [NewTotal | Samples]} + end, {Now, Slide, undefined, []}, Data). + +create_sample(E, Incremental, SampleSize, NewTotal) -> + case {Incremental, SampleSize} of + {false, small} -> E; + {true, small} -> + zero_tuple(E); + {false, _} -> + %% Guarantees a monotonically increasing counter + NewTotal; + {true, _} -> E + end. + +zero_tuple(E) -> + Length = length(tuple_to_list(E)), + list_to_tuple([0 || _ <- lists:seq(1, Length)]). + +min_wait(_, []) -> + 0; +min_wait(Interval, Data) -> + %% Send at constant intervals for Interval * 3 ms. This eventually ensures several samples + %% on the same interval, max execution time of Interval * 5 and also enough samples to + %% generate a rate. + case round((Interval * 3) / length(Data)) of + 0 -> 1; + Min -> Min + end. + +is_average_time(Atom) -> + case re:run(atom_to_list(Atom), "_avg_time$") of + nomatch -> + false; + _ -> + true + end. + +rate_check(RateCheck) -> + fun(Results, _, _, Table) -> + Check = + fun(Detail) -> + Rate = proplists:get_value(rate, proplists:get_value(Detail, Results), 0), + RateCheck(Rate) + end, + lists:all(Check, details(Table)) + end. + +check_total(Results, Totals, _Samples, Table) -> + Expected = lists:zip(?stats_per_table(Table), tuple_to_list(Totals)), + lists:all(fun({K, _} = E) -> + case is_average_time(K) of + false -> lists:member(E, Results); + true -> lists:keymember(K, 1, Results) + end + end, Expected). + +is_avg_time_details(Detail) -> + match == re:run(atom_to_list(Detail), "avg_time_details$", [{capture, none}]). + +check_samples(Results, _Totals, Samples, Table) -> + Details = details(Table), + %% Lookup list for the position of the key in the stats tuple + Pairs = lists:zip(Details, lists:seq(1, length(Details))), + + NonAvgTimeDetails = lists:filter(fun(D) -> + not is_avg_time_details(D) + end, Details), + + %% Check that all samples in the results match one of the samples in the inputs + lists:all(fun(Detail) -> + RSamples = get_from_detail(samples, Detail, Results), + lists:all(fun(RS) -> + Value = proplists:get_value(sample, RS), + case Value of + 0 -> + true; + _ -> + lists:keymember(Value, + proplists:get_value(Detail, Pairs), + Samples) + end + end, RSamples) + end, NonAvgTimeDetails) + %% ensure that not all samples are 0 + andalso lists:all(fun(Detail) -> + RSamples = get_from_detail(samples, Detail, Results), + lists:any(fun(RS) -> + 0 =/= proplists:get_value(sample, RS) + end, RSamples) + end, Details). + +check_avg_rate(Results, _Totals, _Samples, Table) -> + Details = details(Table), + + NonAvgTimeDetails = lists:filter(fun(D) -> + not is_avg_time_details(D) + end, Details), + + AvgTimeDetails = lists:filter(fun(D) -> + is_avg_time_details(D) + end, Details), + + lists:all(fun(Detail) -> + AvgRate = get_from_detail(avg_rate, Detail, Results), + Samples = get_from_detail(samples, Detail, Results), + S2 = proplists:get_value(sample, hd(Samples)), + T2 = proplists:get_value(timestamp, hd(Samples)), + S1 = proplists:get_value(sample, lists:last(Samples)), + T1 = proplists:get_value(timestamp, lists:last(Samples)), + AvgRate == ((S2 - S1) * 1000 / (T2 - T1)) + end, NonAvgTimeDetails) andalso + lists:all(fun(Detail) -> + Avg = get_from_detail(avg_rate, Detail, Results), + Samples = get_from_detail(samples, Detail, Results), + First = proplists:get_value(sample, hd(Samples)), + Avg == First + end, AvgTimeDetails). + +check_avg(Results, _Totals, _Samples, Table) -> + Details = details(Table), + + NonAvgTimeDetails = lists:filter(fun(D) -> + not is_avg_time_details(D) + end, Details), + + AvgTimeDetails = lists:filter(fun(D) -> + is_avg_time_details(D) + end, Details), + + lists:all(fun(Detail) -> + Avg = get_from_detail(avg, Detail, Results), + Samples = get_from_detail(samples, Detail, Results), + Sum = lists:sum([proplists:get_value(sample, S) || S <- Samples]), + Avg == (Sum / length(Samples)) + end, NonAvgTimeDetails) andalso + lists:all(fun(Detail) -> + Avg = get_from_detail(avg, Detail, Results), + Samples = get_from_detail(samples, Detail, Results), + First = proplists:get_value(sample, hd(Samples)), + Avg == First + end, AvgTimeDetails). + +get_from_detail(Tag, Detail, Results) -> + proplists:get_value(Tag, proplists:get_value(Detail, Results), []). + +full_range(Slide, Last, Interval) -> + LastTS = case exometer_slide:last_two(Slide) of + [] -> Last; + [{L, _} | _] -> L + end, + #range{first = 0, last = LastTS, incr = Interval}. + +full_range_plus_interval(Slide, Last, Interval) -> + LastTS = case exometer_slide:last_two(Slide) of + [] -> Last; + [{L, _} | _] -> L + end, + % were adding two intervals here due to rounding occasionally pushing the last + % sample into the next time "bucket" + #range{first = 0, last = LastTS + Interval + Interval, incr = Interval}. + +no_range(_Slide, _LastTS, _Interval) -> + no_range. + +%% Generate a well-formed interval from Start using Interval steps +last_ts(First, Last, Interval) -> + ceil(((Last - First) / Interval)) * Interval + First. + +ceil(X) when X < 0 -> + trunc(X); +ceil(X) -> + T = trunc(X), + case X - T == 0 of + true -> T; + false -> T + 1 + end. diff --git a/deps/rabbitmq_management/test/rabbit_mgmt_test_db_SUITE.erl b/deps/rabbitmq_management/test/rabbit_mgmt_test_db_SUITE.erl new file mode 100644 index 0000000000..03a36ee138 --- /dev/null +++ b/deps/rabbitmq_management/test/rabbit_mgmt_test_db_SUITE.erl @@ -0,0 +1,469 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2016-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_test_db_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("rabbit_common/include/rabbit_core_metrics.hrl"). +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_metrics.hrl"). +-include("rabbit_mgmt.hrl"). +-include_lib("rabbitmq_ct_helpers/include/rabbit_mgmt_test.hrl"). +-import(rabbit_mgmt_test_util, [assert_list/2, + reset_management_settings/1]). + +-import(rabbit_misc, [pget/2]). + +-compile(export_all). +-compile({no_auto_import, [ceil/1]}). + +all() -> + [ + {group, non_parallel_tests} + ]. + +groups() -> + [ + {non_parallel_tests, [], [ + queue_coarse_test, + connection_coarse_test, + fine_stats_aggregation_time_test, + fine_stats_aggregation_test, + all_consumers_test + ]} + ]. + +%% ------------------------------------------------------------------- +%% Testsuite setup/teardown. +%% ------------------------------------------------------------------- + +init_per_suite(Config) -> + rabbit_ct_helpers:log_environment(), + inets:start(), + Config. + +end_per_suite(Config) -> + Config. + +init_per_group(_, Config) -> + Config1 = rabbit_ct_helpers:set_config(Config, [ + {rmq_nodename_suffix, ?MODULE} + ]), + Config2 = rabbit_ct_helpers:merge_app_env( + rabbit_mgmt_test_util:merge_stats_app_env(Config1, 1000, 1), + {rabbitmq_management_agent, [{rates_mode, detailed}]}), + rabbit_ct_helpers:run_setup_steps(Config2, + rabbit_ct_broker_helpers:setup_steps() ++ + rabbit_ct_client_helpers:setup_steps()). + +end_per_group(_, Config) -> + rabbit_ct_helpers:run_teardown_steps(Config, + rabbit_ct_client_helpers:teardown_steps() ++ + rabbit_ct_broker_helpers:teardown_steps()). + +init_per_testcase(Testcase, Config) -> + reset_management_settings(Config), + rabbit_ct_helpers:testcase_started(Config, Testcase). + +end_per_testcase(Testcase, Config) -> + reset_management_settings(Config), + rabbit_ct_helpers:testcase_finished(Config, Testcase). + +%% ------------------------------------------------------------------- +%% Testcases. +%% ------------------------------------------------------------------- + +trace_fun(Config, MFs) -> + Nodename1 = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename), + dbg:tracer(process, {fun(A,_) -> + ct:pal(?LOW_IMPORTANCE, + "TRACE: ~p", [A]) + end, ok}), + dbg:n(Nodename1), + dbg:p(all,c), + [ dbg:tpl(M, F, cx) || {M, F} <- MFs]. + +queue_coarse_test(Config) -> + ok = rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, queue_coarse_test1, [Config]). + +queue_coarse_test1(_Config) -> + [rabbit_mgmt_metrics_collector:override_lookups(T, [{exchange, fun dummy_lookup/1}, + {queue, fun dummy_lookup/1}]) + || {T, _} <- ?CORE_TABLES], + First = exometer_slide:timestamp(), + stats_series(fun stats_q/2, [[{test, 1}, {test2, 1}], [{test, 10}], [{test, 20}]]), + timer:sleep(1150 * 2), %% The x2 factor is arbitrary: it makes CI happy. + Last = exometer_slide:timestamp(), + Interval = 1, + R = range(First, Last, Interval), + simple_details(get_q(test, R), messages, 20, R), + simple_details(get_vhost(R), messages, 21, R), + simple_details(get_overview_q(R), messages, 21, R), + delete_q(test), + timer:sleep(1150), + Next = last_ts(First, Interval), + R1 = range(First, Next, Interval), + simple_details(get_vhost(R1), messages, 1, R1), + simple_details(get_overview_q(R1), messages, 1, R1), + delete_q(test2), + timer:sleep(1150), + Next2 = last_ts(First, Interval), + R2 = range(First, Next2, Interval), + simple_details(get_vhost(R2), messages, 0, R2), + simple_details(get_overview_q(R2), messages, 0, R2), + [rabbit_mgmt_metrics_collector:reset_lookups(T) || {T, _} <- ?CORE_TABLES], + ok. + +%% Generate a well-formed interval from Start using Interval steps +last_ts(First, Interval) -> + Now = exometer_slide:timestamp(), + ceil(((Now - First) / Interval * 1000)) * Interval + First. + +ceil(X) when X < 0 -> + trunc(X); +ceil(X) -> + T = trunc(X), + case X - T == 0 of + true -> T; + false -> T + 1 + end. + +connection_coarse_test(Config) -> + ok = rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, connection_coarse_test1, [Config]). + +connection_coarse_test1(_Config) -> + First = exometer_slide:timestamp(), + create_conn(test), + create_conn(test2), + stats_series(fun stats_conn/2, [[{test, 2}, {test2, 5}], [{test, 5}, {test2, 1}], + [{test, 10}]]), + Last = last_ts(First, 5), + R = range(First, Last, 5), + simple_details(get_conn(test, R), recv_oct, 10, R), + simple_details(get_conn(test2, R), recv_oct, 1, R), + delete_conn(test), + delete_conn(test2), + timer:sleep(1150), + assert_list([], rabbit_mgmt_db:get_all_connections(R)), + ok. + +fine_stats_aggregation_test(Config) -> + ok = rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, fine_stats_aggregation_test1, [Config]). + +fine_stats_aggregation_test1(_Config) -> + [rabbit_mgmt_metrics_collector:override_lookups(T, [{exchange, fun dummy_lookup/1}, + {queue, fun dummy_lookup/1}]) + || {T, _} <- ?CORE_TABLES], + First = exometer_slide:timestamp(), + create_conn(test), + create_conn(test2), + create_ch(ch1, [{connection, pid(test)}]), + create_ch(ch2, [{connection, pid(test2)}]), + %% Publish differences + channel_series(ch1, [{[{x, 50}], [{q1, x, 15}, {q2, x, 2}], [{q1, 5}, {q2, 5}]}, + {[{x, 25}], [{q1, x, 10}, {q2, x, 3}], [{q1, -2}, {q2, -3}]}, + {[{x, 25}], [{q1, x, 25}, {q2, x, 5}], [{q1, -1}, {q2, -1}]}]), + channel_series(ch2, [{[{x, 5}], [{q1, x, 15}, {q2, x, 1}], []}, + {[{x, 2}], [{q1, x, 10}, {q2, x, 2}], []}, + {[{x, 3}], [{q1, x, 25}, {q2, x, 2}], []}]), + timer:sleep(1150), + + fine_stats_aggregation_test0(true, First), + delete_q(q2), + timer:sleep(1150), + fine_stats_aggregation_test0(false, First), + delete_ch(ch1), + delete_ch(ch2), + delete_conn(test), + delete_conn(test2), + delete_x(x), + delete_v(<<"/">>), + [rabbit_mgmt_metrics_collector:reset_lookups(T) || {T, _} <- ?CORE_TABLES], + ok. + +fine_stats_aggregation_test0(Q2Exists, First) -> + Last = exometer_slide:timestamp(), + R = range(First, Last, 1), + Ch1 = get_ch(ch1, R), + + Ch2 = get_ch(ch2, R), + X = get_x(x, R), + Q1 = get_q(q1, R), + V = get_vhost(R), + O = get_overview(R), + assert_fine_stats(m, publish, 100, Ch1, R), + assert_fine_stats(m, publish, 10, Ch2, R), + assert_fine_stats(m, publish_in, 110, X, R), + assert_fine_stats(m, publish_out, 115, X, R), + assert_fine_stats(m, publish, 100, Q1, R), + assert_fine_stats(m, deliver_get, 2, Q1, R), + assert_fine_stats(m, deliver_get, 3, Ch1, R), + assert_fine_stats(m, publish, 110, V, R), + assert_fine_stats(m, deliver_get, 3, V, R), + assert_fine_stats(m, publish, 110, O, R), + assert_fine_stats(m, deliver_get, 3, O, R), + assert_fine_stats({pub, x}, publish, 100, Ch1, R), + assert_fine_stats({pub, x}, publish, 10, Ch2, R), + assert_fine_stats({in, ch1}, publish, 100, X, R), + assert_fine_stats({in, ch2}, publish, 10, X, R), + assert_fine_stats({out, q1}, publish, 100, X, R), + assert_fine_stats({in, x}, publish, 100, Q1, R), + assert_fine_stats({del, ch1}, deliver_get, 2, Q1, R), + assert_fine_stats({del, q1}, deliver_get, 2, Ch1, R), + case Q2Exists of + true -> Q2 = get_q(q2, R), + assert_fine_stats(m, publish, 15, Q2, R), + assert_fine_stats(m, deliver_get, 1, Q2, R), + assert_fine_stats({out, q2}, publish, 15, X, R), + assert_fine_stats({in, x}, publish, 15, Q2, R), + assert_fine_stats({del, ch1}, deliver_get, 1, Q2, R), + assert_fine_stats({del, q2}, deliver_get, 1, Ch1, R); + false -> assert_fine_stats_neg({out, q2}, X), + assert_fine_stats_neg({del, q2}, Ch1) + end, + ok. + +fine_stats_aggregation_time_test(Config) -> + %% trace_fun(Config, [{rabbit_mgmt_db, get_data_from_nodes}]), + ok = rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, fine_stats_aggregation_time_test1, [Config]). + +fine_stats_aggregation_time_test1(_Config) -> + [rabbit_mgmt_metrics_collector:override_lookups(T, [{exchange, fun dummy_lookup/1}, + {queue, fun dummy_lookup/1}]) + || {T, _} <- ?CORE_TABLES], + First = exometer_slide:timestamp(), + create_ch(ch), + channel_series(ch, [{[{x, 50}], [{q, x, 15}], [{q, 5}]}, + {[{x, 25}], [{q, x, 10}], [{q, 5}]}, + {[{x, 25}], [{q, x, 25}], [{q, 10}]}]), + timer:sleep(1150), + Last = exometer_slide:timestamp(), + + channel_series(ch, [{[{x, 10}], [{q, x, 5}], [{q, 2}]}]), + Next = exometer_slide:timestamp(), + + + R1 = range(First, Last, 1), + assert_fine_stats(m, publish, 100, get_ch(ch, R1), R1), + assert_fine_stats(m, publish, 50, get_q(q, R1), R1), + assert_fine_stats(m, deliver_get, 20, get_q(q, R1), R1), + + + R2 = range(Last, Next, 1), + assert_fine_stats(m, publish, 110, get_ch(ch, R2), R2), + assert_fine_stats(m, publish, 55, get_q(q, R2), R2), + assert_fine_stats(m, deliver_get, 22, get_q(q, R2), R2), + + delete_q(q), + delete_ch(ch), + delete_x(x), + delete_v(<<"/">>), + + [rabbit_mgmt_metrics_collector:reset_lookups(T) || {T, _} <- ?CORE_TABLES], + ok. + +assert_fine_stats(m, Type, N, Obj, R) -> + Act = pget(message_stats, Obj), + simple_details(Act, Type, N, R); +assert_fine_stats({T2, Name}, Type, N, Obj, R) -> + Act = find_detailed_stats(Name, pget(expand(T2), Obj)), + simple_details(Act, Type, N, R). + +assert_fine_stats_neg({T2, Name}, Obj) -> + detailed_stats_absent(Name, pget(expand(T2), Obj)). + + %% {{{resource,<<"/">>,queue,<<"test_lazy">>}, + %% <0.953.0>,<<"amq.ctag-sPlBtNl8zwIGkYhNjJrATA">>}, + %% false,true,0,[]}, +all_consumers_test(Config) -> + ok = rabbit_ct_broker_helpers:rpc(Config, 0, ?MODULE, all_consumers_test1, [Config]). + +all_consumers_test1(_Config) -> + %% Tests that we can list all the consumers while channels are in the process of + %% being deleted. Thus, channel details might be missing. + %% `merge_channel_into_obj` is also used when listing queues, which might now report + %% empty channel details while the queue is being deleted. But will not crash. + [rabbit_mgmt_metrics_collector:override_lookups(T, [{exchange, fun dummy_lookup/1}, + {queue, fun dummy_lookup/1}]) + || {T, _} <- ?CORE_TABLES], + create_cons(q1, ch1, <<"ctag">>, false, true, 0, []), + timer:sleep(1150), + [Consumer] = rabbit_mgmt_db:get_all_consumers(), + [] = proplists:get_value(channel_details, Consumer), + %% delete_cons(co), + [rabbit_mgmt_metrics_collector:reset_lookups(T) || {T, _} <- ?CORE_TABLES], + ok. +%%---------------------------------------------------------------------------- +%% Events in +%%---------------------------------------------------------------------------- + +create_conn(Name) -> + rabbit_core_metrics:connection_created(pid(Name), [{pid, pid(Name)}, + {name, a2b(Name)}]). + +create_ch(Name, Extra) -> + rabbit_core_metrics:channel_created(pid(Name), [{pid, pid(Name)}, + {name, a2b(Name)}] ++ Extra). +create_ch(Name) -> + create_ch(Name, []). + +create_cons(QName, ChName, Tag, Exclusive, AckRequired, PrefetchCount, Args) -> + rabbit_core_metrics:consumer_created(pid(ChName), Tag, Exclusive, + AckRequired, q(QName), + PrefetchCount, false, waiting, Args). + +stats_series(Fun, ListsOfPairs) -> + [begin + [Fun(Name, Msgs) || {Name, Msgs} <- List], + timer:sleep(1150) + end || List <- ListsOfPairs]. + +stats_q(Name, Msgs) -> + rabbit_core_metrics:queue_stats(q(Name), Msgs, Msgs, Msgs, Msgs). + +stats_conn(Name, Oct) -> + rabbit_core_metrics:connection_stats(pid(Name), Oct, Oct, Oct). + +channel_series(Name, ListOfStats) -> + [begin + stats_ch(Name, XStats, QXStats, QStats), + timer:sleep(1150) + end || {XStats, QXStats, QStats} <- ListOfStats]. + +stats_ch(Name, XStats, QXStats, QStats) -> + [rabbit_core_metrics:channel_stats(exchange_stats, publish, {pid(Name), x(XName)}, N) + || {XName, N} <- XStats], + [rabbit_core_metrics:channel_stats(queue_exchange_stats, publish, + {pid(Name), {q(QName), x(XName)}}, N) + || {QName, XName, N} <- QXStats], + [rabbit_core_metrics:channel_stats(queue_stats, deliver_no_ack, {pid(Name), q(QName)}, N) + || {QName, N} <- QStats], + ok. + +delete_q(Name) -> + rabbit_core_metrics:queue_deleted(q(Name)), + rabbit_event:notify(queue_deleted, [{name, q(Name)}]). + +delete_conn(Name) -> + Pid = pid_del(Name), + rabbit_core_metrics:connection_closed(Pid), + rabbit_event:notify(connection_closed, [{pid, Pid}]). + +delete_cons(QName, ChName, Tag) -> + Pid = pid_del(ChName), + rabbit_core_metrics:consumer_deleted(Pid, Tag, q(QName)), + rabbit_event:notify(consumer_deleted, [{channel, Pid}, + {queue, q(QName)}, + {consumer_tag, Tag}]). + +delete_ch(Name) -> + Pid = pid_del(Name), + rabbit_core_metrics:channel_closed(Pid), + rabbit_core_metrics:channel_exchange_down({Pid, x(x)}), + rabbit_event:notify(channel_closed, [{pid, Pid}]). + +delete_x(Name) -> + rabbit_event:notify(exchange_deleted, [{name, x(Name)}]). + +delete_v(Name) -> + rabbit_event:notify(vhost_deleted, [{name, Name}]). + +%%---------------------------------------------------------------------------- +%% Events out +%%---------------------------------------------------------------------------- + +range(F, L, I) -> + R = #range{first = F, last = L, incr = I * 1000}, + {R, R, R, R}. + +get_x(Name, Range) -> + [X] = rabbit_mgmt_db:augment_exchanges([x2(Name)], Range, full), + X. + +get_q(Name, Range) -> + [Q] = rabbit_mgmt_db:augment_queues([q2(Name)], Range, full), + Q. + +get_vhost(Range) -> + [VHost] = rabbit_mgmt_db:augment_vhosts([[{name, <<"/">>}]], Range), + VHost. + +get_conn(Name, Range) -> rabbit_mgmt_db:get_connection(a2b(Name), Range). +get_ch(Name, Range) -> rabbit_mgmt_db:get_channel(a2b(Name), Range). + +get_overview(Range) -> rabbit_mgmt_db:get_overview(Range). +get_overview_q(Range) -> pget(queue_totals, get_overview(Range)). + +details0(R, AR, A, L) -> + [{rate, R}, + {samples, [[{sample, S}, {timestamp, T}] || {T, S} <- L]}, + {avg_rate, AR}, + {avg, A}]. + +simple_details(Result, Thing, N, {#range{first = First, last = Last}, _, _, _} = _R) -> + ?assertEqual(N, proplists:get_value(Thing, Result)), + Details = proplists:get_value(atom_suffix(Thing, "_details"), Result), + ?assert(0 =/= proplists:get_value(rate, Details)), + Samples = proplists:get_value(samples, Details), + TSs = [proplists:get_value(timestamp, S) || S <- Samples], + ?assert(First =< lists:min(TSs)), + ?assert(Last >= lists:max(TSs)). + +atom_suffix(Atom, Suffix) -> + list_to_atom(atom_to_list(Atom) ++ Suffix). + +find_detailed_stats(Name, List) -> + [S] = filter_detailed_stats(Name, List), + S. + +detailed_stats_absent(Name, List) -> + [] = filter_detailed_stats(Name, List). + +filter_detailed_stats(Name, List) -> + lists:foldl(fun(L, Acc) -> + {[{stats, Stats}], [{_, Details}]} = + lists:partition(fun({K, _}) -> K == stats end, L), + case (pget(name, Details) =:= a2b(Name)) of + true -> + [Stats | Acc]; + false -> + Acc + end + end, [], List). + +expand(in) -> incoming; +expand(out) -> outgoing; +expand(del) -> deliveries; +expand(pub) -> publishes. + +%%---------------------------------------------------------------------------- +%% Util +%%---------------------------------------------------------------------------- + +x(Name) -> rabbit_misc:r(<<"/">>, exchange, a2b(Name)). +x2(Name) -> q2(Name). +q(Name) -> rabbit_misc:r(<<"/">>, queue, a2b(Name)). +q2(Name) -> [{name, a2b(Name)}, + {pid, self()}, % fake a local pid + {vhost, <<"/">>}]. + +pid(Name) -> + case get({pid, Name}) of + undefined -> P = spawn(fun() -> ok end), + put({pid, Name}, P), + P; + Pid -> Pid + end. + +pid_del(Name) -> + Pid = pid(Name), + erase({pid, Name}), + Pid. + +a2b(A) -> list_to_binary(atom_to_list(A)). + +dummy_lookup(_Thing) -> true. diff --git a/deps/rabbitmq_management/test/rabbit_mgmt_test_unit_SUITE.erl b/deps/rabbitmq_management/test/rabbit_mgmt_test_unit_SUITE.erl new file mode 100644 index 0000000000..32194bd5c8 --- /dev/null +++ b/deps/rabbitmq_management/test/rabbit_mgmt_test_unit_SUITE.erl @@ -0,0 +1,88 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2007-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(rabbit_mgmt_test_unit_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-compile(export_all). + +all() -> + [ + {group, parallel_tests} + ]. + +groups() -> + [ + {parallel_tests, [parallel], [ + tokenise_test, + pack_binding_test, + path_prefix_test + ]} + ]. + +%% ------------------------------------------------------------------- +%% Setup/teardown. +%% ------------------------------------------------------------------- + +init_per_group(_, Config) -> + Config. + +end_per_group(_, Config) -> + Config. + +%% ------------------------------------------------------------------- +%% Test cases. +%% ------------------------------------------------------------------- + +tokenise_test(_Config) -> + [] = rabbit_mgmt_format:tokenise(""), + ["foo"] = rabbit_mgmt_format:tokenise("foo"), + ["foo", "bar"] = rabbit_mgmt_format:tokenise("foo~bar"), + ["foo", "", "bar"] = rabbit_mgmt_format:tokenise("foo~~bar"), + ok. + +pack_binding_test(_Config) -> + assert_binding(<<"~">>, + <<"">>, []), + assert_binding(<<"foo">>, + <<"foo">>, []), + assert_binding(<<"foo%7Ebar%2Fbash">>, + <<"foo~bar/bash">>, []), + assert_binding(<<"foo%7Ebar%7Ebash">>, + <<"foo~bar~bash">>, []), + ok. + +path_prefix_test(_Config) -> + Got0 = rabbit_mgmt_util:get_path_prefix(), + ?assertEqual("", Got0), + + Pfx0 = "/custom-prefix", + application:set_env(rabbitmq_management, path_prefix, Pfx0), + Got1 = rabbit_mgmt_util:get_path_prefix(), + ?assertEqual(Pfx0, Got1), + + Pfx1 = "custom-prefix", + application:set_env(rabbitmq_management, path_prefix, Pfx1), + Got2 = rabbit_mgmt_util:get_path_prefix(), + ?assertEqual(Pfx0, Got2), + + Pfx2 = <<"custom-prefix">>, + application:set_env(rabbitmq_management, path_prefix, Pfx2), + Got3 = rabbit_mgmt_util:get_path_prefix(), + ?assertEqual(Pfx0, Got3). + +%%-------------------------------------------------------------------- + +assert_binding(Packed, Routing, Args) -> + case rabbit_mgmt_format:pack_binding_props(Routing, Args) of + Packed -> + ok; + Act -> + throw({pack, Routing, Args, expected, Packed, got, Act}) + end. diff --git a/deps/rabbitmq_management/test/stats_SUITE.erl b/deps/rabbitmq_management/test/stats_SUITE.erl new file mode 100644 index 0000000000..99de1a532e --- /dev/null +++ b/deps/rabbitmq_management/test/stats_SUITE.erl @@ -0,0 +1,178 @@ +%% This Source Code Form is subject to the terms of the Mozilla Public +%% License, v. 2.0. If a copy of the MPL was not distributed with this +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%% +%% Copyright (c) 2016-2020 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(stats_SUITE). + +-include_lib("proper/include/proper.hrl"). +-include_lib("rabbitmq_management_agent/include/rabbit_mgmt_records.hrl"). + +-compile(export_all). + +-import(rabbit_misc, [pget/2]). + +all() -> + [ + {group, parallel_tests} + ]. + +groups() -> + [ + {parallel_tests, [parallel], [ + format_range_empty_slide, + format_range, + format_range_missing_middle, + format_range_missing_middle_drop, + format_range_incremental_pad, + format_range_incremental_pad2, + format_range_constant + ]} + ]. + +%% ------------------------------------------------------------------- +%% Testsuite setup/teardown. +%% ------------------------------------------------------------------- +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + ok. + +init_per_group(_, Config) -> + Config. + +end_per_group(_, _Config) -> + ok. + +init_per_testcase(_, Config) -> + Config. + +end_per_testcase(_, _Config) -> + ok. + +format_range_empty_slide(_Config) -> + Slide = exometer_slide:new(0, 60000, [{incremental, true}, + {interval, 5000}]), + Range = #range{first = 100, last = 200, incr = 10}, + Table = vhost_stats_fine_stats, + SamplesFun = fun() -> [Slide] end, + Got = rabbit_mgmt_stats:format_range(Range, 200, Table, 0, fun() -> + ok + end, + SamplesFun), + PublishDetails = proplists:get_value(publish_details, Got), + Samples = proplists:get_value(samples, PublishDetails), + 11 = length(Samples). + +format_range(_Config) -> + Slide = exometer_slide:new(0, 60000, [{incremental, true}, + {interval, 10}]), + Slide1 = exometer_slide:add_element(197, {10}, Slide), + Range = #range{first = 100, last = 200, incr = 10}, + Table = queue_stats_publish, + SamplesFun = fun() -> [Slide1] end, + Got = rabbit_mgmt_stats:format_range(Range, 200, Table, 0, fun() -> + ok + end, + SamplesFun), + PublishDetails = proplists:get_value(publish_details, Got), + [S1, S2 | _Rest] = Samples = proplists:get_value(samples, PublishDetails), + 0 = proplists:get_value(sample, S2), + 10 = proplists:get_value(sample, S1), + 11 = length(Samples). + +format_range_missing_middle(_Config) -> + Slide = exometer_slide:new(0, 60000, [{incremental, false}, + {interval, 10}]), + Slide1 = exometer_slide:add_element(167, {10}, Slide), + Slide2 = exometer_slide:add_element(197, {5}, Slide1), + Range = #range{first = 100, last = 200, incr = 10}, + Table = queue_stats_publish, + SamplesFun = fun() -> [Slide2] end, + Got = rabbit_mgmt_stats:format_range(Range, 200, Table, 0, fun() -> + ok + end, + SamplesFun), + PublishDetails = proplists:get_value(publish_details, Got), + [S1, S2, S3, S4 | Rest] = Samples = proplists:get_value(samples, PublishDetails), + 10 = proplists:get_value(sample, S4), + 10 = proplists:get_value(sample, S3), + 10 = proplists:get_value(sample, S2), + 5 = proplists:get_value(sample, S1), + true = lists:all(fun(P) -> + 0 == proplists:get_value(sample, P) + end, Rest), + 11 = length(Samples). + +format_range_missing_middle_drop(_Config) -> + Slide = exometer_slide:new(0, 60000, [{incremental, false}, + {max_n, 12}, + {interval, 10}]), + Slide1 = exometer_slide:add_element(167, {10}, Slide), + Slide2 = exometer_slide:add_element(197, {10}, Slide1), + Range = #range{first = 100, last = 200, incr = 10}, + Table = queue_stats_publish, + SamplesFun = fun() -> [Slide2] end, + Got = rabbit_mgmt_stats:format_range(Range, 200, Table, 0, fun() -> + ok + end, + SamplesFun), + PublishDetails = proplists:get_value(publish_details, Got), + [S1, S2, S3, S4 | Rest] = Samples = proplists:get_value(samples, PublishDetails), + 10 = proplists:get_value(sample, S4), + 10 = proplists:get_value(sample, S3), + 10 = proplists:get_value(sample, S2), + 10 = proplists:get_value(sample, S1), + true = lists:all(fun(P) -> + 0 == proplists:get_value(sample, P) + end, Rest), + 11 = length(Samples). + +format_range_incremental_pad(_Config) -> + Slide = exometer_slide:new(0, 10, [{incremental, true}, + {interval, 5}]), + Slide1 = exometer_slide:add_element(15, {3}, Slide), + Range = #range{first = 5, last = 15, incr = 5}, + Table = queue_stats_publish, + SamplesFun = fun() -> [Slide1] end, + Got = rabbit_mgmt_stats:format_range(Range, 0, Table, 0, fun() -> ok end, + SamplesFun), + PublishDetails = proplists:get_value(publish_details, Got), + [{3, 15}, {0,10}, {0, 5}] = [{pget(sample, V), pget(timestamp, V)} + || V <- pget(samples, PublishDetails)]. + +format_range_incremental_pad2(_Config) -> + Slide = exometer_slide:new(0, 10, [{incremental, true}, + {interval, 5}]), + {_, Slide1} = lists:foldl(fun (V, {TS, S}) -> + {TS + 5, exometer_slide:add_element(TS, {V}, S)} + end, {5, Slide}, [1,1,0,0,0,1]), + Range = #range{first = 10, last = 30, incr = 5}, + Table = queue_stats_publish, + SamplesFun = fun() -> [Slide1] end, + Got = rabbit_mgmt_stats:format_range(Range, 0, Table, 0, fun() -> ok end, + SamplesFun), + PublishDetails = pget(publish_details, Got), + [{3, 30}, {2, 25}, {2, 20}, {2, 15}, {2, 10}] = + [{pget(sample, V), pget(timestamp, V)} + || V <- pget(samples, PublishDetails)]. + +format_range_constant(_Config) -> + Now = 0, + Slide = exometer_slide:new(0, 20, [{incremental, false}, + {max_n, 100}, + {interval, 5}]), + Slide1 = lists:foldl(fun(N, Acc) -> + exometer_slide:add_element(Now + N, {5}, Acc) + end, Slide, lists:seq(0, 100, 5)), + Range = #range{first = 5, last = 50, incr = 5}, + Table = queue_stats_publish, + SamplesFun = fun() -> [Slide1] end, + Got = rabbit_mgmt_stats:format_range(Range, 0, Table, 0, fun() -> ok end, + SamplesFun), + 5 = proplists:get_value(publish, Got), + PD = proplists:get_value(publish_details, Got), + 0.0 = proplists:get_value(rate, PD). |