prop_exhook_hooks.erl 19 KB

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