emqx/apps/emqx_dashboard/test/emqx_swagger_parameter_SUIT...

583 lines
18 KiB
Erlang

%%--------------------------------------------------------------------
%% Copyright (c) 2022-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_swagger_parameter_SUITE).
-behaviour(minirest_api).
-behaviour(hocon_schema).
%% API
-export([paths/0, api_spec/0, schema/1, namespace/0, fields/1]).
-export([init_per_suite/1, end_per_suite/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_query_enum/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_query_enum,
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) ->
emqx_mgmt_api_test_util:init_suite([emqx_conf]),
Config.
end_per_suite(_Config) ->
emqx_mgmt_api_test_util:end_suite([emqx_conf]).
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 => #{maximum => 100, minimum => 1, type => integer}
},
#{
description => <<"QOS">>,
in => query,
name => qos,
schema => #{minimum => 0, maximum => 2, type => integer, example => 0}
}
],
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, RouteOpts} = emqx_dashboard_swagger:parse_spec_ref(
?MODULE, Path, #{}
),
{OperationId, Spec, Refs, RouteOpts} = 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 10000)">>,
in => query,
name => limit,
example => 50,
schema => #{
default => 100,
maximum => 10000,
minimum => 1,
type => integer
}
}
},
#{
<<"public.page">> => #{
description => <<"Page number of the results to fetch.">>,
in => query,
name => page,
example => 1,
schema => #{default => 1, minimum => 1, type => integer}
}
}
],
?assertEqual(ExpectRefs, emqx_dashboard_swagger:components(Refs, #{})),
ok.
t_in_mix(_Config) ->
Expect =
[
#{
description => <<"Indicates which sorts of issues to return">>,
example => <<"all">>,
in => query,
name => filter,
schema => #{enum => [assigned, created, mentioned, all], type => string}
},
#{
description => <<"Indicates the state of the issues to return.">>,
example => <<"12m">>,
in => path,
name => state,
required => true,
schema => #{example => <<"1h">>, type => string}
},
#{
example => 10,
in => query,
name => per_page,
required => false,
schema => #{default => 5, maximum => 50, minimum => 1, type => integer}
},
#{in => query, name => is_admin, schema => #{type => boolean}},
#{
in => query,
name => timeout,
schema => #{
<<"oneOf">> => [
#{enum => [infinity], type => string},
#{maximum => 60, minimum => 30, type => integer}
]
}
}
],
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 => #{type => string}
}
],
validate("/required/false", ExpectSpec),
ok.
t_query_enum(_Config) ->
ExpectSpec = [
#{
in => query,
name => userid,
schema => #{type => string, enum => [<<"a">>], default => <<"a">>}
}
],
validate("/query/enum", ExpectSpec),
ok.
t_nullable(_Config) ->
NullableFalse = [
#{
in => query,
name => userid,
required => true,
schema => #{type => string}
}
],
NullableTrue = [
#{
in => query,
name => userid,
schema => #{type => string},
required => false
}
],
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, <<"qos">> => 1}
}},
?assertEqual(Expect, trans_parameters(Path, #{}, #{<<"per_page">> => 100, <<"qos">> => 1})),
ok.
t_ref_trans(_Config) ->
LocalPath = "/test/in/ref/local",
Path = "/test/in/ref",
Expect =
{ok, #{
bindings => #{},
body => #{},
query_string => #{<<"per_page">> => 100}
}},
?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">>},
?assertMatch({400, 'BAD_REQUEST', _}, trans_parameters(Path, Bindings, #{})),
ok.
t_in_query_trans_error(_Config) ->
Path = "/test/in/query",
{400, 'BAD_REQUEST', Reason} = trans_parameters(Path, #{}, #{<<"per_page">> => 101}),
?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">>
},
?assertMatch({400, 'BAD_REQUEST', _}, 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
}
)},
{qos, mk(emqx_schema:qos(), #{in => query, desc => <<"QOS">>})}
],
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],
desc => <<"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("/query/enum") ->
to_schema([{'userid', mk(binary(), #{in => query, enum => [<<"a">>], default => <<"a">>})}]);
schema("/nullable/false") ->
to_schema([{'userid', mk(binary(), #{in => query, required => true})}]);
schema("/nullable/true") ->
to_schema([{'userid', mk(binary(), #{in => query, required => false})}]);
schema("/method/ok") ->
Response = #{responses => #{200 => <<"ok">>}},
lists:foldl(
fun(Method, Acc) -> Acc#{Method => Response} end,
#{operationId => test},
?METHODS
);
schema("/method/error") ->
#{operationId => test, bar => #{200 => <<"ok">>}}.
namespace() -> undefined.
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">>}
}
}.