%%-------------------------------------------------------------------- %% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %%-------------------------------------------------------------------- -module(emqx_variform_tests). -compile(export_all). -compile(nowarn_export_all). -include_lib("eunit/include/eunit.hrl"). -define(SYNTAX_ERROR, {error, "syntax error before:" ++ _}). redner_test_() -> [ {"direct var reference", fun() -> ?assertEqual({ok, <<"1">>}, render("a", #{a => 1})) end}, {"concat strings", fun() -> ?assertEqual({ok, <<"a,b">>}, render("concat(['a',',','b'])", #{})) end}, {"concat empty string", fun() -> ?assertEqual({ok, <<"">>}, render("concat([''])", #{})) end}, {"tokens 1st", fun() -> ?assertEqual({ok, <<"a">>}, render("nth(1,tokens(var, ','))", #{var => <<"a,b">>})) end}, {"unknown var return error", fun() -> ?assertMatch({error, #{reason := var_unbound}}, render("var", #{})) end}, {"out of range nth index", fun() -> ?assertEqual({ok, <<>>}, render("nth(2, tokens(var, ','))", #{var => <<"a">>})) end}, {"string for nth index", fun() -> ?assertEqual({ok, <<"a">>}, render("nth('1', tokens(var, ','))", #{var => <<"a">>})) end}, {"not a index number for nth", fun() -> ?assertMatch( {error, #{reason := invalid_argument, func := nth, index := <<"notnum">>}}, render("nth('notnum', tokens(var, ','))", #{var => <<"a">>}) ) end}, {"substr", fun() -> ?assertMatch( {ok, <<"b">>}, render("substr(var,1)", #{var => <<"ab">>}) ) end}, {"result in integer", fun() -> ?assertMatch( {ok, <<"2">>}, render("strlen(var)", #{var => <<"ab">>}) ) end}, {"result in float", fun() -> ?assertMatch( {ok, <<"2.2">>}, render("var", #{var => 2.2}) ) end}, {"concat a number", fun() -> ?assertMatch( {ok, <<"2.2">>}, render("concat(strlen(var),'.2')", #{var => <<"xy">>}) ) end}, {"var is an array", fun() -> ?assertMatch( {ok, <<"y">>}, render("nth(2,var)", #{var => [<<"x">>, <<"y">>]}) ) end} ]. unknown_func_test_() -> [ {"unknown function", fun() -> ?assertMatch( {error, #{reason := unknown_variform_function}}, render("nonexistingatom__(a)", #{}) ) end}, {"unknown module", fun() -> ?assertMatch( {error, #{reason := unknown_variform_module}}, render("nonexistingatom__.nonexistingatom__(a)", #{}) ) end}, {"unknown function in a known module", fun() -> ?assertMatch( {error, #{reason := unknown_variform_function}}, render("emqx_variform_bif.nonexistingatom__(a)", #{}) ) end}, {"invalid func reference", fun() -> ?assertMatch( {error, #{reason := invalid_function_reference, function := "a.b.c"}}, render("a.b.c(var)", #{}) ) end} ]. concat(L) -> iolist_to_binary(L). inject_allowed_module_test() -> try emqx_variform:inject_allowed_module(?MODULE), ?assertEqual({ok, <<"ab">>}, render(atom_to_list(?MODULE) ++ ".concat(['a','b'])", #{})), ?assertMatch( {error, #{ reason := unknown_variform_function, module := ?MODULE, function := concat, arity := 2 }}, render(atom_to_list(?MODULE) ++ ".concat('a','b')", #{}) ), ?assertMatch( {error, #{reason := unallowed_veriform_module, module := emqx}}, render("emqx.concat('a','b')", #{}) ) after emqx_variform:erase_allowed_module(?MODULE) end. coalesce_test_() -> [ {"first", fun() -> ?assertEqual({ok, <<"a">>}, render("coalesce(['a','b'])", #{})) end}, {"second", fun() -> ?assertEqual({ok, <<"b">>}, render("coalesce(['', 'b'])", #{})) end}, {"first var", fun() -> ?assertEqual({ok, <<"a">>}, render("coalesce([a,b])", #{a => <<"a">>, b => <<"b">>})) end}, {"second var", fun() -> ?assertEqual({ok, <<"b">>}, render("coalesce([a,b])", #{b => <<"b">>})) end}, {"empty", fun() -> ?assertEqual({ok, <<>>}, render("coalesce([a,b])", #{})) end}, {"arg from other func", fun() -> ?assertEqual({ok, <<"b">>}, render("coalesce(tokens(a,','))", #{a => <<",,b,c">>})) end}, {"arg from other func, but no result", fun() -> ?assertEqual({ok, <<"">>}, render("coalesce(tokens(a,','))", #{a => <<",,,">>})) end}, {"var unbound", fun() -> ?assertEqual({ok, <<>>}, render("coalesce(a)", #{})) end}, {"var unbound in call", fun() -> ?assertEqual({ok, <<>>}, render("coalesce(concat(a))", #{})) end}, {"var unbound in calls", fun() -> ?assertEqual({ok, <<"c">>}, render("coalesce([any_to_str(a),any_to_str(b),'c'])", #{})) end}, {"coalesce n-args", fun() -> ?assertEqual( {ok, <<"2">>}, render("coalesce(a,b)", #{a => <<"">>, b => 2}) ) end}, {"coalesce 1-arg", fun() -> ?assertMatch( {error, #{reason := coalesce_badarg}}, render("coalesce(any_to_str(a))", #{a => 1}) ) end} ]. compare_string_test_() -> [ %% Testing str_eq/2 ?_assertEqual({ok, <<"true">>}, render("str_eq('a', 'a')", #{})), ?_assertEqual({ok, <<"false">>}, render("str_eq('a', 'b')", #{})), ?_assertEqual({ok, <<"true">>}, render("str_eq('', '')", #{})), ?_assertEqual({ok, <<"false">>}, render("str_eq('a', '')", #{})), %% Testing str_lt/2 ?_assertEqual({ok, <<"true">>}, render("str_lt('a', 'b')", #{})), ?_assertEqual({ok, <<"false">>}, render("str_lt('b', 'a')", #{})), ?_assertEqual({ok, <<"false">>}, render("str_lt('a', 'a')", #{})), ?_assertEqual({ok, <<"false">>}, render("str_lt('', '')", #{})), ?_assertEqual({ok, <<"true">>}, render("str_gt('b', 'a')", #{})), ?_assertEqual({ok, <<"false">>}, render("str_gt('a', 'b')", #{})), ?_assertEqual({ok, <<"false">>}, render("str_gt('a', 'a')", #{})), ?_assertEqual({ok, <<"false">>}, render("str_gt('', '')", #{})), ?_assertEqual({ok, <<"true">>}, render("str_lte('a', 'b')", #{})), ?_assertEqual({ok, <<"true">>}, render("str_lte('a', 'a')", #{})), ?_assertEqual({ok, <<"false">>}, render("str_lte('b', 'a')", #{})), ?_assertEqual({ok, <<"true">>}, render("str_lte('', '')", #{})), ?_assertEqual({ok, <<"true">>}, render("str_gte('b', 'a')", #{})), ?_assertEqual({ok, <<"true">>}, render("str_gte('a', 'a')", #{})), ?_assertEqual({ok, <<"false">>}, render("str_gte('a', 'b')", #{})), ?_assertEqual({ok, <<"true">>}, render("str_gte('', '')", #{})), ?_assertEqual({ok, <<"true">>}, render("str_gt(9, 10)", #{})) ]. compare_numbers_test_() -> [ ?_assertEqual({ok, <<"true">>}, render("num_eq(1, 1)", #{})), ?_assertEqual({ok, <<"false">>}, render("num_eq(2, 1)", #{})), ?_assertEqual({ok, <<"true">>}, render("num_lt(1, 2)", #{})), ?_assertEqual({ok, <<"false">>}, render("num_lt(2, 2)", #{})), ?_assertEqual({ok, <<"true">>}, render("num_gt(2, 1)", #{})), ?_assertEqual({ok, <<"false">>}, render("num_gt(1, 1)", #{})), ?_assertEqual({ok, <<"true">>}, render("num_lte(1, 1)", #{})), ?_assertEqual({ok, <<"true">>}, render("num_lte(1, 2)", #{})), ?_assertEqual({ok, <<"false">>}, render("num_lte(2, 1)", #{})), ?_assertEqual({ok, <<"true">>}, render("num_gte(2, -1)", #{})), ?_assertEqual({ok, <<"true">>}, render("num_gte(2, 2)", #{})), ?_assertEqual({ok, <<"false">>}, render("num_gte(-1, 2)", #{})) ]. syntax_error_test_() -> [ {"empty expression", fun() -> ?assertMatch(?SYNTAX_ERROR, render("", #{})) end}, {"const string single quote", fun() -> ?assertMatch(?SYNTAX_ERROR, render("'a'", #{})) end}, {"const string double quote", fun() -> ?assertMatch(?SYNTAX_ERROR, render(<<"\"a\"">>, #{})) end}, {"no arity", fun() -> ?assertMatch(?SYNTAX_ERROR, render("concat()", #{})) end} ]. render(Expression, Bindings) -> emqx_variform:render(Expression, Bindings). hash_pick_test() -> lists:foreach( fun(_) -> {ok, Res} = render("nth(hash_to_range(rand_str(10),1,5),[1,2,3,4,5])", #{}), ?assert(Res >= <<"1">> andalso Res =< <<"5">>) end, lists:seq(1, 100) ). map_to_range_pick_test() -> lists:foreach( fun(_) -> {ok, Res} = render("nth(map_to_range(rand_str(10),1,5),[1,2,3,4,5])", #{}), ?assert(Res >= <<"1">> andalso Res =< <<"5">>) end, lists:seq(1, 100) ). -define(ASSERT_BADARG(FUNC, ARGS), ?_assertEqual( {error, #{reason => badarg, function => FUNC}}, render(atom_to_list(FUNC) ++ ARGS, #{}) ) ). to_range_badarg_test_() -> [ ?ASSERT_BADARG(hash_to_range, "(1,1,2)"), ?ASSERT_BADARG(hash_to_range, "('',1,2)"), ?ASSERT_BADARG(hash_to_range, "('a','1',2)"), ?ASSERT_BADARG(hash_to_range, "('a',2,1)"), ?ASSERT_BADARG(map_to_range, "('',1,2)"), ?ASSERT_BADARG(map_to_range, "('a','1',2)"), ?ASSERT_BADARG(map_to_range, "('a',2,1)") ]. iif_test_() -> %% if clientid has two words separated by a -, take the suffix, and append with `/#` Expr1 = "iif(nth(2,tokens(clientid,'-')),concat([nth(2,tokens(clientid,'-')),'/#']),'')", [ ?_assertEqual({ok, <<"yes-A">>}, render("iif(a,'yes-A','no-A')", #{a => <<"x">>})), ?_assertEqual({ok, <<"no-A">>}, render("iif(a,'yes-A','no-A')", #{})), ?_assertEqual({ok, <<"2">>}, render("iif(str_eq(a,1),2,3)", #{a => 1})), ?_assertEqual({ok, <<"3">>}, render("iif(str_eq(a,1),2,3)", #{a => <<"not-1">>})), ?_assertEqual({ok, <<"3">>}, render("iif(str_eq(a,1),2,3)", #{})), ?_assertEqual({ok, <<"">>}, render(Expr1, #{clientid => <<"a">>})), ?_assertEqual({ok, <<"suffix/#">>}, render(Expr1, #{clientid => <<"a-suffix">>})) ].