From f64cd83b5842903e9409c2de33d19a3f1daccc07 Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Mon, 16 Aug 2021 12:03:52 -0500 Subject: [PATCH] Add tests for Twilio AMD, fix update of error_reason --- core/ivr/twiml/twiml.go | 10 +-- core/ivr/vonage/vonage.go | 17 ++--- core/models/channel_connection.go | 5 +- web/ivr/ivr_test.go | 116 ++++++++++++++++++++---------- 4 files changed, 95 insertions(+), 53 deletions(-) diff --git a/core/ivr/twiml/twiml.go b/core/ivr/twiml/twiml.go index dd67d3e34..3bdc2c707 100644 --- a/core/ivr/twiml/twiml.go +++ b/core/ivr/twiml/twiml.go @@ -297,28 +297,28 @@ func (c *client) StatusForRequest(r *http.Request) (models.ConnectionStatus, mod case "machine_start", "fax": return models.ConnectionStatusErrored, models.ConnectionErrorMachine, 0 } - return models.ConnectionStatusInProgress, models.ConnectionNoError, 0 + return models.ConnectionStatusInProgress, "", 0 } status := r.Form.Get("CallStatus") switch status { case "queued", "ringing": - return models.ConnectionStatusWired, models.ConnectionNoError, 0 + return models.ConnectionStatusWired, "", 0 case "in-progress", "initiated": - return models.ConnectionStatusInProgress, models.ConnectionNoError, 0 + return models.ConnectionStatusInProgress, "", 0 case "completed": duration, _ := strconv.Atoi(r.Form.Get("CallDuration")) - return models.ConnectionStatusCompleted, models.ConnectionNoError, duration + return models.ConnectionStatusCompleted, "", duration case "busy": return models.ConnectionStatusErrored, models.ConnectionErrorBusy, 0 case "no-answer": return models.ConnectionStatusErrored, models.ConnectionErrorNoAnswer, 0 case "canceled", "failed": - return models.ConnectionStatusErrored, models.ConnectionNoError, 0 + return models.ConnectionStatusErrored, "", 0 default: logrus.WithField("call_status", status).Error("unknown call status in status callback") diff --git a/core/ivr/vonage/vonage.go b/core/ivr/vonage/vonage.go index a00f78f81..a05b628dd 100644 --- a/core/ivr/vonage/vonage.go +++ b/core/ivr/vonage/vonage.go @@ -546,37 +546,38 @@ type StatusRequest struct { func (c *client) StatusForRequest(r *http.Request) (models.ConnectionStatus, models.ConnectionError, int) { // this is a resume, call is in progress, no need to look at the body if r.Form.Get("action") == "resume" { - return models.ConnectionStatusInProgress, models.ConnectionNoError, 0 + return models.ConnectionStatusInProgress, "", 0 } - status := &StatusRequest{} bb, err := readBody(r) if err != nil { logrus.WithError(err).Error("error reading status request body") - return models.ConnectionStatusErrored, models.ConnectionNoError, 0 + return models.ConnectionStatusErrored, models.ConnectionErrorProvider, 0 } + + status := &StatusRequest{} err = json.Unmarshal(bb, status) if err != nil { logrus.WithError(err).WithField("body", string(bb)).Error("error unmarshalling ncco status") - return models.ConnectionStatusErrored, models.ConnectionNoError, 0 + return models.ConnectionStatusErrored, models.ConnectionErrorProvider, 0 } // transfer status callbacks have no status, safe to ignore them if status.Status == "" { - return models.ConnectionStatusInProgress, models.ConnectionNoError, 0 + return models.ConnectionStatusInProgress, "", 0 } switch status.Status { case "started", "ringing": - return models.ConnectionStatusWired, models.ConnectionNoError, 0 + return models.ConnectionStatusWired, "", 0 case "answered": - return models.ConnectionStatusInProgress, models.ConnectionNoError, 0 + return models.ConnectionStatusInProgress, "", 0 case "completed": duration, _ := strconv.Atoi(status.Duration) - return models.ConnectionStatusCompleted, models.ConnectionNoError, duration + return models.ConnectionStatusCompleted, "", duration case "busy": return models.ConnectionStatusErrored, models.ConnectionErrorBusy, 0 diff --git a/core/models/channel_connection.go b/core/models/channel_connection.go index 0a7c50138..b6953c7dc 100644 --- a/core/models/channel_connection.go +++ b/core/models/channel_connection.go @@ -54,7 +54,6 @@ const ( ConnectionErrorBusy = ConnectionError("B") ConnectionErrorNoAnswer = ConnectionError("N") ConnectionErrorMachine = ConnectionError("M") - ConnectionNoError = ConnectionError("") ConnectionMaxRetries = 3 @@ -387,8 +386,8 @@ func (c *ChannelConnection) MarkErrored(ctx context.Context, db Queryer, now tim } _, err := db.ExecContext(ctx, - `UPDATE channels_channelconnection SET status = $2, ended_on = $3, retry_count = $4, error_count = $5, next_attempt = $6, modified_on = NOW() WHERE id = $1`, - c.c.ID, c.c.Status, c.c.EndedOn, c.c.RetryCount, c.c.ErrorCount, c.c.NextAttempt, + `UPDATE channels_channelconnection SET status = $2, ended_on = $3, retry_count = $4, error_reason = $5, error_count = $6, next_attempt = $7, modified_on = NOW() WHERE id = $1`, + c.c.ID, c.c.Status, c.c.EndedOn, c.c.RetryCount, c.c.ErrorReason, c.c.ErrorCount, c.c.NextAttempt, ) if err != nil { diff --git a/web/ivr/ivr_test.go b/web/ivr/ivr_test.go index ba7fa3d8c..1ebc8c8d4 100644 --- a/web/ivr/ivr_test.go +++ b/web/ivr/ivr_test.go @@ -11,6 +11,7 @@ import ( "sync" "testing" + "github.com/nyaruka/gocommon/jsonx" "github.com/nyaruka/goflow/test" "github.com/nyaruka/mailroom/config" "github.com/nyaruka/mailroom/core/models" @@ -30,6 +31,30 @@ import ( ivr_tasks "github.com/nyaruka/mailroom/core/tasks/ivr" ) +// mocks the Twilio API +func mockTwilioHandler(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + logrus.WithField("method", r.Method).WithField("url", r.URL.String()).WithField("form", r.Form).Info("test server called") + if strings.HasSuffix(r.URL.String(), "Calls.json") { + to := r.Form.Get("To") + if to == "+16055741111" { + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{"sid": "Call1"}`)) + } else if to == "+16055742222" { + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{"sid": "Call2"}`)) + } else if to == "+16055743333" { + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{"sid": "Call3"}`)) + } else { + w.WriteHeader(http.StatusNotFound) + } + } + if strings.HasSuffix(r.URL.String(), "recording.mp3") { + w.WriteHeader(http.StatusOK) + } +} + func TestTwilioIVR(t *testing.T) { ctx, _, db, rp := testsuite.Get() rc := rp.Get() @@ -41,25 +66,7 @@ func TestTwilioIVR(t *testing.T) { }() // start test server - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - r.ParseForm() - logrus.WithField("method", r.Method).WithField("url", r.URL.String()).WithField("form", r.Form).Info("test server called") - if strings.HasSuffix(r.URL.String(), "Calls.json") { - to := r.Form.Get("To") - if to == "+16055741111" { - w.WriteHeader(http.StatusCreated) - w.Write([]byte(`{"sid": "Call1"}`)) - } else if to == "+16055743333" { - w.WriteHeader(http.StatusCreated) - w.Write([]byte(`{"sid": "Call2"}`)) - } else { - w.WriteHeader(http.StatusNotFound) - } - } - if strings.HasSuffix(r.URL.String(), "recording.mp3") { - w.WriteHeader(http.StatusOK) - } - })) + ts := httptest.NewServer(http.HandlerFunc(mockTwilioHandler)) defer ts.Close() twiml.BaseURL = ts.URL @@ -73,37 +80,51 @@ func TestTwilioIVR(t *testing.T) { // add auth tokens db.MustExec(`UPDATE channels_channel SET config = '{"auth_token": "token", "account_sid": "sid", "callback_domain": "localhost:8090"}' WHERE id = $1`, testdata.TwilioChannel.ID) - // create a flow start for cathy and george - parentSummary := json.RawMessage(`{"flow": {"name": "IVR Flow", "uuid": "2f81d0ea-4d75-4843-9371-3f7465311cce"}, "uuid": "8bc73097-ac57-47fb-82e5-184f8ec6dbef", "status": "active", "contact": {"id": 10000, "name": "Cathy", "urns": ["tel:+16055741111?id=10000&priority=50"], "uuid": "6393abc0-283d-4c9b-a1b3-641a035c34bf", "fields": {"gender": {"text": "F"}}, "groups": [{"name": "Doctors", "uuid": "c153e265-f7c9-4539-9dbc-9b358714b638"}], "timezone": "America/Los_Angeles", "created_on": "2019-07-23T09:35:01.439614-07:00"}, "results": {}}`) - + // create a flow start for cathy bob, and george + parentSummary := json.RawMessage(`{ + "flow": {"name": "IVR Flow", "uuid": "2f81d0ea-4d75-4843-9371-3f7465311cce"}, + "uuid": "8bc73097-ac57-47fb-82e5-184f8ec6dbef", + "status": "active", + "contact": { + "id": 10000, + "name": "Cathy", + "urns": ["tel:+16055741111?id=10000&priority=50"], + "uuid": "6393abc0-283d-4c9b-a1b3-641a035c34bf", + "fields": {"gender": {"text": "F"}}, + "groups": [{"name": "Doctors", "uuid": "c153e265-f7c9-4539-9dbc-9b358714b638"}], + "timezone": "America/Los_Angeles", + "created_on": "2019-07-23T09:35:01.439614-07:00" + }, + "results": {} + }`) start := models.NewFlowStart(testdata.Org1.ID, models.StartTypeTrigger, models.FlowTypeVoice, testdata.IVRFlow.ID, models.DoRestartParticipants, models.DoIncludeActive). - WithContactIDs([]models.ContactID{testdata.Cathy.ID, testdata.George.ID}). + WithContactIDs([]models.ContactID{testdata.Cathy.ID, testdata.Bob.ID, testdata.George.ID}). WithParentSummary(parentSummary) err := models.InsertFlowStarts(ctx, db, []*models.FlowStart{start}) - assert.NoError(t, err) + require.NoError(t, err) // call our master starter err = starts.CreateFlowBatches(ctx, db, rp, nil, start) - assert.NoError(t, err) + require.NoError(t, err) // start our task - task, err := queue.PopNextTask(rc, queue.HandlerQueue) - assert.NoError(t, err) + task, err := queue.PopNextTask(rc, queue.BatchQueue) + require.NoError(t, err) batch := &models.FlowStartBatch{} - err = json.Unmarshal(task.Task, batch) - assert.NoError(t, err) + jsonx.MustUnmarshal(task.Task, batch) - // request our call to start + // request our calls to start err = ivr_tasks.HandleFlowStartBatch(ctx, config.Mailroom, db, rp, batch) - assert.NoError(t, err) + require.NoError(t, err) + // check our 3 contacts have 3 wired calls testsuite.AssertQuery(t, db, `SELECT COUNT(*) FROM channels_channelconnection WHERE contact_id = $1 AND status = $2 AND external_id = $3`, testdata.Cathy.ID, models.ConnectionStatusWired, "Call1").Returns(1) - - testsuite.AssertQuery(t, db, - `SELECT COUNT(*) FROM channels_channelconnection WHERE contact_id = $1 AND status = $2 AND external_id = $3`, - testdata.George.ID, models.ConnectionStatusWired, "Call2").Returns(1) + testsuite.AssertQuery(t, db, `SELECT COUNT(*) FROM channels_channelconnection WHERE contact_id = $1 AND status = $2 AND external_id = $3`, + testdata.Bob.ID, models.ConnectionStatusWired, "Call2").Returns(1) + testsuite.AssertQuery(t, db, `SELECT COUNT(*) FROM channels_channelconnection WHERE contact_id = $1 AND status = $2 AND external_id = $3`, + testdata.George.ID, models.ConnectionStatusWired, "Call3").Returns(1) tcs := []struct { label string @@ -240,6 +261,25 @@ func TestTwilioIVR(t *testing.T) { expectedStatus: 200, expectedResponse: "", }, + { + label: "call3 started", + url: fmt.Sprintf("/ivr/c/%s/handle?action=start&connection=3", testdata.TwilioChannel.UUID), + form: nil, + expectedStatus: 200, + contains: []string{`Hello there. Please enter one or two. This flow was triggered by Cathy`}, + }, + { + label: "answer machine detection sent to tell us we're talking to a voicemail", + url: fmt.Sprintf("/ivr/c/%s/status", testdata.TwilioChannel.UUID), + form: url.Values{ + "CallSid": []string{"Call3"}, + "AccountSid": []string{"sid"}, + "AnsweredBy": []string{"machine_start"}, + "MachineDetectionDuration": []string{"2000"}, + }, + expectedStatus: 200, + contains: []string{"