prop_exhook_hooks.erl 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2020-2021 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_takeovered() ->
  238. ?ALL({ClientInfo, SessInfo}, {clientinfo(), sessioninfo()},
  239. begin
  240. ok = emqx_hooks:run('session.takeovered', [ClientInfo, SessInfo]),
  241. {'on_session_takeovered', 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 = emqx_message:to_map(Msg),
  275. emqx_message:from_map(
  276. MsgMap#{qos => 0,
  277. topic => <<"">>,
  278. payload => <<"">>
  279. });
  280. <<"gooduser">> = From ->
  281. MsgMap = emqx_message:to_map(Msg),
  282. emqx_message:from_map(
  283. MsgMap#{topic => From,
  284. payload => From
  285. });
  286. _ -> Msg
  287. end,
  288. ?assertEqual(ExpectedOutMsg, OutMsg),
  289. {'on_message_publish', Resp} = emqx_exhook_demo_svr:take(),
  290. Expected =
  291. #{message => from_message(Msg)
  292. },
  293. ?assertEqual(Expected, Resp)
  294. end,
  295. true
  296. end).
  297. prop_message_dropped() ->
  298. ?ALL({Msg, By, Reason}, {message(), hardcoded, shutdown_reason()},
  299. begin
  300. ok = emqx_hooks:run('message.dropped', [Msg, By, Reason]),
  301. case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
  302. true -> skip;
  303. _ ->
  304. {'on_message_dropped', Resp} = emqx_exhook_demo_svr:take(),
  305. Expected =
  306. #{reason => stringfy(Reason),
  307. message => from_message(Msg)
  308. },
  309. ?assertEqual(Expected, Resp)
  310. end,
  311. true
  312. end).
  313. prop_message_delivered() ->
  314. ?ALL({ClientInfo, Msg}, {clientinfo(), message()},
  315. begin
  316. ok = emqx_hooks:run('message.delivered', [ClientInfo, Msg]),
  317. case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
  318. true -> skip;
  319. _ ->
  320. {'on_message_delivered', Resp} = emqx_exhook_demo_svr:take(),
  321. Expected =
  322. #{clientinfo => from_clientinfo(ClientInfo),
  323. message => from_message(Msg)
  324. },
  325. ?assertEqual(Expected, Resp)
  326. end,
  327. true
  328. end).
  329. prop_message_acked() ->
  330. ?ALL({ClientInfo, Msg}, {clientinfo(), message()},
  331. begin
  332. ok = emqx_hooks:run('message.acked', [ClientInfo, Msg]),
  333. case emqx_topic:match(emqx_message:topic(Msg), <<"$SYS/#">>) of
  334. true -> skip;
  335. _ ->
  336. {'on_message_acked', Resp} = emqx_exhook_demo_svr:take(),
  337. Expected =
  338. #{clientinfo => from_clientinfo(ClientInfo),
  339. message => from_message(Msg)
  340. },
  341. ?assertEqual(Expected, Resp)
  342. end,
  343. true
  344. end).
  345. nodestr() ->
  346. stringfy(node()).
  347. peerhost(#{peername := {Host, _}}) ->
  348. ntoa(Host).
  349. sockport(#{sockname := {_, Port}}) ->
  350. Port.
  351. %% copied from emqx_exhook
  352. ntoa({0,0,0,0,0,16#ffff,AB,CD}) ->
  353. list_to_binary(inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256}));
  354. ntoa(IP) ->
  355. list_to_binary(inet_parse:ntoa(IP)).
  356. maybe(undefined) -> <<>>;
  357. maybe(B) -> B.
  358. properties(undefined) -> [];
  359. properties(M) when is_map(M) ->
  360. maps:fold(fun(K, V, Acc) ->
  361. [#{name => stringfy(K),
  362. value => stringfy(V)} | Acc]
  363. end, [], M).
  364. topicfilters(Tfs) when is_list(Tfs) ->
  365. [#{name => Topic, qos => Qos} || {Topic, #{qos := Qos}} <- Tfs].
  366. %% @private
  367. stringfy(Term) when is_binary(Term) ->
  368. Term;
  369. stringfy(Term) when is_integer(Term) ->
  370. integer_to_binary(Term);
  371. stringfy(Term) when is_atom(Term) ->
  372. atom_to_binary(Term, utf8);
  373. stringfy(Term) ->
  374. unicode:characters_to_binary((io_lib:format("~0p", [Term]))).
  375. subopts(SubOpts) ->
  376. #{qos => maps:get(qos, SubOpts, 0),
  377. rh => maps:get(rh, SubOpts, 0),
  378. rap => maps:get(rap, SubOpts, 0),
  379. nl => maps:get(nl, SubOpts, 0),
  380. share => maps:get(share, SubOpts, <<>>)
  381. }.
  382. authresult_to_bool(AuthResult) ->
  383. AuthResult == ok.
  384. aclresult_to_bool(Result) ->
  385. Result == allow.
  386. pubsub_to_enum(publish) -> 'PUBLISH';
  387. pubsub_to_enum(subscribe) -> 'SUBSCRIBE'.
  388. from_conninfo(ConnInfo) ->
  389. #{node => nodestr(),
  390. clientid => maps:get(clientid, ConnInfo),
  391. username => maybe(maps:get(username, ConnInfo, <<>>)),
  392. peerhost => peerhost(ConnInfo),
  393. sockport => sockport(ConnInfo),
  394. proto_name => maps:get(proto_name, ConnInfo),
  395. proto_ver => stringfy(maps:get(proto_ver, ConnInfo)),
  396. keepalive => maps:get(keepalive, ConnInfo)
  397. }.
  398. from_clientinfo(ClientInfo) ->
  399. #{node => nodestr(),
  400. clientid => maps:get(clientid, ClientInfo),
  401. username => maybe(maps:get(username, ClientInfo, <<>>)),
  402. password => maybe(maps:get(password, ClientInfo, <<>>)),
  403. peerhost => ntoa(maps:get(peerhost, ClientInfo)),
  404. sockport => maps:get(sockport, ClientInfo),
  405. protocol => stringfy(maps:get(protocol, ClientInfo)),
  406. mountpoint => maybe(maps:get(mountpoint, ClientInfo, <<>>)),
  407. is_superuser => maps:get(is_superuser, ClientInfo, false),
  408. anonymous => maps:get(anonymous, ClientInfo, true),
  409. cn => maybe(maps:get(cn, ClientInfo, <<>>)),
  410. dn => maybe(maps:get(dn, ClientInfo, <<>>))
  411. }.
  412. from_message(Msg) ->
  413. #{node => nodestr(),
  414. id => emqx_guid:to_hexstr(emqx_message:id(Msg)),
  415. qos => emqx_message:qos(Msg),
  416. from => stringfy(emqx_message:from(Msg)),
  417. topic => emqx_message:topic(Msg),
  418. payload => emqx_message:payload(Msg),
  419. timestamp => emqx_message:timestamp(Msg)
  420. }.
  421. %%--------------------------------------------------------------------
  422. %% Helper
  423. %%--------------------------------------------------------------------
  424. do_setup() ->
  425. logger:set_primary_config(#{level => warning}),
  426. _ = emqx_exhook_demo_svr:start(),
  427. ok = emqx_config:init_load(emqx_exhook_schema, ?CONF_DEFAULT),
  428. emqx_common_test_helpers:start_apps([emqx_exhook]),
  429. %% waiting first loaded event
  430. {'on_provider_loaded', _} = emqx_exhook_demo_svr:take(),
  431. ok.
  432. do_teardown(_) ->
  433. emqx_common_test_helpers:stop_apps([emqx_exhook]),
  434. %% waiting last unloaded event
  435. {'on_provider_unloaded', _} = emqx_exhook_demo_svr:take(),
  436. _ = emqx_exhook_demo_svr:stop(),
  437. logger:set_primary_config(#{level => notice}),
  438. timer:sleep(2000),
  439. ok.
  440. %%--------------------------------------------------------------------
  441. %% Generators
  442. %%--------------------------------------------------------------------
  443. conn_properties() ->
  444. #{}.
  445. ack_properties() ->
  446. #{}.
  447. sub_properties() ->
  448. #{}.
  449. unsub_properties() ->
  450. #{}.
  451. shutdown_reason() ->
  452. oneof([utf8(), {shutdown, emqx_proper_types:limited_atom()}]).
  453. authresult() ->
  454. ?LET(RC, connack_return_code(),
  455. case RC of
  456. success -> ok;
  457. _ -> {error, RC}
  458. end).
  459. inject_magic_into(Key, Object) ->
  460. case castspell() of
  461. muggles -> Object;
  462. Spell ->
  463. Object#{Key => Spell}
  464. end.
  465. castspell() ->
  466. L = [<<"baduser">>, <<"gooduser">>, <<"normaluser">>, muggles],
  467. lists:nth(rand:uniform(length(L)), L).