package com.hubspot.jinjava.el; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import java.math.BigDecimal; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import org.junit.Before; import org.junit.Test; import com.google.common.collect.ForwardingList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.hubspot.jinjava.Jinjava; import com.hubspot.jinjava.JinjavaConfig; import com.hubspot.jinjava.interpret.Context; import com.hubspot.jinjava.interpret.Context.Library; import com.hubspot.jinjava.interpret.JinjavaInterpreter; import com.hubspot.jinjava.interpret.RenderResult; import com.hubspot.jinjava.interpret.TemplateError; import com.hubspot.jinjava.interpret.TemplateError.ErrorItem; import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; import com.hubspot.jinjava.objects.PyWrapper; import com.hubspot.jinjava.objects.date.PyishDate; @SuppressWarnings("unchecked") public class ExpressionResolverTest { private JinjavaInterpreter interpreter; private Context context; private Jinjava jinjava; @Before public void setup() { jinjava = new Jinjava(); interpreter = jinjava.newInterpreter(); context = interpreter.getContext(); } @Test public void itResolvesListLiterals() throws Exception { Object val = interpreter.resolveELExpression("['0.5','50']", -1); List<Object> list = (List<Object>) val; assertThat(list).containsExactly("0.5", "50"); } @Test public void itResolvesImmutableListLiterals() throws Exception { Object val = interpreter.resolveELExpression("('0.5','50')", -1); List<Object> list = (List<Object>) val; assertThat(list).containsExactly("0.5", "50"); } @Test(expected = UnsupportedOperationException.class) public void testTuplesAreImmutable() throws Exception { Object val = interpreter.resolveELExpression("('0.5','50')", -1); List<Object> list = (List<Object>) val; list.add("foo"); } @Test public void itCanCompareStrings() throws Exception { context.put("foo", "white"); assertThat(interpreter.resolveELExpression("'2013-12-08 16:00:00+00:00' > '2013-12-08 13:00:00+00:00'", -1)).isEqualTo(Boolean.TRUE); assertThat(interpreter.resolveELExpression("foo == \"white\"", -1)).isEqualTo(Boolean.TRUE); } @Test public void itResolvesUntrimmedExprs() throws Exception { context.put("foo", "bar"); Object val = interpreter.resolveELExpression(" foo ", -1); assertThat(val).isEqualTo("bar"); assertThat(interpreter.getContext().wasExpressionResolved("foo")).isTrue(); } @Test public void itResolvesMathVals() throws Exception { context.put("i_am_seven", 7L); Object val = interpreter.resolveELExpression("(i_am_seven * 2 + 1)/3", -1); assertThat(val).isEqualTo(5.0); assertThat(interpreter.getContext().wasValueResolved("i_am_seven")).isTrue(); } @Test public void itResolvesListVal() throws Exception { context.put("thelist", Lists.newArrayList(1L, 2L, 3L)); Object val = interpreter.resolveELExpression("thelist[1]", -1); assertThat(val).isEqualTo(2L); } @Test public void itResolvesDictValWithBracket() throws Exception { Map<String, Object> dict = Maps.newHashMap(); dict.put("foo", "bar"); context.put("thedict", dict); Object val = interpreter.resolveELExpression("thedict['foo']", -1); assertThat(val).isEqualTo("bar"); assertThat(interpreter.getContext().wasExpressionResolved("thedict['foo']")).isTrue(); } @Test public void itResolvesDictValWithDotParam() throws Exception { Map<String, Object> dict = Maps.newHashMap(); dict.put("foo", "bar"); context.put("thedict", dict); Object val = interpreter.resolveELExpression("thedict.foo", -1); assertThat(val).isEqualTo("bar"); assertThat(interpreter.getContext().wasExpressionResolved("thedict.foo")).isTrue(); } @Test public void itResolvesMapValOnCustomObject() throws Exception { MyCustomMap dict = new MyCustomMap(); context.put("thedict", dict); Object val = interpreter.resolveELExpression("thedict['foo']", -1); assertThat(val).isEqualTo("bar"); assertThat(interpreter.getContext().wasExpressionResolved("thedict['foo']")).isTrue(); Object val2 = interpreter.resolveELExpression("thedict.two", -1); assertThat(val2).isEqualTo("2"); assertThat(interpreter.getContext().wasExpressionResolved("thedict.two")).isTrue(); } @Test public void itResolvesOtherMethodsOnCustomMapObject() throws Exception { MyCustomMap dict = new MyCustomMap(); context.put("thedict", dict); Object val = interpreter.resolveELExpression("thedict.size", -1); assertThat(val).isEqualTo("777"); Object val1 = interpreter.resolveELExpression("thedict.size()", -1); assertThat(val1).isEqualTo(3); Object val2 = interpreter.resolveELExpression("thedict.items()", -1); assertThat(val2.toString()).isEqualTo("[foo=bar, two=2, size=777]"); } public static final class MyCustomMap implements Map<String, String> { Map<String, String> data = ImmutableMap.of("foo", "bar", "two", "2", "size", "777"); @Override public int size() { return data.size(); } @Override public boolean isEmpty() { return data.isEmpty(); } @Override public boolean containsKey(Object key) { return data.containsKey(key); } @Override public boolean containsValue(Object value) { return data.containsValue(value); } @Override public String get(Object key) { return data.get(key); } @Override public String put(String key, String value) { return null; } @Override public String remove(Object key) { return null; } @Override public void putAll(Map<? extends String, ? extends String> m) { } @Override public void clear() { } @Override public Set<String> keySet() { return data.keySet(); } @Override public Collection<String> values() { return data.values(); } @Override public Set<Entry<String, String>> entrySet() { return data.entrySet(); } } @Test public void itResolvesInnerDictVal() throws Exception { Map<String, Object> dict = Maps.newHashMap(); Map<String, Object> inner = Maps.newHashMap(); inner.put("test", "val"); dict.put("inner", inner); context.put("thedict", dict); Object val = interpreter.resolveELExpression("thedict.inner[\"test\"]", -1); assertThat(val).isEqualTo("val"); } @Test public void itResolvesInnerListVal() throws Exception { Map<String, Object> dict = Maps.newHashMap(); List<String> inner = Lists.newArrayList("val"); dict.put("inner", inner); context.put("thedict", dict); Object val = interpreter.resolveELExpression("thedict.inner[0]", -1); assertThat(val).isEqualTo("val"); } public static class MyCustomList<T> extends ForwardingList<T> implements PyWrapper { private final List<T> list; public MyCustomList(List<T> list) { this.list = list; } @Override protected List<T> delegate() { return list; } public int getTotalCount() { return list.size(); } } @Test public void itRecordsFilterNames() throws Exception { Object val = interpreter.resolveELExpression("2.3 | round", -1); assertThat(val).isEqualTo(new BigDecimal(2)); assertThat(interpreter.getContext().wasValueResolved("filter:round")).isTrue(); } @Test public void callCustomListProperty() throws Exception { List<Integer> myList = new MyCustomList<>(Lists.newArrayList(1, 2, 3, 4)); context.put("mylist", myList); Object val = interpreter.resolveELExpression("mylist.total_count", -1); assertThat(val).isEqualTo(4); } @Test public void complexInWithOrCondition() throws Exception { context.put("foo", "this is<hr>something"); context.put("bar", "this is<hr/>something"); assertThat(interpreter.resolveELExpression("\"<hr>\" in foo or \"<hr/>\" in foo", -1)).isEqualTo(true); assertThat(interpreter.resolveELExpression("\"<hr>\" in bar or \"<hr/>\" in bar", -1)).isEqualTo(true); assertThat(interpreter.resolveELExpression("\"<har>\" in foo or \"<har/>\" in foo", -1)).isEqualTo(false); } @Test public void unknownProperty() throws Exception { interpreter.resolveELExpression("foo", 23); assertThat(interpreter.getErrors()).isEmpty(); context.put("foo", new Object()); interpreter.resolveELExpression("foo.bar", 23); assertThat(interpreter.getErrors()).hasSize(1); TemplateError e = interpreter.getErrors().get(0); assertThat(e.getReason()).isEqualTo(ErrorReason.UNKNOWN); assertThat(e.getLineno()).isEqualTo(23); assertThat(e.getFieldName()).isEqualTo("bar"); assertThat(e.getMessage()).contains("Cannot resolve property 'bar'"); } @Test public void syntaxError() throws Exception { interpreter.resolveELExpression("(*&W", 123); assertThat(interpreter.getErrors()).hasSize(1); TemplateError e = interpreter.getErrors().get(0); assertThat(e.getReason()).isEqualTo(ErrorReason.SYNTAX_ERROR); assertThat(e.getLineno()).isEqualTo(123); assertThat(e.getMessage()).contains("invalid character"); } @Test public void itWrapsDates() throws Exception { context.put("myobj", new MyClass(new Date(0))); Object result = interpreter.resolveELExpression("myobj.date", -1); assertThat(result).isInstanceOf(PyishDate.class); assertThat(result.toString()).isEqualTo("1970-01-01 00:00:00"); } @Test public void blackListedProperties() throws Exception { context.put("myobj", new MyClass(new Date(0))); interpreter.resolveELExpression("myobj.class.methods[0]", -1); assertThat(interpreter.getErrors()).isNotEmpty(); TemplateError e = interpreter.getErrors().get(0); assertThat(e.getReason()).isEqualTo(ErrorReason.UNKNOWN); assertThat(e.getFieldName()).isEqualTo("class"); assertThat(e.getMessage()).contains("Cannot resolve property 'class'"); } @Test public void blackListedMethods() throws Exception { context.put("myobj", new MyClass(new Date(0))); interpreter.resolveELExpression("myobj.wait()", -1); assertThat(interpreter.getErrors()).isNotEmpty(); TemplateError e = interpreter.getErrors().get(0); assertThat(e.getMessage()).contains("Cannot find method 'wait'"); } @Test public void itBlocksDisabledTags() throws Exception { Map<Context.Library, Set<String>> disabled = ImmutableMap.of(Context.Library.TAG, ImmutableSet.of("raw")); assertThat(interpreter.render("{% raw %}foo{% endraw %}")).isEqualTo("foo"); try (JinjavaInterpreter.InterpreterScopeClosable c = interpreter.enterScope(disabled)) { interpreter.render("{% raw %} foo {% endraw %}"); } TemplateError e = interpreter.getErrors().get(0); assertThat(e.getItem()).isEqualTo(ErrorItem.TAG); assertThat(e.getReason()).isEqualTo(ErrorReason.DISABLED); assertThat(e.getMessage()).contains("'raw' is disabled in this context"); } @Test public void itBlocksDisabledTagsInIncludes() throws Exception { final String jinja = "top {% include \"tags/includetag/raw.html\" %}"; Map<Context.Library, Set<String>> disabled = ImmutableMap.of(Context.Library.TAG, ImmutableSet.of("raw")); assertThat(interpreter.render(jinja)).isEqualTo("top before raw after\n"); try (JinjavaInterpreter.InterpreterScopeClosable c = interpreter.enterScope(disabled)) { interpreter.render(jinja); } TemplateError e = interpreter.getErrors().get(0); assertThat(e.getItem()).isEqualTo(ErrorItem.TAG); assertThat(e.getReason()).isEqualTo(ErrorReason.DISABLED); assertThat(e.getMessage()).contains("'raw' is disabled in this context"); } @Test public void itBlocksDisabledFilters() throws Exception { Map<Context.Library, Set<String>> disabled = ImmutableMap.of(Context.Library.FILTER, ImmutableSet.of("truncate")); assertThat(interpreter.resolveELExpression("\"hey\"|truncate(2)", -1)).isEqualTo("h..."); try (JinjavaInterpreter.InterpreterScopeClosable c = interpreter.enterScope(disabled)) { interpreter.resolveELExpression("\"hey\"|truncate(2)", -1); TemplateError e = interpreter.getErrors().get(0); assertThat(e.getItem()).isEqualTo(ErrorItem.FILTER); assertThat(e.getReason()).isEqualTo(ErrorReason.DISABLED); assertThat(e.getMessage()).contains("truncate' is disabled in this context"); } } @Test public void itBlocksDisabledFunctions() throws Exception { Map<Context.Library, Set<String>> disabled = ImmutableMap.of(Library.FUNCTION, ImmutableSet.of(":range")); String template = "hi {% for i in range(1, 3) %}{{i}} {% endfor %}"; String rendered = jinjava.render(template, context); assertEquals("hi 1 2 ", rendered); final JinjavaConfig config = JinjavaConfig.newBuilder().withDisabled(disabled).build(); final RenderResult renderResult = jinjava.renderForResult(template, context, config); assertEquals("hi ", renderResult.getOutput()); TemplateError e = renderResult.getErrors().get(0); assertThat(e.getItem()).isEqualTo(ErrorItem.FUNCTION); assertThat(e.getReason()).isEqualTo(ErrorReason.DISABLED); assertThat(e.getMessage()).contains("':range' is disabled in this context"); } @Test public void itBlocksDisabledExpTests() throws Exception { Map<Context.Library, Set<String>> disabled = ImmutableMap.of(Context.Library.EXP_TEST, ImmutableSet.of("even")); assertThat(interpreter.render("{% if 2 is even %}yes{% endif %}")).isEqualTo("yes"); try (JinjavaInterpreter.InterpreterScopeClosable c = interpreter.enterScope(disabled)) { interpreter.render("{% if 2 is even %}yes{% endif %}"); TemplateError e = interpreter.getErrors().get(0); assertThat(e.getItem()).isEqualTo(ErrorItem.EXPRESSION_TEST); assertThat(e.getReason()).isEqualTo(ErrorReason.DISABLED); assertThat(e.getMessage()).contains("even' is disabled in this context"); } } @Test public void itStoresResolvedFunctions() throws Exception { context.put("datetime", 12345); final JinjavaConfig config = JinjavaConfig.newBuilder().build(); String template = "{% for i in range(1, 5) %}{{i}} {% endfor %}\n{{ unixtimestamp(datetime) }}"; final RenderResult renderResult = jinjava.renderForResult(template, context, config); assertThat(renderResult.getOutput()).isEqualTo("1 2 3 4 \n12000"); assertThat(renderResult.getContext().getResolvedFunctions()).hasSameElementsAs(ImmutableSet.of(":range", ":unixtimestamp")); } @Test public void presentOptionalProperty() { context.put("myobj", new OptionalProperty(null, "foo")); assertThat(interpreter.resolveELExpression("myobj.val", -1)).isEqualTo("foo"); assertThat(interpreter.getErrors()).isEmpty(); } @Test public void emptyOptionalProperty() { context.put("myobj", new OptionalProperty(null, null)); assertThat(interpreter.resolveELExpression("myobj.val", -1)).isNull(); assertThat(interpreter.getErrors()).isEmpty(); } @Test public void presentNestedOptionalProperty() { context.put("myobj", new OptionalProperty(new MyClass(new Date(0)), "foo")); assertThat(Objects.toString(interpreter.resolveELExpression("myobj.nested.date", -1))).isEqualTo( "1970-01-01 00:00:00"); assertThat(interpreter.getErrors()).isEmpty(); } @Test public void emptyNestedOptionalProperty() { context.put("myobj", new OptionalProperty(null, null)); assertThat(interpreter.resolveELExpression("myobj.nested.date", -1)).isNull(); assertThat(interpreter.getErrors()).isEmpty(); } @Test public void presentNestedNestedOptionalProperty() { context.put("myobj", new NestedOptionalProperty(new OptionalProperty(new MyClass(new Date(0)), "foo"))); assertThat(Objects.toString(interpreter.resolveELExpression("myobj.nested.nested.date", -1))).isEqualTo( "1970-01-01 00:00:00"); assertThat(interpreter.getErrors()).isEmpty(); } public static final class MyClass { private Date date; MyClass(Date date) { this.date = date; } public Date getDate() { return date; } } public static final class OptionalProperty { private MyClass nested; private String val; OptionalProperty(MyClass nested, String val) { this.nested = nested; this.val = val; } public Optional<MyClass> getNested() { return Optional.ofNullable(nested); } public Optional<String> getVal() { return Optional.ofNullable(val); } } public static final class NestedOptionalProperty { private OptionalProperty nested; public NestedOptionalProperty(OptionalProperty nested) { this.nested = nested; } public Optional<OptionalProperty> getNested() { return Optional.ofNullable(nested); } } }