Explorar el Código

feat: obfuscate sensitive values default_password

Zhongwen Deng hace 3 años
padre
commit
5223c3ee61

+ 1 - 1
apps/emqx/rebar.config

@@ -29,7 +29,7 @@
     {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.1"}}},
     {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.12.3"}}},
     {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}},
-    {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.27.2"}}},
+    {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.27.3"}}},
     {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}},
     {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}},
     {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.18.0"}}}

+ 11 - 6
apps/emqx/src/emqx_config.erl

@@ -26,6 +26,7 @@
     check_config/2,
     fill_defaults/1,
     fill_defaults/2,
+    fill_defaults/3,
     save_configs/5,
     save_to_app_env/1,
     save_to_config_map/2,
@@ -246,7 +247,7 @@ get_default_value([RootName | _] = KeyPath) ->
     case find_raw([RootName]) of
         {ok, RawConf} ->
             RawConf1 = emqx_map_lib:deep_remove(BinKeyPath, #{bin(RootName) => RawConf}),
-            try fill_defaults(get_schema_mod(RootName), RawConf1) of
+            try fill_defaults(get_schema_mod(RootName), RawConf1, #{}) of
                 FullConf ->
                     case emqx_map_lib:deep_find(BinKeyPath, FullConf) of
                         {not_found, _, _} -> {error, no_default_value};
@@ -360,15 +361,18 @@ check_config(SchemaMod, RawConf, Opts0) ->
         hocon_tconf:map_translate(SchemaMod, RawConf, Opts),
     {AppEnvs, emqx_map_lib:unsafe_atom_key_map(CheckedConf)}.
 
--spec fill_defaults(raw_config()) -> map().
 fill_defaults(RawConf) ->
+    fill_defaults(RawConf, #{}).
+
+-spec fill_defaults(raw_config(), hocon_tconf:opts()) -> map().
+fill_defaults(RawConf, Opts) ->
     RootNames = get_root_names(),
     maps:fold(
         fun(Key, Conf, Acc) ->
             SubMap = #{Key => Conf},
             WithDefaults =
                 case lists:member(Key, RootNames) of
-                    true -> fill_defaults(get_schema_mod(Key), SubMap);
+                    true -> fill_defaults(get_schema_mod(Key), SubMap, Opts);
                     false -> SubMap
                 end,
             maps:merge(Acc, WithDefaults)
@@ -377,12 +381,13 @@ fill_defaults(RawConf) ->
         RawConf
     ).
 
--spec fill_defaults(module(), raw_config()) -> map().
-fill_defaults(SchemaMod, RawConf) ->
+-spec fill_defaults(module(), raw_config(), hocon_tconf:opts()) -> map().
+fill_defaults(SchemaMod, RawConf, Opts0) ->
+    Opts = maps:merge(#{required => false, only_fill_defaults => true}, Opts0),
     hocon_tconf:check_plain(
         SchemaMod,
         RawConf,
-        #{required => false, only_fill_defaults => true},
+        Opts,
         root_names_from_conf(RawConf)
     ).
 

+ 1 - 1
apps/emqx/src/emqx_listeners.erl

@@ -88,7 +88,7 @@ do_list_raw() ->
     Key = <<"listeners">>,
     Raw = emqx_config:get_raw([Key], #{}),
     SchemaMod = emqx_config:get_schema_mod(Key),
-    #{Key := RawWithDefault} = emqx_config:fill_defaults(SchemaMod, #{Key => Raw}),
+    #{Key := RawWithDefault} = emqx_config:fill_defaults(SchemaMod, #{Key => Raw}, #{}),
     Listeners = maps:to_list(RawWithDefault),
     lists:flatmap(fun format_raw_listeners/1, Listeners).
 

+ 1 - 1
apps/emqx_bridge/src/emqx_bridge_api.erl

@@ -532,7 +532,7 @@ format_metrics(#{
 
 fill_defaults(Type, RawConf) ->
     PackedConf = pack_bridge_conf(Type, RawConf),
-    FullConf = emqx_config:fill_defaults(emqx_bridge_schema, PackedConf),
+    FullConf = emqx_config:fill_defaults(emqx_bridge_schema, PackedConf, #{}),
     unpack_bridge_conf(Type, FullConf).
 
 pack_bridge_conf(Type, RawConf) ->

+ 3 - 3
apps/emqx_dashboard/i18n/emqx_dashboard_schema.conf

@@ -19,7 +19,7 @@ but use the same port."""
     desc {
       en: """How often to update metrics displayed in the dashboard.<br/>
 Note: `sample_interval` should be a divisor of 60."""
-      zh: """更新仪表板中显示的指标的时间间隔。"""
+      zh: """更新仪表板中显示的指标的时间间隔。必须小于60,且被60的整除。"""
     }
   }
   token_expired_time {
@@ -135,7 +135,7 @@ Note: `sample_interval` should be a divisor of 60."""
   bind {
     desc {
       en: "Port without IP(18083) or port with specified IP(127.0.0.1:18083)."
-      zh: "监听的地址与端口"
+      zh: "监听的地址与端口,在dashboard更新此配置时,会重启dashboard服务。"
     }
     label {
       en: "Bind"
@@ -180,7 +180,7 @@ its own from which a browser should permit loading resources."""
   i18n_lang {
     desc {
       en: "Internationalization language support."
-      zh: "多语言支持"
+      zh: "swagger多语言支持"
     }
     label {
       en: "I18n language"

+ 10 - 6
apps/emqx_dashboard/src/emqx_dashboard.erl

@@ -47,12 +47,10 @@
 %%--------------------------------------------------------------------
 
 start_listeners() ->
-    Listeners = emqx_conf:get([dashboard, listeners], []),
-    start_listeners(Listeners).
+    start_listeners(listeners()).
 
 stop_listeners() ->
-    Listeners = emqx_conf:get([dashboard, listeners], []),
-    stop_listeners(Listeners).
+    stop_listeners(listeners()).
 
 start_listeners(Listeners) ->
     {ok, _} = application:ensure_all_started(minirest),
@@ -155,10 +153,13 @@ apps() ->
     ].
 
 listeners(Listeners) ->
-    lists:map(fun({Protocol, Conf}) ->
+    lists:map(
+        fun({Protocol, Conf}) ->
             {Conf1, Bind} = ip_port(Conf),
             {listener_name(Protocol, Conf1), Protocol, Bind, ranch_opts(Conf1)}
-        end, maps:to_list(Listeners)).
+        end,
+        maps:to_list(Listeners)
+    ).
 
 ip_port(Opts) -> ip_port(maps:take(bind, Opts), Opts).
 
@@ -268,3 +269,6 @@ i18n_file() ->
         undefined -> emqx:etc_file("i18n.conf");
         {ok, File} -> File
     end.
+
+listeners() ->
+    emqx_conf:get([dashboard, listeners], []).

+ 20 - 29
apps/emqx_dashboard/src/emqx_dashboard_schema.erl

@@ -32,7 +32,7 @@ fields("dashboard") ->
         {listeners,
             sc(
                 ref("listeners"),
-                #{ desc => ?DESC(listeners)}
+                #{desc => ?DESC(listeners)}
             )},
         {default_username, fun default_username/1},
         {default_password, fun default_password/1},
@@ -146,38 +146,26 @@ bind(required) -> true;
 bind(desc) -> ?DESC(bind);
 bind(_) -> undefined.
 
-default_username(type) -> string();
+default_username(type) -> binary();
 default_username(default) -> "admin";
 default_username(required) -> true;
-default_username(desc) ->  ?DESC(default_username);
+default_username(desc) -> ?DESC(default_username);
 default_username('readOnly') -> true;
 default_username(_) -> undefined.
 
-default_password(type) ->
-    string();
-default_password(default) ->
-    "public";
-default_password(required) ->
-    true;
-default_password('readOnly') ->
-    true;
-default_password(sensitive) ->
-    true;
-default_password(desc) ->
-    ?DESC(default_password);
-default_password(_) ->
-    undefined.
+default_password(type) -> binary();
+default_password(default) -> "public";
+default_password(required) -> true;
+default_password('readOnly') -> true;
+default_password(sensitive) -> true;
+default_password(desc) -> ?DESC(default_password);
+default_password(_) -> undefined.
 
-cors(type) ->
-    boolean();
-cors(default) ->
-    false;
-cors(required) ->
-    false;
-cors(desc) ->
-    ?DESC(cors);
-cors(_) ->
-    undefined.
+cors(type) -> boolean();
+cors(default) -> false;
+cors(required) -> false;
+cors(desc) -> ?DESC(cors);
+cors(_) -> undefined.
 
 i18n_lang(type) -> ?ENUM([en, zh]);
 i18n_lang(default) -> en;
@@ -187,8 +175,11 @@ i18n_lang(_) -> undefined.
 
 validate_sample_interval(Second) ->
     case Second >= 1 andalso Second =< 60 andalso (60 rem Second =:= 0) of
-        true -> ok;
-        false -> error({"Sample interval must be between 1 and 60 and be a divisor of 60.", Second})
+        true ->
+            ok;
+        false ->
+            Msg = "must be between 1 and 60 and be a divisor of 60.",
+            {error, Msg}
     end.
 
 sc(Type, Meta) -> hoconsc:mk(Type, Meta).

+ 24 - 7
apps/emqx_dashboard/src/emqx_dashboard_swagger.erl

@@ -138,7 +138,7 @@ fields(limit) ->
     [{limit, hoconsc:mk(range(1, ?MAX_ROW_LIMIT), Meta)}];
 fields(count) ->
     Meta = #{desc => <<"Results count.">>, required => true},
-    [{count, hoconsc:mk(range(0, inf), Meta)}];
+    [{count, hoconsc:mk(non_neg_integer(), Meta)}];
 fields(meta) ->
     fields(page) ++ fields(limit) ++ fields(count).
 
@@ -184,12 +184,14 @@ translate_req(Request, #{module := Module, path := Path, method := Method}, Chec
         NewBody = check_request_body(Request, Body, Module, CheckFun, hoconsc:is_schema(Body)),
         {ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}}
     catch
-        throw:{_, ValidErrors} ->
-            Msg = [
-                io_lib:format("~ts : ~p", [Key, Reason])
-             || {validation_error, #{path := Key, reason := Reason}} <- ValidErrors
-            ],
-            {400, 'BAD_REQUEST', iolist_to_binary(string:join(Msg, ","))}
+        throw:HoconError ->
+            Msg = serialize_hocon_error_msg(HoconError),
+            %Msg = [
+            %    io_lib:format("~ts : ~p", [Key -- "root.", Reason])
+            %    || {validation_error, #{path := Key, reason := Reason}} <- ValidErrors
+            % ],
+            % iolist_to_binary(string:join(Msg, ",")
+            {400, 'BAD_REQUEST', Msg}
     end.
 
 check_and_translate(Schema, Map, Opts) ->
@@ -808,3 +810,18 @@ to_ref(Mod, StructName, Acc, RefsAcc) ->
 
 schema_converter(Options) ->
     maps:get(schema_converter, Options, fun hocon_schema_to_spec/2).
+
+serialize_hocon_error_msg({_Schema, Errors}) ->
+    Msg = lists:map(fun hocon_error/1, Errors),
+    iolist_to_binary(string:join(Msg, ",")).
+
+hocon_error({validation_error, #{reason := #{exception := Exception}, path := Path}}) ->
+    io_lib:format("~ts: ~p", [sub_path(Path), Exception]);
+hocon_error({validation_error, #{reason := Reason, path := Path, value := Value}}) ->
+    io_lib:format("~ts: ~p ~p", [sub_path(Path), Value, Reason]);
+hocon_error({validation_error, #{reason := Reason, path := Path}}) ->
+    io_lib:format("~ts: ~p", [sub_path(Path), Reason]);
+hocon_error({translation_error, #{reason := Reason, value_path := Path}}) ->
+    io_lib:format("~ts: ~p", [sub_path(Path), Reason]).
+
+sub_path(Path) -> string:trim(Path, leading, "root.").

+ 82 - 45
apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl

@@ -19,11 +19,14 @@
 -compile(nowarn_export_all).
 -compile(export_all).
 
--import(emqx_common_test_http,
-        [ request_api/3
-        , request_api/5
-        , get_http_data/1
-        ]).
+-import(
+    emqx_common_test_http,
+    [
+        request_api/3,
+        request_api/5,
+        get_http_data/1
+    ]
+).
 
 -include_lib("eunit/include/eunit.hrl").
 -include_lib("emqx/include/emqx.hrl").
@@ -40,18 +43,19 @@
 -define(APP_DASHBOARD, emqx_dashboard).
 -define(APP_MANAGEMENT, emqx_management).
 
--define(OVERVIEWS, ['alarms/activated',
-                    'alarms/deactivated',
-                    banned,
-                    brokers,
-                    stats,
-                    metrics,
-                    listeners,
-                    clients,
-                    subscriptions,
-                    routes,
-                    plugins
-                   ]).
+-define(OVERVIEWS, [
+    'alarms/activated',
+    'alarms/deactivated',
+    banned,
+    brokers,
+    stats,
+    metrics,
+    listeners,
+    clients,
+    subscriptions,
+    routes,
+    plugins
+]).
 
 all() ->
     %% TODO: V5 API
@@ -66,8 +70,10 @@ end_suite(Apps) ->
     emqx_common_test_helpers:stop_apps(Apps ++ [emqx_dashboard]).
 
 init_per_suite(Config) ->
-    emqx_common_test_helpers:start_apps([emqx_management, emqx_dashboard],
-        fun set_special_configs/1),
+    emqx_common_test_helpers:start_apps(
+        [emqx_management, emqx_dashboard],
+        fun set_special_configs/1
+    ),
     Config.
 
 end_per_suite(_Config) ->
@@ -76,8 +82,10 @@ end_per_suite(_Config) ->
 
 set_special_configs(emqx_management) ->
     Listeners = #{http => #{port => 8081}},
-    Config = #{listeners => Listeners,
-               applications => [#{id => "admin", secret => "public"}]},
+    Config = #{
+        listeners => Listeners,
+        applications => [#{id => "admin", secret => "public"}]
+    },
     emqx_config:put([emqx_management], Config),
     ok;
 set_special_configs(emqx_dashboard) ->
@@ -89,8 +97,16 @@ set_special_configs(_) ->
 t_overview(_) ->
     mnesia:clear_table(?ADMIN),
     emqx_dashboard_admin:add_user(<<"admin">>, <<"public">>, <<"simple_description">>),
-    [?assert(request_dashboard(get, api_path(erlang:atom_to_list(Overview)),
-                               auth_header_())) || Overview <- ?OVERVIEWS].
+    [
+        ?assert(
+            request_dashboard(
+                get,
+                api_path(erlang:atom_to_list(Overview)),
+                auth_header_()
+            )
+        )
+     || Overview <- ?OVERVIEWS
+    ].
 
 t_admins_add_delete(_) ->
     mnesia:clear_table(?ADMIN),
@@ -102,9 +118,11 @@ t_admins_add_delete(_) ->
     ok = emqx_dashboard_admin:remove_user(<<"username1">>),
     Users = emqx_dashboard_admin:all_users(),
     ?assertEqual(1, length(Users)),
-    ok = emqx_dashboard_admin:change_password(<<"username">>,
-                                              <<"password">>,
-                                              <<"pwd">>),
+    ok = emqx_dashboard_admin:change_password(
+        <<"username">>,
+        <<"password">>,
+        <<"pwd">>
+    ),
     timer:sleep(10),
     Header = auth_header_(<<"username">>, <<"pwd">>),
     ?assert(request_dashboard(get, api_path("brokers"), Header)),
@@ -117,25 +135,38 @@ t_rest_api(_Config) ->
     Desc = <<"administrator">>,
     emqx_dashboard_admin:add_user(<<"admin">>, <<"public">>, Desc),
     {ok, 200, Res0} = http_get(["users"]),
-    ?assertEqual([#{<<"username">> => <<"admin">>,
-                    <<"description">> => <<"administrator">>}], get_http_data(Res0)),
+    ?assertEqual(
+        [
+            #{
+                <<"username">> => <<"admin">>,
+                <<"description">> => <<"administrator">>
+            }
+        ],
+        get_http_data(Res0)
+    ),
     {ok, 200, _} = http_put(["users", "admin"], #{<<"description">> => <<"a_new_description">>}),
-    {ok, 200, _} = http_post(["users"], #{<<"username">> => <<"usera">>,
-                                          <<"password">> => <<"passwd">>,
-                                          <<"description">> => Desc}),
+    {ok, 200, _} = http_post(["users"], #{
+        <<"username">> => <<"usera">>,
+        <<"password">> => <<"passwd">>,
+        <<"description">> => Desc
+    }),
     {ok, 204, _} = http_delete(["users", "usera"]),
     {ok, 404, _} = http_delete(["users", "usera"]),
-    {ok, 204, _} = http_put( ["users", "admin", "change_pwd"]
-                           , #{<<"old_pwd">> => <<"public">>,
-                               <<"new_pwd">> => <<"newpwd">>}),
+    {ok, 204, _} = http_put(
+        ["users", "admin", "change_pwd"],
+        #{
+            <<"old_pwd">> => <<"public">>,
+            <<"new_pwd">> => <<"newpwd">>
+        }
+    ),
     mnesia:clear_table(?ADMIN),
     emqx_dashboard_admin:add_user(<<"admin">>, <<"public">>, <<"administrator">>),
     ok.
 
 t_cli(_Config) ->
-    [mria:dirty_delete(?ADMIN, Admin) ||  Admin <- mnesia:dirty_all_keys(?ADMIN)],
+    [mria:dirty_delete(?ADMIN, Admin) || Admin <- mnesia:dirty_all_keys(?ADMIN)],
     emqx_dashboard_cli:admins(["add", "username", "password"]),
-    [#?ADMIN{ username = <<"username">>, pwdhash = <<Salt:4/binary, Hash/binary>>}] =
+    [#?ADMIN{username = <<"username">>, pwdhash = <<Salt:4/binary, Hash/binary>>}] =
         emqx_dashboard_admin:lookup_user(<<"username">>),
     ?assertEqual(Hash, crypto:hash(sha256, <<Salt/binary, <<"password">>/binary>>)),
     emqx_dashboard_cli:admins(["passwd", "username", "newpassword"]),
@@ -153,8 +184,10 @@ t_lookup_by_username_jwt(_Config) ->
     User = bin(["user-", integer_to_list(random_num())]),
     Pwd = bin(integer_to_list(random_num())),
     emqx_dashboard_token:sign(User, Pwd),
-    ?assertMatch([#?ADMIN_JWT{username = User}],
-                 emqx_dashboard_token:lookup_by_username(User)),
+    ?assertMatch(
+        [#?ADMIN_JWT{username = User}],
+        emqx_dashboard_token:lookup_by_username(User)
+    ),
     ok = emqx_dashboard_token:destroy_by_username(User),
     %% issue a gen_server call to sync the async destroy gen_server cast
     ok = gen_server:call(emqx_dashboard_token, dummy, infinity),
@@ -168,8 +201,10 @@ t_clean_expired_jwt(_Config) ->
     [#?ADMIN_JWT{username = User, exptime = ExpTime}] =
         emqx_dashboard_token:lookup_by_username(User),
     ok = emqx_dashboard_token:clean_expired_jwt(_Now1 = ExpTime),
-    ?assertMatch([#?ADMIN_JWT{username = User}],
-                 emqx_dashboard_token:lookup_by_username(User)),
+    ?assertMatch(
+        [#?ADMIN_JWT{username = User}],
+        emqx_dashboard_token:lookup_by_username(User)
+    ),
     ok = emqx_dashboard_token:clean_expired_jwt(_Now2 = ExpTime + 1),
     ?assertMatch([], emqx_dashboard_token:lookup_by_username(User)),
     ok.
@@ -201,13 +236,14 @@ request_dashboard(Method, Url, Auth) ->
 request_dashboard(Method, Url, QueryParams, Auth) ->
     Request = {Url ++ "?" ++ QueryParams, [Auth]},
     do_request_dashboard(Method, Request).
-do_request_dashboard(Method, Request)->
+do_request_dashboard(Method, Request) ->
     ct:pal("Method: ~p, Request: ~p", [Method, Request]),
     case httpc:request(Method, Request, [], []) of
         {error, socket_closed_remotely} ->
             {error, socket_closed_remotely};
-        {ok, {{"HTTP/1.1", Code, _}, _Headers, Return} }
-          when Code >= 200 andalso Code =< 299 ->
+        {ok, {{"HTTP/1.1", Code, _}, _Headers, Return}} when
+            Code >= 200 andalso Code =< 299
+        ->
             {ok, Return};
         {ok, {Reason, _, _}} ->
             {error, Reason}
@@ -218,10 +254,11 @@ auth_header_() ->
 
 auth_header_(Username, Password) ->
     {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password),
-    {"Authorization","Bearer " ++ binary_to_list(Token)}.
+    {"Authorization", "Bearer " ++ binary_to_list(Token)}.
 
 api_path(Parts) ->
     ?HOST ++ filename:join([?BASE_PATH | Parts]).
 
 json(Data) ->
-    {ok, Jsx} = emqx_json:safe_decode(Data, [return_maps]), Jsx.
+    {ok, Jsx} = emqx_json:safe_decode(Data, [return_maps]),
+    Jsx.

+ 31 - 23
apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl

@@ -16,13 +16,15 @@
 
 -module(emqx_dashboard_api_test_helpers).
 
--export([set_default_config/0,
-         set_default_config/1,
-         request/2,
-         request/3,
-         request/4,
-         uri/0,
-         uri/1]).
+-export([
+    set_default_config/0,
+    set_default_config/1,
+    request/2,
+    request/3,
+    request/4,
+    uri/0,
+    uri/1
+]).
 
 -define(HOST, "http://127.0.0.1:18083/").
 -define(API_VERSION, "v5").
@@ -32,15 +34,16 @@ set_default_config() ->
     set_default_config(<<"admin">>).
 
 set_default_config(DefaultUsername) ->
-    Config = #{listeners => #{
-                  http => #{
-                    port => 18083
-                  }
-               },
-               default_username => DefaultUsername,
-               default_password => <<"public">>,
-               i18n_lang => en
-              },
+    Config = #{
+        listeners => #{
+            http => #{
+                port => 18083
+            }
+        },
+        default_username => DefaultUsername,
+        default_password => <<"public">>,
+        i18n_lang => en
+    },
     emqx_config:put([dashboard], Config),
     I18nFile = filename:join([
         filename:dirname(code:priv_dir(emqx_dashboard)),
@@ -57,17 +60,22 @@ request(Method, Url, Body) ->
     request(<<"admin">>, Method, Url, Body).
 
 request(Username, Method, Url, Body) ->
-    Request = case Body of
-        [] when Method =:= get orelse Method =:= put orelse
-                Method =:= head orelse Method =:= delete orelse
-                Method =:= trace -> {Url, [auth_header(Username)]};
-        _ -> {Url, [auth_header(Username)], "application/json", jsx:encode(Body)}
-    end,
+    Request =
+        case Body of
+            [] when
+                Method =:= get orelse Method =:= put orelse
+                    Method =:= head orelse Method =:= delete orelse
+                    Method =:= trace
+            ->
+                {Url, [auth_header(Username)]};
+            _ ->
+                {Url, [auth_header(Username)], "application/json", jsx:encode(Body)}
+        end,
     ct:pal("Method: ~p, Request: ~p", [Method, Request]),
     case httpc:request(Method, Request, [], [{body_format, binary}]) of
         {error, socket_closed_remotely} ->
             {error, socket_closed_remotely};
-        {ok, {{"HTTP/1.1", Code, _}, _Headers, Return} } ->
+        {ok, {{"HTTP/1.1", Code, _}, _Headers, Return}} ->
             {ok, Code, Return};
         {ok, {Reason, _, _}} ->
             {error, Reason}

+ 4 - 3
apps/emqx_dashboard/test/emqx_dashboard_bad_api_SUITE.erl

@@ -50,7 +50,7 @@ end_suite() ->
 
 t_bad_api_path(_) ->
     Url = ?SERVER ++ "/for/test/some/path/not/exist",
-    {error,{"HTTP/1.1", 404, "Not Found"}} = request(Url),
+    {error, {"HTTP/1.1", 404, "Not Found"}} = request(Url),
     ok.
 
 request(Url) ->
@@ -58,8 +58,9 @@ request(Url) ->
     case httpc:request(get, Request, [], []) of
         {error, Reason} ->
             {error, Reason};
-        {ok, {{"HTTP/1.1", Code, _}, _, Return} }
-            when Code >= 200 andalso Code =< 299 ->
+        {ok, {{"HTTP/1.1", Code, _}, _, Return}} when
+            Code >= 200 andalso Code =< 299
+        ->
             {ok, emqx_json:decode(Return, [return_maps])};
         {ok, {Reason, _, _}} ->
             {error, Reason}

+ 11 - 5
apps/emqx_dashboard/test/emqx_dashboard_error_code_SUITE.erl

@@ -82,13 +82,18 @@ t_format_code(_) ->
 t_api_codes(_) ->
     Url = ?SERVER ++ "/error_codes",
     {ok, List} = request(Url),
-    [?assert(exist(atom_to_binary(CodeName, utf8), List)) || CodeName <- emqx_dashboard_error_code:all()],
+    [
+        ?assert(exist(atom_to_binary(CodeName, utf8), List))
+     || CodeName <- emqx_dashboard_error_code:all()
+    ],
     ok.
 
 t_api_code(_) ->
     Url = ?SERVER ++ "/error_codes/BAD_REQUEST",
-    {ok, #{<<"code">> := <<"BAD_REQUEST">>,
-           <<"description">> := <<"Request parameters are not legal">>}} = request(Url),
+    {ok, #{
+        <<"code">> := <<"BAD_REQUEST">>,
+        <<"description">> := <<"Request parameters are not legal">>
+    }} = request(Url),
     ok.
 
 exist(_CodeName, []) ->
@@ -105,8 +110,9 @@ request(Url) ->
     case httpc:request(get, Request, [], []) of
         {error, Reason} ->
             {error, Reason};
-        {ok, {{"HTTP/1.1", Code, _}, _, Return} }
-            when Code >= 200 andalso Code =< 299 ->
+        {ok, {{"HTTP/1.1", Code, _}, _, Return}} when
+            Code >= 200 andalso Code =< 299
+        ->
             {ok, emqx_json:decode(Return, [return_maps])};
         {ok, {Reason, _, _}} ->
             {error, Reason}

+ 23 - 15
apps/emqx_dashboard/test/emqx_dashboard_monitor_SUITE.erl

@@ -47,10 +47,10 @@ set_special_configs(_) ->
 
 t_monitor_samplers_all(_Config) ->
     timer:sleep(?DEFAULT_SAMPLE_INTERVAL * 2 * 1000 + 20),
-    Size = mnesia:table_info(emqx_dashboard_monitor,size),
-    All  = emqx_dashboard_monitor:samplers(all, infinity),
+    Size = mnesia:table_info(emqx_dashboard_monitor, size),
+    All = emqx_dashboard_monitor:samplers(all, infinity),
     All2 = emqx_dashboard_monitor:samplers(),
-    ?assert(erlang:length(All)  == Size),
+    ?assert(erlang:length(All) == Size),
     ?assert(erlang:length(All2) == Size),
     ok.
 
@@ -87,18 +87,24 @@ t_monitor_api(_) ->
 t_monitor_current_api(_) ->
     timer:sleep(?DEFAULT_SAMPLE_INTERVAL * 2 * 1000 + 20),
     {ok, Rate} = request(["monitor_current"]),
-    [?assert(maps:is_key(atom_to_binary(Key, utf8), Rate))
-        || Key <- maps:values(?DELTA_SAMPLER_RATE_MAP) ++ ?GAUGE_SAMPLER_LIST],
+    [
+        ?assert(maps:is_key(atom_to_binary(Key, utf8), Rate))
+     || Key <- maps:values(?DELTA_SAMPLER_RATE_MAP) ++ ?GAUGE_SAMPLER_LIST
+    ],
     {ok, NodeRate} = request(["monitor_current", "nodes", node()]),
-    [?assert(maps:is_key(atom_to_binary(Key, utf8), NodeRate))
-        || Key <- maps:values(?DELTA_SAMPLER_RATE_MAP) ++ ?GAUGE_SAMPLER_LIST],
+    [
+        ?assert(maps:is_key(atom_to_binary(Key, utf8), NodeRate))
+     || Key <- maps:values(?DELTA_SAMPLER_RATE_MAP) ++ ?GAUGE_SAMPLER_LIST
+    ],
     ok.
 
 t_monitor_reset(_) ->
     restart_monitor(),
     {ok, Rate} = request(["monitor_current"]),
-    [?assert(maps:is_key(atom_to_binary(Key, utf8), Rate))
-        || Key <- maps:values(?DELTA_SAMPLER_RATE_MAP) ++ ?GAUGE_SAMPLER_LIST],
+    [
+        ?assert(maps:is_key(atom_to_binary(Key, utf8), Rate))
+     || Key <- maps:values(?DELTA_SAMPLER_RATE_MAP) ++ ?GAUGE_SAMPLER_LIST
+    ],
     {ok, Samplers} = request(["monitor"], "latest=1"),
     ?assertEqual(1, erlang:length(Samplers)),
     ok.
@@ -121,7 +127,7 @@ request(Path, QS) ->
     Url = url(Path, QS),
     do_request_api(get, {Url, [auth_header_()]}).
 
-url(Parts, QS)->
+url(Parts, QS) ->
     case QS of
         "" ->
             ?SERVER ++ filename:join([?BASE_PATH | Parts]);
@@ -129,16 +135,17 @@ url(Parts, QS)->
             ?SERVER ++ filename:join([?BASE_PATH | Parts]) ++ "?" ++ QS
     end.
 
-do_request_api(Method, Request)->
+do_request_api(Method, Request) ->
     ct:pal("Req ~p ~p~n", [Method, Request]),
     case httpc:request(Method, Request, [], []) of
         {error, socket_closed_remotely} ->
             {error, socket_closed_remotely};
-        {ok, {{"HTTP/1.1", Code, _}, _, Return} }
-            when Code >= 200 andalso Code =< 299 ->
+        {ok, {{"HTTP/1.1", Code, _}, _, Return}} when
+            Code >= 200 andalso Code =< 299
+        ->
             ct:pal("Resp ~p ~p~n", [Code, Return]),
             {ok, emqx_json:decode(Return, [return_maps])};
-        {ok, {{"HTTP/1.1", Code, _}, _, Return} } ->
+        {ok, {{"HTTP/1.1", Code, _}, _, Return}} ->
             ct:pal("Resp ~p ~p~n", [Code, Return]),
             {error, {Code, emqx_json:decode(Return, [return_maps])}};
         {error, Reason} ->
@@ -158,7 +165,8 @@ wait_new_monitor(_OldMonitor, Count) when Count =< 0 -> timeout;
 wait_new_monitor(OldMonitor, Count) ->
     NewMonitor = erlang:whereis(emqx_dashboard_monitor),
     case is_pid(NewMonitor) andalso NewMonitor =/= OldMonitor of
-        true -> ok;
+        true ->
+            ok;
         false ->
             timer:sleep(100),
             wait_new_monitor(OldMonitor, Count - 1)

+ 252 - 99
apps/emqx_dashboard/test/emqx_swagger_parameter_SUITE.erl

@@ -20,12 +20,30 @@
 
 all() -> [{group, spec}, {group, validation}].
 suite() -> [{timetrap, {minutes, 1}}].
-groups() -> [
-    {spec, [parallel], [t_api_spec, t_in_path, t_ref, t_in_query, t_in_mix,
-        t_without_in, t_require, t_nullable, t_method, t_public_ref]},
-    {validation, [parallel], [t_in_path_trans, t_ref_trans, t_in_query_trans, t_in_mix_trans,
-        t_in_path_trans_error, t_in_query_trans_error, t_in_mix_trans_error]}
-].
+groups() ->
+    [
+        {spec, [parallel], [
+            t_api_spec,
+            t_in_path,
+            t_ref,
+            t_in_query,
+            t_in_mix,
+            t_without_in,
+            t_require,
+            t_nullable,
+            t_method,
+            t_public_ref
+        ]},
+        {validation, [parallel], [
+            t_in_path_trans,
+            t_ref_trans,
+            t_in_query_trans,
+            t_in_mix_trans,
+            t_in_path_trans_error,
+            t_in_query_trans_error,
+            t_in_mix_trans_error
+        ]}
+    ].
 
 init_per_suite(Config) ->
     mria:start(),
@@ -50,21 +68,36 @@ end_suite() ->
 
 t_in_path(_Config) ->
     Expect =
-        [#{description => <<"Indicates which sorts of issues to return">>,
-            example => <<"all">>, in => path, name => filter,
-            required => true,
-            schema => #{enum => [assigned, created, mentioned, all], type => string}}
+        [
+            #{
+                description => <<"Indicates which sorts of issues to return">>,
+                example => <<"all">>,
+                in => path,
+                name => filter,
+                required => true,
+                schema => #{enum => [assigned, created, mentioned, all], type => string}
+            }
         ],
     validate("/test/in/:filter", Expect),
     ok.
 
 t_in_query(_Config) ->
     Expect =
-        [#{description => <<"results per page (max 100)">>,
-            example => 1, in => query, name => per_page,
-            schema => #{maximum => 100, minimum => 1, type => integer}},
-            #{description => <<"QOS">>, in => query, name => qos,
-                schema => #{enum => [0, 1, 2], type => string}}],
+        [
+            #{
+                description => <<"results per page (max 100)">>,
+                example => 1,
+                in => query,
+                name => per_page,
+                schema => #{maximum => 100, minimum => 1, type => integer}
+            },
+            #{
+                description => <<"QOS">>,
+                in => query,
+                name => qos,
+                schema => #{enum => [0, 1, 2], type => string}
+            }
+        ],
     validate("/test/in/query", Expect),
     ok.
 
@@ -85,68 +118,131 @@ t_public_ref(_Config) ->
     Expect = [
         #{<<"$ref">> => <<"#/components/parameters/public.page">>},
         #{<<"$ref">> => <<"#/components/parameters/public.limit">>}
-        ],
+    ],
     {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
     ?assertEqual(test, OperationId),
     Params = maps:get(parameters, maps:get(post, Spec)),
     ?assertEqual(Expect, Params),
-    ?assertEqual([
-        {emqx_dashboard_swagger, limit, parameter},
-        {emqx_dashboard_swagger, page, parameter}
-    ], Refs),
+    ?assertEqual(
+        [
+            {emqx_dashboard_swagger, limit, parameter},
+            {emqx_dashboard_swagger, page, parameter}
+        ],
+        Refs
+    ),
     ExpectRefs = [
-        #{<<"public.limit">> => #{description => <<"Results per page(max 1000)">>,
-            in => query,name => limit, example => 50,
-            schema => #{default => 100,maximum => 1000,
-                minimum => 1,type => integer}}},
-        #{<<"public.page">> => #{description => <<"Page number of the results to fetch.">>,
-            in => query,name => page,example => 1,
-            schema => #{default => 1,minimum => 1,type => integer}}}],
-    ?assertEqual(ExpectRefs, emqx_dashboard_swagger:components(Refs,#{})),
+        #{
+            <<"public.limit">> => #{
+                description => <<"Results per page(max 1000)">>,
+                in => query,
+                name => limit,
+                example => 50,
+                schema => #{
+                    default => 100,
+                    maximum => 1000,
+                    minimum => 1,
+                    type => integer
+                }
+            }
+        },
+        #{
+            <<"public.page">> => #{
+                description => <<"Page number of the results to fetch.">>,
+                in => query,
+                name => page,
+                example => 1,
+                schema => #{default => 1, minimum => 1, type => integer}
+            }
+        }
+    ],
+    ?assertEqual(ExpectRefs, emqx_dashboard_swagger:components(Refs, #{})),
     ok.
 
 t_in_mix(_Config) ->
     Expect =
-        [#{description => <<"Indicates which sorts of issues to return">>,
-            example => <<"all">>,in => query,name => filter,
-            schema => #{enum => [assigned,created,mentioned,all],type => string}},
-            #{description => <<"Indicates the state of the issues to return.">>,
-                example => <<"12m">>,in => path,name => state,required => true,
-                schema => #{example => <<"1h">>,type => string}},
-            #{example => 10,in => query,name => per_page, required => false,
-                schema => #{default => 5,maximum => 50,minimum => 1, type => integer}},
-            #{in => query,name => is_admin, schema => #{type => boolean}},
-            #{in => query,name => timeout,
-                schema => #{<<"oneOf">> => [#{enum => [infinity],type => string},
-                    #{maximum => 60,minimum => 30, type => integer}]}}],
+        [
+            #{
+                description => <<"Indicates which sorts of issues to return">>,
+                example => <<"all">>,
+                in => query,
+                name => filter,
+                schema => #{enum => [assigned, created, mentioned, all], type => string}
+            },
+            #{
+                description => <<"Indicates the state of the issues to return.">>,
+                example => <<"12m">>,
+                in => path,
+                name => state,
+                required => true,
+                schema => #{example => <<"1h">>, type => string}
+            },
+            #{
+                example => 10,
+                in => query,
+                name => per_page,
+                required => false,
+                schema => #{default => 5, maximum => 50, minimum => 1, type => integer}
+            },
+            #{in => query, name => is_admin, schema => #{type => boolean}},
+            #{
+                in => query,
+                name => timeout,
+                schema => #{
+                    <<"oneOf">> => [
+                        #{enum => [infinity], type => string},
+                        #{maximum => 60, minimum => 30, type => integer}
+                    ]
+                }
+            }
+        ],
     ExpectMeta = #{
-            tags => [tags, good],
-            description => <<"good description">>,
-            summary => <<"good summary">>,
-            security => [],
-            deprecated => true,
-            responses => #{<<"200">> => #{description => <<"ok">>}}},
+        tags => [tags, good],
+        description => <<"good description">>,
+        summary => <<"good summary">>,
+        security => [],
+        deprecated => true,
+        responses => #{<<"200">> => #{description => <<"ok">>}}
+    },
     GotSpec = validate("/test/in/mix/:state", Expect),
     ?assertEqual(ExpectMeta, maps:without([parameters], maps:get(post, GotSpec))),
     ok.
 
 t_without_in(_Config) ->
-    ?assertThrow({error, <<"missing in:path/query field in parameters">>},
-        emqx_dashboard_swagger:parse_spec_ref(?MODULE, "/test/without/in", #{})),
+    ?assertThrow(
+        {error, <<"missing in:path/query field in parameters">>},
+        emqx_dashboard_swagger:parse_spec_ref(?MODULE, "/test/without/in", #{})
+    ),
     ok.
 
 t_require(_Config) ->
-    ExpectSpec = [#{
-        in => query,name => userid, required => false,
-        schema => #{type => string}}],
+    ExpectSpec = [
+        #{
+            in => query,
+            name => userid,
+            required => false,
+            schema => #{type => string}
+        }
+    ],
     validate("/required/false", ExpectSpec),
     ok.
 
 t_nullable(_Config) ->
-    NullableFalse = [#{in => query,name => userid, required => true,
-        schema => #{type => string}}],
-    NullableTrue = [#{in => query,name => userid,
-        schema => #{type => string}, required => false}],
+    NullableFalse = [
+        #{
+            in => query,
+            name => userid,
+            required => true,
+            schema => #{type => string}
+        }
+    ],
+    NullableTrue = [
+        #{
+            in => query,
+            name => userid,
+            schema => #{type => string},
+            required => false
+        }
+    ],
     validate("/nullable/false", NullableFalse),
     validate("/nullable/true", NullableTrue),
     ok.
@@ -156,35 +252,49 @@ t_method(_Config) ->
     PathError = "/method/error",
     {test, Spec, []} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, PathOk, #{}),
     ?assertEqual(lists:sort(?METHODS), lists:sort(maps:keys(Spec))),
-    ?assertThrow({error, #{module := ?MODULE, path := PathError, method := bar}},
-        emqx_dashboard_swagger:parse_spec_ref(?MODULE, PathError, #{})),
+    ?assertThrow(
+        {error, #{module := ?MODULE, path := PathError, method := bar}},
+        emqx_dashboard_swagger:parse_spec_ref(?MODULE, PathError, #{})
+    ),
     ok.
 
 t_in_path_trans(_Config) ->
     Path = "/test/in/:filter",
     Bindings = #{filter => <<"created">>},
-    Expect = {ok,#{bindings => #{filter => created},
-        body => #{}, query_string => #{}}},
+    Expect =
+        {ok, #{
+            bindings => #{filter => created},
+            body => #{},
+            query_string => #{}
+        }},
     ?assertEqual(Expect, trans_parameters(Path, Bindings, #{})),
     ok.
 
 t_in_query_trans(_Config) ->
     Path = "/test/in/query",
-    Expect = {ok, #{bindings => #{},body => #{},
-        query_string => #{<<"per_page">> => 100, <<"qos">> => 1}}},
+    Expect =
+        {ok, #{
+            bindings => #{},
+            body => #{},
+            query_string => #{<<"per_page">> => 100, <<"qos">> => 1}
+        }},
     ?assertEqual(Expect, trans_parameters(Path, #{}, #{<<"per_page">> => 100, <<"qos">> => 1})),
     ok.
 
 t_ref_trans(_Config) ->
     LocalPath = "/test/in/ref/local",
     Path = "/test/in/ref",
-    Expect = {ok, #{bindings => #{},body => #{},
-        query_string => #{<<"per_page">> => 100}}},
+    Expect =
+        {ok, #{
+            bindings => #{},
+            body => #{},
+            query_string => #{<<"per_page">> => 100}
+        }},
     ?assertEqual(Expect, trans_parameters(Path, #{}, #{<<"per_page">> => 100})),
     ?assertEqual(Expect, trans_parameters(LocalPath, #{}, #{<<"per_page">> => 100})),
-    {400,'BAD_REQUEST', Reason} = trans_parameters(Path, #{}, #{<<"per_page">> => 1010}),
+    {400, 'BAD_REQUEST', Reason} = trans_parameters(Path, #{}, #{<<"per_page">> => 1010}),
     ?assertNotEqual(nomatch, binary:match(Reason, [<<"per_page">>])),
-    {400,'BAD_REQUEST', Reason} = trans_parameters(LocalPath, #{}, #{<<"per_page">> => 1010}),
+    {400, 'BAD_REQUEST', Reason} = trans_parameters(LocalPath, #{}, #{<<"per_page">> => 1010}),
     ok.
 
 t_in_mix_trans(_Config) ->
@@ -198,24 +308,29 @@ t_in_mix_trans(_Config) ->
         <<"is_admin">> => true,
         <<"timeout">> => <<"34">>
     },
-    Expect = {ok,
-        #{body => #{},
+    Expect =
+        {ok, #{
+            body => #{},
             bindings => #{state => 720},
-            query_string => #{<<"filter">> => created,<<"is_admin">> => true,
-                <<"per_page">> => 5,<<"timeout">> => 34}}},
+            query_string => #{
+                <<"filter">> => created,
+                <<"is_admin">> => true,
+                <<"per_page">> => 5,
+                <<"timeout">> => 34
+            }
+        }},
     ?assertEqual(Expect, trans_parameters(Path, Bindings, Query)),
     ok.
 
 t_in_path_trans_error(_Config) ->
     Path = "/test/in/:filter",
     Bindings = #{filter => <<"created1">>},
-    Expect = {400,'BAD_REQUEST', <<"filter : unable_to_convert_to_enum_symbol">>},
-    ?assertEqual(Expect, trans_parameters(Path, Bindings, #{})),
+    ?assertMatch({400, 'BAD_REQUEST', _}, trans_parameters(Path, Bindings, #{})),
     ok.
 
 t_in_query_trans_error(_Config) ->
     Path = "/test/in/query",
-    {400,'BAD_REQUEST', Reason} = trans_parameters(Path, #{}, #{<<"per_page">> => 101}),
+    {400, 'BAD_REQUEST', Reason} = trans_parameters(Path, #{}, #{<<"per_page">> => 101}),
     ?assertNotEqual(nomatch, binary:match(Reason, [<<"per_page">>])),
     ok.
 
@@ -230,8 +345,7 @@ t_in_mix_trans_error(_Config) ->
         <<"is_admin">> => true,
         <<"timeout">> => <<"34">>
     },
-    Expect = {400,'BAD_REQUEST', <<"filter : unable_to_convert_to_enum_symbol">>},
-    ?assertEqual(Expect, trans_parameters(Path, Bindings, Query)),
+    ?assertMatch({400, 'BAD_REQUEST', _}, trans_parameters(Path, Bindings, Query)),
     ok.
 
 t_api_spec(_Config) ->
@@ -253,14 +367,16 @@ t_api_spec(_Config) ->
 
     ?assertMatch(
         {ok, #{bindings := #{filter := created}}},
-        trans_parameters(Path, Bindings, #{}, Filter)).
+        trans_parameters(Path, Bindings, #{}, Filter)
+    ).
 
 assert_all_filters_equal(Spec, Filter) ->
     lists:foreach(
         fun({_, _, _, #{filter := F}}) ->
             ?assertEqual(Filter, F)
         end,
-        Spec).
+        Spec
+    ).
 
 validate(Path, ExpectParams) ->
     {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
@@ -284,8 +400,17 @@ trans_parameters(Path, Bindings, QueryStr, Filter) ->
 
 api_spec() -> emqx_dashboard_swagger:spec(?MODULE).
 
-paths() -> ["/test/in/:filter", "/test/in/query", "/test/in/mix/:state", "/test/in/ref",
-    "/required/false", "/nullable/false", "/nullable/true", "/method/ok"].
+paths() ->
+    [
+        "/test/in/:filter",
+        "/test/in/query",
+        "/test/in/mix/:state",
+        "/test/in/ref",
+        "/required/false",
+        "/nullable/false",
+        "/nullable/true",
+        "/method/ok"
+    ].
 
 schema("/test/in/:filter") ->
     #{
@@ -293,11 +418,14 @@ schema("/test/in/:filter") ->
         post => #{
             parameters => [
                 {filter,
-                    mk(hoconsc:enum([assigned, created, mentioned, all]),
-                        #{in => path,
-                          desc => <<"Indicates which sorts of issues to return">>,
-                          example => "all"
-                        })}
+                    mk(
+                        hoconsc:enum([assigned, created, mentioned, all]),
+                        #{
+                            in => path,
+                            desc => <<"Indicates which sorts of issues to return">>,
+                            example => "all"
+                        }
+                    )}
             ],
             responses => #{200 => <<"ok">>}
         }
@@ -308,10 +436,14 @@ schema("/test/in/query") ->
         post => #{
             parameters => [
                 {per_page,
-                    mk(range(1, 100),
-                        #{in => query,
-                        desc => <<"results per page (max 100)">>,
-                        example => 1})},
+                    mk(
+                        range(1, 100),
+                        #{
+                            in => query,
+                            desc => <<"results per page (max 100)">>,
+                            example => 1
+                        }
+                    )},
                 {qos, mk(emqx_schema:qos(), #{in => query, desc => <<"QOS">>})}
             ],
             responses => #{200 => <<"ok">>}
@@ -354,14 +486,30 @@ schema("/test/in/mix/:state") ->
             security => [],
             deprecated => true,
             parameters => [
-                {filter, hoconsc:mk(hoconsc:enum([assigned, created, mentioned, all]),
-                    #{in => query, desc => <<"Indicates which sorts of issues to return">>,
-                        example => "all"})},
-                {state, mk(emqx_schema:duration_s(),
-                    #{in => path, required => true, example => "12m",
-                        desc => <<"Indicates the state of the issues to return.">>})},
-                {per_page, mk(range(1, 50),
-                    #{in => query, required => false, example => 10, default => 5})},
+                {filter,
+                    hoconsc:mk(
+                        hoconsc:enum([assigned, created, mentioned, all]),
+                        #{
+                            in => query,
+                            desc => <<"Indicates which sorts of issues to return">>,
+                            example => "all"
+                        }
+                    )},
+                {state,
+                    mk(
+                        emqx_schema:duration_s(),
+                        #{
+                            in => path,
+                            required => true,
+                            example => "12m",
+                            desc => <<"Indicates the state of the issues to return.">>
+                        }
+                    )},
+                {per_page,
+                    mk(
+                        range(1, 50),
+                        #{in => query, required => false, example => 10, default => 5}
+                    )},
                 {is_admin, mk(boolean(), #{in => query})},
                 {timeout, mk(hoconsc:union([range(30, 60), infinity]), #{in => query})}
             ],
@@ -386,16 +534,21 @@ schema("/nullable/true") ->
     to_schema([{'userid', mk(binary(), #{in => query, required => false})}]);
 schema("/method/ok") ->
     Response = #{responses => #{200 => <<"ok">>}},
-    lists:foldl(fun(Method, Acc) ->  Acc#{Method => Response} end,
-        #{operationId => test}, ?METHODS);
+    lists:foldl(
+        fun(Method, Acc) -> Acc#{Method => Response} end,
+        #{operationId => test},
+        ?METHODS
+    );
 schema("/method/error") ->
     #{operationId => test, bar => #{200 => <<"ok">>}}.
 
 fields(page) ->
     [
         {per_page,
-            mk(range(1, 100),
-                #{in => query, desc => <<"results per page (max 100)">>, example => 1})}
+            mk(
+                range(1, 100),
+                #{in => query, desc => <<"results per page (max 100)">>, example => 1}
+            )}
     ].
 to_schema(Params) ->
     #{

+ 10 - 7
apps/emqx_dashboard/test/emqx_swagger_remote_schema.erl

@@ -17,36 +17,39 @@
 
 -include_lib("typerefl/include/types.hrl").
 
--export([ roots/0, fields/1]).
+-export([roots/0, fields/1]).
 -import(hoconsc, [mk/2]).
 roots() -> ["root"].
 
 fields("root") ->
     [
-        {listeners, hoconsc:array(hoconsc:union([hoconsc:ref(?MODULE, "ref1"),
-        hoconsc:ref(?MODULE, "ref2")]))},
+        {listeners,
+            hoconsc:array(
+                hoconsc:union([
+                    hoconsc:ref(?MODULE, "ref1"),
+                    hoconsc:ref(?MODULE, "ref2")
+                ])
+            )},
         {default_username, fun default_username/1},
         {default_password, fun default_password/1},
         {sample_interval, mk(emqx_schema:duration_s(), #{default => "10s"})},
         {token_expired_time, mk(emqx_schema:duration(), #{default => "30m"})}
     ];
-
 fields("ref1") ->
     [
         {"protocol", hoconsc:enum([http, https])},
         {"port", mk(integer(), #{default => 18083})}
     ];
-
 fields("ref2") ->
     [
-        {page, mk(range(1,100), #{desc => <<"good page">>})},
+        {page, mk(range(1, 100), #{desc => <<"good page">>})},
         {another_ref, hoconsc:ref(?MODULE, "ref3")}
     ];
 fields("ref3") ->
     [
         {ip, mk(emqx_schema:ip_port(), #{desc => <<"IP:Port">>, example => "127.0.0.1:80"})},
         {version, mk(string(), #{desc => "a good version", example => "1.0.0"})}
-        ].
+    ].
 
 default_username(type) -> string();
 default_username(default) -> "admin";

+ 396 - 161
apps/emqx_dashboard/test/emqx_swagger_requestBody_SUITE.erl

@@ -36,125 +36,262 @@ end_suite() ->
 
 t_object(_Config) ->
     Spec = #{
-        post => #{parameters => [],
-            requestBody => #{<<"content">> =>
-            #{<<"application/json">> =>
-            #{<<"schema">> =>
-            #{required => [<<"timeout">>, <<"per_page">>],
-                <<"properties">> =>[
-                    {<<"per_page">>, #{description => <<"good per page desc">>,
-                        maximum => 100, minimum => 1, type => integer}},
-                    {<<"timeout">>, #{default => 5, <<"oneOf">> =>
-                    [#{example => <<"1h">>, type => string},
-                        #{enum => [infinity], type => string}]}},
-                    {<<"inner_ref">>,
-                        #{<<"$ref">> =>
-                        <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>}}],
-                <<"type">> => object}}}},
-            responses => #{<<"200">> => #{description => <<"ok">>}}}},
+        post => #{
+            parameters => [],
+            requestBody => #{
+                <<"content">> =>
+                    #{
+                        <<"application/json">> =>
+                            #{
+                                <<"schema">> =>
+                                    #{
+                                        required => [<<"timeout">>, <<"per_page">>],
+                                        <<"properties">> => [
+                                            {<<"per_page">>, #{
+                                                description => <<"good per page desc">>,
+                                                maximum => 100,
+                                                minimum => 1,
+                                                type => integer
+                                            }},
+                                            {<<"timeout">>, #{
+                                                default => 5,
+                                                <<"oneOf">> =>
+                                                    [
+                                                        #{example => <<"1h">>, type => string},
+                                                        #{enum => [infinity], type => string}
+                                                    ]
+                                            }},
+                                            {<<"inner_ref">>, #{
+                                                <<"$ref">> =>
+                                                    <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>
+                                            }}
+                                        ],
+                                        <<"type">> => object
+                                    }
+                            }
+                    }
+            },
+            responses => #{<<"200">> => #{description => <<"ok">>}}
+        }
+    },
     Refs = [{?MODULE, good_ref}],
     validate("/object", Spec, Refs),
     ok.
 
 t_nest_object(_Config) ->
+    GoodRef = <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>,
     Spec = #{
-        post => #{parameters => [],
-            requestBody => #{<<"content">> => #{<<"application/json">> =>
-            #{<<"schema">> =>
-            #{required => [<<"timeout">>],
-                <<"properties">> =>
-                [{<<"per_page">>, #{description => <<"good per page desc">>,
-                    maximum => 100, minimum => 1, type => integer}},
-                    {<<"timeout">>, #{default => 5, <<"oneOf">> =>
-                    [#{example => <<"1h">>, type => string},
-                        #{enum => [infinity], type => string}]}},
-                    {<<"nest_object">>,
-                        #{<<"properties">> =>
-                        [{<<"good_nest_1">>, #{type => integer}},
-                            {<<"good_nest_2">>, #{<<"$ref">> =>
-                            <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>}}],
-                            <<"type">> => object}},
-                    {<<"inner_ref">>,
-                        #{<<"$ref">> =>
-                        <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>}}],
-                <<"type">> => object}}}},
-            responses => #{<<"200">> => #{description => <<"ok">>}}}},
+        post => #{
+            parameters => [],
+            requestBody => #{
+                <<"content">> => #{
+                    <<"application/json">> =>
+                        #{
+                            <<"schema">> =>
+                                #{
+                                    required => [<<"timeout">>],
+                                    <<"properties">> =>
+                                        [
+                                            {<<"per_page">>, #{
+                                                description => <<"good per page desc">>,
+                                                maximum => 100,
+                                                minimum => 1,
+                                                type => integer
+                                            }},
+                                            {<<"timeout">>, #{
+                                                default => 5,
+                                                <<"oneOf">> =>
+                                                    [
+                                                        #{example => <<"1h">>, type => string},
+                                                        #{enum => [infinity], type => string}
+                                                    ]
+                                            }},
+                                            {<<"nest_object">>, #{
+                                                <<"properties">> =>
+                                                    [
+                                                        {<<"good_nest_1">>, #{type => integer}},
+                                                        {<<"good_nest_2">>, #{
+                                                            <<"$ref">> => GoodRef
+                                                        }}
+                                                    ],
+                                                <<"type">> => object
+                                            }},
+                                            {<<"inner_ref">>, #{
+                                                <<"$ref">> =>
+                                                    <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>
+                                            }}
+                                        ],
+                                    <<"type">> => object
+                                }
+                        }
+                }
+            },
+            responses => #{<<"200">> => #{description => <<"ok">>}}
+        }
+    },
     Refs = [{?MODULE, good_ref}],
     validate("/nest/object", Spec, Refs),
     ok.
 
 t_local_ref(_Config) ->
     Spec = #{
-        post => #{parameters => [],
-            requestBody => #{<<"content">> => #{<<"application/json">> =>
-            #{<<"schema">> => #{<<"$ref">> =>
-            <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>}}}},
-            responses => #{<<"200">> => #{description => <<"ok">>}}}},
+        post => #{
+            parameters => [],
+            requestBody => #{
+                <<"content">> => #{
+                    <<"application/json">> =>
+                        #{
+                            <<"schema">> => #{
+                                <<"$ref">> =>
+                                    <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>
+                            }
+                        }
+                }
+            },
+            responses => #{<<"200">> => #{description => <<"ok">>}}
+        }
+    },
     Refs = [{?MODULE, good_ref}],
     validate("/ref/local", Spec, Refs),
     ok.
 
 t_remote_ref(_Config) ->
     Spec = #{
-        post => #{parameters => [],
-            requestBody => #{<<"content">> => #{<<"application/json">> =>
-            #{<<"schema">> => #{<<"$ref">> =>
-            <<"#/components/schemas/emqx_swagger_remote_schema.ref2">>}}}},
-            responses => #{<<"200">> => #{description => <<"ok">>}}}},
+        post => #{
+            parameters => [],
+            requestBody => #{
+                <<"content">> => #{
+                    <<"application/json">> =>
+                        #{
+                            <<"schema">> => #{
+                                <<"$ref">> =>
+                                    <<"#/components/schemas/emqx_swagger_remote_schema.ref2">>
+                            }
+                        }
+                }
+            },
+            responses => #{<<"200">> => #{description => <<"ok">>}}
+        }
+    },
     Refs = [{emqx_swagger_remote_schema, "ref2"}],
     {_, Components} = validate("/ref/remote", Spec, Refs),
     ExpectComponents = [
-        #{<<"emqx_swagger_remote_schema.ref2">> => #{<<"properties">> => [
-            {<<"page">>, #{description => <<"good page">>,
-                maximum => 100,minimum => 1,type => integer}},
-        {<<"another_ref">>, #{<<"$ref">> =>
-        <<"#/components/schemas/emqx_swagger_remote_schema.ref3">>}}], <<"type">> => object}},
-        #{<<"emqx_swagger_remote_schema.ref3">> => #{<<"properties">> => [
-            {<<"ip">>, #{description => <<"IP:Port">>,
-                example => <<"127.0.0.1:80">>,type => string}},
-            {<<"version">>, #{description => <<"a good version">>,
-                example => <<"1.0.0">>,type => string}}],
-            <<"type">> => object}}],
+        #{
+            <<"emqx_swagger_remote_schema.ref2">> => #{
+                <<"properties">> => [
+                    {<<"page">>, #{
+                        description => <<"good page">>,
+                        maximum => 100,
+                        minimum => 1,
+                        type => integer
+                    }},
+                    {<<"another_ref">>, #{
+                        <<"$ref">> =>
+                            <<"#/components/schemas/emqx_swagger_remote_schema.ref3">>
+                    }}
+                ],
+                <<"type">> => object
+            }
+        },
+        #{
+            <<"emqx_swagger_remote_schema.ref3">> => #{
+                <<"properties">> => [
+                    {<<"ip">>, #{
+                        description => <<"IP:Port">>,
+                        example => <<"127.0.0.1:80">>,
+                        type => string
+                    }},
+                    {<<"version">>, #{
+                        description => <<"a good version">>,
+                        example => <<"1.0.0">>,
+                        type => string
+                    }}
+                ],
+                <<"type">> => object
+            }
+        }
+    ],
     ?assertEqual(ExpectComponents, Components),
     ok.
 
 t_nest_ref(_Config) ->
     Spec = #{
-        post => #{parameters => [],
-            requestBody => #{<<"content">> => #{<<"application/json">> =>
-            #{<<"schema">> => #{<<"$ref">> =>
-            <<"#/components/schemas/emqx_swagger_requestBody_SUITE.nest_ref">>}}}},
-            responses => #{<<"200">> => #{description => <<"ok">>}}}},
+        post => #{
+            parameters => [],
+            requestBody => #{
+                <<"content">> => #{
+                    <<"application/json">> =>
+                        #{
+                            <<"schema">> => #{
+                                <<"$ref">> =>
+                                    <<"#/components/schemas/emqx_swagger_requestBody_SUITE.nest_ref">>
+                            }
+                        }
+                }
+            },
+            responses => #{<<"200">> => #{description => <<"ok">>}}
+        }
+    },
     Refs = [{?MODULE, nest_ref}],
     ExpectComponents = lists:sort([
-        #{<<"emqx_swagger_requestBody_SUITE.nest_ref">> => #{<<"properties">> => [
-            {<<"env">>, #{enum => [test,dev,prod],type => string}},
-            {<<"another_ref">>, #{description => <<"nest ref">>,
-                <<"$ref">> => <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>}}],
-            <<"type">> => object}},
-        #{<<"emqx_swagger_requestBody_SUITE.good_ref">> => #{<<"properties">> => [
-            {<<"webhook-host">>, #{default => <<"127.0.0.1:80">>,
-                example => <<"127.0.0.1:80">>,type => string}},
-            {<<"log_dir">>, #{example => <<"var/log/emqx">>,type => string}},
-            {<<"tag">>, #{description => <<"tag">>,type => string}}],
-            <<"type">> => object}}]),
+        #{
+            <<"emqx_swagger_requestBody_SUITE.nest_ref">> => #{
+                <<"properties">> => [
+                    {<<"env">>, #{enum => [test, dev, prod], type => string}},
+                    {<<"another_ref">>, #{
+                        description => <<"nest ref">>,
+                        <<"$ref">> =>
+                            <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>
+                    }}
+                ],
+                <<"type">> => object
+            }
+        },
+        #{
+            <<"emqx_swagger_requestBody_SUITE.good_ref">> => #{
+                <<"properties">> => [
+                    {<<"webhook-host">>, #{
+                        default => <<"127.0.0.1:80">>,
+                        example => <<"127.0.0.1:80">>,
+                        type => string
+                    }},
+                    {<<"log_dir">>, #{example => <<"var/log/emqx">>, type => string}},
+                    {<<"tag">>, #{description => <<"tag">>, type => string}}
+                ],
+                <<"type">> => object
+            }
+        }
+    ]),
     {_, Components} = validate("/ref/nest/ref", Spec, Refs),
     ?assertEqual(ExpectComponents, Components),
     ok.
 
 t_none_ref(_Config) ->
     Path = "/ref/none",
-    ?assertThrow({error, #{mfa := {?MODULE, schema, [Path]}}},
-        emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{})),
+    ?assertThrow(
+        {error, #{mfa := {?MODULE, schema, [Path]}}},
+        emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{})
+    ),
     ok.
 
 t_sub_fields(_Config) ->
     Spec = #{
-        post => #{parameters => [],
-            requestBody => #{<<"content">> => #{<<"application/json">> =>
-            #{<<"schema">> => #{<<"$ref">> =>
-            <<"#/components/schemas/emqx_swagger_requestBody_SUITE.sub_fields">>}}}},
-            responses => #{<<"200">> => #{description => <<"ok">>}}}},
+        post => #{
+            parameters => [],
+            requestBody => #{
+                <<"content">> => #{
+                    <<"application/json">> =>
+                        #{
+                            <<"schema">> => #{
+                                <<"$ref">> =>
+                                    <<"#/components/schemas/emqx_swagger_requestBody_SUITE.sub_fields">>
+                            }
+                        }
+                }
+            },
+            responses => #{<<"200">> => #{description => <<"ok">>}}
+        }
+    },
     Refs = [{?MODULE, sub_fields}],
     validate("/fields/sub", Spec, Refs),
     ok.
@@ -162,44 +299,97 @@ t_sub_fields(_Config) ->
 t_bad_ref(_Config) ->
     Path = "/ref/bad",
     Spec = #{
-        post => #{parameters => [],
-            requestBody => #{<<"content">> => #{<<"application/json">> => #{<<"schema">> =>
-            #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_requestBody_SUITE.bad_ref">>}}}},
-            responses => #{<<"200">> => #{description => <<"ok">>}}}},
+        post => #{
+            parameters => [],
+            requestBody => #{
+                <<"content">> => #{
+                    <<"application/json">> => #{
+                        <<"schema">> =>
+                            #{
+                                <<"$ref">> =>
+                                    <<"#/components/schemas/emqx_swagger_requestBody_SUITE.bad_ref">>
+                            }
+                    }
+                }
+            },
+            responses => #{<<"200">> => #{description => <<"ok">>}}
+        }
+    },
     Refs = [{?MODULE, bad_ref}],
     Fields = fields(bad_ref),
-    ?assertThrow({error, #{msg := <<"Object only supports not empty proplists">>, args := Fields}},
-        validate(Path, Spec, Refs)),
+    ?assertThrow(
+        {error, #{msg := <<"Object only supports not empty proplists">>, args := Fields}},
+        validate(Path, Spec, Refs)
+    ),
     ok.
 
 t_ref_array_with_key(_Config) ->
     Spec = #{
-        post => #{parameters => [],
-            requestBody => #{<<"content">> => #{<<"application/json">> =>
-            #{<<"schema">> => #{required => [<<"timeout">>],
-                <<"type">> => object, <<"properties">> =>
-                [
-                    {<<"per_page">>, #{description => <<"good per page desc">>,
-                        maximum => 100, minimum => 1, type => integer}},
-                    {<<"timeout">>, #{default => 5, <<"oneOf">> =>
-                    [#{example => <<"1h">>, type => string},
-                        #{enum => [infinity], type => string}]}},
-                    {<<"array_refs">>, #{items => #{<<"$ref">> =>
-                    <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>},
-                        type => array}}
-                ]}}}},
-            responses => #{<<"200">> => #{description => <<"ok">>}}}},
+        post => #{
+            parameters => [],
+            requestBody => #{
+                <<"content">> => #{
+                    <<"application/json">> =>
+                        #{
+                            <<"schema">> => #{
+                                required => [<<"timeout">>],
+                                <<"type">> => object,
+                                <<"properties">> =>
+                                    [
+                                        {<<"per_page">>, #{
+                                            description => <<"good per page desc">>,
+                                            maximum => 100,
+                                            minimum => 1,
+                                            type => integer
+                                        }},
+                                        {<<"timeout">>, #{
+                                            default => 5,
+                                            <<"oneOf">> =>
+                                                [
+                                                    #{example => <<"1h">>, type => string},
+                                                    #{enum => [infinity], type => string}
+                                                ]
+                                        }},
+                                        {<<"array_refs">>, #{
+                                            items => #{
+                                                <<"$ref">> =>
+                                                    <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>
+                                            },
+                                            type => array
+                                        }}
+                                    ]
+                            }
+                        }
+                }
+            },
+            responses => #{<<"200">> => #{description => <<"ok">>}}
+        }
+    },
     Refs = [{?MODULE, good_ref}],
     validate("/ref/array/with/key", Spec, Refs),
     ok.
 
 t_ref_array_without_key(_Config) ->
     Spec = #{
-        post => #{parameters => [],
-            requestBody => #{<<"content">> => #{<<"application/json">> => #{<<"schema">> =>
-            #{items => #{<<"$ref">> =>
-            <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>}, type => array}}}},
-            responses => #{<<"200">> => #{description => <<"ok">>}}}},
+        post => #{
+            parameters => [],
+            requestBody => #{
+                <<"content">> => #{
+                    <<"application/json">> => #{
+                        <<"schema">> =>
+                            #{
+                                items => #{
+                                    <<"$ref">> =>
+                                        <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>
+                                },
+                                type => array
+                            }
+                    }
+                }
+            },
+            responses => #{<<"200">> => #{description => <<"ok">>}}
+        }
+    },
     Refs = [{?MODULE, good_ref}],
     validate("/ref/array/without/key", Spec, Refs),
     ok.
@@ -220,15 +410,18 @@ t_api_spec(_Config) ->
     Filter0 = filter(Spec0, Path),
     ?assertMatch(
         {ok, #{body := #{<<"timeout">> := <<"infinity">>}}},
-        trans_requestBody(Path, Body, Filter0)),
+        trans_requestBody(Path, Body, Filter0)
+    ),
 
-    {Spec1, _} = emqx_dashboard_swagger:spec(?MODULE,
-        #{check_schema => true, translate_body => true}),
+    {Spec1, _} = emqx_dashboard_swagger:spec(
+        ?MODULE,
+        #{check_schema => true, translate_body => true}
+    ),
     Filter1 = filter(Spec1, Path),
     ?assertMatch(
         {ok, #{body := #{<<"timeout">> := infinity}}},
-        trans_requestBody(Path, Body, Filter1)).
-
+        trans_requestBody(Path, Body, Filter1)
+    ).
 
 t_object_trans(_Config) ->
     Path = "/object",
@@ -246,14 +439,15 @@ t_object_trans(_Config) ->
             bindings => #{},
             query_string => #{},
             body =>
-            #{
-                <<"per_page">> => 1,
-                <<"timeout">> => infinity,
-                <<"inner_ref">> => #{
-                    <<"log_dir">> => "var/log/test",
-                    <<"tag">> => <<"god_tag">>,
-                    <<"webhook-host">> => {{127, 0, 0, 1}, 80}}
-            }
+                #{
+                    <<"per_page">> => 1,
+                    <<"timeout">> => infinity,
+                    <<"inner_ref">> => #{
+                        <<"log_dir">> => "var/log/test",
+                        <<"tag">> => <<"god_tag">>,
+                        <<"webhook-host">> => {{127, 0, 0, 1}, 80}
+                    }
+                }
         },
     {ok, ActualBody} = trans_requestBody(Path, Body),
     ?assertEqual(Expect, ActualBody),
@@ -270,8 +464,11 @@ t_object_notrans(_Config) ->
             <<"tag">> => <<"god_tag">>
         }
     },
-    {ok, #{body := ActualBody}} = trans_requestBody(Path, Body,
-        fun emqx_dashboard_swagger:filter_check_request/2),
+    {ok, #{body := ActualBody}} = trans_requestBody(
+        Path,
+        Body,
+        fun emqx_dashboard_swagger:filter_check_request/2
+    ),
     ?assertEqual(Body, ActualBody),
     ok.
 
@@ -297,8 +494,10 @@ todo_t_nest_object_check(_Config) ->
     Expect = #{
         bindings => #{},
         query_string => #{},
-        body => #{<<"per_page">> => 10,
-            <<"timeout">> => 600}
+        body => #{
+            <<"per_page">> => 10,
+            <<"timeout">> => 600
+        }
     },
     {ok, NewRequest} = check_requestBody(Path, Body),
     ?assertEqual(Expect, NewRequest),
@@ -330,7 +529,8 @@ t_remote_ref_trans(_Config) ->
         <<"page">> => 10,
         <<"another_ref">> => #{
             <<"version">> => "2.1.0",
-            <<"ip">> => <<"198.12.2.1:89">>}
+            <<"ip">> => <<"198.12.2.1:89">>
+        }
     },
     Expect = #{
         bindings => #{},
@@ -339,7 +539,8 @@ t_remote_ref_trans(_Config) ->
             <<"page">> => 10,
             <<"another_ref">> => #{
                 <<"version">> => "2.1.0",
-                <<"ip">> => {{198,12,2,1}, 89}}
+                <<"ip">> => {{198, 12, 2, 1}, 89}
+            }
         }
     },
     {ok, NewRequest} = trans_requestBody(Path, Body),
@@ -348,20 +549,25 @@ t_remote_ref_trans(_Config) ->
 
 t_nest_ref_trans(_Config) ->
     Path = "/ref/nest/ref",
-    Body = #{<<"env">> => <<"prod">>,
+    Body = #{
+        <<"env">> => <<"prod">>,
         <<"another_ref">> => #{
             <<"log_dir">> => "var/log/dev",
             <<"tag">> => <<"A">>,
             <<"webhook-host">> => "127.0.0.1:80"
-        }},
+        }
+    },
     Expect = #{
         bindings => #{},
         query_string => #{},
         body => #{
             <<"another_ref">> => #{
-                <<"log_dir">> => "var/log/dev", <<"tag">> => <<"A">>,
-                <<"webhook-host">> => {{127, 0, 0, 1}, 80}},
-            <<"env">> => prod}
+                <<"log_dir">> => "var/log/dev",
+                <<"tag">> => <<"A">>,
+                <<"webhook-host">> => {{127, 0, 0, 1}, 80}
+            },
+            <<"env">> => prod
+        }
     },
     {ok, NewRequest} = trans_requestBody(Path, Body),
     ?assertEqual(Expect, NewRequest),
@@ -382,7 +588,8 @@ t_ref_array_with_key_trans(_Config) ->
                 <<"log_dir">> => "var/log/test",
                 <<"tag">> => <<"B">>,
                 <<"webhook-host">> => "127.0.0.1:81"
-            }]
+            }
+        ]
     },
     Expect = #{
         bindings => #{},
@@ -410,16 +617,18 @@ t_ref_array_with_key_trans(_Config) ->
 
 t_ref_array_without_key_trans(_Config) ->
     Path = "/ref/array/without/key",
-    Body = [#{
-        <<"log_dir">> => "var/log/dev",
-        <<"tag">> => <<"A">>,
-        <<"webhook-host">> => "127.0.0.1:80"
-    },
+    Body = [
+        #{
+            <<"log_dir">> => "var/log/dev",
+            <<"tag">> => <<"A">>,
+            <<"webhook-host">> => "127.0.0.1:80"
+        },
         #{
             <<"log_dir">> => "var/log/test",
             <<"tag">> => <<"B">>,
             <<"webhook-host">> => "127.0.0.1:81"
-        }],
+        }
+    ],
     Expect = #{
         bindings => #{},
         query_string => #{},
@@ -433,7 +642,8 @@ t_ref_array_without_key_trans(_Config) ->
                 <<"log_dir">> => "var/log/test",
                 <<"tag">> => <<"B">>,
                 <<"webhook-host">> => {{127, 0, 0, 1}, 81}
-            }]
+            }
+        ]
     },
     {ok, NewRequest} = trans_requestBody(Path, Body),
     ?assertEqual(Expect, NewRequest),
@@ -441,12 +651,14 @@ t_ref_array_without_key_trans(_Config) ->
 
 t_ref_trans_error(_Config) ->
     Path = "/ref/nest/ref",
-    Body = #{<<"env">> => <<"prod">>,
+    Body = #{
+        <<"env">> => <<"prod">>,
         <<"another_ref">> => #{
             <<"log_dir">> => "var/log/dev",
             <<"tag">> => <<"A">>,
             <<"webhook-host">> => "127.0..0.1:80"
-        }},
+        }
+    },
     {400, 'BAD_REQUEST', _} = trans_requestBody(Path, Body),
     ok.
 
@@ -472,18 +684,23 @@ validate(Path, ExpectSpec, ExpectRefs) ->
     ?assertEqual(ExpectRefs, Refs),
     {Spec, emqx_dashboard_swagger:components(Refs, #{})}.
 
-
 filter(ApiSpec, Path) ->
     [Filter] = [F || {P, _, _, #{filter := F}} <- ApiSpec, P =:= Path],
     Filter.
 
 trans_requestBody(Path, Body) ->
-    trans_requestBody(Path, Body,
-        fun emqx_dashboard_swagger:filter_check_request_and_translate_body/2).
+    trans_requestBody(
+        Path,
+        Body,
+        fun emqx_dashboard_swagger:filter_check_request_and_translate_body/2
+    ).
 
 check_requestBody(Path, Body) ->
-    trans_requestBody(Path, Body,
-        fun emqx_dashboard_swagger:filter_check_request/2).
+    trans_requestBody(
+        Path,
+        Body,
+        fun emqx_dashboard_swagger:filter_check_request/2
+    ).
 
 trans_requestBody(Path, Body, Filter) ->
     Meta = #{module => ?MODULE, method => post, path => Path},
@@ -492,21 +709,34 @@ trans_requestBody(Path, Body, Filter) ->
 
 api_spec() -> emqx_dashboard_swagger:spec(?MODULE).
 paths() ->
-    ["/object", "/nest/object", "/ref/local", "/ref/nest/ref", "/fields/sub",
-        "/ref/array/with/key", "/ref/array/without/key"].
+    [
+        "/object",
+        "/nest/object",
+        "/ref/local",
+        "/ref/nest/ref",
+        "/fields/sub",
+        "/ref/array/with/key",
+        "/ref/array/without/key"
+    ].
 
 schema("/object") ->
     to_schema([
         {per_page, mk(range(1, 100), #{required => true, desc => <<"good per page desc">>})},
-        {timeout, mk(hoconsc:union([infinity, emqx_schema:duration_s()]),
-            #{default => 5, required => true})},
+        {timeout,
+            mk(
+                hoconsc:union([infinity, emqx_schema:duration_s()]),
+                #{default => 5, required => true}
+            )},
         {inner_ref, mk(hoconsc:ref(?MODULE, good_ref), #{})}
     ]);
 schema("/nest/object") ->
     to_schema([
         {per_page, mk(range(1, 100), #{desc => <<"good per page desc">>})},
-        {timeout, mk(hoconsc:union([infinity, emqx_schema:duration_s()]),
-            #{default => 5, required => true})},
+        {timeout,
+            mk(
+                hoconsc:union([infinity, emqx_schema:duration_s()]),
+                #{default => 5, required => true}
+            )},
         {nest_object, [
             {good_nest_1, mk(integer(), #{})},
             {good_nest_2, mk(hoconsc:ref(?MODULE, good_ref), #{})}
@@ -526,8 +756,11 @@ schema("/ref/nest/ref") ->
 schema("/ref/array/with/key") ->
     to_schema([
         {per_page, mk(range(1, 100), #{desc => <<"good per page desc">>})},
-        {timeout, mk(hoconsc:union([infinity, emqx_schema:duration_s()]),
-            #{default => 5, required => true})},
+        {timeout,
+            mk(
+                hoconsc:union([infinity, emqx_schema:duration_s()]),
+                #{default => 5, required => true}
+            )},
         {array_refs, mk(hoconsc:array(hoconsc:ref(?MODULE, good_ref)), #{})}
     ]);
 schema("/ref/array/without/key") ->
@@ -550,18 +783,20 @@ fields(nest_ref) ->
         {env, mk(hoconsc:enum([test, dev, prod]), #{})},
         {another_ref, mk(hoconsc:ref(good_ref), #{desc => "nest ref"})}
     ];
-
-fields(bad_ref) -> %% don't support maps
+%% don't support maps
+fields(bad_ref) ->
     #{
         username => mk(string(), #{}),
         is_admin => mk(boolean(), #{})
     };
 fields(sub_fields) ->
-    #{fields => [
-        {enable,     fun enable/1},
-        {init_file,  fun init_file/1}
-    ],
-        desc => <<"test sub fields">>}.
+    #{
+        fields => [
+            {enable, fun enable/1},
+            {init_file, fun init_file/1}
+        ],
+        desc => <<"test sub fields">>
+    }.
 
 enable(type) -> boolean();
 enable(desc) -> <<"Whether to enable tls psk support">>;

+ 457 - 184
apps/emqx_dashboard/test/emqx_swagger_response_SUITE.erl

@@ -46,37 +46,78 @@ t_simple_binary(_config) ->
 t_object(_config) ->
     Path = "/object",
     Object =
-        #{<<"content">> => #{<<"application/json">> =>
-        #{<<"schema">> => #{required => [<<"timeout">>, <<"per_page">>],
-            <<"properties">> => [
-                {<<"per_page">>, #{description => <<"good per page desc">>,
-                    maximum => 100, minimum => 1, type => integer}},
-                {<<"timeout">>, #{default => 5, <<"oneOf">> =>
-                [#{example => <<"1h">>, type => string}, #{enum => [infinity], type => string}]}},
-                {<<"inner_ref">>, #{<<"$ref">> =>
-                <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>}}],
-            <<"type">> => object}}}},
+        #{
+            <<"content">> => #{
+                <<"application/json">> =>
+                    #{
+                        <<"schema">> => #{
+                            required => [<<"timeout">>, <<"per_page">>],
+                            <<"properties">> => [
+                                {<<"per_page">>, #{
+                                    description => <<"good per page desc">>,
+                                    maximum => 100,
+                                    minimum => 1,
+                                    type => integer
+                                }},
+                                {<<"timeout">>, #{
+                                    default => 5,
+                                    <<"oneOf">> =>
+                                        [
+                                            #{example => <<"1h">>, type => string},
+                                            #{enum => [infinity], type => string}
+                                        ]
+                                }},
+                                {<<"inner_ref">>, #{
+                                    <<"$ref">> =>
+                                        <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>
+                                }}
+                            ],
+                            <<"type">> => object
+                        }
+                    }
+            }
+        },
     ExpectRefs = [{?MODULE, good_ref}],
     validate(Path, Object, ExpectRefs),
     ok.
 
 t_error(_Config) ->
     Path = "/error",
-    Error400 = #{<<"content">> =>
-    #{<<"application/json">> => #{<<"schema">> => #{<<"type">> => object,
-        <<"properties">> =>
-        [
-            {<<"code">>, #{enum => ['Bad1', 'Bad2'], type => string}},
-            {<<"message">>, #{description => <<"Bad request desc">>, type => string}}]
-    }}}},
-    Error404 = #{<<"content">> =>
-    #{<<"application/json">> => #{<<"schema">> => #{<<"type">> => object,
-        <<"properties">> =>
-        [
-            {<<"code">>, #{enum => ['Not-Found'], type => string}},
-            {<<"message">>, #{
-                description => <<"Error code to troubleshoot problems.">>, type => string}}]
-    }}}},
+    Error400 = #{
+        <<"content">> =>
+            #{
+                <<"application/json">> => #{
+                    <<"schema">> => #{
+                        <<"type">> => object,
+                        <<"properties">> =>
+                            [
+                                {<<"code">>, #{enum => ['Bad1', 'Bad2'], type => string}},
+                                {<<"message">>, #{
+                                    description => <<"Bad request desc">>, type => string
+                                }}
+                            ]
+                    }
+                }
+            }
+    },
+    Error404 = #{
+        <<"content">> =>
+            #{
+                <<"application/json">> => #{
+                    <<"schema">> => #{
+                        <<"type">> => object,
+                        <<"properties">> =>
+                            [
+                                {<<"code">>, #{enum => ['Not-Found'], type => string}},
+                                {<<"message">>, #{
+                                    description => <<"Error code to troubleshoot problems.">>,
+                                    type => string
+                                }}
+                            ]
+                    }
+                }
+            }
+    },
     {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
     ?assertEqual(test, OperationId),
     Response = maps:get(responses, maps:get(get, Spec)),
@@ -89,121 +130,243 @@ t_error(_Config) ->
 t_nest_object(_Config) ->
     Path = "/nest/object",
     Object =
-        #{<<"content">> => #{<<"application/json">> => #{<<"schema">> =>
-        #{required => [<<"timeout">>], <<"type">> => object, <<"properties">> => [
-            {<<"per_page">>, #{description => <<"good per page desc">>,
-                maximum => 100, minimum => 1, type => integer}},
-            {<<"timeout">>, #{default => 5, <<"oneOf">> =>
-            [#{example => <<"1h">>, type => string}, #{enum => [infinity], type => string}]}},
-            {<<"nest_object">>, #{<<"type">> => object, <<"properties">> => [
-                {<<"good_nest_1">>, #{type => integer}},
-                {<<"good_nest_2">>, #{<<"$ref">> =>
-                <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>}
-                }]}},
-            {<<"inner_ref">>, #{<<"$ref">> =>
-            <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>}}]
-        }}}},
+        #{
+            <<"content">> => #{
+                <<"application/json">> => #{
+                    <<"schema">> =>
+                        #{
+                            required => [<<"timeout">>],
+                            <<"type">> => object,
+                            <<"properties">> => [
+                                {<<"per_page">>, #{
+                                    description => <<"good per page desc">>,
+                                    maximum => 100,
+                                    minimum => 1,
+                                    type => integer
+                                }},
+                                {<<"timeout">>, #{
+                                    default => 5,
+                                    <<"oneOf">> =>
+                                        [
+                                            #{example => <<"1h">>, type => string},
+                                            #{enum => [infinity], type => string}
+                                        ]
+                                }},
+                                {<<"nest_object">>, #{
+                                    <<"type">> => object,
+                                    <<"properties">> => [
+                                        {<<"good_nest_1">>, #{type => integer}},
+                                        {<<"good_nest_2">>, #{
+                                            <<"$ref">> =>
+                                                <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>
+                                        }}
+                                    ]
+                                }},
+                                {<<"inner_ref">>, #{
+                                    <<"$ref">> =>
+                                        <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>
+                                }}
+                            ]
+                        }
+                }
+            }
+        },
     ExpectRefs = [{?MODULE, good_ref}],
     validate(Path, Object, ExpectRefs),
     ok.
 
 t_empty(_Config) ->
-    ?assertThrow({error,
-        #{msg := <<"Object only supports not empty proplists">>,
-            args := [], module := ?MODULE}}, validate("/empty", error, [])),
+    ?assertThrow(
+        {error, #{
+            msg := <<"Object only supports not empty proplists">>,
+            args := [],
+            module := ?MODULE
+        }},
+        validate("/empty", error, [])
+    ),
     ok.
 
 t_raw_local_ref(_Config) ->
     Path = "/raw/ref/local",
-    Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => #{
-        <<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>}}}},
+    Object = #{
+        <<"content">> => #{
+            <<"application/json">> => #{
+                <<"schema">> => #{
+                    <<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>
+                }
+            }
+        }
+    },
     ExpectRefs = [{?MODULE, good_ref}],
     validate(Path, Object, ExpectRefs),
     ok.
 
 t_raw_remote_ref(_Config) ->
     Path = "/raw/ref/remote",
-    Object = #{<<"content">> =>
-    #{<<"application/json">> => #{<<"schema">> => #{
-        <<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.ref1">>}}}},
+    Object = #{
+        <<"content">> =>
+            #{
+                <<"application/json">> => #{
+                    <<"schema">> => #{
+                        <<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.ref1">>
+                    }
+                }
+            }
+    },
     ExpectRefs = [{emqx_swagger_remote_schema, "ref1"}],
     validate(Path, Object, ExpectRefs),
     ok.
 
 t_local_ref(_Config) ->
     Path = "/ref/local",
-    Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => #{
-        <<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>}}}},
+    Object = #{
+        <<"content">> => #{
+            <<"application/json">> => #{
+                <<"schema">> => #{
+                    <<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>
+                }
+            }
+        }
+    },
     ExpectRefs = [{?MODULE, good_ref}],
     validate(Path, Object, ExpectRefs),
     ok.
 
 t_remote_ref(_Config) ->
     Path = "/ref/remote",
-    Object = #{<<"content">> =>
-    #{<<"application/json">> => #{<<"schema">> => #{
-        <<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.ref1">>}}}},
+    Object = #{
+        <<"content">> =>
+            #{
+                <<"application/json">> => #{
+                    <<"schema">> => #{
+                        <<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.ref1">>
+                    }
+                }
+            }
+    },
     ExpectRefs = [{emqx_swagger_remote_schema, "ref1"}],
     validate(Path, Object, ExpectRefs),
     ok.
 
 t_bad_ref(_Config) ->
     Path = "/ref/bad",
-    Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> =>
-    #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.bad_ref">>}}}},
+    Object = #{
+        <<"content">> => #{
+            <<"application/json">> => #{
+                <<"schema">> =>
+                    #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.bad_ref">>}
+            }
+        }
+    },
     ExpectRefs = [{?MODULE, bad_ref}],
-    ?assertThrow({error, #{module := ?MODULE,
-        msg := <<"Object only supports not empty proplists">>}},
-        validate(Path, Object, ExpectRefs)),
+    ?assertThrow(
+        {error, #{
+            module := ?MODULE,
+            msg := <<"Object only supports not empty proplists">>
+        }},
+        validate(Path, Object, ExpectRefs)
+    ),
     ok.
 
 t_none_ref(_Config) ->
     Path = "/ref/none",
-    ?assertThrow({error, #{mfa := {?MODULE, schema, ["/ref/none"]},
-        reason := function_clause}}, validate(Path, #{}, [])),
+    ?assertThrow(
+        {error, #{
+            mfa := {?MODULE, schema, ["/ref/none"]},
+            reason := function_clause
+        }},
+        validate(Path, #{}, [])
+    ),
     ok.
 
 t_nest_ref(_Config) ->
     Path = "/ref/nest/ref",
-    Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => #{
-        <<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.nest_ref">>}}}},
+    Object = #{
+        <<"content">> => #{
+            <<"application/json">> => #{
+                <<"schema">> => #{
+                    <<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.nest_ref">>
+                }
+            }
+        }
+    },
     ExpectRefs = [{?MODULE, nest_ref}],
     validate(Path, Object, ExpectRefs),
     ok.
 
 t_sub_fields(_Config) ->
     Path = "/fields/sub",
-    Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => #{
-        <<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.sub_fields">>}}}},
+    Object = #{
+        <<"content">> => #{
+            <<"application/json">> => #{
+                <<"schema">> => #{
+                    <<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.sub_fields">>
+                }
+            }
+        }
+    },
     ExpectRefs = [{?MODULE, sub_fields}],
     validate(Path, Object, ExpectRefs),
     ok.
 
 t_complicated_type(_Config) ->
     Path = "/ref/complicated_type",
-    Object = #{<<"content">> => #{<<"application/json">> =>
-    #{<<"schema">> => #{<<"properties">> =>
-    [
-        {<<"no_neg_integer">>, #{minimum => 0, type => integer}},
-        {<<"url">>, #{example => <<"http://127.0.0.1">>, type => string}},
-        {<<"server">>, #{example => <<"127.0.0.1:80">>, type => string}},
-        {<<"connect_timeout">>, #{example => infinity, <<"oneOf">> => [
-            #{example => infinity, type => string},
-            #{type => integer}]}},
-        {<<"pool_type">>, #{enum => [random, hash], type => string}},
-        {<<"timeout">>, #{example => infinity,
-            <<"oneOf">> => [#{example => infinity, type => string}, #{type => integer}]}},
-        {<<"bytesize">>, #{example => <<"32MB">>, type => string}},
-        {<<"wordsize">>, #{example => <<"1024KB">>, type => string}},
-        {<<"maps">>, #{example => #{}, type => object}},
-        {<<"comma_separated_list">>, #{example => <<"item1,item2">>, type => string}},
-        {<<"comma_separated_atoms">>, #{example => <<"item1,item2">>, type => string}},
-        {<<"log_level">>,
-            #{enum => [debug, info, notice, warning, error, critical, alert, emergency, all],
-                type => string}},
-        {<<"fix_integer">>, #{default => 100, enum => [100],type => integer}}
-    ],
-        <<"type">> => object}}}},
+    Object = #{
+        <<"content">> => #{
+            <<"application/json">> =>
+                #{
+                    <<"schema">> => #{
+                        <<"properties">> =>
+                            [
+                                {<<"no_neg_integer">>, #{minimum => 0, type => integer}},
+                                {<<"url">>, #{example => <<"http://127.0.0.1">>, type => string}},
+                                {<<"server">>, #{example => <<"127.0.0.1:80">>, type => string}},
+                                {<<"connect_timeout">>, #{
+                                    example => infinity,
+                                    <<"oneOf">> => [
+                                        #{example => infinity, type => string},
+                                        #{type => integer}
+                                    ]
+                                }},
+                                {<<"pool_type">>, #{enum => [random, hash], type => string}},
+                                {<<"timeout">>, #{
+                                    example => infinity,
+                                    <<"oneOf">> => [
+                                        #{example => infinity, type => string}, #{type => integer}
+                                    ]
+                                }},
+                                {<<"bytesize">>, #{example => <<"32MB">>, type => string}},
+                                {<<"wordsize">>, #{example => <<"1024KB">>, type => string}},
+                                {<<"maps">>, #{example => #{}, type => object}},
+                                {<<"comma_separated_list">>, #{
+                                    example => <<"item1,item2">>, type => string
+                                }},
+                                {<<"comma_separated_atoms">>, #{
+                                    example => <<"item1,item2">>, type => string
+                                }},
+                                {<<"log_level">>, #{
+                                    enum => [
+                                        debug,
+                                        info,
+                                        notice,
+                                        warning,
+                                        error,
+                                        critical,
+                                        alert,
+                                        emergency,
+                                        all
+                                    ],
+                                    type => string
+                                }},
+                                {<<"fix_integer">>, #{
+                                    default => 100, enum => [100], type => integer
+                                }}
+                            ],
+                        <<"type">> => object
+                    }
+                }
+        }
+    },
     {OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
     ?assertEqual(test, OperationId),
     Response = maps:get(responses, maps:get(post, Spec)),
@@ -211,79 +374,165 @@ t_complicated_type(_Config) ->
     ?assertEqual([], Refs),
     ok.
 
-
 t_ref_array_with_key(_Config) ->
     Path = "/ref/array/with/key",
-    Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => #{
-        required => [<<"timeout">>], <<"type">> => object, <<"properties">> => [
-            {<<"per_page">>, #{description => <<"good per page desc">>,
-                maximum => 100, minimum => 1, type => integer}},
-            {<<"timeout">>, #{default => 5, <<"oneOf">> =>
-            [#{example => <<"1h">>, type => string}, #{enum => [infinity], type => string}]}},
-            {<<"assert">>, #{description => <<"money">>, type => number}},
-            {<<"number_ex">>, #{description => <<"number example">>, type => number}},
-            {<<"percent_ex">>, #{description => <<"percent example">>,
-                example => <<"12%">>, type => number}},
-            {<<"duration_ms_ex">>, #{description => <<"duration ms example">>,
-                example => <<"32s">>, type => string}},
-            {<<"atom_ex">>, #{description => <<"atom ex">>, type => string}},
-            {<<"array_refs">>, #{items => #{<<"$ref">> =>
-            <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>}, type => array}}
-        ]}
-    }}},
+    Object = #{
+        <<"content">> => #{
+            <<"application/json">> => #{
+                <<"schema">> => #{
+                    required => [<<"timeout">>],
+                    <<"type">> => object,
+                    <<"properties">> => [
+                        {<<"per_page">>, #{
+                            description => <<"good per page desc">>,
+                            maximum => 100,
+                            minimum => 1,
+                            type => integer
+                        }},
+                        {<<"timeout">>, #{
+                            default => 5,
+                            <<"oneOf">> =>
+                                [
+                                    #{example => <<"1h">>, type => string},
+                                    #{enum => [infinity], type => string}
+                                ]
+                        }},
+                        {<<"assert">>, #{description => <<"money">>, type => number}},
+                        {<<"number_ex">>, #{description => <<"number example">>, type => number}},
+                        {<<"percent_ex">>, #{
+                            description => <<"percent example">>,
+                            example => <<"12%">>,
+                            type => number
+                        }},
+                        {<<"duration_ms_ex">>, #{
+                            description => <<"duration ms example">>,
+                            example => <<"32s">>,
+                            type => string
+                        }},
+                        {<<"atom_ex">>, #{description => <<"atom ex">>, type => string}},
+                        {<<"array_refs">>, #{
+                            items => #{
+                                <<"$ref">> =>
+                                    <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>
+                            },
+                            type => array
+                        }}
+                    ]
+                }
+            }
+        }
+    },
     ExpectRefs = [{?MODULE, good_ref}],
     validate(Path, Object, ExpectRefs),
     ok.
 
 t_ref_array_without_key(_Config) ->
     Path = "/ref/array/without/key",
-    Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> => #{
-        items => #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>},
-        type => array}}}},
+    Object = #{
+        <<"content">> => #{
+            <<"application/json">> => #{
+                <<"schema">> => #{
+                    items => #{
+                        <<"$ref">> =>
+                            <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>
+                    },
+                    type => array
+                }
+            }
+        }
+    },
     ExpectRefs = [{?MODULE, good_ref}],
     validate(Path, Object, ExpectRefs),
     ok.
 t_hocon_schema_function(_Config) ->
     Path = "/ref/hocon/schema/function",
-    Object = #{<<"content">> => #{<<"application/json">> => #{<<"schema">> =>
-    #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.root">>}}}},
+    Object = #{
+        <<"content">> => #{
+            <<"application/json">> => #{
+                <<"schema">> =>
+                    #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.root">>}
+            }
+        }
+    },
     ExpectComponents = [
-        #{<<"emqx_swagger_remote_schema.ref1">> => #{<<"type">> => object,
-            <<"properties">> => [
-                {<<"protocol">>, #{enum => [http, https], type => string}},
-                {<<"port">>, #{default => 18083, type => integer}}]
-        }},
-        #{<<"emqx_swagger_remote_schema.ref2">> => #{<<"type">> => object,
-            <<"properties">> => [
-                {<<"page">>, #{description => <<"good page">>,
-                    maximum => 100, minimum => 1, type => integer}},
-                {<<"another_ref">>, #{<<"$ref">> =>
-                <<"#/components/schemas/emqx_swagger_remote_schema.ref3">>}}
-            ]
-        }},
-        #{<<"emqx_swagger_remote_schema.ref3">> => #{<<"type">> => object,
-            <<"properties">> => [
-                {<<"ip">>, #{description => <<"IP:Port">>,
-                    example => <<"127.0.0.1:80">>, type => string}},
-                {<<"version">>, #{description => <<"a good version">>,
-                    example => <<"1.0.0">>, type => string}}]
-        }},
-        #{<<"emqx_swagger_remote_schema.root">> =>
-        #{required => [<<"default_password">>, <<"default_username">>],
-            <<"properties">> => [{<<"listeners">>, #{items =>
-            #{<<"oneOf">> =>
-            [#{<<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.ref2">>},
-                #{<<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.ref1">>}]},
-                type => array}},
-                {<<"default_username">>,
-                    #{default => <<"admin">>, type => string}},
-                {<<"default_password">>,
-                    #{default => <<"public">>, type => string}},
-                {<<"sample_interval">>,
-                    #{default => <<"10s">>, example => <<"1h">>, type => string}},
-                {<<"token_expired_time">>,
-                    #{default => <<"30m">>, example => <<"12m">>, type => string}}],
-            <<"type">> => object}}],
+        #{
+            <<"emqx_swagger_remote_schema.ref1">> => #{
+                <<"type">> => object,
+                <<"properties">> => [
+                    {<<"protocol">>, #{enum => [http, https], type => string}},
+                    {<<"port">>, #{default => 18083, type => integer}}
+                ]
+            }
+        },
+        #{
+            <<"emqx_swagger_remote_schema.ref2">> => #{
+                <<"type">> => object,
+                <<"properties">> => [
+                    {<<"page">>, #{
+                        description => <<"good page">>,
+                        maximum => 100,
+                        minimum => 1,
+                        type => integer
+                    }},
+                    {<<"another_ref">>, #{
+                        <<"$ref">> =>
+                            <<"#/components/schemas/emqx_swagger_remote_schema.ref3">>
+                    }}
+                ]
+            }
+        },
+        #{
+            <<"emqx_swagger_remote_schema.ref3">> => #{
+                <<"type">> => object,
+                <<"properties">> => [
+                    {<<"ip">>, #{
+                        description => <<"IP:Port">>,
+                        example => <<"127.0.0.1:80">>,
+                        type => string
+                    }},
+                    {<<"version">>, #{
+                        description => <<"a good version">>,
+                        example => <<"1.0.0">>,
+                        type => string
+                    }}
+                ]
+            }
+        },
+        #{
+            <<"emqx_swagger_remote_schema.root">> =>
+                #{
+                    required => [<<"default_password">>, <<"default_username">>],
+                    <<"properties">> => [
+                        {<<"listeners">>, #{
+                            items =>
+                                #{
+                                    <<"oneOf">> =>
+                                        [
+                                            #{
+                                                <<"$ref">> =>
+                                                    <<"#/components/schemas/emqx_swagger_remote_schema.ref2">>
+                                            },
+                                            #{
+                                                <<"$ref">> =>
+                                                    <<"#/components/schemas/emqx_swagger_remote_schema.ref1">>
+                                            }
+                                        ]
+                                },
+                            type => array
+                        }},
+                        {<<"default_username">>, #{default => <<"admin">>, type => string}},
+                        {<<"default_password">>, #{default => <<"public">>, type => string}},
+                        {<<"sample_interval">>, #{
+                            default => <<"10s">>, example => <<"1h">>, type => string
+                        }},
+                        {<<"token_expired_time">>, #{
+                            default => <<"30m">>, example => <<"12m">>, type => string
+                        }}
+                    ],
+                    <<"type">> => object
+                }
+        }
+    ],
     ExpectRefs = [{emqx_swagger_remote_schema, "root"}],
     {_, Components} = validate(Path, Object, ExpectRefs),
     ?assertEqual(ExpectComponents, Components),
@@ -296,31 +545,46 @@ t_api_spec(_Config) ->
 api_spec() -> emqx_dashboard_swagger:spec(?MODULE).
 
 paths() ->
-    ["/simple/bin", "/object", "/nest/object", "/ref/local",
-        "/ref/nest/ref", "/raw/ref/local", "/raw/ref/remote",
-        "/ref/array/with/key", "/ref/array/without/key",
-        "/ref/hocon/schema/function"].
+    [
+        "/simple/bin",
+        "/object",
+        "/nest/object",
+        "/ref/local",
+        "/ref/nest/ref",
+        "/raw/ref/local",
+        "/raw/ref/remote",
+        "/ref/array/with/key",
+        "/ref/array/without/key",
+        "/ref/hocon/schema/function"
+    ].
 
 schema("/simple/bin") ->
     to_schema(<<"binary ok">>);
 schema("/object") ->
     Object = [
         {per_page, mk(range(1, 100), #{required => true, desc => <<"good per page desc">>})},
-        {timeout, mk(hoconsc:union([infinity, emqx_schema:duration_s()]),
-            #{default => 5, required => true})},
+        {timeout,
+            mk(
+                hoconsc:union([infinity, emqx_schema:duration_s()]),
+                #{default => 5, required => true}
+            )},
         {inner_ref, mk(hoconsc:ref(?MODULE, good_ref), #{})}
     ],
     to_schema(Object);
 schema("/nest/object") ->
     Response = [
         {per_page, mk(range(1, 100), #{desc => <<"good per page desc">>})},
-        {timeout, mk(hoconsc:union([infinity, emqx_schema:duration_s()]),
-            #{default => 5, required => true})},
+        {timeout,
+            mk(
+                hoconsc:union([infinity, emqx_schema:duration_s()]),
+                #{default => 5, required => true}
+            )},
         {nest_object, [
             {good_nest_1, mk(integer(), #{})},
             {good_nest_2, mk(hoconsc:ref(?MODULE, good_ref), #{})}
         ]},
-        {inner_ref, mk(hoconsc:ref(?MODULE, good_ref), #{})}],
+        {inner_ref, mk(hoconsc:ref(?MODULE, good_ref), #{})}
+    ],
     to_schema(Response);
 schema("/empty") ->
     to_schema([]);
@@ -339,8 +603,11 @@ schema("/ref/nest/ref") ->
 schema("/ref/array/with/key") ->
     to_schema([
         {per_page, mk(range(1, 100), #{desc => <<"good per page desc">>})},
-        {timeout, mk(hoconsc:union([infinity, emqx_schema:duration_s()]),
-            #{default => 5, required => true})},
+        {timeout,
+            mk(
+                hoconsc:union([infinity, emqx_schema:duration_s()]),
+                #{default => 5, required => true}
+            )},
         {assert, mk(float(), #{desc => <<"money">>})},
         {number_ex, mk(number(), #{desc => <<"number example">>})},
         {percent_ex, mk(emqx_schema:percent(), #{desc => <<"percent example">>})},
@@ -355,31 +622,35 @@ schema("/ref/hocon/schema/function") ->
 schema("/error") ->
     #{
         operationId => test,
-        get => #{responses => #{
-            400 => emqx_dashboard_swagger:error_codes(['Bad1', 'Bad2'], <<"Bad request desc">>),
-            404 => emqx_dashboard_swagger:error_codes(['Not-Found'])
-        }}
+        get => #{
+            responses => #{
+                400 => emqx_dashboard_swagger:error_codes(['Bad1', 'Bad2'], <<"Bad request desc">>),
+                404 => emqx_dashboard_swagger:error_codes(['Not-Found'])
+            }
+        }
     };
 schema("/ref/complicated_type") ->
     #{
         operationId => test,
-        post => #{responses => #{
-            200 => [
-                {no_neg_integer, hoconsc:mk(non_neg_integer(), #{})},
-                {url, hoconsc:mk(emqx_connector_http:url(), #{})},
-                {server, hoconsc:mk(emqx_schema:ip_port(), #{})},
-                {connect_timeout, hoconsc:mk(emqx_connector_http:connect_timeout(), #{})},
-                {pool_type, hoconsc:mk(emqx_connector_http:pool_type(), #{})},
-                {timeout, hoconsc:mk(timeout(), #{})},
-                {bytesize, hoconsc:mk(emqx_schema:bytesize(), #{})},
-                {wordsize, hoconsc:mk(emqx_schema:wordsize(), #{})},
-                {maps, hoconsc:mk(map(), #{})},
-                {comma_separated_list, hoconsc:mk(emqx_schema:comma_separated_list(), #{})},
-                {comma_separated_atoms, hoconsc:mk(emqx_schema:comma_separated_atoms(), #{})},
-                {log_level, hoconsc:mk(emqx_conf_schema:log_level(), #{})},
-                {fix_integer, hoconsc:mk(typerefl:integer(100), #{})}
-            ]
-        }}
+        post => #{
+            responses => #{
+                200 => [
+                    {no_neg_integer, hoconsc:mk(non_neg_integer(), #{})},
+                    {url, hoconsc:mk(emqx_connector_http:url(), #{})},
+                    {server, hoconsc:mk(emqx_schema:ip_port(), #{})},
+                    {connect_timeout, hoconsc:mk(emqx_connector_http:connect_timeout(), #{})},
+                    {pool_type, hoconsc:mk(emqx_connector_http:pool_type(), #{})},
+                    {timeout, hoconsc:mk(timeout(), #{})},
+                    {bytesize, hoconsc:mk(emqx_schema:bytesize(), #{})},
+                    {wordsize, hoconsc:mk(emqx_schema:wordsize(), #{})},
+                    {maps, hoconsc:mk(map(), #{})},
+                    {comma_separated_list, hoconsc:mk(emqx_schema:comma_separated_list(), #{})},
+                    {comma_separated_atoms, hoconsc:mk(emqx_schema:comma_separated_atoms(), #{})},
+                    {log_level, hoconsc:mk(emqx_conf_schema:log_level(), #{})},
+                    {fix_integer, hoconsc:mk(typerefl:integer(100), #{})}
+                ]
+            }
+        }
     };
 schema("/fields/sub") ->
     to_schema(hoconsc:ref(sub_fields)).
@@ -411,18 +682,20 @@ fields(nest_ref) ->
         {env, mk(hoconsc:enum([test, dev, prod]), #{})},
         {another_ref, mk(hoconsc:ref(good_ref), #{desc => "nest ref"})}
     ];
-
-fields(bad_ref) -> %% don't support maps
+%% don't support maps
+fields(bad_ref) ->
     #{
         username => mk(string(), #{}),
         is_admin => mk(boolean(), #{})
     };
 fields(sub_fields) ->
-    #{fields => [
-        {enable,     fun enable/1},
-        {init_file,  fun init_file/1}
-    ],
-        desc => <<"test sub fields">>}.
+    #{
+        fields => [
+            {enable, fun enable/1},
+            {init_file, fun init_file/1}
+        ],
+        desc => <<"test sub fields">>
+    }.
 
 enable(type) -> boolean();
 enable(desc) -> <<"Whether to enable tls psk support">>;

+ 2 - 1
apps/emqx_management/src/emqx_mgmt_api_configs.erl

@@ -284,7 +284,8 @@ get_full_config() ->
         maps:without(
             ?EXCLUDES,
             emqx:get_raw_config([])
-        )
+        ),
+        #{obfuscate_sensitive_values => true}
     ).
 
 get_config_with_default(Path) ->

+ 1 - 1
apps/emqx_prometheus/rebar.config

@@ -4,7 +4,7 @@
  [ {emqx, {path, "../emqx"}},
    %% FIXME: tag this as v3.1.3
    {prometheus, {git, "https://github.com/deadtrickster/prometheus.erl", {tag, "v4.8.1"}}},
-   {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.27.2"}}}
+   {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.27.3"}}}
  ]}.
 
 {edoc_opts, [{preprocess, true}]}.

+ 1 - 1
mix.exs

@@ -68,7 +68,7 @@ defmodule EMQXUmbrella.MixProject do
       # in conflict by emqtt and hocon
       {:getopt, "1.0.2", override: true},
       {:snabbkaffe, github: "kafka4beam/snabbkaffe", tag: "0.18.0", override: true},
-      {:hocon, github: "emqx/hocon", tag: "0.27.2", override: true},
+      {:hocon, github: "emqx/hocon", tag: "0.27.3", override: true},
       {:emqx_http_lib, github: "emqx/emqx_http_lib", tag: "0.5.1", override: true},
       {:esasl, github: "emqx/esasl", tag: "0.2.0"},
       {:jose, github: "potatosalad/erlang-jose", tag: "1.11.2"},

+ 1 - 1
rebar.config

@@ -66,7 +66,7 @@
     , {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.3"}}}
     , {getopt, "1.0.2"}
     , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.18.0"}}}
-    , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.27.2"}}}
+    , {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.27.3"}}}
     , {emqx_http_lib, {git, "https://github.com/emqx/emqx_http_lib.git", {tag, "0.5.1"}}}
     , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}}
     , {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.2"}}}

+ 1 - 1
scripts/merge-i18n.escript

@@ -3,7 +3,7 @@
 -mode(compile).
 
 main(_) ->
-    {ok, BaseConf} = file:read_file("apps/emqx_dashboard/i18n/emqx_dashboard_schema.conf"),
+    {ok, BaseConf} = file:read_file("apps/emqx_dashboard/i18n/emqx_dashboard_i18n.conf"),
 
     Cfgs = get_all_cfgs("apps/"),
     Conf = [merge(BaseConf, Cfgs),