diff --git a/cmd/flowrunner/testdata/flows/all_actions.json b/cmd/flowrunner/testdata/flows/all_actions.json index 53325e5b5..a64670f0e 100644 --- a/cmd/flowrunner/testdata/flows/all_actions.json +++ b/cmd/flowrunner/testdata/flows/all_actions.json @@ -65,7 +65,7 @@ "body": "Hi @contact.fields.first_name, Your activation token is @contact.fields.activation_token, your coupon is @trigger.params.coupons.0.code", "addresses": [ "@contact.urns.mailto", - "test@example.com" + "test@@example.com" ] }, { diff --git a/cmd/flowrunner/testdata/flows/two_questions.json b/cmd/flowrunner/testdata/flows/two_questions.json index a57d64751..bcb81cf51 100644 --- a/cmd/flowrunner/testdata/flows/two_questions.json +++ b/cmd/flowrunner/testdata/flows/two_questions.json @@ -82,7 +82,7 @@ { "uuid": "e97cd6d5-3354-4dbd-85bc-6c1f87849eec", "type": "send_msg", - "text": "Hi @contact.name! What is your favorite color? (red/blue) Your number is @contact.urn" + "text": "Hi @contact.name! What is your favorite color? (red/blue) Your number is @(format_urn(contact.urns.0))" } ], "wait": { diff --git a/cmd/flowrunner/testdata/flows/two_questions_test.json b/cmd/flowrunner/testdata/flows/two_questions_test.json index 8e2810043..2483313b8 100644 --- a/cmd/flowrunner/testdata/flows/two_questions_test.json +++ b/cmd/flowrunner/testdata/flows/two_questions_test.json @@ -44,7 +44,7 @@ "name": "Android Channel", "uuid": "57f1078f-88aa-46f4-a59a-948a5739c03d" }, - "text": "Hi Ben Haggerty! What is your favorite color? (red/blue) Your number is @contact.urn", + "text": "Hi Ben Haggerty! What is your favorite color? (red/blue) Your number is (206) 555-1212", "urn": "tel:+12065551212", "uuid": "8720f157-ca1c-432f-9c0b-2014ddc77094" }, diff --git a/excellent/evaluator.go b/excellent/evaluator.go index 4cd078435..925e67dc9 100644 --- a/excellent/evaluator.go +++ b/excellent/evaluator.go @@ -310,12 +310,13 @@ func EvaluateTemplateAsString(env utils.Environment, resolver utils.VariableReso if value == nil { value = "" } - _, isErr := value.(error) + err, isErr := value.(error) // we got an error, return our raw variable if isErr { buf.WriteString("@") buf.WriteString(token) + errors = append(errors, err) } else { strValue, _ := utils.ToString(env, value) if urlEncode { diff --git a/excellent/evaluator_test.go b/excellent/evaluator_test.go index fa3f8a86b..ce98589be 100644 --- a/excellent/evaluator_test.go +++ b/excellent/evaluator_test.go @@ -1,43 +1,68 @@ package excellent import ( + "fmt" "reflect" "strings" "testing" "github.com/nyaruka/goflow/utils" + + "github.com/stretchr/testify/assert" ) +type testResolvable struct{} + +func (r *testResolvable) Resolve(key string) interface{} { + switch key { + case "foo": + return "bar" + case "zed": + return 123 + default: + return fmt.Errorf("no such thing") + } +} + +func (r *testResolvable) Default() interface{} { + return r +} + +func (r *testResolvable) String() string { + return "hello" +} + func TestEvaluateTemplateAsString(t *testing.T) { - varMap := make(map[string]interface{}) - varMap["string1"] = "foo" - varMap["string2"] = "bar" - varMap["汉字"] = "simplified chinese" - varMap["int1"] = 1 - varMap["int2"] = 2 - varMap["dec1"] = 1.5 - varMap["dec2"] = 2.5 - varMap["words"] = "one two three" - varMap["array"] = []string{"one", "two", "three"} - vars := utils.NewMapResolver(varMap) + + vars := utils.NewMapResolver(map[string]interface{}{ + "string1": "foo", + "string2": "bar", + "汉字": "simplified chinese", + "int1": 1, + "int2": 2, + "dec1": 1.5, + "dec2": 2.5, + "words": "one two three", + "array": []string{"one", "two", "three"}, + "thing": &testResolvable{}, + }) evaluateAsStringTests := []struct { template string expected string hasError bool }{ - {"hello world", "hello world", false}, {"@(\"hello\\nworld\")", "hello\nworld", false}, {"@(\"hello😁world\")", "hello😁world", false}, {"@(\"hello\\U0001F601world\")", "hello😁world", false}, - {"@hello", "@hello", false}, - {"@hello.bar", "@hello.bar", false}, + {"@hello", "@hello", true}, + {"@hello.bar", "@hello.bar", true}, {"@(title(\"hello\"))", "Hello", false}, {"@(title(hello))", "", true}, {"Hello @(title(string1))", "Hello Foo", false}, {"Hello @@string1", "Hello @string1", false}, - {"My email is foo@bar.com", "My email is foo@bar.com", false}, + {"My email is foo@bar.com", "My email is foo@bar.com", true}, {"1 + 2", "1 + 2", false}, {"@(1 + 2)", "3", false}, @@ -46,7 +71,7 @@ func TestEvaluateTemplateAsString(t *testing.T) { {"@string1@string2", "foobar", false}, {"@(string1 & string2)", "foobar", false}, {"@string1.@string2", "foo.bar", false}, - {"@string1.@string2.@string3", "foo.bar.@string3", false}, + {"@string1.@string2.@string3", "foo.bar.@string3", true}, {"@(汉字)", "simplified chinese", false}, {"@(string1", "@(string1", false}, @@ -60,7 +85,7 @@ func TestEvaluateTemplateAsString(t *testing.T) { {"@(dec1 + dec2)", "4", false}, - {"@missing", "@missing", false}, + {"@missing", "@missing", true}, {"@(TITLE(missing))", "", true}, {"@array", "one, two, three", false}, @@ -69,28 +94,32 @@ func TestEvaluateTemplateAsString(t *testing.T) { {"@(array [0])", "one", false}, {"@(array[0])", "one", false}, {"@(array.0)", "one", false}, - - {"@(array[-1])", "three", false}, - {"@(array.-1)", "", true}, + {"@(array[-1])", "three", false}, // negative index + {"@(array.-1)", "", true}, // invalid negative index {"@(split(words, \" \").0)", "one", false}, {"@(split(words, \" \")[1])", "two", false}, {"@(split(words, \" \")[-1])", "three", false}, + + {"@(thing.foo)", "bar", false}, + {"@(thing.zed)", "123", false}, + {"@(thing.xxx)", "", true}, + + {"@(has_error(array[100]))", "true", false}, // errors are like any other value + {"@(has_error(array.100))", "true", false}, + {"@(has_error(thing.foo))", "false", false}, + {"@(has_error(thing.xxx))", "true", false}, } env := utils.NewDefaultEnvironment() for _, test := range evaluateAsStringTests { eval, err := EvaluateTemplateAsString(env, vars, test.template, false) - if err != nil { - if !test.hasError { - t.Errorf("Received error evaluating '%s': %s", test.template, err) - } + + if test.hasError { + assert.Error(t, err, "expected error evaluating template '%s'", test.template) } else { - if test.hasError { - t.Errorf("Did not receive error evaluating '%s'", test.template) - } + assert.NoError(t, err, "unexpected error evaluating template '%s'", test.template) } - if eval != test.expected { t.Errorf("Actual '%s' does not match expected '%s' evaluating template: '%s'", eval, test.expected, test.template) } @@ -100,39 +129,38 @@ func TestEvaluateTemplateAsString(t *testing.T) { func TestEvaluateTemplate(t *testing.T) { arr := []string{"a", "b", "c"} - strMap := make(map[string]string) - strMap["1"] = "one" - strMap["2"] = "two" - strMap["3"] = "three" - strMap["four"] = "four" - strMap["with space"] = "spacy" - strMap["with-dash"] = "dashy" - strMap["汉字"] = "simplified chinese" + strMap := map[string]string{ + "1": "one", + "2": "two", + "3": "three", + "four": "four", + "with space": "spacy", + "with-dash": "dashy", + "汉字": "simplified chinese", + } - intMap := make(map[int]string) - intMap[1] = "one" - intMap[2] = "two" - intMap[3] = "three" + intMap := map[int]string{1: "one", 2: "two", 3: "three"} - innerMap := make(map[string]interface{}) - innerMap["int_map"] = intMap + innerMap := map[string]interface{}{"int_map": intMap} innerArr := []map[string]string{strMap} - varMap := make(map[string]interface{}) - varMap["string1"] = "foo" - varMap["string2"] = "bar" - varMap["key"] = "four" - varMap["int1"] = 1 - varMap["int2"] = 2 - varMap["dec1"] = 1.5 - varMap["dec2"] = 2.5 - varMap["words"] = "one two three" - varMap["array1"] = arr - varMap["str_map"] = strMap - varMap["int_map"] = intMap - varMap["inner_map"] = innerMap - varMap["inner_arr"] = innerArr + varMap := map[string]interface{}{ + "string1": "foo", + "string2": "bar", + "key": "four", + "int1": 1, + "int2": 2, + "dec1": 1.5, + "dec2": 2.5, + "words": "one two three", + "array1": arr, + "str_map": strMap, + "int_map": intMap, + "inner_map": innerMap, + "inner_arr": innerArr, + } + vars := utils.NewMapResolver(varMap) env := utils.NewDefaultEnvironment() @@ -238,18 +266,12 @@ func TestEvaluateTemplate(t *testing.T) { for _, test := range evaluateTests { eval, err := EvaluateTemplate(env, vars, test.template) - if err != nil { - if !test.hasError { - t.Errorf("Received error evaluating '%s': %s", test.template, err) - } - } else { - if test.hasError { - t.Errorf("Did not receive error evaluating '%s'", test.template) - } - } if test.hasError { + assert.Error(t, err, "expected error evaluating template '%s'", test.template) continue + } else { + assert.NoError(t, err, "unexpected error evaluating template '%s'", test.template) } // first try reflect comparison diff --git a/flows/engine/session_test.go b/flows/engine/session_test.go index fa814b371..87ada599e 100644 --- a/flows/engine/session_test.go +++ b/flows/engine/session_test.go @@ -18,7 +18,7 @@ type testRequest struct { Events []*utils.TypedEnvelope `json:"events"` } -func TestEvaluateTemplate(t *testing.T) { +func TestEvaluateTemplateAsString(t *testing.T) { tests := []struct { template string expected string @@ -44,8 +44,8 @@ func TestEvaluateTemplate(t *testing.T) { {"@(format_urn(contact.urns.0))", "(206) 555-1212", false}, {"@contact.groups", "Survey Audience", false}, // TODO should be list {"@contact.fields.state", "Azuay", false}, - {"@contact.fields.favorite_icecream", "", false}, // TODO should be error - {"@(has_error(contact.fields.favorite_icecream))", "false", false}, // TODO should be true + {"@contact.fields.favorite_icecream", "", false}, // TODO should be error? + {"@(has_error(contact.fields.favorite_icecream))", "false", false}, // TODO should be true? {"@run.input", "Hi there\nhttp://s3.amazon.com/bucket/test_en.jpg?a=Azuay", false}, {"@run.input.text", "Hi there", false}, @@ -54,8 +54,8 @@ func TestEvaluateTemplate(t *testing.T) { {"@run.input.created_on", "2000-01-01T00:00:00.000000Z", false}, {"@run.input.channel.name", "Nexmo", false}, {"@run.results", "", false}, // TODO should be empty dict? - {"@run.results.favorite_icecream", "", false}, // TODO should be error - {"@(has_error(run.results.favorite_icecream))", "false", false}, // TODO should be true + {"@run.results.favorite_icecream", "", false}, // TODO should be error? + {"@(has_error(run.results.favorite_icecream))", "false", false}, // TODO should be true? {"@run.exited_on", "", false}, {"@trigger.params", "{\n \"coupons\": [\n {\n \"code\": \"AAA-BBB-CCC\",\n \"expiration\": \"2000-01-01T00:00:00.000000000-00:00\"\n }\n ]\n }", false}, @@ -92,12 +92,11 @@ func TestEvaluateTemplate(t *testing.T) { for _, test := range tests { eval, err := excellent.EvaluateTemplateAsString(session.Environment(), run.Context(), test.template, false) - if err != nil { - assert.True(t, test.hasError, "Received error evaluating '%s': %s", test.template, err) + if test.hasError { + assert.Error(t, err, "expected error evaluating template '%s'", test.template) } else { - assert.False(t, test.hasError, "Did not receive error evaluating '%s'", test.template) + assert.NoError(t, err, "unexpected error evaluating template '%s'", test.template) + assert.Equal(t, test.expected, eval, "Actual '%s' does not match expected '%s' evaluating template: '%s'", eval, test.expected, test.template) } - - assert.Equal(t, test.expected, eval, "Actual '%s' does not match expected '%s' evaluating template: '%s'", eval, test.expected, test.template) } } diff --git a/legacy/expressions.go b/legacy/expressions.go index ea6db1a29..743f15f1f 100644 --- a/legacy/expressions.go +++ b/legacy/expressions.go @@ -17,13 +17,13 @@ import ( var topLevelScopes = []string{"contact", "child", "parent", "run", "trigger"} // ExtraVarsMapping defines how @extra.* variables should be migrated -type ExtraVarsMapping string +type ExtraVarsMapping int // different ways of mapping @extra in legacy flows const ( - ExtraAsWebhookJSON ExtraVarsMapping = "run.webhook.json" - ExtraAsTriggerParams ExtraVarsMapping = "trigger.params" - ExtraAsFunction ExtraVarsMapping = "IF(trigger.params.%s, trigger.params.%s, run.webhook.json.%s)" + ExtraAsWebhookJSON ExtraVarsMapping = iota + ExtraAsTriggerParams + ExtraAsFunction ) type varMapper struct { diff --git a/legacy/expressions_test.go b/legacy/expressions_test.go index e365895d1..80c5a7bfe 100644 --- a/legacy/expressions_test.go +++ b/legacy/expressions_test.go @@ -11,7 +11,7 @@ type testTemplate struct { extraAs ExtraVarsMapping } -func TestTranslate(t *testing.T) { +func TestMigrateTemplate(t *testing.T) { var tests = []testTemplate{ // contact variables @@ -187,9 +187,6 @@ func TestTranslate(t *testing.T) { for _, test := range tests { for i := 0; i < 1; i++ { - if test.extraAs == "" { - test.extraAs = ExtraAsFunction - } translation, err := MigrateTemplate(test.old, test.extraAs) if err != nil {