prop_exhook_hooks.erl 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2020-2022 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(prop_exhook_hooks).
  17. -include_lib("proper/include/proper.hrl").
  18. -include_lib("eunit/include/eunit.hrl").
  19. -import(
  20. emqx_proper_types,
  21. [
  22. conninfo/0,
  23. clientinfo/0,
  24. sessioninfo/0,
  25. message/0,
  26. connack_return_code/0,
  27. topictab/0,
  28. topic/0,
  29. subopts/0
  30. ]
  31. ).
  32. -define(CONF_DEFAULT, <<
  33. "\n"
  34. "exhook {\n"
  35. " servers =\n"
  36. " [ { name = default,\n"
  37. " url = \"http://127.0.0.1:9000\"\n"
  38. " }\n"
  39. " ]\n"
  40. "}\n"
  41. >>).
  42. -define(ALL(Vars, Types, Exprs),
  43. ?SETUP(
  44. fun() ->
  45. State = do_setup(),
  46. fun() -> do_teardown(State) end
  47. end,
  48. ?FORALL(Vars, Types, Exprs)
  49. )
  50. ).
  51. -define(DEFAULT_CLUSTER_NAME_ATOM, emqxcl).
  52. -define(DEFAULT_CLUSTER_NAME_BIN, <<"emqxcl">>).
  53. %%--------------------------------------------------------------------
  54. %% Properties
  55. %%--------------------------------------------------------------------
  56. prop_client_connect() ->
  57. ?ALL(
  58. {ConnInfo, ConnProps, Meta},
  59. {conninfo(), conn_properties(), request_meta()},
  60. begin
  61. ok = emqx_hooks:run('client.connect', [ConnInfo, ConnProps]),
  62. {'on_client_connect', Resp} = emqx_exhook_demo_svr:take(),
  63. Expected =
  64. #{
  65. props => properties(ConnProps),
  66. conninfo => from_conninfo(ConnInfo),
  67. meta => Meta
  68. },
  69. ?assertEqual(Expected, Resp),
  70. true
  71. end
  72. ).
  73. prop_client_connack() ->
  74. ?ALL(
  75. {ConnInfo, Rc, AckProps, Meta},
  76. {conninfo(), connack_return_code(), ack_properties(), request_meta()},
  77. begin
  78. ok = emqx_hooks:run('client.connack', [ConnInfo, Rc, AckProps]),
  79. {'on_client_connack', Resp} = emqx_exhook_demo_svr:take(),
  80. Expected =
  81. #{
  82. props => properties(AckProps),
  83. result_code => atom_to_binary(Rc, utf8),
  84. conninfo => from_conninfo(ConnInfo),
  85. meta => Meta
  86. },
  87. ?assertEqual(Expected, Resp),
  88. true
  89. end
  90. ).
  91. prop_client_authenticate() ->
  92. ?ALL(
  93. {ClientInfo0, AuthResult, Meta},
  94. {clientinfo(), authresult(), request_meta()},
  95. begin
  96. ClientInfo = inject_magic_into(username, ClientInfo0),
  97. OutAuthResult = emqx_hooks:run_fold('client.authenticate', [ClientInfo], AuthResult),
  98. ExpectedAuthResult =
  99. case maps:get(username, ClientInfo) of
  100. <<"baduser">> ->
  101. {error, not_authorized};
  102. <<"gooduser">> ->
  103. ok;
  104. <<"normaluser">> ->
  105. ok;
  106. _ ->
  107. case AuthResult of
  108. ok -> ok;
  109. _ -> {error, not_authorized}
  110. end
  111. end,
  112. ?assertEqual(ExpectedAuthResult, OutAuthResult),
  113. {'on_client_authenticate', Resp} = emqx_exhook_demo_svr:take(),
  114. Expected =
  115. #{
  116. result => authresult_to_bool(AuthResult),
  117. clientinfo => from_clientinfo(ClientInfo),
  118. meta => Meta
  119. },
  120. ?assertEqual(Expected, Resp),
  121. true
  122. end
  123. ).
  124. prop_client_authorize() ->
  125. MkResult = fun(Result) -> #{result => Result, from => exhook} end,
  126. ?ALL(
  127. {ClientInfo0, PubSub, Topic, Result, Meta},
  128. {
  129. clientinfo(),
  130. oneof([publish, subscribe]),
  131. topic(),
  132. oneof([MkResult(allow), MkResult(deny)]),
  133. request_meta()
  134. },
  135. begin
  136. ClientInfo = inject_magic_into(username, ClientInfo0),
  137. OutResult = emqx_hooks:run_fold(
  138. 'client.authorize',
  139. [ClientInfo, PubSub, Topic],
  140. Result
  141. ),
  142. ExpectedOutResult =
  143. case maps:get(username, ClientInfo) of
  144. <<"baduser">> -> MkResult(deny);
  145. <<"gooduser">> -> MkResult(allow);
  146. <<"normaluser">> -> MkResult(allow);
  147. _ -> Result
  148. end,
  149. ?assertEqual(ExpectedOutResult, OutResult),
  150. {'on_client_authorize', Resp} = emqx_exhook_demo_svr:take(),
  151. Expected =
  152. #{
  153. result => aclresult_to_bool(Result),
  154. type => pubsub_to_enum(PubSub),
  155. topic => Topic,
  156. clientinfo => from_clientinfo(ClientInfo),
  157. meta => Meta
  158. },
  159. ?assertEqual(Expected, Resp),
  160. true
  161. end
  162. ).
  163. prop_client_connected() ->
  164. ?ALL(
  165. {ClientInfo, ConnInfo, Meta},
  166. {clientinfo(), conninfo(), request_meta()},
  167. begin
  168. ok = emqx_hooks:run('client.connected', [ClientInfo, ConnInfo]),
  169. {'on_client_connected', Resp} = emqx_exhook_demo_svr:take(),
  170. Expected =
  171. #{
  172. clientinfo => from_clientinfo(ClientInfo),
  173. meta => Meta
  174. },
  175. ?assertEqual(Expected, Resp),
  176. true
  177. end
  178. ).
  179. prop_client_disconnected() ->
  180. ?ALL(
  181. {ClientInfo, Reason, ConnInfo, Meta},
  182. {clientinfo(), shutdown_reason(), conninfo(), request_meta()},
  183. begin
  184. ok = emqx_hooks:run('client.disconnected', [ClientInfo, Reason, ConnInfo]),
  185. {'on_client_disconnected', Resp} = emqx_exhook_demo_svr:take(),
  186. Expected =
  187. #{
  188. reason => stringfy(Reason),
  189. clientinfo => from_clientinfo(ClientInfo),
  190. meta => Meta
  191. },
  192. ?assertEqual(Expected, Resp),
  193. true
  194. end
  195. ).
  196. prop_client_subscribe() ->
  197. ?ALL(
  198. {ClientInfo, SubProps, TopicTab, Meta},
  199. {clientinfo(), sub_properties(), topictab(), request_meta()},
  200. begin
  201. ok = emqx_hooks:run('client.subscribe', [ClientInfo, SubProps, TopicTab]),
  202. {'on_client_subscribe', Resp} = emqx_exhook_demo_svr:take(),
  203. Expected =
  204. #{
  205. props => properties(SubProps),
  206. topic_filters => topicfilters(TopicTab),
  207. clientinfo => from_clientinfo(ClientInfo),
  208. meta => Meta
  209. },
  210. ?assertEqual(Expected, Resp),
  211. true
  212. end
  213. ).
  214. prop_client_unsubscribe() ->
  215. ?ALL(
  216. {ClientInfo, UnSubProps, TopicTab, Meta},
  217. {clientinfo(), unsub_properties(), topictab(), request_meta()},
  218. begin
  219. ok = emqx_hooks:run('client.unsubscribe', [ClientInfo, UnSubProps, TopicTab]),
  220. {'on_client_unsubscribe', Resp} = emqx_exhook_demo_svr:take(),
  221. Expected =
  222. #{
  223. props => properties(UnSubProps),
  224. topic_filters => topicfilters(TopicTab),
  225. clientinfo => from_clientinfo(ClientInfo),
  226. meta => Meta
  227. },
  228. ?assertEqual(Expected, Resp),
  229. true
  230. end
  231. ).
  232. prop_session_created() ->
  233. ?ALL(
  234. {ClientInfo, SessInfo, Meta},
  235. {clientinfo(), sessioninfo(), request_meta()},
  236. begin
  237. ok = emqx_hooks:run('session.created', [ClientInfo, SessInfo]),
  238. {'on_session_created', Resp} = emqx_exhook_demo_svr:take(),
  239. Expected =
  240. #{
  241. clientinfo => from_clientinfo(ClientInfo),
  242. meta => Meta
  243. },
  244. ?assertEqual(Expected, Resp),
  245. true
  246. end
  247. ).
  248. prop_session_subscribed() ->
  249. ?ALL(
  250. {ClientInfo, Topic, SubOpts, Meta},
  251. {clientinfo(), topic(), subopts(), request_meta()},
  252. begin
  253. ok = emqx_hooks:run('session.subscribed', [ClientInfo, Topic, SubOpts]),
  254. {'on_session_subscribed', Resp} = emqx_exhook_demo_svr:take(),
  255. Expected =
  256. #{
  257. topic => Topic,
  258. subopts => subopts(SubOpts),
  259. clientinfo => from_clientinfo(ClientInfo),
  260. meta => Meta
  261. },
  262. ?assertEqual(Expected, Resp),
  263. true
  264. end
  265. ).
  266. prop_session_unsubscribed() ->
  267. ?ALL(
  268. {ClientInfo, Topic, SubOpts, Meta},
  269. {clientinfo(), topic(), subopts(), request_meta()},
  270. begin
  271. ok = emqx_hooks:run('session.unsubscribed', [ClientInfo, Topic, SubOpts]),
  272. {'on_session_unsubscribed', Resp} = emqx_exhook_demo_svr:take(),
  273. Expected =
  274. #{
  275. topic => Topic,
  276. clientinfo => from_clientinfo(ClientInfo),
  277. meta => Meta
  278. },
  279. ?assertEqual(Expected, Resp),
  280. true
  281. end
  282. ).
  283. prop_session_resumed() ->
  284. ?ALL(
  285. {ClientInfo, SessInfo, Meta},
  286. {clientinfo(), sessioninfo(), request_meta()},
  287. begin
  288. ok = emqx_hooks:run('session.resumed', [ClientInfo, SessInfo]),
  289. {'on_session_resumed', Resp} = emqx_exhook_demo_svr:take(),
  290. Expected =
  291. #{
  292. clientinfo => from_clientinfo(ClientInfo),
  293. meta => Meta
  294. },
  295. ?assertEqual(Expected, Resp),
  296. true
  297. end
  298. ).
  299. prop_session_discared() ->
  300. ?ALL(
  301. {ClientInfo, SessInfo, Meta},
  302. {clientinfo(), sessioninfo(), request_meta()},
  303. begin
  304. ok = emqx_hooks:run('session.discarded', [ClientInfo, SessInfo]),
  305. {'on_session_discarded', Resp} = emqx_exhook_demo_svr:take(),
  306. Expected =
  307. #{clientinfo => from_clientinfo(ClientInfo), meta => Meta},
  308. ?assertEqual(Expected, Resp),
  309. true
  310. end
  311. ).
  312. prop_session_takenover() ->
  313. ?ALL(
  314. {ClientInfo, SessInfo, Meta},
  315. {clientinfo(), sessioninfo(), request_meta()},
  316. begin
  317. ok = emqx_hooks:run('session.takenover', [ClientInfo, SessInfo]),
  318. {'on_session_takenover', Resp} = emqx_exhook_demo_svr:take(),
  319. Expected =
  320. #{clientinfo => from_clientinfo(ClientInfo), meta => Meta},
  321. ?assertEqual(Expected, Resp),
  322. true
  323. end
  324. ).
  325. prop_session_terminated() ->
  326. ?ALL(
  327. {ClientInfo, Reason, SessInfo, Meta},
  328. {clientinfo(), shutdown_reason(), sessioninfo(), request_meta()},
  329. begin
  330. ok = emqx_hooks:run('session.terminated', [ClientInfo, Reason, SessInfo]),
  331. {'on_session_terminated', Resp} = emqx_exhook_demo_svr:take(),
  332. Expected =
  333. #{
  334. reason => stringfy(Reason),
  335. clientinfo => from_clientinfo(ClientInfo),
  336. meta => Meta
  337. },
  338. ?assertEqual(Expected, Resp),
  339. true
  340. end
  341. ).
  342. prop_message_publish() ->
  343. ?ALL(
  344. {Msg0, Meta},
  345. {message(), request_meta()},
  346. begin
  347. Msg = emqx_message:from_map(
  348. inject_magic_into(from, emqx_message:to_map(Msg0))
  349. ),
  350. OutMsg = emqx_hooks:run_fold('message.publish', [], Msg),
  351. case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
  352. true ->
  353. ?assertEqual(Msg, OutMsg),
  354. skip;
  355. _ ->
  356. ExpectedOutMsg =
  357. case emqx_message:from(Msg) of
  358. <<"baduser">> ->
  359. MsgMap =
  360. #{headers := Headers} =
  361. emqx_message:to_map(Msg),
  362. emqx_message:from_map(
  363. MsgMap#{
  364. qos => 0,
  365. topic => <<"">>,
  366. payload => <<"">>,
  367. headers => maps:put(allow_publish, false, Headers)
  368. }
  369. );
  370. <<"gooduser">> = From ->
  371. MsgMap =
  372. #{headers := Headers} =
  373. emqx_message:to_map(Msg),
  374. emqx_message:from_map(
  375. MsgMap#{
  376. topic => From,
  377. payload => From,
  378. headers => maps:put(allow_publish, true, Headers)
  379. }
  380. );
  381. _ ->
  382. Msg
  383. end,
  384. ?assertEqual(ExpectedOutMsg, OutMsg),
  385. {'on_message_publish', Resp} = emqx_exhook_demo_svr:take(),
  386. Expected =
  387. #{
  388. message => from_message(Msg),
  389. meta => Meta
  390. },
  391. ?assertEqual(Expected, Resp)
  392. end,
  393. true
  394. end
  395. ).
  396. prop_message_dropped() ->
  397. ?ALL(
  398. {Msg, By, Reason, Meta},
  399. {message(), hardcoded, shutdown_reason(), request_meta()},
  400. begin
  401. ok = emqx_hooks:run('message.dropped', [Msg, By, Reason]),
  402. case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
  403. true ->
  404. skip;
  405. _ ->
  406. {'on_message_dropped', Resp} = emqx_exhook_demo_svr:take(),
  407. Expected =
  408. #{
  409. reason => stringfy(Reason),
  410. message => from_message(Msg),
  411. meta => Meta
  412. },
  413. ?assertEqual(Expected, Resp)
  414. end,
  415. true
  416. end
  417. ).
  418. prop_message_delivered() ->
  419. ?ALL(
  420. {ClientInfo, Msg, Meta},
  421. {clientinfo(), message(), request_meta()},
  422. begin
  423. ok = emqx_hooks:run('message.delivered', [ClientInfo, Msg]),
  424. case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
  425. true ->
  426. skip;
  427. _ ->
  428. {'on_message_delivered', Resp} = emqx_exhook_demo_svr:take(),
  429. Expected =
  430. #{
  431. clientinfo => from_clientinfo(ClientInfo),
  432. message => from_message(Msg),
  433. meta => Meta
  434. },
  435. ?assertEqual(Expected, Resp)
  436. end,
  437. true
  438. end
  439. ).
  440. prop_message_acked() ->
  441. ?ALL(
  442. {ClientInfo, Msg, Meta},
  443. {clientinfo(), message(), request_meta()},
  444. begin
  445. ok = emqx_hooks:run('message.acked', [ClientInfo, Msg]),
  446. case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
  447. true ->
  448. skip;
  449. _ ->
  450. {'on_message_acked', Resp} = emqx_exhook_demo_svr:take(),
  451. Expected =
  452. #{
  453. clientinfo => from_clientinfo(ClientInfo),
  454. message => from_message(Msg),
  455. meta => Meta
  456. },
  457. ?assertEqual(Expected, Resp)
  458. end,
  459. true
  460. end
  461. ).
  462. nodestr() ->
  463. stringfy(node()).
  464. peerhost(#{peername := {Host, _}}) ->
  465. ntoa(Host).
  466. sockport(#{sockname := {_, Port}}) ->
  467. Port.
  468. %% copied from emqx_exhook
  469. ntoa({0, 0, 0, 0, 0, 16#ffff, AB, CD}) ->
  470. list_to_binary(inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256}));
  471. ntoa(IP) ->
  472. list_to_binary(inet_parse:ntoa(IP)).
  473. maybe(undefined) -> <<>>;
  474. maybe(B) -> B.
  475. properties(undefined) ->
  476. [];
  477. properties(M) when is_map(M) ->
  478. maps:fold(
  479. fun(K, V, Acc) ->
  480. [
  481. #{
  482. name => stringfy(K),
  483. value => stringfy(V)
  484. }
  485. | Acc
  486. ]
  487. end,
  488. [],
  489. M
  490. ).
  491. topicfilters(Tfs) when is_list(Tfs) ->
  492. [#{name => Topic, qos => Qos} || {Topic, #{qos := Qos}} <- Tfs].
  493. %% @private
  494. stringfy(Term) when is_binary(Term) ->
  495. Term;
  496. stringfy(Term) when is_integer(Term) ->
  497. integer_to_binary(Term);
  498. stringfy(Term) when is_atom(Term) ->
  499. atom_to_binary(Term, utf8);
  500. stringfy(Term) when is_list(Term) ->
  501. list_to_binary(Term);
  502. stringfy(Term) ->
  503. unicode:characters_to_binary((io_lib:format("~0p", [Term]))).
  504. subopts(SubOpts) ->
  505. #{
  506. qos => maps:get(qos, SubOpts, 0),
  507. rh => maps:get(rh, SubOpts, 0),
  508. rap => maps:get(rap, SubOpts, 0),
  509. nl => maps:get(nl, SubOpts, 0),
  510. share => maps:get(share, SubOpts, <<>>)
  511. }.
  512. authresult_to_bool(AuthResult) ->
  513. AuthResult == ok.
  514. aclresult_to_bool(#{result := Result}) ->
  515. Result == allow.
  516. pubsub_to_enum(publish) -> 'PUBLISH';
  517. pubsub_to_enum(subscribe) -> 'SUBSCRIBE'.
  518. from_conninfo(ConnInfo) ->
  519. #{
  520. node => nodestr(),
  521. clientid => maps:get(clientid, ConnInfo),
  522. username => maybe(maps:get(username, ConnInfo, <<>>)),
  523. peerhost => peerhost(ConnInfo),
  524. sockport => sockport(ConnInfo),
  525. proto_name => maps:get(proto_name, ConnInfo),
  526. proto_ver => stringfy(maps:get(proto_ver, ConnInfo)),
  527. keepalive => maps:get(keepalive, ConnInfo)
  528. }.
  529. from_clientinfo(ClientInfo) ->
  530. #{
  531. node => nodestr(),
  532. clientid => maps:get(clientid, ClientInfo),
  533. username => maybe(maps:get(username, ClientInfo, <<>>)),
  534. password => maybe(maps:get(password, ClientInfo, <<>>)),
  535. peerhost => ntoa(maps:get(peerhost, ClientInfo)),
  536. sockport => maps:get(sockport, ClientInfo),
  537. protocol => stringfy(maps:get(protocol, ClientInfo)),
  538. mountpoint => maybe(maps:get(mountpoint, ClientInfo, <<>>)),
  539. is_superuser => maps:get(is_superuser, ClientInfo, false),
  540. anonymous => maps:get(anonymous, ClientInfo, true),
  541. cn => maybe(maps:get(cn, ClientInfo, <<>>)),
  542. dn => maybe(maps:get(dn, ClientInfo, <<>>))
  543. }.
  544. from_message(Msg) ->
  545. #{
  546. node => nodestr(),
  547. id => emqx_guid:to_hexstr(emqx_message:id(Msg)),
  548. qos => emqx_message:qos(Msg),
  549. from => stringfy(emqx_message:from(Msg)),
  550. topic => emqx_message:topic(Msg),
  551. payload => emqx_message:payload(Msg),
  552. timestamp => emqx_message:timestamp(Msg),
  553. headers => emqx_exhook_handler:headers(
  554. emqx_message:get_headers(Msg)
  555. )
  556. }.
  557. %%--------------------------------------------------------------------
  558. %% Helper
  559. %%--------------------------------------------------------------------
  560. do_setup() ->
  561. logger:set_primary_config(#{level => warning}),
  562. ok = ekka:start(),
  563. application:set_env(ekka, cluster_name, ?DEFAULT_CLUSTER_NAME_ATOM),
  564. _ = emqx_exhook_demo_svr:start(),
  565. ok = emqx_config:init_load(emqx_exhook_schema, ?CONF_DEFAULT),
  566. emqx_common_test_helpers:start_apps([emqx_exhook]),
  567. %% waiting first loaded event
  568. {'on_provider_loaded', _} = emqx_exhook_demo_svr:take(),
  569. ok.
  570. do_teardown(_) ->
  571. emqx_common_test_helpers:stop_apps([emqx_exhook]),
  572. %% waiting last unloaded event
  573. {'on_provider_unloaded', _} = emqx_exhook_demo_svr:take(),
  574. _ = emqx_exhook_demo_svr:stop(),
  575. logger:set_primary_config(#{level => notice}),
  576. timer:sleep(2000),
  577. ok.
  578. %%--------------------------------------------------------------------
  579. %% Generators
  580. %%--------------------------------------------------------------------
  581. conn_properties() ->
  582. #{}.
  583. ack_properties() ->
  584. #{}.
  585. sub_properties() ->
  586. #{}.
  587. unsub_properties() ->
  588. #{}.
  589. shutdown_reason() ->
  590. oneof([utf8(), {shutdown, emqx_proper_types:limited_atom()}]).
  591. authresult() ->
  592. ?LET(
  593. RC,
  594. connack_return_code(),
  595. case RC of
  596. success -> ok;
  597. _ -> {error, RC}
  598. end
  599. ).
  600. inject_magic_into(Key, Object) ->
  601. case castspell() of
  602. muggles -> Object;
  603. Spell -> Object#{Key => Spell}
  604. end.
  605. castspell() ->
  606. L = [<<"baduser">>, <<"gooduser">>, <<"normaluser">>, muggles],
  607. lists:nth(rand:uniform(length(L)), L).
  608. request_meta() ->
  609. #{
  610. node => nodestr(),
  611. version => stringfy(emqx_sys:version()),
  612. sysdescr => stringfy(emqx_sys:sysdescr()),
  613. cluster_name => ?DEFAULT_CLUSTER_NAME_BIN
  614. }.