emqx_swagger_parameter_SUITE.erl 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2022-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
  3. %%
  4. %% Licensed under the Apache License, Version 2.0 (the "License");
  5. %% you may not use this file except in compliance with the License.
  6. %% You may obtain a copy of the License at
  7. %%
  8. %% http://www.apache.org/licenses/LICENSE-2.0
  9. %%
  10. %% Unless required by applicable law or agreed to in writing, software
  11. %% distributed under the License is distributed on an "AS IS" BASIS,
  12. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. %% See the License for the specific language governing permissions and
  14. %% limitations under the License.
  15. %%--------------------------------------------------------------------
  16. -module(emqx_swagger_parameter_SUITE).
  17. -behaviour(minirest_api).
  18. -behaviour(hocon_schema).
  19. %% API
  20. -export([paths/0, api_spec/0, schema/1, roots/0, namespace/0, fields/1]).
  21. -export([init_per_suite/1, end_per_suite/1]).
  22. -export([t_in_path/1, t_in_query/1, t_in_mix/1, t_without_in/1, t_ref/1, t_public_ref/1]).
  23. -export([t_require/1, t_query_enum/1, t_nullable/1, t_method/1, t_api_spec/1]).
  24. -export([t_in_path_trans/1, t_in_query_trans/1, t_in_mix_trans/1, t_ref_trans/1]).
  25. -export([t_in_path_trans_error/1, t_in_query_trans_error/1, t_in_mix_trans_error/1]).
  26. -export([all/0, suite/0, groups/0]).
  27. -include_lib("eunit/include/eunit.hrl").
  28. -include_lib("typerefl/include/types.hrl").
  29. -include_lib("hocon/include/hoconsc.hrl").
  30. -import(hoconsc, [mk/2]).
  31. -define(METHODS, [get, post, put, head, delete, patch, options, trace]).
  32. all() -> [{group, spec}, {group, validation}].
  33. suite() -> [{timetrap, {minutes, 1}}].
  34. groups() ->
  35. [
  36. {spec, [parallel], [
  37. t_api_spec,
  38. t_in_path,
  39. t_ref,
  40. t_in_query,
  41. t_in_mix,
  42. t_without_in,
  43. t_require,
  44. t_query_enum,
  45. t_nullable,
  46. t_method,
  47. t_public_ref
  48. ]},
  49. {validation, [parallel], [
  50. t_in_path_trans,
  51. t_ref_trans,
  52. t_in_query_trans,
  53. t_in_mix_trans,
  54. t_in_path_trans_error,
  55. t_in_query_trans_error,
  56. t_in_mix_trans_error
  57. ]}
  58. ].
  59. init_per_suite(Config) ->
  60. emqx_mgmt_api_test_util:init_suite([emqx_conf]),
  61. Config.
  62. end_per_suite(_Config) ->
  63. emqx_mgmt_api_test_util:end_suite([emqx_conf]).
  64. t_in_path(_Config) ->
  65. Expect =
  66. [
  67. #{
  68. description => <<"Indicates which sorts of issues to return">>,
  69. example => <<"all">>,
  70. in => path,
  71. name => filter,
  72. required => true,
  73. schema => #{enum => [assigned, created, mentioned, all], type => string}
  74. }
  75. ],
  76. validate("/test/in/:filter", Expect),
  77. ok.
  78. t_in_query(_Config) ->
  79. Expect =
  80. [
  81. #{
  82. description => <<"results per page (max 100)">>,
  83. example => 1,
  84. in => query,
  85. name => per_page,
  86. schema => #{maximum => 100, minimum => 1, type => integer}
  87. },
  88. #{
  89. description => <<"QOS">>,
  90. in => query,
  91. name => qos,
  92. schema => #{minimum => 0, maximum => 2, type => integer, example => 0}
  93. }
  94. ],
  95. validate("/test/in/query", Expect),
  96. ok.
  97. t_ref(_Config) ->
  98. LocalPath = "/test/in/ref/local",
  99. Path = "/test/in/ref",
  100. Expect = [#{<<"$ref">> => <<"#/components/parameters/emqx_swagger_parameter_SUITE.page">>}],
  101. {OperationId, Spec, Refs, RouteOpts} = emqx_dashboard_swagger:parse_spec_ref(
  102. ?MODULE, Path, #{}
  103. ),
  104. {OperationId, Spec, Refs, RouteOpts} = emqx_dashboard_swagger:parse_spec_ref(
  105. ?MODULE, LocalPath, #{}
  106. ),
  107. ?assertEqual(test, OperationId),
  108. Params = maps:get(parameters, maps:get(post, Spec)),
  109. ?assertEqual(Expect, Params),
  110. ?assertEqual([{?MODULE, page, parameter}], Refs),
  111. ok.
  112. t_public_ref(_Config) ->
  113. Path = "/test/in/ref/public",
  114. Expect = [
  115. #{<<"$ref">> => <<"#/components/parameters/public.page">>},
  116. #{<<"$ref">> => <<"#/components/parameters/public.limit">>}
  117. ],
  118. {OperationId, Spec, Refs, #{}} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
  119. ?assertEqual(test, OperationId),
  120. Params = maps:get(parameters, maps:get(post, Spec)),
  121. ?assertEqual(Expect, Params),
  122. ?assertEqual(
  123. [
  124. {emqx_dashboard_swagger, limit, parameter},
  125. {emqx_dashboard_swagger, page, parameter}
  126. ],
  127. Refs
  128. ),
  129. ExpectRefs = [
  130. #{
  131. <<"public.limit">> => #{
  132. description => <<"Results per page(max 10000)">>,
  133. in => query,
  134. name => limit,
  135. example => 50,
  136. schema => #{
  137. default => 100,
  138. maximum => 10000,
  139. minimum => 1,
  140. type => integer
  141. }
  142. }
  143. },
  144. #{
  145. <<"public.page">> => #{
  146. description => <<"Page number of the results to fetch.">>,
  147. in => query,
  148. name => page,
  149. example => 1,
  150. schema => #{default => 1, minimum => 1, type => integer}
  151. }
  152. }
  153. ],
  154. ?assertEqual(ExpectRefs, emqx_dashboard_swagger:components(Refs, #{})),
  155. ok.
  156. t_in_mix(_Config) ->
  157. Expect =
  158. [
  159. #{
  160. description => <<"Indicates which sorts of issues to return">>,
  161. example => <<"all">>,
  162. in => query,
  163. name => filter,
  164. schema => #{enum => [assigned, created, mentioned, all], type => string}
  165. },
  166. #{
  167. description => <<"Indicates the state of the issues to return.">>,
  168. example => <<"12m">>,
  169. in => path,
  170. name => state,
  171. required => true,
  172. schema => #{example => <<"1h">>, type => string}
  173. },
  174. #{
  175. example => 10,
  176. in => query,
  177. name => per_page,
  178. required => false,
  179. schema => #{default => 5, maximum => 50, minimum => 1, type => integer}
  180. },
  181. #{in => query, name => is_admin, schema => #{type => boolean}},
  182. #{
  183. in => query,
  184. name => timeout,
  185. schema => #{
  186. <<"oneOf">> => [
  187. #{enum => [infinity], type => string},
  188. #{maximum => 60, minimum => 30, type => integer}
  189. ]
  190. }
  191. }
  192. ],
  193. ExpectMeta = #{
  194. tags => [<<"Tags">>, <<"Good">>],
  195. description => <<"good description">>,
  196. summary => <<"good summary">>,
  197. security => [],
  198. deprecated => true,
  199. responses => #{<<"200">> => #{description => <<"ok">>}}
  200. },
  201. GotSpec = validate("/test/in/mix/:state", Expect),
  202. ?assertEqual(ExpectMeta, maps:without([parameters], maps:get(post, GotSpec))),
  203. ok.
  204. t_without_in(_Config) ->
  205. ?assertThrow(
  206. {error, <<"missing in:path/query field in parameters">>},
  207. emqx_dashboard_swagger:parse_spec_ref(?MODULE, "/test/without/in", #{})
  208. ),
  209. ok.
  210. t_require(_Config) ->
  211. ExpectSpec = [
  212. #{
  213. in => query,
  214. name => userid,
  215. required => false,
  216. schema => #{type => string}
  217. }
  218. ],
  219. validate("/required/false", ExpectSpec),
  220. ok.
  221. t_query_enum(_Config) ->
  222. ExpectSpec = [
  223. #{
  224. in => query,
  225. name => userid,
  226. schema => #{type => string, enum => [<<"a">>], default => <<"a">>}
  227. }
  228. ],
  229. validate("/query/enum", ExpectSpec),
  230. ok.
  231. t_nullable(_Config) ->
  232. NullableFalse = [
  233. #{
  234. in => query,
  235. name => userid,
  236. required => true,
  237. schema => #{type => string}
  238. }
  239. ],
  240. NullableTrue = [
  241. #{
  242. in => query,
  243. name => userid,
  244. schema => #{type => string},
  245. required => false
  246. }
  247. ],
  248. validate("/nullable/false", NullableFalse),
  249. validate("/nullable/true", NullableTrue),
  250. ok.
  251. t_method(_Config) ->
  252. PathOk = "/method/ok",
  253. PathError = "/method/error",
  254. {test, Spec, [], #{}} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, PathOk, #{}),
  255. ?assertEqual(lists:sort(?METHODS), lists:sort(maps:keys(Spec))),
  256. ?assertThrow(
  257. {error, #{module := ?MODULE, path := PathError, method := bar}},
  258. emqx_dashboard_swagger:parse_spec_ref(?MODULE, PathError, #{})
  259. ),
  260. ok.
  261. t_in_path_trans(_Config) ->
  262. Path = "/test/in/:filter",
  263. Bindings = #{filter => <<"created">>},
  264. Expect =
  265. {ok, #{
  266. bindings => #{filter => created},
  267. body => #{},
  268. query_string => #{}
  269. }},
  270. ?assertEqual(Expect, trans_parameters(Path, Bindings, #{})),
  271. ok.
  272. t_in_query_trans(_Config) ->
  273. Path = "/test/in/query",
  274. Expect =
  275. {ok, #{
  276. bindings => #{},
  277. body => #{},
  278. query_string => #{<<"per_page">> => 100, <<"qos">> => 1}
  279. }},
  280. ?assertEqual(Expect, trans_parameters(Path, #{}, #{<<"per_page">> => 100, <<"qos">> => 1})),
  281. ok.
  282. t_ref_trans(_Config) ->
  283. LocalPath = "/test/in/ref/local",
  284. Path = "/test/in/ref",
  285. Expect =
  286. {ok, #{
  287. bindings => #{},
  288. body => #{},
  289. query_string => #{<<"per_page">> => 100}
  290. }},
  291. ?assertEqual(Expect, trans_parameters(Path, #{}, #{<<"per_page">> => 100})),
  292. ?assertEqual(Expect, trans_parameters(LocalPath, #{}, #{<<"per_page">> => 100})),
  293. {400, 'BAD_REQUEST', Reason} = trans_parameters(Path, #{}, #{<<"per_page">> => 1010}),
  294. ?assertNotEqual(nomatch, binary:match(Reason, [<<"per_page">>])),
  295. {400, 'BAD_REQUEST', Reason} = trans_parameters(LocalPath, #{}, #{<<"per_page">> => 1010}),
  296. ok.
  297. t_in_mix_trans(_Config) ->
  298. Path = "/test/in/mix/:state",
  299. Bindings = #{
  300. state => <<"12m">>,
  301. per_page => <<"1">>
  302. },
  303. Query = #{
  304. <<"filter">> => <<"created">>,
  305. <<"is_admin">> => true,
  306. <<"timeout">> => <<"34">>
  307. },
  308. Expect =
  309. {ok, #{
  310. body => #{},
  311. bindings => #{state => 720},
  312. query_string => #{
  313. <<"filter">> => created,
  314. <<"is_admin">> => true,
  315. <<"per_page">> => 5,
  316. <<"timeout">> => 34
  317. }
  318. }},
  319. ?assertEqual(Expect, trans_parameters(Path, Bindings, Query)),
  320. ok.
  321. t_in_path_trans_error(_Config) ->
  322. Path = "/test/in/:filter",
  323. Bindings = #{filter => <<"created1">>},
  324. ?assertMatch({400, 'BAD_REQUEST', _}, trans_parameters(Path, Bindings, #{})),
  325. ok.
  326. t_in_query_trans_error(_Config) ->
  327. Path = "/test/in/query",
  328. {400, 'BAD_REQUEST', Reason} = trans_parameters(Path, #{}, #{<<"per_page">> => 101}),
  329. ?assertNotEqual(nomatch, binary:match(Reason, [<<"per_page">>])),
  330. ok.
  331. t_in_mix_trans_error(_Config) ->
  332. Path = "/test/in/mix/:state",
  333. Bindings = #{
  334. state => <<"1d2m">>,
  335. per_page => <<"1">>
  336. },
  337. Query = #{
  338. <<"filter">> => <<"cdreated">>,
  339. <<"is_admin">> => true,
  340. <<"timeout">> => <<"34">>
  341. },
  342. ?assertMatch({400, 'BAD_REQUEST', _}, trans_parameters(Path, Bindings, Query)),
  343. ok.
  344. t_api_spec(_Config) ->
  345. {Spec0, _} = emqx_dashboard_swagger:spec(?MODULE),
  346. assert_all_filters_equal(Spec0, undefined),
  347. {Spec1, _} = emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false}),
  348. assert_all_filters_equal(Spec1, undefined),
  349. CustomFilter = fun(Request, _RequestMeta) -> {ok, Request} end,
  350. {Spec2, _} = emqx_dashboard_swagger:spec(?MODULE, #{check_schema => CustomFilter}),
  351. assert_all_filters_equal(Spec2, CustomFilter),
  352. {Spec3, _} = emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}),
  353. Path = "/test/in/:filter",
  354. Filter = filter(Spec3, Path),
  355. Bindings = #{filter => <<"created">>},
  356. ?assertMatch(
  357. {ok, #{bindings := #{filter := created}}},
  358. trans_parameters(Path, Bindings, #{}, Filter)
  359. ).
  360. assert_all_filters_equal(Spec, Filter) ->
  361. lists:foreach(
  362. fun({_, _, _, #{filter := F}}) ->
  363. ?assertEqual(Filter, F)
  364. end,
  365. Spec
  366. ).
  367. validate(Path, ExpectParams) ->
  368. {OperationId, Spec, Refs, #{}} = emqx_dashboard_swagger:parse_spec_ref(?MODULE, Path, #{}),
  369. ?assertEqual(test, OperationId),
  370. Params = maps:get(parameters, maps:get(post, Spec)),
  371. ?assertEqual(ExpectParams, Params),
  372. ?assertEqual([], Refs),
  373. Spec.
  374. filter(ApiSpec, Path) ->
  375. [Filter] = [F || {P, _, _, #{filter := F}} <- ApiSpec, P =:= Path],
  376. Filter.
  377. trans_parameters(Path, Bindings, QueryStr) ->
  378. trans_parameters(Path, Bindings, QueryStr, fun emqx_dashboard_swagger:filter_check_request/2).
  379. trans_parameters(Path, Bindings, QueryStr, Filter) ->
  380. Meta = #{module => ?MODULE, method => post, path => Path},
  381. Request = #{bindings => Bindings, query_string => QueryStr, body => #{}},
  382. Filter(Request, Meta).
  383. api_spec() -> emqx_dashboard_swagger:spec(?MODULE).
  384. paths() ->
  385. [
  386. "/test/in/:filter",
  387. "/test/in/query",
  388. "/test/in/mix/:state",
  389. "/test/in/ref",
  390. "/required/false",
  391. "/nullable/false",
  392. "/nullable/true",
  393. "/method/ok"
  394. ].
  395. schema("/test/in/:filter") ->
  396. #{
  397. operationId => test,
  398. post => #{
  399. parameters => [
  400. {filter,
  401. mk(
  402. hoconsc:enum([assigned, created, mentioned, all]),
  403. #{
  404. in => path,
  405. desc => <<"Indicates which sorts of issues to return">>,
  406. example => "all"
  407. }
  408. )}
  409. ],
  410. responses => #{200 => <<"ok">>}
  411. }
  412. };
  413. schema("/test/in/query") ->
  414. #{
  415. operationId => test,
  416. post => #{
  417. parameters => [
  418. {per_page,
  419. mk(
  420. range(1, 100),
  421. #{
  422. in => query,
  423. desc => <<"results per page (max 100)">>,
  424. example => 1
  425. }
  426. )},
  427. {qos, mk(emqx_schema:qos(), #{in => query, desc => <<"QOS">>})}
  428. ],
  429. responses => #{200 => <<"ok">>}
  430. }
  431. };
  432. schema("/test/in/ref/local") ->
  433. #{
  434. operationId => test,
  435. post => #{
  436. parameters => [hoconsc:ref(page)],
  437. responses => #{200 => <<"ok">>}
  438. }
  439. };
  440. schema("/test/in/ref") ->
  441. #{
  442. operationId => test,
  443. post => #{
  444. parameters => [hoconsc:ref(?MODULE, page)],
  445. responses => #{200 => <<"ok">>}
  446. }
  447. };
  448. schema("/test/in/ref/public") ->
  449. #{
  450. operationId => test,
  451. post => #{
  452. parameters => [
  453. hoconsc:ref(emqx_dashboard_swagger, page),
  454. hoconsc:ref(emqx_dashboard_swagger, limit)
  455. ],
  456. responses => #{200 => <<"ok">>}
  457. }
  458. };
  459. schema("/test/in/mix/:state") ->
  460. #{
  461. operationId => test,
  462. post => #{
  463. tags => [tags, good],
  464. desc => <<"good description">>,
  465. summary => <<"good summary">>,
  466. security => [],
  467. deprecated => true,
  468. parameters => [
  469. {filter,
  470. hoconsc:mk(
  471. hoconsc:enum([assigned, created, mentioned, all]),
  472. #{
  473. in => query,
  474. desc => <<"Indicates which sorts of issues to return">>,
  475. example => "all"
  476. }
  477. )},
  478. {state,
  479. mk(
  480. emqx_schema:duration_s(),
  481. #{
  482. in => path,
  483. required => true,
  484. example => "12m",
  485. desc => <<"Indicates the state of the issues to return.">>
  486. }
  487. )},
  488. {per_page,
  489. mk(
  490. range(1, 50),
  491. #{in => query, required => false, example => 10, default => 5}
  492. )},
  493. {is_admin, mk(boolean(), #{in => query})},
  494. {timeout, mk(hoconsc:union([range(30, 60), infinity]), #{in => query})}
  495. ],
  496. responses => #{200 => <<"ok">>}
  497. }
  498. };
  499. schema("/test/without/in") ->
  500. #{
  501. operationId => test,
  502. post => #{
  503. parameters => [
  504. {'x-request-id', mk(binary(), #{})}
  505. ],
  506. responses => #{200 => <<"ok">>}
  507. }
  508. };
  509. schema("/required/false") ->
  510. to_schema([{'userid', mk(binary(), #{in => query, required => false})}]);
  511. schema("/query/enum") ->
  512. to_schema([{'userid', mk(binary(), #{in => query, enum => [<<"a">>], default => <<"a">>})}]);
  513. schema("/nullable/false") ->
  514. to_schema([{'userid', mk(binary(), #{in => query, required => true})}]);
  515. schema("/nullable/true") ->
  516. to_schema([{'userid', mk(binary(), #{in => query, required => false})}]);
  517. schema("/method/ok") ->
  518. Response = #{responses => #{200 => <<"ok">>}},
  519. lists:foldl(
  520. fun(Method, Acc) -> Acc#{Method => Response} end,
  521. #{operationId => test},
  522. ?METHODS
  523. );
  524. schema("/method/error") ->
  525. #{operationId => test, bar => #{200 => <<"ok">>}}.
  526. namespace() -> undefined.
  527. roots() -> [].
  528. fields(page) ->
  529. [
  530. {per_page,
  531. mk(
  532. range(1, 100),
  533. #{in => query, desc => <<"results per page (max 100)">>, example => 1}
  534. )}
  535. ].
  536. to_schema(Params) ->
  537. #{
  538. operationId => test,
  539. post => #{
  540. parameters => Params,
  541. responses => #{200 => <<"ok">>}
  542. }
  543. }.