emqx_variform_tests.erl 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 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_variform_tests).
  17. -compile(export_all).
  18. -compile(nowarn_export_all).
  19. -include_lib("eunit/include/eunit.hrl").
  20. -define(SYNTAX_ERROR, {error, "syntax error before:" ++ _}).
  21. render_test_() ->
  22. [
  23. {"direct var reference", fun() -> ?assertEqual({ok, <<"1">>}, render("a", #{a => 1})) end},
  24. {"concat strings", fun() ->
  25. ?assertEqual({ok, <<"a,b">>}, render("concat(['a',',','b'])", #{}))
  26. end},
  27. {"concat empty string", fun() ->
  28. ?assertEqual({ok, <<"">>}, render("concat([''])", #{}))
  29. end},
  30. {"identifier with hyphen", fun() ->
  31. ?assertEqual(
  32. {ok, <<"10">>},
  33. render(
  34. "pub_props.Message-Expiry-Interval",
  35. #{pub_props => #{'Message-Expiry-Interval' => 10}}
  36. )
  37. )
  38. end},
  39. {"tokens 1st", fun() ->
  40. ?assertEqual({ok, <<"a">>}, render("nth(1,tokens(var, ','))", #{var => <<"a,b">>}))
  41. end},
  42. {"unknown var return error", fun() ->
  43. ?assertMatch({error, #{reason := var_unbound}}, render("var", #{}))
  44. end},
  45. {"out of range nth index", fun() ->
  46. ?assertEqual({ok, <<>>}, render("nth(2, tokens(var, ','))", #{var => <<"a">>}))
  47. end},
  48. {"string for nth index", fun() ->
  49. ?assertEqual({ok, <<"a">>}, render("nth('1', tokens(var, ','))", #{var => <<"a">>}))
  50. end},
  51. {"not a index number for nth", fun() ->
  52. ?assertMatch(
  53. {error, #{reason := invalid_argument, func := nth, index := <<"notnum">>}},
  54. render("nth('notnum', tokens(var, ','))", #{var => <<"a">>})
  55. )
  56. end},
  57. {"substr", fun() ->
  58. ?assertMatch(
  59. {ok, <<"b">>},
  60. render("substr(var,1)", #{var => <<"ab">>})
  61. )
  62. end},
  63. {"result in integer", fun() ->
  64. ?assertMatch(
  65. {ok, <<"2">>},
  66. render("strlen(var)", #{var => <<"ab">>})
  67. )
  68. end},
  69. {"result in float", fun() ->
  70. ?assertMatch(
  71. {ok, <<"2.2">>},
  72. render("var", #{var => 2.2})
  73. )
  74. end},
  75. {"concat a number", fun() ->
  76. ?assertMatch(
  77. {ok, <<"2.2">>},
  78. render("concat(strlen(var),'.2')", #{var => <<"xy">>})
  79. )
  80. end},
  81. {"var is an array", fun() ->
  82. ?assertMatch(
  83. {ok, <<"y">>},
  84. render("nth(2,var)", #{var => [<<"x">>, <<"y">>]})
  85. )
  86. end}
  87. ].
  88. unknown_func_test_() ->
  89. [
  90. {"unknown function", fun() ->
  91. ?assertMatch(
  92. {error, #{reason := unknown_variform_function}},
  93. render("nonexistingatom__(a)", #{})
  94. )
  95. end},
  96. {"unknown module", fun() ->
  97. ?assertMatch(
  98. {error, #{reason := unknown_variform_module}},
  99. render("nonexistingatom__.nonexistingatom__(a)", #{})
  100. )
  101. end},
  102. {"unknown function in a known module", fun() ->
  103. ?assertMatch(
  104. {error, #{reason := unknown_variform_function}},
  105. render("emqx_variform_bif.nonexistingatom__(a)", #{})
  106. )
  107. end},
  108. {"invalid func reference", fun() ->
  109. ?assertMatch(
  110. {error, #{reason := invalid_function_reference, function := "a.b.c"}},
  111. render("a.b.c(var)", #{})
  112. )
  113. end}
  114. ].
  115. concat(L) -> iolist_to_binary(L).
  116. inject_allowed_module_test() ->
  117. try
  118. emqx_variform:inject_allowed_module(?MODULE),
  119. ?assertEqual({ok, <<"ab">>}, render(atom_to_list(?MODULE) ++ ".concat(['a','b'])", #{})),
  120. ?assertMatch(
  121. {error, #{
  122. reason := unknown_variform_function,
  123. module := ?MODULE,
  124. function := concat,
  125. arity := 2
  126. }},
  127. render(atom_to_list(?MODULE) ++ ".concat('a','b')", #{})
  128. ),
  129. ?assertMatch(
  130. {error, #{reason := unallowed_variform_module, module := emqx}},
  131. render("emqx.concat('a','b')", #{})
  132. )
  133. after
  134. emqx_variform:erase_allowed_module(?MODULE)
  135. end.
  136. coalesce_test_() ->
  137. [
  138. {"first", fun() ->
  139. ?assertEqual({ok, <<"a">>}, render("coalesce(['a','b'])", #{}))
  140. end},
  141. {"second", fun() ->
  142. ?assertEqual({ok, <<"b">>}, render("coalesce(['', 'b'])", #{}))
  143. end},
  144. {"first var", fun() ->
  145. ?assertEqual({ok, <<"a">>}, render("coalesce([a,b])", #{a => <<"a">>, b => <<"b">>}))
  146. end},
  147. {"second var", fun() ->
  148. ?assertEqual({ok, <<"b">>}, render("coalesce([a,b])", #{b => <<"b">>}))
  149. end},
  150. {"empty", fun() -> ?assertEqual({ok, <<>>}, render("coalesce([a,b])", #{})) end},
  151. {"arg from other func", fun() ->
  152. ?assertEqual({ok, <<"b">>}, render("coalesce(tokens(a,','))", #{a => <<",,b,c">>}))
  153. end},
  154. {"arg from other func, but no result", fun() ->
  155. ?assertEqual({ok, <<"">>}, render("coalesce(tokens(a,','))", #{a => <<",,,">>}))
  156. end},
  157. {"var unbound", fun() -> ?assertEqual({ok, <<>>}, render("coalesce(a)", #{})) end},
  158. {"var unbound in call", fun() ->
  159. ?assertEqual({ok, <<>>}, render("coalesce(concat(a))", #{}))
  160. end},
  161. {"var unbound in calls", fun() ->
  162. ?assertEqual({ok, <<"c">>}, render("coalesce([any_to_str(a),any_to_str(b),'c'])", #{}))
  163. end},
  164. {"coalesce n-args", fun() ->
  165. ?assertEqual(
  166. {ok, <<"2">>}, render("coalesce(a,b)", #{a => <<"">>, b => 2})
  167. )
  168. end},
  169. {"coalesce 1-arg", fun() ->
  170. ?assertMatch(
  171. {error, #{reason := coalesce_badarg}}, render("coalesce(any_to_str(a))", #{a => 1})
  172. )
  173. end}
  174. ].
  175. boolean_literal_test_() ->
  176. [
  177. ?_assertEqual({ok, <<"true">>}, render("true", #{})),
  178. ?_assertEqual({ok, <<"T">>}, render("iif(true,'T','F')", #{}))
  179. ].
  180. compare_string_test_() ->
  181. [
  182. %% is_nil test
  183. ?_assertEqual({ok, <<"true">>}, render("is_empty('')", #{})),
  184. ?_assertEqual({ok, <<"true">>}, render("is_empty(a)", #{<<"a">> => undefined})),
  185. ?_assertEqual({ok, <<"true">>}, render("is_empty(a)", #{<<"a">> => null})),
  186. ?_assertEqual({ok, <<"false">>}, render("is_empty('a')", #{})),
  187. ?_assertEqual({ok, <<"false">>}, render("is_empty(a)", #{<<"a">> => "1"})),
  188. %% Testing str_eq/2
  189. ?_assertEqual({ok, <<"true">>}, render("str_eq('a', 'a')", #{})),
  190. ?_assertEqual({ok, <<"false">>}, render("str_eq('a', 'b')", #{})),
  191. ?_assertEqual({ok, <<"true">>}, render("str_eq('', '')", #{})),
  192. ?_assertEqual({ok, <<"false">>}, render("str_eq('a', '')", #{})),
  193. ?_assertEqual(
  194. {ok, <<"true">>}, render("str_eq(a, b)", #{<<"a">> => <<"1">>, <<"b">> => <<"1">>})
  195. ),
  196. %% Testing str_neq/2
  197. ?_assertEqual({ok, <<"false">>}, render("str_neq('a', 'a')", #{})),
  198. ?_assertEqual({ok, <<"true">>}, render("str_neq('a', 'b')", #{})),
  199. ?_assertEqual({ok, <<"false">>}, render("str_neq('', '')", #{})),
  200. ?_assertEqual({ok, <<"true">>}, render("str_neq('a', '')", #{})),
  201. ?_assertEqual(
  202. {ok, <<"false">>}, render("str_neq(a, b)", #{<<"a">> => <<"1">>, <<"b">> => <<"1">>})
  203. ),
  204. %% Testing str_lt/2
  205. ?_assertEqual({ok, <<"true">>}, render("str_lt('a', 'b')", #{})),
  206. ?_assertEqual({ok, <<"false">>}, render("str_lt('b', 'a')", #{})),
  207. ?_assertEqual({ok, <<"false">>}, render("str_lt('a', 'a')", #{})),
  208. ?_assertEqual({ok, <<"false">>}, render("str_lt('', '')", #{})),
  209. ?_assertEqual({ok, <<"true">>}, render("str_gt('b', 'a')", #{})),
  210. ?_assertEqual({ok, <<"false">>}, render("str_gt('a', 'b')", #{})),
  211. ?_assertEqual({ok, <<"false">>}, render("str_gt('a', 'a')", #{})),
  212. ?_assertEqual({ok, <<"false">>}, render("str_gt('', '')", #{})),
  213. ?_assertEqual({ok, <<"true">>}, render("str_lte('a', 'b')", #{})),
  214. ?_assertEqual({ok, <<"true">>}, render("str_lte('a', 'a')", #{})),
  215. ?_assertEqual({ok, <<"false">>}, render("str_lte('b', 'a')", #{})),
  216. ?_assertEqual({ok, <<"true">>}, render("str_lte('', '')", #{})),
  217. ?_assertEqual({ok, <<"true">>}, render("str_gte('b', 'a')", #{})),
  218. ?_assertEqual({ok, <<"true">>}, render("str_gte('a', 'a')", #{})),
  219. ?_assertEqual({ok, <<"false">>}, render("str_gte('a', 'b')", #{})),
  220. ?_assertEqual({ok, <<"true">>}, render("str_gte('', '')", #{})),
  221. ?_assertEqual({ok, <<"true">>}, render("str_gt(9, 10)", #{}))
  222. ].
  223. compare_numbers_test_() ->
  224. [
  225. ?_assertEqual({ok, <<"true">>}, render("num_eq(1, 1)", #{})),
  226. ?_assertEqual({ok, <<"false">>}, render("num_eq(2, 1)", #{})),
  227. ?_assertEqual({ok, <<"false">>}, render("num_eq(a, b)", #{<<"a">> => 1, <<"b">> => 2})),
  228. ?_assertEqual({ok, <<"false">>}, render("num_neq(1, 1)", #{})),
  229. ?_assertEqual({ok, <<"true">>}, render("num_neq(2, 1)", #{})),
  230. ?_assertEqual({ok, <<"true">>}, render("num_neq(a, b)", #{<<"a">> => 1, <<"b">> => 2})),
  231. ?_assertEqual({ok, <<"true">>}, render("num_lt(1, 2)", #{})),
  232. ?_assertEqual({ok, <<"false">>}, render("num_lt(2, 2)", #{})),
  233. ?_assertEqual({ok, <<"true">>}, render("num_gt(2, 1)", #{})),
  234. ?_assertEqual({ok, <<"false">>}, render("num_gt(1, 1)", #{})),
  235. ?_assertEqual({ok, <<"true">>}, render("num_lte(1, 1)", #{})),
  236. ?_assertEqual({ok, <<"true">>}, render("num_lte(1, 2)", #{})),
  237. ?_assertEqual({ok, <<"false">>}, render("num_lte(2, 1)", #{})),
  238. ?_assertEqual({ok, <<"true">>}, render("num_gte(2, -1)", #{})),
  239. ?_assertEqual({ok, <<"true">>}, render("num_gte(2, 2)", #{})),
  240. ?_assertEqual({ok, <<"false">>}, render("num_gte(-1, 2)", #{}))
  241. ].
  242. syntax_error_test_() ->
  243. [
  244. {"empty expression", fun() -> ?assertMatch(?SYNTAX_ERROR, render("", #{})) end},
  245. {"const string single quote", fun() -> ?assertMatch(?SYNTAX_ERROR, render("'a'", #{})) end},
  246. {"const string double quote", fun() ->
  247. ?assertMatch(?SYNTAX_ERROR, render(<<"\"a\"">>, #{}))
  248. end}
  249. ].
  250. maps_test_() ->
  251. [
  252. {"arity zero", ?_assertEqual({ok, <<"0">>}, render(<<"maps.size(maps.new())">>, #{}))}
  253. ].
  254. render(Expression, Bindings) ->
  255. emqx_variform:render(Expression, Bindings).
  256. hash_pick_test() ->
  257. lists:foreach(
  258. fun(_) ->
  259. {ok, Res} = render("nth(hash_to_range(rand_str(10),1,5),[1,2,3,4,5])", #{}),
  260. ?assert(Res >= <<"1">> andalso Res =< <<"5">>)
  261. end,
  262. lists:seq(1, 100)
  263. ).
  264. map_to_range_pick_test() ->
  265. lists:foreach(
  266. fun(_) ->
  267. {ok, Res} = render("nth(map_to_range(rand_str(10),1,5),[1,2,3,4,5])", #{}),
  268. ?assert(Res >= <<"1">> andalso Res =< <<"5">>)
  269. end,
  270. lists:seq(1, 100)
  271. ).
  272. -define(ASSERT_BADARG(FUNC, ARGS),
  273. ?_assertEqual(
  274. {error, #{reason => badarg, function => FUNC}},
  275. render(atom_to_list(FUNC) ++ ARGS, #{})
  276. )
  277. ).
  278. to_range_badarg_test_() ->
  279. [
  280. ?ASSERT_BADARG(hash_to_range, "(1,1,2)"),
  281. ?ASSERT_BADARG(hash_to_range, "('',1,2)"),
  282. ?ASSERT_BADARG(hash_to_range, "('a','1',2)"),
  283. ?ASSERT_BADARG(hash_to_range, "('a',2,1)"),
  284. ?ASSERT_BADARG(map_to_range, "('',1,2)"),
  285. ?ASSERT_BADARG(map_to_range, "('a','1',2)"),
  286. ?ASSERT_BADARG(map_to_range, "('a',2,1)")
  287. ].
  288. iif_test_() ->
  289. %% if clientid has two words separated by a -, take the suffix, and append with `/#`
  290. Expr1 = "iif(nth(2,tokens(clientid,'-')),concat([nth(2,tokens(clientid,'-')),'/#']),'')",
  291. [
  292. ?_assertEqual({ok, <<"yes-A">>}, render("iif(a,'yes-A','no-A')", #{a => <<"x">>})),
  293. ?_assertEqual({ok, <<"no-A">>}, render("iif(a,'yes-A','no-A')", #{})),
  294. ?_assertEqual({ok, <<"2">>}, render("iif(str_eq(a,1),2,3)", #{a => 1})),
  295. ?_assertEqual({ok, <<"3">>}, render("iif(str_eq(a,1),2,3)", #{a => <<"not-1">>})),
  296. ?_assertEqual({ok, <<"3">>}, render("iif(str_eq(a,1),2,3)", #{})),
  297. ?_assertEqual({ok, <<"">>}, render(Expr1, #{clientid => <<"a">>})),
  298. ?_assertEqual({ok, <<"suffix/#">>}, render(Expr1, #{clientid => <<"a-suffix">>}))
  299. ].