summaryrefslogtreecommitdiff
path: root/deps/rabbitmq_management/test
diff options
context:
space:
mode:
Diffstat (limited to 'deps/rabbitmq_management/test')
-rw-r--r--deps/rabbitmq_management/test/cache_SUITE.erl109
-rw-r--r--deps/rabbitmq_management/test/clustering_SUITE.erl874
-rw-r--r--deps/rabbitmq_management/test/clustering_prop_SUITE.erl280
-rw-r--r--deps/rabbitmq_management/test/config_schema_SUITE.erl55
-rw-r--r--deps/rabbitmq_management/test/config_schema_SUITE_data/certs/cacert.pem1
-rw-r--r--deps/rabbitmq_management/test/config_schema_SUITE_data/certs/cert.pem1
-rw-r--r--deps/rabbitmq_management/test/config_schema_SUITE_data/certs/key.pem1
-rw-r--r--deps/rabbitmq_management/test/config_schema_SUITE_data/rabbit-mgmt/access.log0
-rw-r--r--deps/rabbitmq_management/test/config_schema_SUITE_data/rabbitmq_management.snippets525
-rw-r--r--deps/rabbitmq_management/test/listener_config_SUITE.erl135
-rw-r--r--deps/rabbitmq_management/test/rabbit_mgmt_http_SUITE.erl3545
-rw-r--r--deps/rabbitmq_management/test/rabbit_mgmt_http_health_checks_SUITE.erl399
-rw-r--r--deps/rabbitmq_management/test/rabbit_mgmt_only_http_SUITE.erl1716
-rw-r--r--deps/rabbitmq_management/test/rabbit_mgmt_rabbitmqadmin_SUITE.erl512
-rw-r--r--deps/rabbitmq_management/test/rabbit_mgmt_runtime_parameters_util.erl63
-rw-r--r--deps/rabbitmq_management/test/rabbit_mgmt_stats_SUITE.erl458
-rw-r--r--deps/rabbitmq_management/test/rabbit_mgmt_test_db_SUITE.erl469
-rw-r--r--deps/rabbitmq_management/test/rabbit_mgmt_test_unit_SUITE.erl88
-rw-r--r--deps/rabbitmq_management/test/stats_SUITE.erl178
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).