emqx_template_SUITE.erl 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2020-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_template_SUITE).
  17. -compile(export_all).
  18. -compile(nowarn_export_all).
  19. -include_lib("emqx/include/emqx_placeholder.hrl").
  20. -include_lib("eunit/include/eunit.hrl").
  21. all() -> emqx_common_test_helpers:all(?MODULE).
  22. t_render(_) ->
  23. Context = #{
  24. a => <<"1">>,
  25. b => 1,
  26. c => 1.0,
  27. d => #{<<"d1">> => <<"hi">>},
  28. l => [0, 1, 1000],
  29. u => "utf-8 is ǝɹǝɥ"
  30. },
  31. Template = emqx_template:parse(
  32. <<"a:${a},b:${b},c:${c},d:${d},d1:${d.d1},l:${l},u:${u}">>
  33. ),
  34. ?assertEqual(
  35. {<<"a:1,b:1,c:1.0,d:{\"d1\":\"hi\"},d1:hi,l:[0,1,1000],u:utf-8 is ǝɹǝɥ"/utf8>>, []},
  36. render_string(Template, Context)
  37. ).
  38. t_render_var_trans(_) ->
  39. Context = #{a => <<"1">>, b => 1, c => #{prop => 1.0}},
  40. Template = emqx_template:parse(<<"a:${a},b:${b},c:${c.prop}">>),
  41. {String, Errors} = emqx_template:render(
  42. Template,
  43. Context,
  44. #{var_trans => fun(Name, _) -> "<" ++ Name ++ ">" end}
  45. ),
  46. ?assertEqual(
  47. {<<"a:<a>,b:<b>,c:<c.prop>">>, []},
  48. {bin(String), Errors}
  49. ).
  50. t_render_path(_) ->
  51. Context = #{d => #{d1 => <<"hi">>}},
  52. Template = emqx_template:parse(<<"d.d1:${d.d1}">>),
  53. ?assertEqual(
  54. ok,
  55. emqx_template:validate(["d.d1"], Template)
  56. ),
  57. ?assertEqual(
  58. {<<"d.d1:hi">>, []},
  59. render_string(Template, Context)
  60. ).
  61. t_render_custom_ph(_) ->
  62. Context = #{a => <<"a">>, b => <<"b">>},
  63. Template = emqx_template:parse(<<"a:${a},b:${b}">>),
  64. ?assertEqual(
  65. {error, [{"b", disallowed}]},
  66. emqx_template:validate(["a"], Template)
  67. ),
  68. ?assertEqual(
  69. <<"a:a,b:b">>,
  70. render_strict_string(Template, Context)
  71. ).
  72. t_render_this(_) ->
  73. Context = #{a => <<"a">>, b => [1, 2, 3]},
  74. Template = emqx_template:parse(<<"this:${} / also:${.}">>),
  75. ?assertEqual(ok, emqx_template:validate(["."], Template)),
  76. ?assertEqual(
  77. % NOTE: order of the keys in the JSON object depends on the JSON encoder
  78. <<"this:{\"b\":[1,2,3],\"a\":\"a\"} / also:{\"b\":[1,2,3],\"a\":\"a\"}">>,
  79. render_strict_string(Template, Context)
  80. ).
  81. t_render_missing_bindings(_) ->
  82. Context = #{no => #{}, c => #{<<"c1">> => 42}},
  83. Template = emqx_template:parse(
  84. <<"a:${a},b:${b},c:${c.c1.c2},d:${d.d1},e:${no.such_atom_i_swear}">>
  85. ),
  86. ?assertEqual(
  87. {<<"a:undefined,b:undefined,c:undefined,d:undefined,e:undefined">>, [
  88. {"no.such_atom_i_swear", undefined},
  89. {"d.d1", undefined},
  90. {"c.c1.c2", {2, number}},
  91. {"b", undefined},
  92. {"a", undefined}
  93. ]},
  94. render_string(Template, Context)
  95. ),
  96. ?assertError(
  97. [
  98. {"no.such_atom_i_swear", undefined},
  99. {"d.d1", undefined},
  100. {"c.c1.c2", {2, number}},
  101. {"b", undefined},
  102. {"a", undefined}
  103. ],
  104. render_strict_string(Template, Context)
  105. ).
  106. t_render_custom_bindings(_) ->
  107. _ = erlang:put(a, <<"foo">>),
  108. _ = erlang:put(b, #{<<"bar">> => #{atom => 42}}),
  109. Template = emqx_template:parse(
  110. <<"a:${a},b:${b.bar.atom},c:${c},oops:${b.bar.atom.oops}">>
  111. ),
  112. ?assertEqual(
  113. {<<"a:foo,b:42,c:undefined,oops:undefined">>, [
  114. {"b.bar.atom.oops", {2, number}},
  115. {"c", undefined}
  116. ]},
  117. render_string(Template, {?MODULE, []})
  118. ).
  119. t_unparse(_) ->
  120. TString = <<"a:${a},b:${b},c:$${c},d:{${d.d1}},e:${$}{e},lit:${$}{$}">>,
  121. Template = emqx_template:parse(TString),
  122. ?assertEqual(
  123. TString,
  124. unicode:characters_to_binary(emqx_template:unparse(Template))
  125. ).
  126. t_const(_) ->
  127. ?assertEqual(
  128. true,
  129. emqx_template:is_const(emqx_template:parse(<<"">>))
  130. ),
  131. ?assertEqual(
  132. false,
  133. emqx_template:is_const(
  134. emqx_template:parse(<<"a:${a},b:${b},c:${$}{c}">>)
  135. )
  136. ),
  137. ?assertEqual(
  138. true,
  139. emqx_template:is_const(
  140. emqx_template:parse(<<"a:${$}{a},b:${$}{b}">>)
  141. )
  142. ).
  143. t_render_partial_ph(_) ->
  144. Context = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}},
  145. Template = emqx_template:parse(<<"a:$a,b:b},c:{c},d:${d">>),
  146. ?assertEqual(
  147. <<"a:$a,b:b},c:{c},d:${d">>,
  148. render_strict_string(Template, Context)
  149. ).
  150. t_parse_escaped(_) ->
  151. Context = #{a => <<"1">>, b => 1, c => "VAR"},
  152. Template = emqx_template:parse(<<"a:${a},b:${$}{b},c:${$}{${c}},lit:${$}{$}">>),
  153. ?assertEqual(
  154. <<"a:1,b:${b},c:${VAR},lit:${$}">>,
  155. render_strict_string(Template, Context)
  156. ).
  157. t_parse_escaped_dquote(_) ->
  158. Context = #{a => <<"1">>, b => 1},
  159. Template = emqx_template:parse(<<"a:\"${a}\",b:\"${$}{b}\"">>, #{
  160. strip_double_quote => true
  161. }),
  162. ?assertEqual(
  163. <<"a:1,b:\"${b}\"">>,
  164. render_strict_string(Template, Context)
  165. ).
  166. t_parse_sql_prepstmt(_) ->
  167. Context = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}},
  168. {PrepareStatement, RowTemplate} =
  169. emqx_template_sql:parse_prepstmt(<<"a:${a},b:${b},c:${c},d:${d}">>, #{
  170. parameters => '?'
  171. }),
  172. ?assertEqual(<<"a:?,b:?,c:?,d:?">>, bin(PrepareStatement)),
  173. ?assertEqual(
  174. {[<<"1">>, 1, 1.0, <<"{\"d1\":\"hi\"}">>], _Errors = []},
  175. emqx_template_sql:render_prepstmt(RowTemplate, Context)
  176. ).
  177. t_parse_sql_prepstmt_n(_) ->
  178. Context = #{a => undefined, b => true, c => atom, d => #{d1 => 42.1337}},
  179. {PrepareStatement, RowTemplate} =
  180. emqx_template_sql:parse_prepstmt(<<"a:${a},b:${b},c:${c},d:${d}">>, #{
  181. parameters => '$n'
  182. }),
  183. ?assertEqual(<<"a:$1,b:$2,c:$3,d:$4">>, bin(PrepareStatement)),
  184. ?assertEqual(
  185. [null, true, <<"atom">>, <<"{\"d1\":42.1337}">>],
  186. emqx_template_sql:render_prepstmt_strict(RowTemplate, Context)
  187. ).
  188. t_parse_sql_prepstmt_colon(_) ->
  189. {PrepareStatement, _RowTemplate} =
  190. emqx_template_sql:parse_prepstmt(<<"a=${a},b=${b},c=${c},d=${d}">>, #{
  191. parameters => ':n'
  192. }),
  193. ?assertEqual(<<"a=:1,b=:2,c=:3,d=:4">>, bin(PrepareStatement)).
  194. t_parse_sql_prepstmt_partial_ph(_) ->
  195. Context = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}},
  196. {PrepareStatement, RowTemplate} =
  197. emqx_template_sql:parse_prepstmt(<<"a:$a,b:b},c:{c},d:${d">>, #{parameters => '?'}),
  198. ?assertEqual(<<"a:$a,b:b},c:{c},d:${d">>, bin(PrepareStatement)),
  199. ?assertEqual([], emqx_template_sql:render_prepstmt_strict(RowTemplate, Context)).
  200. t_render_sql(_) ->
  201. Context = #{
  202. a => <<"1">>,
  203. b => 1,
  204. c => 1.0,
  205. d => #{d1 => <<"hi">>},
  206. n => undefined,
  207. u => "utf8's cool 🐸"
  208. },
  209. Template = emqx_template:parse(<<"a:${a},b:${b},c:${c},d:${d},n:${n},u:${u}">>),
  210. ?assertMatch(
  211. {_String, _Errors = []},
  212. emqx_template_sql:render(Template, Context, #{})
  213. ),
  214. ?assertEqual(
  215. <<"a:'1',b:1,c:1.0,d:'{\"d1\":\"hi\"}',n:NULL,u:'utf8\\'s cool 🐸'"/utf8>>,
  216. bin(emqx_template_sql:render_strict(Template, Context, #{}))
  217. ),
  218. ?assertEqual(
  219. <<"a:'1',b:1,c:1.0,d:'{\"d1\":\"hi\"}',n:'undefined',u:'utf8\\'s cool 🐸'"/utf8>>,
  220. bin(emqx_template_sql:render_strict(Template, Context, #{undefined => "undefined"}))
  221. ).
  222. t_render_mysql(_) ->
  223. %% with apostrophes
  224. %% https://github.com/emqx/emqx/issues/4135
  225. Context = #{
  226. a => <<"1''2">>,
  227. b => 1,
  228. c => 1.0,
  229. d => #{d1 => <<"someone's phone">>},
  230. e => <<$\\, 0, "💩"/utf8>>,
  231. f => <<"non-utf8", 16#DCC900:24>>,
  232. g => "utf8's cool 🐸",
  233. h => imgood
  234. },
  235. Template = emqx_template_sql:parse(
  236. <<"a:${a},b:${b},c:${c},d:${d},e:${e},f:${f},g:${g},h:${h}">>
  237. ),
  238. ?assertEqual(
  239. <<
  240. "a:'1\\'\\'2',b:1,c:1.0,d:'{\"d1\":\"someone\\'s phone\"}',"
  241. "e:'\\\\\\0💩',f:0x6E6F6E2D75746638DCC900,g:'utf8\\'s cool 🐸',"/utf8,
  242. "h:'imgood'"
  243. >>,
  244. bin(emqx_template_sql:render_strict(Template, Context, #{escaping => mysql}))
  245. ).
  246. t_render_cql(_) ->
  247. %% with apostrophes for cassandra
  248. %% https://github.com/emqx/emqx/issues/4148
  249. Context = #{
  250. a => <<"1''2">>,
  251. b => 1,
  252. c => 1.0,
  253. d => #{d1 => <<"someone's phone">>}
  254. },
  255. Template = emqx_template:parse(<<"a:${a},b:${b},c:${c},d:${d}">>),
  256. ?assertEqual(
  257. <<"a:'1''''2',b:1,c:1.0,d:'{\"d1\":\"someone''s phone\"}'">>,
  258. bin(emqx_template_sql:render_strict(Template, Context, #{escaping => cql}))
  259. ).
  260. t_render_sql_custom_ph(_) ->
  261. {PrepareStatement, RowTemplate} =
  262. emqx_template_sql:parse_prepstmt(<<"a:${a},b:${b.c}">>, #{parameters => '$n'}),
  263. ?assertEqual(
  264. {error, [{"b.c", disallowed}]},
  265. emqx_template:validate(["a"], RowTemplate)
  266. ),
  267. ?assertEqual(<<"a:$1,b:$2">>, bin(PrepareStatement)).
  268. t_render_sql_strip_double_quote(_) ->
  269. Context = #{a => <<"a">>, b => <<"b">>},
  270. %% no strip_double_quote option: "${key}" -> "value"
  271. {PrepareStatement1, RowTemplate1} = emqx_template_sql:parse_prepstmt(
  272. <<"a:\"${a}\",b:\"${b}\"">>,
  273. #{parameters => '$n'}
  274. ),
  275. ?assertEqual(<<"a:\"$1\",b:\"$2\"">>, bin(PrepareStatement1)),
  276. ?assertEqual(
  277. [<<"a">>, <<"b">>],
  278. emqx_template_sql:render_prepstmt_strict(RowTemplate1, Context)
  279. ),
  280. %% strip_double_quote = true: "${key}" -> value
  281. {PrepareStatement2, RowTemplate2} = emqx_template_sql:parse_prepstmt(
  282. <<"a:\"${a}\",b:\"${b}\"">>,
  283. #{parameters => '$n', strip_double_quote => true}
  284. ),
  285. ?assertEqual(<<"a:$1,b:$2">>, bin(PrepareStatement2)),
  286. ?assertEqual(
  287. [<<"a">>, <<"b">>],
  288. emqx_template_sql:render_prepstmt_strict(RowTemplate2, Context)
  289. ).
  290. t_render_tmpl_deep(_) ->
  291. Context = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}},
  292. Template = emqx_template:parse_deep(
  293. #{<<"${a}">> => [<<"$${b}">>, "c", 2, 3.0, '${d}', {[<<"${c}">>, <<"${$}{d}">>], 0}]}
  294. ),
  295. ?assertEqual(
  296. {error, [{V, disallowed} || V <- ["b", "c"]]},
  297. emqx_template:validate(["a"], Template)
  298. ),
  299. ?assertEqual(
  300. #{<<"1">> => [<<"$1">>, "c", 2, 3.0, '${d}', {[<<"1.0">>, <<"${d}">>], 0}]},
  301. emqx_template:render_strict(Template, Context)
  302. ).
  303. t_unparse_tmpl_deep(_) ->
  304. Term = #{<<"${a}">> => [<<"$${b}">>, "c", 2, 3.0, '${d}', {[<<"${c}">>], <<"${$}{d}">>, 0}]},
  305. Template = emqx_template:parse_deep(Term),
  306. ?assertEqual(Term, emqx_template:unparse(Template)).
  307. t_allow_var_by_namespace(_) ->
  308. Context = #{d => #{d1 => <<"hi">>}},
  309. Template = emqx_template:parse(<<"d.d1:${d.d1}">>),
  310. ?assertEqual(
  311. ok,
  312. emqx_template:validate([{var_namespace, "d"}], Template)
  313. ),
  314. ?assertEqual(
  315. {<<"d.d1:hi">>, []},
  316. render_string(Template, Context)
  317. ).
  318. %%
  319. render_string(Template, Context) ->
  320. {String, Errors} = emqx_template:render(Template, Context),
  321. {bin(String), Errors}.
  322. render_strict_string(Template, Context) ->
  323. bin(emqx_template:render_strict(Template, Context)).
  324. bin(String) ->
  325. unicode:characters_to_binary(String).
  326. %% Access module API
  327. lookup([], _) ->
  328. {error, undefined};
  329. lookup([Prop | Rest], _) ->
  330. case erlang:get(binary_to_atom(Prop)) of
  331. undefined -> {error, undefined};
  332. Value -> emqx_template:lookup_var(Rest, Value)
  333. end.