emqx/apps/emqx_dashboard/test/emqx_swagger_response_SUITE...

720 lines
25 KiB
Erlang

%%--------------------------------------------------------------------
%% Copyright (c) 2020-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_response_SUITE).
-behaviour(minirest_api).
-behaviour(hocon_schema).
-include_lib("eunit/include/eunit.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("typerefl/include/types.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-import(hoconsc, [mk/2]).
-compile(nowarn_export_all).
-compile(export_all).
-type url() :: emqx_http_lib:uri_map().
-reflect_type([url/0]).
-typerefl_from_string({url/0, emqx_http_lib, uri_parse}).
all() -> emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) ->
emqx_mgmt_api_test_util:init_suite([emqx_conf]),
Config.
end_per_suite(Config) ->
end_suite(),
Config.
end_suite() ->
application:unload(emqx_management),
emqx_mgmt_api_test_util:end_suite([emqx_conf]).
t_simple_binary(_config) ->
Path = "/simple/bin",
ExpectSpec = #{description => <<"binary ok">>},
ExpectRefs = [],
validate(Path, ExpectSpec, ExpectRefs),
ok.
t_object(_config) ->
Path = "/object",
Object =
#{
<<"content">> => #{
<<"application/json">> =>
#{
<<"schema">> => #{
required => [<<"per_page">>, <<"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}
]
}},
{<<"inner_ref">>, #{
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>
}}
],
<<"type">> => object
}
}
}
},
ExpectRefs = [{?MODULE, good_ref}],
validate(Path, Object, ExpectRefs),
ok.
t_error(_Config) ->
Path = "/error",
Error400 = #{
<<"content">> =>
#{
<<"application/json">> => #{
<<"schema">> => #{
<<"type">> => object,
<<"properties">> =>
[
{<<"code">>, #{enum => ['Bad1', 'Bad2'], type => string}},
{<<"message">>, #{
description => <<"Bad request desc">>, type => string
}}
]
}
}
}
},
Error404 = #{
<<"content">> =>
#{
<<"application/json">> => #{
<<"schema">> => #{
<<"type">> => object,
<<"properties">> =>
[
{<<"code">>, #{enum => ['Not-Found'], type => string}},
{<<"message">>, #{
description => <<"Error code to troubleshoot problems.">>,
type => string
}}
]
}
}
}
},
{OperationId, Spec, Refs, #{}} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
?assertEqual(test, OperationId),
Response = maps:get(responses, maps:get(get, Spec)),
?assertEqual(Error400, maps:get(<<"400">>, Response)),
?assertEqual(Error404, maps:get(<<"404">>, Response)),
?assertEqual(#{}, maps:without([<<"400">>, <<"404">>], Response)),
?assertEqual([], Refs),
ok.
t_nest_object(_Config) ->
Path = "/nest/object",
Object =
#{
<<"content">> => #{
<<"application/json">> => #{
<<"schema">> =>
#{
required => [<<"timeout">>],
<<"type">> => object,
<<"properties">> => [
{<<"per_page">>, #{
description => <<"good per page desc">>,
maximum => 100,
minimum => 1,
type => integer
}},
{<<"timeout">>, #{
default => 5,
<<"oneOf">> =>
[
#{example => <<"1h">>, type => string},
#{enum => [infinity], type => string}
]
}},
{<<"nest_object">>, #{
<<"type">> => object,
<<"properties">> => [
{<<"good_nest_1">>, #{type => integer}},
{<<"good_nest_2">>, #{
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>
}}
]
}},
{<<"inner_ref">>, #{
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>
}}
]
}
}
}
},
ExpectRefs = [{?MODULE, good_ref}],
validate(Path, Object, ExpectRefs),
ok.
t_empty(_Config) ->
?assertThrow(
{error, #{
msg := <<"Object only supports not empty proplists">>,
args := [],
module := ?MODULE
}},
validate("/empty", error, [])
),
ok.
t_raw_local_ref(_Config) ->
Path = "/raw/ref/local",
Object = #{
<<"content">> => #{
<<"application/json">> => #{
<<"schema">> => #{
<<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>
}
}
}
},
ExpectRefs = [{?MODULE, good_ref}],
validate(Path, Object, ExpectRefs),
ok.
t_raw_remote_ref(_Config) ->
Path = "/raw/ref/remote",
Object = #{
<<"content">> =>
#{
<<"application/json">> => #{
<<"schema">> => #{
<<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.ref1">>
}
}
}
},
ExpectRefs = [{emqx_swagger_remote_schema, "ref1"}],
validate(Path, Object, ExpectRefs),
ok.
t_local_ref(_Config) ->
Path = "/ref/local",
Object = #{
<<"content">> => #{
<<"application/json">> => #{
<<"schema">> => #{
<<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>
}
}
}
},
ExpectRefs = [{?MODULE, good_ref}],
validate(Path, Object, ExpectRefs),
ok.
t_remote_ref(_Config) ->
Path = "/ref/remote",
Object = #{
<<"content">> =>
#{
<<"application/json">> => #{
<<"schema">> => #{
<<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.ref1">>
}
}
}
},
ExpectRefs = [{emqx_swagger_remote_schema, "ref1"}],
validate(Path, Object, ExpectRefs),
ok.
t_bad_ref(_Config) ->
Path = "/ref/bad",
Object = #{
<<"content">> => #{
<<"application/json">> => #{
<<"schema">> =>
#{<<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.bad_ref">>}
}
}
},
ExpectRefs = [{?MODULE, bad_ref}],
?assertThrow(
{error, #{
module := ?MODULE,
msg := <<"Object only supports not empty proplists">>
}},
validate(Path, Object, ExpectRefs)
),
ok.
t_none_ref(_Config) ->
Path = "/ref/none",
?assertError(
{failed_to_generate_swagger_spec, ?MODULE, Path},
validate(Path, #{}, [])
),
ok.
t_nest_ref(_Config) ->
Path = "/ref/nest/ref",
Object = #{
<<"content">> => #{
<<"application/json">> => #{
<<"schema">> => #{
<<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.nest_ref">>
}
}
}
},
ExpectRefs = [{?MODULE, nest_ref}],
validate(Path, Object, ExpectRefs),
ok.
t_sub_fields(_Config) ->
Path = "/fields/sub",
Object = #{
<<"content">> => #{
<<"application/json">> => #{
<<"schema">> => #{
<<"$ref">> => <<"#/components/schemas/emqx_swagger_response_SUITE.sub_fields">>
}
}
}
},
ExpectRefs = [{?MODULE, sub_fields}],
validate(Path, Object, ExpectRefs),
ok.
t_complex_type(_Config) ->
Path = "/ref/complex_type",
{OperationId, Spec, Refs, #{}} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
?assertEqual(test, OperationId),
Response = maps:get(responses, maps:get(post, Spec)),
ResponseBody = maps:get(<<"200">>, Response),
Content = maps:get(<<"content">>, ResponseBody),
JsonContent = maps:get(<<"application/json">>, Content),
Schema = maps:get(<<"schema">>, JsonContent),
?assertMatch(#{<<"type">> := object}, Schema),
Properties = maps:get(<<"properties">>, Schema),
?assertMatch(
[
{<<"no_neg_integer">>, #{minimum := 0, type := integer}},
{<<"url">>, #{
example := <<"http://127.0.0.1">>, type := string
}},
{<<"server">>, #{
example := <<"127.0.0.1:80">>, type := string
}},
{<<"connect_timeout">>, #{
example := _, type := string
}},
{<<"pool_type">>, #{
enum := [random, hash], type := string
}},
{<<"timeout">>, #{
<<"oneOf">> := [
#{example := _, type := string},
#{enum := [infinity], type := string}
]
}},
{<<"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
}}
],
Properties
),
?assertEqual([], Refs),
ok.
t_ref_array_with_key(_Config) ->
Path = "/ref/array/with/key",
Object = #{
<<"content">> => #{
<<"application/json">> => #{
<<"schema">> => #{
required => [<<"timeout">>],
<<"type">> => object,
<<"properties">> => [
{<<"per_page">>, #{
description => <<"good per page desc">>,
maximum => 100,
minimum => 1,
type => integer
}},
{<<"timeout">>, #{
default => 5,
<<"oneOf">> =>
[
#{example => <<"1h">>, type => string},
#{enum => [infinity], type => string}
]
}},
{<<"assert">>, #{description => <<"money">>, type => number}},
{<<"number_ex">>, #{description => <<"number example">>, type => number}},
{<<"percent_ex">>, #{
description => <<"percent example">>,
example => <<"12%">>,
type => string
}},
{<<"duration_ms_ex">>, #{
description => <<"duration ms example">>,
example => <<"32s">>,
type => string
}},
{<<"atom_ex">>, #{description => <<"atom ex">>, type => string}},
{<<"array_refs">>, #{
items => #{
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>
},
type => array
}}
]
}
}
}
},
ExpectRefs = [{?MODULE, good_ref}],
validate(Path, Object, ExpectRefs),
ok.
t_ref_array_without_key(_Config) ->
Path = "/ref/array/without/key",
Object = #{
<<"content">> => #{
<<"application/json">> => #{
<<"schema">> => #{
items => #{
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_response_SUITE.good_ref">>
},
type => array
}
}
}
},
ExpectRefs = [{?MODULE, good_ref}],
validate(Path, Object, ExpectRefs),
ok.
t_hocon_schema_function(_Config) ->
Path = "/ref/hocon/schema/function",
Object = #{
<<"content">> => #{
<<"application/json">> => #{
<<"schema">> =>
#{<<"$ref">> => <<"#/components/schemas/emqx_swagger_remote_schema.root">>}
}
}
},
ExpectComponents = [
#{
<<"emqx_swagger_remote_schema.ref1">> => #{
<<"type">> => object,
<<"properties">> => [
{<<"protocol">>, #{enum => [http, https], type => string}},
{<<"port">>, #{default => 18083, type => integer}}
]
}
},
#{
<<"emqx_swagger_remote_schema.ref2">> => #{
<<"type">> => object,
<<"properties">> => [
{<<"page">>, #{
description => <<"good page">>,
maximum => 100,
minimum => 1,
type => integer
}},
{<<"another_ref">>, #{
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_remote_schema.ref3">>
}}
]
}
},
#{
<<"emqx_swagger_remote_schema.ref3">> => #{
<<"type">> => object,
<<"properties">> => [
{<<"ip">>, #{
description => <<"IP:Port">>,
example => <<"127.0.0.1:80">>,
type => string
}},
{<<"version">>, #{
description => <<"a good version">>,
example => <<"1.0.0">>,
type => string
}}
]
}
},
#{
<<"emqx_swagger_remote_schema.root">> =>
#{
required => [<<"default_password">>, <<"default_username">>],
<<"properties">> => [
{<<"listeners">>, #{
items =>
#{
<<"oneOf">> =>
[
#{
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_remote_schema.ref2">>
},
#{
<<"$ref">> =>
<<"#/components/schemas/emqx_swagger_remote_schema.ref1">>
}
]
},
type => array
}},
{<<"default_username">>, #{default => <<"admin">>, type => string}},
{<<"default_password">>, #{default => <<"public">>, type => string}},
{<<"sample_interval">>, #{
default => <<"10s">>, example => <<"1h">>, type => string
}},
{<<"token_expired_time">>, #{
default => <<"30m">>, example => <<"12m">>, type => string
}}
],
<<"type">> => object
}
}
],
ExpectRefs = [{emqx_swagger_remote_schema, "root"}],
{_, Components} = validate(Path, Object, ExpectRefs),
?assertEqual(ExpectComponents, Components),
ok.
t_api_spec(_Config) ->
emqx_dashboard_swagger:spec(?MODULE),
ok.
api_spec() -> emqx_dashboard_swagger:spec(?MODULE).
paths() ->
[
"/simple/bin",
"/object",
"/nest/object",
"/ref/local",
"/ref/nest/ref",
"/raw/ref/local",
"/raw/ref/remote",
"/ref/array/with/key",
"/ref/array/without/key",
"/ref/hocon/schema/function"
].
schema("/simple/bin") ->
to_schema(<<"binary ok">>);
schema("/object") ->
Object = [
{per_page, mk(range(1, 100), #{required => true, desc => <<"good per page desc">>})},
{timeout,
mk(
hoconsc:union([infinity, emqx_schema:timeout_duration_s()]),
#{default => 5, required => true}
)},
{inner_ref, mk(hoconsc:ref(?MODULE, good_ref), #{})}
],
to_schema(Object);
schema("/nest/object") ->
Response = [
{per_page, mk(range(1, 100), #{desc => <<"good per page desc">>})},
{timeout,
mk(
hoconsc:union([infinity, emqx_schema:timeout_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), #{})}
],
to_schema(Response);
schema("/empty") ->
to_schema([]);
schema("/raw/ref/local") ->
to_schema(hoconsc:ref(good_ref));
schema("/raw/ref/remote") ->
to_schema(hoconsc:ref(emqx_swagger_remote_schema, "ref1"));
schema("/ref/local") ->
to_schema(mk(hoconsc:ref(good_ref), #{}));
schema("/ref/remote") ->
to_schema(mk(hoconsc:ref(emqx_swagger_remote_schema, "ref1"), #{}));
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:timeout_duration_s()]),
#{default => 5, required => true}
)},
{assert, mk(float(), #{desc => <<"money">>})},
{number_ex, mk(number(), #{desc => <<"number example">>})},
{percent_ex, mk(emqx_schema:percent(), #{desc => <<"percent example">>})},
{duration_ms_ex,
mk(emqx_schema:timeout_duration_ms(), #{desc => <<"duration ms example">>})},
{atom_ex, mk(atom(), #{desc => <<"atom ex">>})},
{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)), #{}));
schema("/ref/hocon/schema/function") ->
to_schema(mk(hoconsc:ref(emqx_swagger_remote_schema, "root"), #{}));
schema("/error") ->
#{
operationId => test,
get => #{
responses => #{
400 => emqx_dashboard_swagger:error_codes(['Bad1', 'Bad2'], <<"Bad request desc">>),
404 => emqx_dashboard_swagger:error_codes(['Not-Found'])
}
}
};
schema("/ref/complex_type") ->
#{
operationId => test,
post => #{
responses => #{
200 => [
{no_neg_integer, hoconsc:mk(non_neg_integer(), #{})},
{url, hoconsc:mk(url(), #{})},
{server, hoconsc:mk(emqx_schema:ip_port(), #{})},
{connect_timeout, hoconsc:mk(emqx_schema:timeout_duration(), #{})},
{pool_type, hoconsc:mk(hoconsc:enum([random, hash]), #{})},
{timeout,
hoconsc:mk(hoconsc:union([infinity, emqx_schema:timeout_duration()]), #{})},
{bytesize, hoconsc:mk(emqx_schema:bytesize(), #{})},
{wordsize, hoconsc:mk(emqx_schema:wordsize(), #{})},
{maps, hoconsc:mk(map(), #{})},
{comma_separated_list, hoconsc:mk(emqx_schema:comma_separated_list(), #{})},
{comma_separated_atoms, hoconsc:mk(emqx_schema:comma_separated_atoms(), #{})},
{log_level, hoconsc:mk(emqx_conf_schema:log_level(), #{})}
]
}
}
};
schema("/fields/sub") ->
to_schema(hoconsc:ref(sub_fields)).
validate(Path, ExpectObject, ExpectRefs) ->
{OperationId, Spec, Refs, #{}} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
?assertEqual(test, OperationId),
Response = maps:get(responses, maps:get(post, Spec)),
?assertEqual(ExpectObject, maps:get(<<"200">>, Response)),
?assertEqual(ExpectObject, maps:get(<<"201">>, Response)),
?assertEqual(#{}, maps:without([<<"201">>, <<"200">>], Response)),
?assertEqual(ExpectRefs, Refs),
{Spec, emqx_dashboard_swagger:components(Refs, #{})}.
to_schema(Object) ->
#{
operationId => test,
post => #{responses => #{200 => Object, 201 => Object}}
}.
namespace() -> undefined.
fields(good_ref) ->
[
{'webhook-host', mk(emqx_schema:ip_port(), #{default => <<"127.0.0.1:80">>})},
{log_dir, mk(string(), #{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.