Skip to content

Commit

Permalink
Merge pull request rapidpro#458 from nyaruka/ivr_sim_fix
Browse files Browse the repository at this point in the history
🧯 Fix triggering new IVR flow from a simulation resume
  • Loading branch information
rowanseymour authored Jul 13, 2021
2 parents adb9eb5 + 13b7078 commit 8c429d8
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 78 deletions.
18 changes: 16 additions & 2 deletions web/simulation/simulation.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"net/http"

"github.com/nyaruka/gocommon/urns"
"github.com/nyaruka/goflow/assets"
"github.com/nyaruka/goflow/assets/static/types"
"github.com/nyaruka/goflow/excellent/tools"
Expand All @@ -21,6 +22,9 @@ import (
"github.com/pkg/errors"
)

var testChannel = assets.NewChannelReference("440099cf-200c-4d45-a8e7-4a564f4a0e8b", "Test Channel")
var testURN = urns.URN("tel:+12065551212")

func init() {
web.RegisterJSONRoute(http.MethodPost, "/mr/sim/start", web.RequireAuthToken(handleStart))
web.RegisterJSONRoute(http.MethodPost, "/mr/sim/resume", web.RequireAuthToken(handleResume))
Expand Down Expand Up @@ -230,8 +234,18 @@ func handleResume(ctx context.Context, rt *runtime.Runtime, r *http.Request) (in
}

if triggeredFlow != nil {
trigger := triggers.NewBuilder(oa.Env(), triggeredFlow.FlowReference(), resume.Contact()).Msg(msgResume.Msg()).WithMatch(trigger.Match()).Build()
return triggerFlow(ctx, rt, oa, trigger)
tb := triggers.NewBuilder(oa.Env(), triggeredFlow.FlowReference(), resume.Contact())

var sessionTrigger flows.Trigger
if triggeredFlow.FlowType() == models.FlowTypeVoice {
// TODO this should trigger a msg trigger with a connection but first we need to rework
// non-simulation IVR triggers to use that so that this is consistent.
sessionTrigger = tb.Manual().WithConnection(testChannel, testURN).Build()
} else {
sessionTrigger = tb.Msg(msgResume.Msg()).WithMatch(trigger.Match()).Build()
}

return triggerFlow(ctx, rt, oa, sessionTrigger)
}
}
}
Expand Down
111 changes: 35 additions & 76 deletions web/simulation/simulation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,60 +88,7 @@ const (
"name": "Twitter",
"uuid": "0f661e8b-ea9d-4bd3-9953-d368340acf91"
},
"text": "I like blue!",
"urn": "tel:+12065551212",
"uuid": "9bf91c2b-ce58-4cef-aacc-281e03f69ab5"
},
"resumed_on": "2000-01-01T00:00:00.000000000-00:00",
"type": "msg"
},
"assets": {
"channels": [
{
"uuid": "440099cf-200c-4d45-a8e7-4a564f4a0e8b",
"name": "Test Channel",
"address": "+18005551212",
"schemes": ["tel"],
"roles": ["send", "receive", "call"],
"country": "US"
}
]
},
"session": $$SESSION$$
}`

triggerResumeBody = `
{
"org_id": 1,
"resume": {
"contact": {
"created_on": "2000-01-01T00:00:00.000000000-00:00",
"fields": {},
"id": 1234567,
"language": "eng",
"name": "Ben Haggerty",
"timezone": "America/Guayaquil",
"urns": [
"tel:+12065551212"
],
"uuid": "ba96bf7f-bc2a-4873-a7c7-254d1927c4e3"
},
"environment": {
"allowed_languages": [
"eng",
"fra"
],
"date_format": "YYYY-MM-DD",
"default_language": "eng",
"time_format": "hh:mm",
"timezone": "America/New_York"
},
"msg": {
"channel": {
"name": "Twitter",
"uuid": "0f661e8b-ea9d-4bd3-9953-d368340acf91"
},
"text": "trigger",
"text": "$$MESSAGE$$",
"urn": "tel:+12065551212",
"uuid": "9bf91c2b-ce58-4cef-aacc-281e03f69ab5"
},
Expand Down Expand Up @@ -263,39 +210,52 @@ func TestServer(t *testing.T) {
time.Sleep(time.Second)

defer server.Stop()
session := ""

var session json.RawMessage

// add a trigger for our campaign flow with 'trigger'
testdata.InsertKeywordTrigger(db, testdata.Org1, testdata.CampaignFlow, "trigger", models.MatchOnly, nil, nil)

// and a trigger which will trigger an IVR flow
testdata.InsertKeywordTrigger(db, testdata.Org1, testdata.IVRFlow, "ivr", models.MatchOnly, nil, nil)

// also add a catch all
testdata.InsertCatchallTrigger(db, testdata.Org1, testdata.CampaignFlow, nil, nil)

tcs := []struct {
URL string
Method string
Body string
Status int
Response string
URL string
Method string
Body string
Message string
ExpectedStatus int
ExpectedResponse string
}{
{"/mr/sim/start", "GET", "", 405, "illegal"},
{"/mr/sim/start", "POST", startBody, 200, "What is your favorite color?"},
{"/mr/sim/resume", "GET", "", 405, "illegal"},
{"/mr/sim/resume", "POST", resumeBody, 200, "Good choice, I like Blue too! What is your favorite beer?"},
{"/mr/sim/start", "POST", customStartBody, 200, "Your channel is Test Channel"},
{"/mr/sim/start", "POST", startBody, 200, "What is your favorite color?"},
{"/mr/sim/resume", "POST", triggerResumeBody, 200, "it is time to consult with your patients"},
{"/mr/sim/resume", "POST", resumeBody, 200, "it is time to consult with your patients"},
{"/mr/sim/start", "GET", "", "", 405, "illegal"},
{"/mr/sim/start", "POST", startBody, "", 200, "What is your favorite color?"},
{"/mr/sim/resume", "POST", resumeBody, "I like blue!", 200, "Good choice, I like Blue too! What is your favorite beer?"},

// start with a definition of the flow to override what we have in assets
{"/mr/sim/start", "POST", customStartBody, "", 200, "Your channel is Test Channel"},

// start regular flow again but resume with a message that matches the campaign flow trigger
{"/mr/sim/start", "POST", startBody, "", 200, "What is your favorite color?"},
{"/mr/sim/resume", "POST", resumeBody, "trigger", 200, "it is time to consult with your patients"},
{"/mr/sim/resume", "POST", resumeBody, "I like blue!", 200, "it is time to consult with your patients"},

// start favorties again but this time resume with a message that matches the IVR flow trigger
{"/mr/sim/start", "POST", startBody, "", 200, "What is your favorite color?"},
{"/mr/sim/resume", "POST", resumeBody, "ivr", 200, "Hello there. Please enter one or two."},
}

for i, tc := range tcs {
var body io.Reader
bodyStr := strings.Replace(tc.Body, "$$MESSAGE$$", tc.Message, -1)

// in the case of a resume, we have to sub in our session body from our start
tc.Body = strings.Replace(tc.Body, "$$SESSION$$", session, -1)
bodyStr = strings.Replace(bodyStr, "$$SESSION$$", string(session), -1)

var body io.Reader
if tc.Body != "" {
body = bytes.NewReader([]byte(tc.Body))
body = bytes.NewReader([]byte(bodyStr))
}

req, err := http.NewRequest(tc.Method, "http://localhost:8090"+tc.URL, body)
Expand All @@ -304,7 +264,7 @@ func TestServer(t *testing.T) {
resp, err := http.DefaultClient.Do(req)
assert.NoError(t, err, "%d: error making request", i)

assert.Equal(t, tc.Status, resp.StatusCode, "%d: unexpected status", i)
assert.Equal(t, tc.ExpectedStatus, resp.StatusCode, "%d: unexpected status", i)

content, err := ioutil.ReadAll(resp.Body)
assert.NoError(t, err, "%d: error reading body", i)
Expand All @@ -313,9 +273,8 @@ func TestServer(t *testing.T) {
if resp.StatusCode == 200 {
// save the session for use in a resume
parsed := make(map[string]interface{})
json.Unmarshal(content, &parsed)
sessionJSON := jsonx.MustMarshal(parsed["session"])
session = string(sessionJSON)
jsonx.MustUnmarshal(content, &parsed)
session = jsonx.MustMarshal(parsed["session"])

context, hasContext := parsed["context"]
if hasContext {
Expand All @@ -324,6 +283,6 @@ func TestServer(t *testing.T) {
}
}

assert.True(t, strings.Contains(string(content), tc.Response), "%d: did not find string: %s in body: %s", i, tc.Response, string(content))
assert.Contains(t, string(content), tc.ExpectedResponse, "%d: did not find expected response content")
}
}

0 comments on commit 8c429d8

Please sign in to comment.