feat: obfuscate sensitive values default_password

This commit is contained in:
Zhongwen Deng 2022-04-24 09:23:16 +08:00
parent 575d34cb69
commit 5223c3ee61
22 changed files with 1343 additions and 601 deletions

View File

@ -29,7 +29,7 @@
{esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.1"}}}, {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.1"}}},
{ekka, {git, "https://github.com/emqx/ekka", {tag, "0.12.3"}}}, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.12.3"}}},
{gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}}, {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"}}}, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}},
{recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}}, {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}},
{snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.18.0"}}} {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.18.0"}}}

View File

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

View File

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

View File

@ -532,7 +532,7 @@ format_metrics(#{
fill_defaults(Type, RawConf) -> fill_defaults(Type, RawConf) ->
PackedConf = pack_bridge_conf(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). unpack_bridge_conf(Type, FullConf).
pack_bridge_conf(Type, RawConf) -> pack_bridge_conf(Type, RawConf) ->

View File

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

View File

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

View File

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

View File

@ -138,7 +138,7 @@ fields(limit) ->
[{limit, hoconsc:mk(range(1, ?MAX_ROW_LIMIT), Meta)}]; [{limit, hoconsc:mk(range(1, ?MAX_ROW_LIMIT), Meta)}];
fields(count) -> fields(count) ->
Meta = #{desc => <<"Results count.">>, required => true}, Meta = #{desc => <<"Results count.">>, required => true},
[{count, hoconsc:mk(range(0, inf), Meta)}]; [{count, hoconsc:mk(non_neg_integer(), Meta)}];
fields(meta) -> fields(meta) ->
fields(page) ++ fields(limit) ++ fields(count). 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)), NewBody = check_request_body(Request, Body, Module, CheckFun, hoconsc:is_schema(Body)),
{ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}} {ok, Request#{bindings => Bindings, query_string => QueryStr, body => NewBody}}
catch catch
throw:{_, ValidErrors} -> throw:HoconError ->
Msg = [ Msg = serialize_hocon_error_msg(HoconError),
io_lib:format("~ts : ~p", [Key, Reason]) %Msg = [
|| {validation_error, #{path := Key, reason := Reason}} <- ValidErrors % io_lib:format("~ts : ~p", [Key -- "root.", Reason])
], % || {validation_error, #{path := Key, reason := Reason}} <- ValidErrors
{400, 'BAD_REQUEST', iolist_to_binary(string:join(Msg, ","))} % ],
% iolist_to_binary(string:join(Msg, ",")
{400, 'BAD_REQUEST', Msg}
end. end.
check_and_translate(Schema, Map, Opts) -> check_and_translate(Schema, Map, Opts) ->
@ -808,3 +810,18 @@ to_ref(Mod, StructName, Acc, RefsAcc) ->
schema_converter(Options) -> schema_converter(Options) ->
maps:get(schema_converter, Options, fun hocon_schema_to_spec/2). 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.").

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
[ {emqx, {path, "../emqx"}}, [ {emqx, {path, "../emqx"}},
%% FIXME: tag this as v3.1.3 %% FIXME: tag this as v3.1.3
{prometheus, {git, "https://github.com/deadtrickster/prometheus.erl", {tag, "v4.8.1"}}}, {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}]}. {edoc_opts, [{preprocess, true}]}.

View File

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

View File

@ -66,7 +66,7 @@
, {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.3"}}} , {system_monitor, {git, "https://github.com/ieQu1/system_monitor", {tag, "3.0.3"}}}
, {getopt, "1.0.2"} , {getopt, "1.0.2"}
, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.18.0"}}} , {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"}}} , {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"}}} , {esasl, {git, "https://github.com/emqx/esasl", {tag, "0.2.0"}}}
, {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.2"}}} , {jose, {git, "https://github.com/potatosalad/erlang-jose", {tag, "1.11.2"}}}

View File

@ -3,7 +3,7 @@
-mode(compile). -mode(compile).
main(_) -> 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/"), Cfgs = get_all_cfgs("apps/"),
Conf = [merge(BaseConf, Cfgs), Conf = [merge(BaseConf, Cfgs),