emqx/apps/emqx_dashboard/test/emqx_swagger_requestBody_SU...

810 lines
26 KiB
Erlang

-module(emqx_swagger_requestBody_SUITE).
-behaviour(minirest_api).
-behaviour(hocon_schema).
-compile(nowarn_export_all).
-compile(export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("typerefl/include/types.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-import(hoconsc, [mk/2]).
all() -> emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) ->
mria:start(),
application:load(emqx_dashboard),
emqx_common_test_helpers:start_apps([emqx_conf, emqx_dashboard], fun set_special_configs/1),
emqx_dashboard:init_i18n(),
Config.
set_special_configs(emqx_dashboard) ->
emqx_dashboard_api_test_helpers:set_default_config(),
ok;
set_special_configs(_) ->
ok.
end_per_suite(Config) ->
end_suite(),
Config.
end_suite() ->
application:unload(emqx_management),
emqx_common_test_helpers:stop_apps([emqx_dashboard]).
t_object(_Config) ->
Spec = #{
post => #{
parameters => [],
requestBody => #{
<<"content">> =>
#{
<<"application/json">> =>
#{
<<"schema">> =>
#{
required => [<<"timeout">>, <<"per_page">>],
<<"properties">> => [
{<<"per_page">>, #{
description => <<"good per page desc">>,
maximum => 100,
minimum => 1,
type => integer
}},
{<<"timeout">>, #{
default => 5,
<<"oneOf">> =>
[
#{example => <<"1h">>, type => string},
#{enum => [infinity], type => string}
]
}},
{<<"inner_ref">>, #{
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>
}}
],
<<"type">> => object
}
}
}
},
responses => #{<<"200">> => #{description => <<"ok">>}}
}
},
Refs = [{?MODULE, good_ref}],
validate("/object", Spec, Refs),
ok.
t_nest_object(_Config) ->
GoodRef = <<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>,
Spec = #{
post => #{
parameters => [],
requestBody => #{
<<"content">> => #{
<<"application/json">> =>
#{
<<"schema">> =>
#{
required => [<<"timeout">>],
<<"properties">> =>
[
{<<"per_page">>, #{
description => <<"good per page desc">>,
maximum => 100,
minimum => 1,
type => integer
}},
{<<"timeout">>, #{
default => 5,
<<"oneOf">> =>
[
#{example => <<"1h">>, type => string},
#{enum => [infinity], type => string}
]
}},
{<<"nest_object">>, #{
<<"properties">> =>
[
{<<"good_nest_1">>, #{type => integer}},
{<<"good_nest_2">>, #{
<<"$ref">> => GoodRef
}}
],
<<"type">> => object
}},
{<<"inner_ref">>, #{
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>
}}
],
<<"type">> => object
}
}
}
},
responses => #{<<"200">> => #{description => <<"ok">>}}
}
},
Refs = [{?MODULE, good_ref}],
validate("/nest/object", Spec, Refs),
ok.
t_local_ref(_Config) ->
Spec = #{
post => #{
parameters => [],
requestBody => #{
<<"content">> => #{
<<"application/json">> =>
#{
<<"schema">> => #{
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>
}
}
}
},
responses => #{<<"200">> => #{description => <<"ok">>}}
}
},
Refs = [{?MODULE, good_ref}],
validate("/ref/local", Spec, Refs),
ok.
t_remote_ref(_Config) ->
Spec = #{
post => #{
parameters => [],
requestBody => #{
<<"content">> => #{
<<"application/json">> =>
#{
<<"schema">> => #{
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_remote_schema.ref2">>
}
}
}
},
responses => #{<<"200">> => #{description => <<"ok">>}}
}
},
Refs = [{emqx_swagger_remote_schema, "ref2"}],
{_, Components} = validate("/ref/remote", Spec, Refs),
ExpectComponents = [
#{
<<"emqx_swagger_remote_schema.ref2">> => #{
<<"properties">> => [
{<<"page">>, #{
description => <<"good page">>,
maximum => 100,
minimum => 1,
type => integer
}},
{<<"another_ref">>, #{
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_remote_schema.ref3">>
}}
],
<<"type">> => object
}
},
#{
<<"emqx_swagger_remote_schema.ref3">> => #{
<<"properties">> => [
{<<"ip">>, #{
description => <<"IP:Port">>,
example => <<"127.0.0.1:80">>,
type => string
}},
{<<"version">>, #{
description => <<"a good version">>,
example => <<"1.0.0">>,
type => string
}}
],
<<"type">> => object
}
}
],
?assertEqual(ExpectComponents, Components),
ok.
t_nest_ref(_Config) ->
Spec = #{
post => #{
parameters => [],
requestBody => #{
<<"content">> => #{
<<"application/json">> =>
#{
<<"schema">> => #{
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_requestBody_SUITE.nest_ref">>
}
}
}
},
responses => #{<<"200">> => #{description => <<"ok">>}}
}
},
Refs = [{?MODULE, nest_ref}],
ExpectComponents = lists:sort([
#{
<<"emqx_swagger_requestBody_SUITE.nest_ref">> => #{
<<"properties">> => [
{<<"env">>, #{enum => [test, dev, prod], type => string}},
{<<"another_ref">>, #{
description => <<"nest ref">>,
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>
}}
],
<<"type">> => object
}
},
#{
<<"emqx_swagger_requestBody_SUITE.good_ref">> => #{
<<"properties">> => [
{<<"webhook-host">>, #{
default => <<"127.0.0.1:80">>,
example => <<"127.0.0.1:80">>,
type => string
}},
{<<"log_dir">>, #{example => <<"var/log/emqx">>, type => string}},
{<<"tag">>, #{description => <<"tag">>, type => string}}
],
<<"type">> => object
}
}
]),
{_, Components} = validate("/ref/nest/ref", Spec, Refs),
?assertEqual(ExpectComponents, Components),
ok.
t_none_ref(_Config) ->
Path = "/ref/none",
?assertThrow(
{error, #{mfa := {?MODULE, schema, [Path]}}},
emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{})
),
ok.
t_sub_fields(_Config) ->
Spec = #{
post => #{
parameters => [],
requestBody => #{
<<"content">> => #{
<<"application/json">> =>
#{
<<"schema">> => #{
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_requestBody_SUITE.sub_fields">>
}
}
}
},
responses => #{<<"200">> => #{description => <<"ok">>}}
}
},
Refs = [{?MODULE, sub_fields}],
validate("/fields/sub", Spec, Refs),
ok.
t_bad_ref(_Config) ->
Path = "/ref/bad",
Spec = #{
post => #{
parameters => [],
requestBody => #{
<<"content">> => #{
<<"application/json">> => #{
<<"schema">> =>
#{
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_requestBody_SUITE.bad_ref">>
}
}
}
},
responses => #{<<"200">> => #{description => <<"ok">>}}
}
},
Refs = [{?MODULE, bad_ref}],
Fields = fields(bad_ref),
?assertThrow(
{error, #{msg := <<"Object only supports not empty proplists">>, args := Fields}},
validate(Path, Spec, Refs)
),
ok.
t_ref_array_with_key(_Config) ->
Spec = #{
post => #{
parameters => [],
requestBody => #{
<<"content">> => #{
<<"application/json">> =>
#{
<<"schema">> => #{
required => [<<"timeout">>],
<<"type">> => object,
<<"properties">> =>
[
{<<"per_page">>, #{
description => <<"good per page desc">>,
maximum => 100,
minimum => 1,
type => integer
}},
{<<"timeout">>, #{
default => 5,
<<"oneOf">> =>
[
#{example => <<"1h">>, type => string},
#{enum => [infinity], type => string}
]
}},
{<<"array_refs">>, #{
items => #{
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>
},
type => array
}}
]
}
}
}
},
responses => #{<<"200">> => #{description => <<"ok">>}}
}
},
Refs = [{?MODULE, good_ref}],
validate("/ref/array/with/key", Spec, Refs),
ok.
t_ref_array_without_key(_Config) ->
Spec = #{
post => #{
parameters => [],
requestBody => #{
<<"content">> => #{
<<"application/json">> => #{
<<"schema">> =>
#{
items => #{
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_requestBody_SUITE.good_ref">>
},
type => array
}
}
}
},
responses => #{<<"200">> => #{description => <<"ok">>}}
}
},
Refs = [{?MODULE, good_ref}],
validate("/ref/array/without/key", Spec, Refs),
ok.
t_api_spec(_Config) ->
{Spec0, _} = emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}),
Path = "/object",
Body = #{
<<"per_page">> => 1,
<<"timeout">> => <<"infinity">>,
<<"inner_ref">> => #{
<<"webhook-host">> => <<"127.0.0.1:80">>,
<<"log_dir">> => <<"var/log/test">>,
<<"tag">> => <<"god_tag">>
}
},
Filter0 = filter(Spec0, Path),
?assertMatch(
{ok, #{body := #{<<"timeout">> := <<"infinity">>}}},
trans_requestBody(Path, Body, Filter0)
),
{Spec1, _} = emqx_dashboard_swagger:spec(
?MODULE,
#{check_schema => true, translate_body => true}
),
Filter1 = filter(Spec1, Path),
?assertMatch(
{ok, #{body := #{<<"timeout">> := infinity}}},
trans_requestBody(Path, Body, Filter1)
).
t_object_trans(_Config) ->
Path = "/object",
Body = #{
<<"per_page">> => 1,
<<"timeout">> => <<"infinity">>,
<<"inner_ref">> => #{
<<"webhook-host">> => <<"127.0.0.1:80">>,
<<"log_dir">> => <<"var/log/test">>,
<<"tag">> => <<"god_tag">>
}
},
Expect =
#{
bindings => #{},
query_string => #{},
body =>
#{
<<"per_page">> => 1,
<<"timeout">> => infinity,
<<"inner_ref">> => #{
<<"log_dir">> => "var/log/test",
<<"tag">> => <<"god_tag">>,
<<"webhook-host">> => {{127, 0, 0, 1}, 80}
}
}
},
{ok, ActualBody} = trans_requestBody(Path, Body),
?assertEqual(Expect, ActualBody),
ok.
t_object_notrans(_Config) ->
Path = "/object",
Body = #{
<<"per_page">> => 1,
<<"timeout">> => <<"infinity">>,
<<"inner_ref">> => #{
<<"webhook-host">> => <<"127.0.0.1:80">>,
<<"log_dir">> => <<"var/log/test">>,
<<"tag">> => <<"god_tag">>
}
},
{ok, #{body := ActualBody}} = trans_requestBody(
Path,
Body,
fun emqx_dashboard_swagger:filter_check_request/2
),
?assertEqual(Body, ActualBody),
ok.
todo_t_nest_object_check(_Config) ->
Path = "/nest/object",
Body = #{
<<"timeout">> => "10m",
<<"per_page">> => 10,
<<"inner_ref">> => #{
<<"webhook-host">> => <<"127.0.0.1:80">>,
<<"log_dir">> => <<"var/log/test">>,
<<"tag">> => <<"god_tag">>
},
<<"nest_object">> => #{
<<"good_nest_1">> => 1,
<<"good_nest_2">> => #{
<<"webhook-host">> => <<"127.0.0.1:80">>,
<<"log_dir">> => <<"var/log/test">>,
<<"tag">> => <<"god_tag">>
}
}
},
Expect = #{
bindings => #{},
query_string => #{},
body => #{
<<"per_page">> => 10,
<<"timeout">> => 600
}
},
{ok, NewRequest} = check_requestBody(Path, Body),
?assertEqual(Expect, NewRequest),
ok.
t_local_ref_trans(_Config) ->
Path = "/ref/local",
Body = #{
<<"webhook-host">> => <<"127.0.0.1:80">>,
<<"log_dir">> => <<"var/log/test">>,
<<"tag">> => <<"A">>
},
Expect = #{
bindings => #{},
query_string => #{},
body => #{
<<"log_dir">> => "var/log/test",
<<"tag">> => <<"A">>,
<<"webhook-host">> => {{127, 0, 0, 1}, 80}
}
},
{ok, NewRequest} = trans_requestBody(Path, Body),
?assertEqual(Expect, NewRequest),
ok.
t_remote_ref_trans(_Config) ->
Path = "/ref/remote",
Body = #{
<<"page">> => 10,
<<"another_ref">> => #{
<<"version">> => "2.1.0",
<<"ip">> => <<"198.12.2.1:89">>
}
},
Expect = #{
bindings => #{},
query_string => #{},
body => #{
<<"page">> => 10,
<<"another_ref">> => #{
<<"version">> => "2.1.0",
<<"ip">> => {{198, 12, 2, 1}, 89}
}
}
},
{ok, NewRequest} = trans_requestBody(Path, Body),
?assertEqual(Expect, NewRequest),
ok.
t_nest_ref_trans(_Config) ->
Path = "/ref/nest/ref",
Body = #{
<<"env">> => <<"prod">>,
<<"another_ref">> => #{
<<"log_dir">> => "var/log/dev",
<<"tag">> => <<"A">>,
<<"webhook-host">> => "127.0.0.1:80"
}
},
Expect = #{
bindings => #{},
query_string => #{},
body => #{
<<"another_ref">> => #{
<<"log_dir">> => "var/log/dev",
<<"tag">> => <<"A">>,
<<"webhook-host">> => {{127, 0, 0, 1}, 80}
},
<<"env">> => prod
}
},
{ok, NewRequest} = trans_requestBody(Path, Body),
?assertEqual(Expect, NewRequest),
ok.
t_ref_array_with_key_trans(_Config) ->
Path = "/ref/array/with/key",
Body = #{
<<"per_page">> => 100,
<<"timeout">> => "100m",
<<"array_refs">> => [
#{
<<"log_dir">> => "var/log/dev",
<<"tag">> => <<"A">>,
<<"webhook-host">> => "127.0.0.1:80"
},
#{
<<"log_dir">> => "var/log/test",
<<"tag">> => <<"B">>,
<<"webhook-host">> => "127.0.0.1:81"
}
]
},
Expect = #{
bindings => #{},
query_string => #{},
body => #{
<<"per_page">> => 100,
<<"timeout">> => 6000,
<<"array_refs">> => [
#{
<<"log_dir">> => "var/log/dev",
<<"tag">> => <<"A">>,
<<"webhook-host">> => {{127, 0, 0, 1}, 80}
},
#{
<<"log_dir">> => "var/log/test",
<<"tag">> => <<"B">>,
<<"webhook-host">> => {{127, 0, 0, 1}, 81}
}
]
}
},
{ok, NewRequest} = trans_requestBody(Path, Body),
?assertEqual(Expect, NewRequest),
ok.
t_ref_array_without_key_trans(_Config) ->
Path = "/ref/array/without/key",
Body = [
#{
<<"log_dir">> => "var/log/dev",
<<"tag">> => <<"A">>,
<<"webhook-host">> => "127.0.0.1:80"
},
#{
<<"log_dir">> => "var/log/test",
<<"tag">> => <<"B">>,
<<"webhook-host">> => "127.0.0.1:81"
}
],
Expect = #{
bindings => #{},
query_string => #{},
body => [
#{
<<"log_dir">> => "var/log/dev",
<<"tag">> => <<"A">>,
<<"webhook-host">> => {{127, 0, 0, 1}, 80}
},
#{
<<"log_dir">> => "var/log/test",
<<"tag">> => <<"B">>,
<<"webhook-host">> => {{127, 0, 0, 1}, 81}
}
]
},
{ok, NewRequest} = trans_requestBody(Path, Body),
?assertEqual(Expect, NewRequest),
ok.
t_ref_trans_error(_Config) ->
Path = "/ref/nest/ref",
Body = #{
<<"env">> => <<"prod">>,
<<"another_ref">> => #{
<<"log_dir">> => "var/log/dev",
<<"tag">> => <<"A">>,
<<"webhook-host">> => "127.0..0.1:80"
}
},
{400, 'BAD_REQUEST', _} = trans_requestBody(Path, Body),
ok.
t_object_trans_error(_Config) ->
Path = "/object",
Body = #{
<<"per_page">> => 99,
<<"timeout">> => <<"infinity">>,
<<"inner_ref">> => #{
<<"webhook-host">> => <<"127.0.0..1:80">>,
<<"log_dir">> => <<"var/log/test">>,
<<"tag">> => <<"god_tag">>
}
},
{400, 'BAD_REQUEST', Reason} = trans_requestBody(Path, Body),
?assertNotEqual(nomatch, binary:match(Reason, [<<"webhook-host">>])),
ok.
validate(Path, ExpectSpec, ExpectRefs) ->
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
?assertEqual(test, OperationId),
?assertEqual(ExpectSpec, Spec),
?assertEqual(ExpectRefs, Refs),
{Spec, emqx_dashboard_swagger:components(Refs, #{})}.
filter(ApiSpec, Path) ->
[Filter] = [F || {P, _, _, #{filter := F}} <- ApiSpec, P =:= Path],
Filter.
trans_requestBody(Path, Body) ->
trans_requestBody(
Path,
Body,
fun emqx_dashboard_swagger:filter_check_request_and_translate_body/2
).
check_requestBody(Path, Body) ->
trans_requestBody(
Path,
Body,
fun emqx_dashboard_swagger:filter_check_request/2
).
trans_requestBody(Path, Body, Filter) ->
Meta = #{module => ?MODULE, method => post, path => Path},
Request = #{bindings => #{}, query_string => #{}, body => Body},
Filter(Request, Meta).
api_spec() -> emqx_dashboard_swagger:spec(?MODULE).
paths() ->
[
"/object",
"/nest/object",
"/ref/local",
"/ref/nest/ref",
"/fields/sub",
"/ref/array/with/key",
"/ref/array/without/key"
].
schema("/object") ->
to_schema([
{per_page, mk(range(1, 100), #{required => true, desc => <<"good per page desc">>})},
{timeout,
mk(
hoconsc:union([infinity, emqx_schema:duration_s()]),
#{default => 5, required => true}
)},
{inner_ref, mk(hoconsc:ref(?MODULE, good_ref), #{})}
]);
schema("/nest/object") ->
to_schema([
{per_page, mk(range(1, 100), #{desc => <<"good per page desc">>})},
{timeout,
mk(
hoconsc:union([infinity, emqx_schema:duration_s()]),
#{default => 5, required => true}
)},
{nest_object, [
{good_nest_1, mk(integer(), #{})},
{good_nest_2, mk(hoconsc:ref(?MODULE, good_ref), #{})}
]},
{inner_ref, mk(hoconsc:ref(?MODULE, good_ref), #{})}
]);
schema("/ref/local") ->
to_schema(mk(hoconsc:ref(good_ref), #{}));
schema("/fields/sub") ->
to_schema(mk(hoconsc:ref(sub_fields), #{}));
schema("/ref/remote") ->
to_schema(mk(hoconsc:ref(emqx_swagger_remote_schema, "ref2"), #{}));
schema("/ref/bad") ->
to_schema(mk(hoconsc:ref(?MODULE, bad_ref), #{}));
schema("/ref/nest/ref") ->
to_schema(mk(hoconsc:ref(?MODULE, nest_ref), #{}));
schema("/ref/array/with/key") ->
to_schema([
{per_page, mk(range(1, 100), #{desc => <<"good per page desc">>})},
{timeout,
mk(
hoconsc:union([infinity, emqx_schema:duration_s()]),
#{default => 5, required => true}
)},
{array_refs, mk(hoconsc:array(hoconsc:ref(?MODULE, good_ref)), #{})}
]);
schema("/ref/array/without/key") ->
to_schema(mk(hoconsc:array(hoconsc:ref(?MODULE, good_ref)), #{})).
to_schema(Body) ->
#{
operationId => test,
post => #{requestBody => Body, responses => #{200 => <<"ok">>}}
}.
fields(good_ref) ->
[
{'webhook-host', mk(emqx_schema:ip_port(), #{default => "127.0.0.1:80"})},
{log_dir, mk(emqx_schema:file(), #{example => "var/log/emqx"})},
{tag, mk(binary(), #{desc => <<"tag">>})}
];
fields(nest_ref) ->
[
{env, mk(hoconsc:enum([test, dev, prod]), #{})},
{another_ref, mk(hoconsc:ref(good_ref), #{desc => "nest ref"})}
];
%% don't support maps
fields(bad_ref) ->
#{
username => mk(string(), #{}),
is_admin => mk(boolean(), #{})
};
fields(sub_fields) ->
#{
fields => [
{enable, fun enable/1},
{init_file, fun init_file/1}
],
desc => <<"test sub fields">>
}.
enable(type) -> boolean();
enable(desc) -> <<"Whether to enable tls psk support">>;
enable(default) -> false;
enable(_) -> undefined.
init_file(type) -> binary();
init_file(desc) -> <<"test test desc">>;
init_file(required) -> false;
init_file(_) -> undefined.