382 lines
14 KiB
Erlang
382 lines
14 KiB
Erlang
-module(emqx_swagger_parameter_SUITE).
|
|
-behaviour(minirest_api).
|
|
-behaviour(hocon_schema).
|
|
|
|
%% API
|
|
-export([paths/0, api_spec/0, schema/1, fields/1]).
|
|
-export([t_in_path/1, t_in_query/1, t_in_mix/1, t_without_in/1, t_ref/1, t_public_ref/1]).
|
|
-export([t_require/1, t_nullable/1, t_method/1, t_api_spec/1]).
|
|
-export([t_in_path_trans/1, t_in_query_trans/1, t_in_mix_trans/1, t_ref_trans/1]).
|
|
-export([t_in_path_trans_error/1, t_in_query_trans_error/1, t_in_mix_trans_error/1]).
|
|
-export([all/0, suite/0, groups/0]).
|
|
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
-include_lib("typerefl/include/types.hrl").
|
|
-include_lib("hocon/include/hoconsc.hrl").
|
|
-import(hoconsc, [mk/2]).
|
|
|
|
-define(METHODS, [get, post, put, head, delete, patch, options, trace]).
|
|
|
|
all() -> [{group, spec}, {group, validation}].
|
|
suite() -> [{timetrap, {minutes, 1}}].
|
|
groups() -> [
|
|
{spec, [parallel], [t_api_spec, t_in_path, t_ref, t_in_query, t_in_mix,
|
|
t_without_in, t_require, t_nullable, t_method, t_public_ref]},
|
|
{validation, [parallel], [t_in_path_trans, t_ref_trans, t_in_query_trans, t_in_mix_trans,
|
|
t_in_path_trans_error, t_in_query_trans_error, t_in_mix_trans_error]}
|
|
].
|
|
|
|
t_in_path(_Config) ->
|
|
Expect =
|
|
[#{description => <<"Indicates which sorts of issues to return">>,
|
|
example => <<"all">>, in => path, name => filter,
|
|
required => true,
|
|
schema => #{enum => [assigned, created, mentioned, all], type => string}}
|
|
],
|
|
validate("/test/in/:filter", Expect),
|
|
ok.
|
|
|
|
t_in_query(_Config) ->
|
|
Expect =
|
|
[#{description => <<"results per page (max 100)">>,
|
|
example => 1, in => query, name => per_page,
|
|
schema => #{example => 1, maximum => 100, minimum => 1, type => integer}}],
|
|
validate("/test/in/query", Expect),
|
|
ok.
|
|
|
|
t_ref(_Config) ->
|
|
LocalPath = "/test/in/ref/local",
|
|
Path = "/test/in/ref",
|
|
Expect = [#{<<"$ref">> => <<"#/components/parameters/emqx_swagger_parameter_SUITE.page">>}],
|
|
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path),
|
|
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, LocalPath),
|
|
?assertEqual(test, OperationId),
|
|
Params = maps:get(parameters, maps:get(post, Spec)),
|
|
?assertEqual(Expect, Params),
|
|
?assertEqual([{?MODULE, page, parameter}], Refs),
|
|
ok.
|
|
|
|
t_public_ref(_Config) ->
|
|
Path = "/test/in/ref/public",
|
|
Expect = [
|
|
#{<<"$ref">> => <<"#/components/parameters/public.page">>},
|
|
#{<<"$ref">> => <<"#/components/parameters/public.limit">>}
|
|
],
|
|
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path),
|
|
?assertEqual(test, OperationId),
|
|
Params = maps:get(parameters, maps:get(post, Spec)),
|
|
?assertEqual(Expect, Params),
|
|
?assertEqual([
|
|
{emqx_dashboard_swagger, limit, parameter},
|
|
{emqx_dashboard_swagger, page, parameter}
|
|
], Refs),
|
|
ExpectRefs = [
|
|
#{<<"public.limit">> => #{description => <<"Results per page(max 1000)">>,
|
|
example => 50,in => query,name => limit,
|
|
schema => #{default => 100,example => 1,maximum => 1000,
|
|
minimum => 1,type => integer}}},
|
|
#{<<"public.page">> => #{description => <<"Page number of the results to fetch.">>,
|
|
example => 1,in => query,name => page,
|
|
schema => #{default => 1,example => 100,type => integer}}}],
|
|
?assertEqual(ExpectRefs, emqx_dashboard_swagger:components(Refs)),
|
|
ok.
|
|
|
|
t_in_mix(_Config) ->
|
|
Expect =
|
|
[#{description => <<"Indicates which sorts of issues to return">>,
|
|
example => <<"all">>,in => query,name => filter,
|
|
schema => #{enum => [assigned,created,mentioned,all],type => string}},
|
|
#{description => <<"Indicates the state of the issues to return.">>,
|
|
example => <<"12m">>,in => path,name => state,required => true,
|
|
schema => #{example => <<"1h">>,type => string}},
|
|
#{example => 10,in => query,name => per_page, required => false,
|
|
schema => #{default => 5,example => 1,maximum => 50,minimum => 1, type => integer}},
|
|
#{in => query,name => is_admin, schema => #{example => true,type => boolean}},
|
|
#{in => query,name => timeout,
|
|
schema => #{<<"oneOf">> => [#{enum => [infinity],type => string},
|
|
#{example => 30,maximum => 60,minimum => 30, type => integer}]}}],
|
|
ExpectMeta = #{
|
|
tags => [tags, good],
|
|
description => <<"good description">>,
|
|
summary => <<"good summary">>,
|
|
security => [],
|
|
deprecated => true,
|
|
responses => #{<<"200">> => #{description => <<"ok">>}}},
|
|
GotSpec = validate("/test/in/mix/:state", Expect),
|
|
?assertEqual(ExpectMeta, maps:without([parameters], maps:get(post, GotSpec))),
|
|
ok.
|
|
|
|
t_without_in(_Config) ->
|
|
?assertThrow({error, <<"missing in:path/query field in parameters">>},
|
|
emqx_dashboard_swagger:parse_spec_ref(?MODULE, "/test/without/in")),
|
|
ok.
|
|
|
|
t_require(_Config) ->
|
|
ExpectSpec = [#{
|
|
in => query,name => userid, required => false,
|
|
schema => #{example => <<"binary-example">>, type => string}}],
|
|
validate("/required/false", ExpectSpec),
|
|
ok.
|
|
|
|
t_nullable(_Config) ->
|
|
NullableFalse = [#{in => query,name => userid, required => true,
|
|
schema => #{example => <<"binary-example">>, type => string}}],
|
|
NullableTrue = [#{in => query,name => userid,
|
|
schema => #{example => <<"binary-example">>, type => string,
|
|
nullable => true}}],
|
|
validate("/nullable/false", NullableFalse),
|
|
validate("/nullable/true", NullableTrue),
|
|
ok.
|
|
|
|
t_method(_Config) ->
|
|
PathOk = "/method/ok",
|
|
PathError = "/method/error",
|
|
{test, Spec, []} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, PathOk),
|
|
?assertEqual(lists:sort(?METHODS), lists:sort(maps:keys(Spec))),
|
|
?assertThrow({error, #{module := ?MODULE, path := PathError, method := bar}},
|
|
emqx_dashboard_swagger:parse_spec_ref(?MODULE, PathError)),
|
|
ok.
|
|
|
|
t_in_path_trans(_Config) ->
|
|
Path = "/test/in/:filter",
|
|
Bindings = #{filter => <<"created">>},
|
|
Expect = {ok,#{bindings => #{filter => created},
|
|
body => #{}, query_string => #{}}},
|
|
?assertEqual(Expect, trans_parameters(Path, Bindings, #{})),
|
|
ok.
|
|
|
|
t_in_query_trans(_Config) ->
|
|
Path = "/test/in/query",
|
|
Expect = {ok, #{bindings => #{},body => #{},
|
|
query_string => #{<<"per_page">> => 100}}},
|
|
?assertEqual(Expect, trans_parameters(Path, #{}, #{<<"per_page">> => 100})),
|
|
ok.
|
|
|
|
t_ref_trans(_Config) ->
|
|
LocalPath = "/test/in/ref/local",
|
|
Path = "/test/in/ref",
|
|
Expect = {ok, #{bindings => #{},body => #{},
|
|
query_string => #{<<"per_page">> => 100}}},
|
|
?assertEqual(Expect, trans_parameters(Path, #{}, #{<<"per_page">> => 100})),
|
|
?assertEqual(Expect, trans_parameters(LocalPath, #{}, #{<<"per_page">> => 100})),
|
|
{400,'BAD_REQUEST', Reason} = trans_parameters(Path, #{}, #{<<"per_page">> => 1010}),
|
|
?assertNotEqual(nomatch, binary:match(Reason, [<<"per_page">>])),
|
|
{400,'BAD_REQUEST', Reason} = trans_parameters(LocalPath, #{}, #{<<"per_page">> => 1010}),
|
|
ok.
|
|
|
|
t_in_mix_trans(_Config) ->
|
|
Path = "/test/in/mix/:state",
|
|
Bindings = #{
|
|
state => <<"12m">>,
|
|
per_page => <<"1">>
|
|
},
|
|
Query = #{
|
|
<<"filter">> => <<"created">>,
|
|
<<"is_admin">> => true,
|
|
<<"timeout">> => <<"34">>
|
|
},
|
|
Expect = {ok,
|
|
#{body => #{},
|
|
bindings => #{state => 720},
|
|
query_string => #{<<"filter">> => created,<<"is_admin">> => true,
|
|
<<"per_page">> => 5,<<"timeout">> => 34}}},
|
|
?assertEqual(Expect, trans_parameters(Path, Bindings, Query)),
|
|
ok.
|
|
|
|
t_in_path_trans_error(_Config) ->
|
|
Path = "/test/in/:filter",
|
|
Bindings = #{filter => <<"created1">>},
|
|
Expect = {400,'BAD_REQUEST', <<"filter : unable_to_convert_to_enum_symbol">>},
|
|
?assertEqual(Expect, trans_parameters(Path, Bindings, #{})),
|
|
ok.
|
|
|
|
t_in_query_trans_error(_Config) ->
|
|
Path = "/test/in/query",
|
|
{400,'BAD_REQUEST', Reason} = trans_parameters(Path, #{}, #{<<"per_page">> => 101}),
|
|
?assertNotEqual(nomatch, binary:match(Reason, [<<"per_page">>])),
|
|
ok.
|
|
|
|
t_in_mix_trans_error(_Config) ->
|
|
Path = "/test/in/mix/:state",
|
|
Bindings = #{
|
|
state => <<"1d2m">>,
|
|
per_page => <<"1">>
|
|
},
|
|
Query = #{
|
|
<<"filter">> => <<"cdreated">>,
|
|
<<"is_admin">> => true,
|
|
<<"timeout">> => <<"34">>
|
|
},
|
|
Expect = {400,'BAD_REQUEST', <<"filter : unable_to_convert_to_enum_symbol">>},
|
|
?assertEqual(Expect, trans_parameters(Path, Bindings, Query)),
|
|
ok.
|
|
|
|
t_api_spec(_Config) ->
|
|
{Spec0, _} = emqx_dashboard_swagger:spec(?MODULE),
|
|
assert_all_filters_equal(Spec0, undefined),
|
|
|
|
{Spec1, _} = emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false}),
|
|
assert_all_filters_equal(Spec1, undefined),
|
|
|
|
CustomFilter = fun(Request, _RequestMeta) -> {ok, Request} end,
|
|
{Spec2, _} = emqx_dashboard_swagger:spec(?MODULE, #{check_schema => CustomFilter}),
|
|
assert_all_filters_equal(Spec2, CustomFilter),
|
|
|
|
{Spec3, _} = emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}),
|
|
Path = "/test/in/:filter",
|
|
|
|
Filter = filter(Spec3, Path),
|
|
Bindings = #{filter => <<"created">>},
|
|
|
|
?assertMatch(
|
|
{ok, #{bindings := #{filter := created}}},
|
|
trans_parameters(Path, Bindings, #{}, Filter)).
|
|
|
|
assert_all_filters_equal(Spec, Filter) ->
|
|
lists:foreach(
|
|
fun({_, _, _, #{filter := F}}) ->
|
|
?assertEqual(Filter, F)
|
|
end,
|
|
Spec).
|
|
|
|
validate(Path, ExpectParams) ->
|
|
{OperationId, Spec, Refs} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path),
|
|
?assertEqual(test, OperationId),
|
|
Params = maps:get(parameters, maps:get(post, Spec)),
|
|
?assertEqual(ExpectParams, Params),
|
|
?assertEqual([], Refs),
|
|
Spec.
|
|
|
|
filter(ApiSpec, Path) ->
|
|
[Filter] = [F || {P, _, _, #{filter := F}} <- ApiSpec, P =:= Path],
|
|
Filter.
|
|
|
|
trans_parameters(Path, Bindings, QueryStr) ->
|
|
trans_parameters(Path, Bindings, QueryStr, fun emqx_dashboard_swagger:filter_check_request/2).
|
|
|
|
trans_parameters(Path, Bindings, QueryStr, Filter) ->
|
|
Meta = #{module => ?MODULE, method => post, path => Path},
|
|
Request = #{bindings => Bindings, query_string => QueryStr, body => #{}},
|
|
Filter(Request, Meta).
|
|
|
|
api_spec() -> emqx_dashboard_swagger:spec(?MODULE).
|
|
|
|
paths() -> ["/test/in/:filter", "/test/in/query", "/test/in/mix/:state", "/test/in/ref",
|
|
"/required/false", "/nullable/false", "/nullable/true", "/method/ok"].
|
|
|
|
schema("/test/in/:filter") ->
|
|
#{
|
|
operationId => test,
|
|
post => #{
|
|
parameters => [
|
|
{filter,
|
|
mk(hoconsc:enum([assigned, created, mentioned, all]),
|
|
#{in => path,
|
|
desc => <<"Indicates which sorts of issues to return">>,
|
|
example => "all"
|
|
})}
|
|
],
|
|
responses => #{200 => <<"ok">>}
|
|
}
|
|
};
|
|
schema("/test/in/query") ->
|
|
#{
|
|
operationId => test,
|
|
post => #{
|
|
parameters => [
|
|
{per_page,
|
|
mk(range(1, 100),
|
|
#{in => query, desc => <<"results per page (max 100)">>, example => 1})}
|
|
],
|
|
responses => #{200 => <<"ok">>}
|
|
}
|
|
};
|
|
schema("/test/in/ref/local") ->
|
|
#{
|
|
operationId => test,
|
|
post => #{
|
|
parameters => [hoconsc:ref(page)],
|
|
responses => #{200 => <<"ok">>}
|
|
}
|
|
};
|
|
schema("/test/in/ref") ->
|
|
#{
|
|
operationId => test,
|
|
post => #{
|
|
parameters => [hoconsc:ref(?MODULE, page)],
|
|
responses => #{200 => <<"ok">>}
|
|
}
|
|
};
|
|
schema("/test/in/ref/public") ->
|
|
#{
|
|
operationId => test,
|
|
post => #{
|
|
parameters => [
|
|
hoconsc:ref(emqx_dashboard_swagger, page),
|
|
hoconsc:ref(emqx_dashboard_swagger, limit)
|
|
],
|
|
responses => #{200 => <<"ok">>}
|
|
}
|
|
};
|
|
schema("/test/in/mix/:state") ->
|
|
#{
|
|
operationId => test,
|
|
post => #{
|
|
tags => [tags, good],
|
|
description => <<"good description">>,
|
|
summary => <<"good summary">>,
|
|
security => [],
|
|
deprecated => true,
|
|
parameters => [
|
|
{filter, hoconsc:mk(hoconsc:enum([assigned, created, mentioned, all]),
|
|
#{in => query, desc => <<"Indicates which sorts of issues to return">>,
|
|
example => "all"})},
|
|
{state, mk(emqx_schema:duration_s(),
|
|
#{in => path, required => true, example => "12m",
|
|
desc => <<"Indicates the state of the issues to return.">>})},
|
|
{per_page, mk(range(1, 50),
|
|
#{in => query, required => false, example => 10, default => 5})},
|
|
{is_admin, mk(boolean(), #{in => query})},
|
|
{timeout, mk(hoconsc:union([range(30, 60), infinity]), #{in => query})}
|
|
],
|
|
responses => #{200 => <<"ok">>}
|
|
}
|
|
};
|
|
schema("/test/without/in") ->
|
|
#{
|
|
operationId => test,
|
|
post => #{
|
|
parameters => [
|
|
{'x-request-id', mk(binary(), #{})}
|
|
],
|
|
responses => #{200 => <<"ok">>}
|
|
}
|
|
};
|
|
schema("/required/false") ->
|
|
to_schema([{'userid', mk(binary(), #{in => query, required => false})}]);
|
|
schema("/nullable/false") ->
|
|
to_schema([{'userid', mk(binary(), #{in => query, nullable => false})}]);
|
|
schema("/nullable/true") ->
|
|
to_schema([{'userid', mk(binary(), #{in => query, nullable => true})}]);
|
|
schema("/method/ok") ->
|
|
Response = #{responses => #{200 => <<"ok">>}},
|
|
lists:foldl(fun(Method, Acc) -> Acc#{Method => Response} end,
|
|
#{operationId => test}, ?METHODS);
|
|
schema("/method/error") ->
|
|
#{operationId => test, bar => #{200 => <<"ok">>}}.
|
|
|
|
fields(page) ->
|
|
[
|
|
{per_page,
|
|
mk(range(1, 100),
|
|
#{in => query, desc => <<"results per page (max 100)">>, example => 1})}
|
|
].
|
|
to_schema(Params) ->
|
|
#{
|
|
operationId => test,
|
|
post => #{
|
|
parameters => Params,
|
|
responses => #{200 => <<"ok">>}
|
|
}
|
|
}.
|