Ejemplo n.º 1
0
    def test_request_failure(self):
        flow = self.get_flow("color")

        with patch("requests.post") as mock_post:
            mock_post.return_value = MockResponse(
                400, '{"errors":["Bad request", "Doh!"]}')

            with self.assertRaises(MailroomException) as e:
                get_client().flow_migrate(flow.as_json())

        self.assertEqual(
            e.exception.as_json(),
            {
                "endpoint": "flow/migrate",
                "request": matchers.Dict(),
                "response": {
                    "errors": ["Bad request", "Doh!"]
                }
            },
        )
Ejemplo n.º 2
0
    def test_tunnel(self):
        response = self.client.post(reverse('api.webhook_tunnel'), dict())
        self.assertEquals(302, response.status_code)

        self.login(self.non_org_user)

        with patch('requests.post') as mock:
            mock.return_value = MockResponse(200, '{ "phone": "+250788123123", "text": "I am success" }')

            response = self.client.post(reverse('api.webhook_tunnel'),
                                        dict(url="http://webhook.url/", data="phone=250788383383&values=foo&bogus=2"))
            self.assertEquals(200, response.status_code)
            self.assertContains(response, "I am success")
            self.assertTrue('values' in mock.call_args[1]['data'])
            self.assertTrue('phone' in mock.call_args[1]['data'])
            self.assertFalse('bogus' in mock.call_args[1]['data'])

            response = self.client.post(reverse('api.webhook_tunnel'), dict())
            self.assertEquals(400, response.status_code)
            self.assertTrue(response.content.find("Must include") >= 0)
Ejemplo n.º 3
0
    def test_claim(self, mock_get):
        url = reverse('channels.types.firebase.claim')

        self.login(self.admin)

        # check that claim page URL appears on claim list page
        response = self.client.get(reverse('channels.channel_claim'))
        self.assertContains(response, url)

        mock_get.return_value = MockResponse(
            200,
            json.dumps({
                'title': 'FCM Channel',
                'key': 'abcde12345',
                'send_notification': 'True'
            }))
        response = self.client.post(url, {
            'title': 'FCM Channel',
            'key': 'abcde12345',
            'send_notification': 'True'
        },
                                    follow=True)

        channel = Channel.objects.get(address='abcde12345')
        self.assertRedirects(
            response,
            reverse('channels.channel_configuration', args=[channel.uuid]))
        self.assertEqual(channel.channel_type, "FCM")
        self.assertEqual(
            channel.config, {
                'FCM_KEY': 'abcde12345',
                'FCM_TITLE': 'FCM Channel',
                'FCM_NOTIFICATION': True
            })

        response = self.client.get(
            reverse('channels.channel_configuration', args=[channel.uuid]))
        self.assertContains(
            response, reverse('courier.fcm', args=[channel.uuid, 'receive']))
        self.assertContains(
            response, reverse('courier.fcm', args=[channel.uuid, 'register']))
Ejemplo n.º 4
0
    def test_claim(self, mock_get):
        url = reverse("channels.types.firebase.claim")

        self.login(self.admin)

        # check that claim page URL appears on claim list page
        response = self.client.get(reverse("channels.channel_claim"))
        self.assertContains(response, url)

        mock_get.return_value = MockResponse(
            200,
            json.dumps({
                "title": "FCM Channel",
                "key": "abcde12345",
                "send_notification": "True"
            }))
        response = self.client.post(url, {
            "title": "FCM Channel",
            "key": "abcde12345",
            "send_notification": "True"
        },
                                    follow=True)

        channel = Channel.objects.get(address="abcde12345")
        self.assertRedirects(
            response,
            reverse("channels.channel_configuration", args=[channel.uuid]))
        self.assertEqual(channel.channel_type, "FCM")
        self.assertEqual(
            channel.config, {
                "FCM_KEY": "abcde12345",
                "FCM_TITLE": "FCM Channel",
                "FCM_NOTIFICATION": True
            })

        response = self.client.get(
            reverse("channels.channel_configuration", args=[channel.uuid]))
        self.assertContains(
            response, reverse("courier.fcm", args=[channel.uuid, "receive"]))
        self.assertContains(
            response, reverse("courier.fcm", args=[channel.uuid, "register"]))
Ejemplo n.º 5
0
    def test_flow_inspect(self):
        flow_def = {"nodes": [{"val": Decimal("1.23")}]}

        with patch("requests.post") as mock_post:
            mock_post.return_value = MockResponse(200, '{"dependencies":[]}')
            info = get_client().flow_inspect(self.org.id, flow_def)

            self.assertEqual({"dependencies": []}, info)

        call = mock_post.call_args

        self.assertEqual(("http://localhost:8090/mr/flow/inspect", ), call[0])
        self.assertEqual(
            {
                "User-Agent": "Temba",
                "Content-Type": "application/json"
            }, call[1]["headers"])
        self.assertEqual({
            "org_id": self.org.id,
            "flow": flow_def
        }, json.loads(call[1]["data"]))
Ejemplo n.º 6
0
    def test_flow_migrate(self):
        flow_def = {"nodes": [{"val": Decimal("1.23")}]}

        with patch("requests.post") as mock_post:
            mock_post.return_value = MockResponse(200, '{"name": "Migrated!"}')
            migrated = get_client().flow_migrate(flow_def, to_version="13.1.0")

            self.assertEqual({"name": "Migrated!"}, migrated)

        call = mock_post.call_args

        self.assertEqual(("http://localhost:8090/mr/flow/migrate", ), call[0])
        self.assertEqual(
            {
                "User-Agent": "Temba",
                "Content-Type": "application/json"
            }, call[1]["headers"])
        self.assertEqual({
            "flow": flow_def,
            "to_version": "13.1.0"
        }, json.loads(call[1]["data"]))
Ejemplo n.º 7
0
    def test_tunnel(self):
        response = self.client.post(reverse("api.webhook_tunnel"), dict())
        self.assertEqual(302, response.status_code)

        self.login(self.non_org_user)

        with patch("requests.post") as mock:
            mock.return_value = MockResponse(200, '{ "phone": "+250788123123", "text": "I am success" }')

            response = self.client.post(
                reverse("api.webhook_tunnel"),
                dict(url="http://webhook.url/", data="phone=250788383383&values=foo&bogus=2"),
            )
            self.assertEqual(200, response.status_code)
            self.assertContains(response, "I am success")
            self.assertIn("values", mock.call_args[1]["data"])
            self.assertIn("phone", mock.call_args[1]["data"])
            self.assertNotIn("bogus", mock.call_args[1]["data"])

            response = self.client.post(reverse("api.webhook_tunnel"), dict())
            self.assertContains(response, "Must include", status_code=400)
Ejemplo n.º 8
0
    def test_send_session_close(self):
        flow = self.get_flow('ussd_session_end')
        contact = self.create_contact("Joe", "+250788383383")

        try:
            settings.SEND_MESSAGES = True

            with patch('requests.put') as mock:
                mock.return_value = MockResponse(200,
                                                 '{ "message_id": "1515" }')
                flow.start([], [contact])

                # our outgoing message
                msg = Msg.objects.filter(direction='O').order_by('id').last()

                self.assertEqual(msg.direction, 'O')
                self.assertTrue(msg.sent_on)
                self.assertEquals("1515", msg.external_id)
                self.assertEquals(msg.session.status, USSDSession.INITIATED)

                # reply and choose an option that doesn't have any destination thus needs to close the session
                USSDSession.handle_incoming(channel=self.channel,
                                            urn="+250788383383",
                                            content="4",
                                            date=timezone.now(),
                                            external_id="21345")

                # our outgoing message
                msg = Msg.objects.filter(direction='O').order_by('id').last()
                self.assertEqual(msg.direction, 'O')
                self.assertTrue(msg.sent_on)
                self.assertEquals("1515", msg.external_id)

                self.assertEquals(msg.session.status, USSDSession.COMPLETED)

                self.assertEquals(2, mock.call_count)

                self.clear_cache()
        finally:
            settings.SEND_MESSAGES = False
Ejemplo n.º 9
0
    def test_non_blocking_rule_ivr(self):

        self.org.connect_twilio("TEST_SID", "TEST_TOKEN", self.admin)
        self.org.save()

        # flow goes: passive -> recording -> msg
        flow = self.get_flow('non_blocking_rule_ivr')

        print json.dumps(flow.as_json(), indent=2)

        # start marshall in the flow
        eminem = self.create_contact('Eminem', '+12345')
        flow.start(groups=[], contacts=[eminem])
        call = IVRCall.objects.filter(direction=IVRCall.OUTGOING).first()
        self.assertNotEquals(call, None)

        # after a call is picked up, twilio will call back to our server
        post_data = dict(CallSid='CallSid', CallStatus='in-progress', CallDuration=20)
        self.client.post(reverse('ivr.ivrcall_handle', args=[call.pk]), post_data)

        # should have two steps so far, right up to the recording
        self.assertEquals(2, FlowStep.objects.all().count())

        # no outbound yet
        self.assertEquals(None, Msg.objects.filter(direction='O', contact=eminem).first())

        # now pretend we got a recording
        from temba.tests import MockResponse
        with patch('requests.get') as response:
            mock = MockResponse(200, 'Fake Recording Bits')
            mock.add_header('Content-Disposition', 'filename="audio0000.wav"')
            mock.add_header('Content-Type', 'audio/x-wav')
            response.return_value = mock

            self.client.post(reverse('ivr.ivrcall_handle', args=[call.pk]),
                             dict(CallStatus='in-progress', Digits='#',
                                  RecordingUrl='http://api.twilio.com/ASID/Recordings/SID', RecordingSid='FAKESID'))

        # now we should have an outbound message
        self.assertEquals('Hi there Eminem', Msg.objects.filter(direction='O', contact=eminem).first().text)
Ejemplo n.º 10
0
    def test_validation_failure(self):
        with patch("requests.post") as mock_post:
            mock_post.return_value = MockResponse(
                422, '{"error":"flow don\'t look right"}')

            with self.assertRaises(FlowValidationException) as e:
                get_client().flow_validate(self.org, '{"nodes:[]"}')

        self.assertEqual(str(e.exception), "flow don't look right")
        self.assertEqual(
            e.exception.as_json(),
            {
                "endpoint": "flow/validate",
                "request": {
                    "flow": '{"nodes:[]"}',
                    "org_id": self.org.id
                },
                "response": {
                    "error": "flow don't look right"
                },
            },
        )
Ejemplo n.º 11
0
    def test_webhook_first(self, mock_send):
        mock_send.return_value = MockResponse(200, "{}")

        self.setupChannel()
        org = self.channel.org
        org.save()

        # set our very first action to be a webhook
        flow = self.get_flow("webhook_rule_first")

        # run a user through this flow
        flow.start([], [self.joe])
        event = WebHookEvent.objects.get()

        # make sure our contact still has a URN
        self.assertEqual(
            event.data["contact"],
            {"uuid": str(self.joe.uuid), "name": self.joe.name, "urn": str(self.joe.get_urn("tel"))},
        )

        # make sure we don't have an input
        self.assertNotIn("input", event.data)
Ejemplo n.º 12
0
    def test_syncing(self):
        # will fail due to missing keys
        self.c1.async_sync()

        # no intents should have been changed / removed as this was an error
        self.assertEqual(2, self.c1.active_intents().count())

        # ok, fix our config
        self.c1.config = {
            WitType.CONFIG_ACCESS_TOKEN: "sesasme",
            WitType.CONFIG_APP_ID: "1234"
        }
        self.c1.save()

        # try again
        with patch("requests.get") as mock_get:
            mock_get.return_value = MockResponse(200, INTENT_RESPONSE)
            self.c1.async_sync()

            # should have three active intents
            intents = self.c1.active_intents()
            self.assertEqual(3, intents.count())
            self.assertEqual("book_car", intents[0].name)
            self.assertEqual("754569408690533", intents[0].external_id)
            self.assertEqual("book_horse", intents[1].name)
            self.assertEqual("754569408690020", intents[1].external_id)
            self.assertEqual("book_hotel", intents[2].name)
            self.assertEqual("754569408690131", intents[2].external_id)

            # one inactive
            self.assertEqual(1,
                             self.c1.intents.filter(is_active=False).count())

            # one classifier log
            self.assertEqual(
                1,
                HTTPLog.objects.filter(classifier=self.c1,
                                       org=self.org).count())
Ejemplo n.º 13
0
    def test_webhook_first(self, mock_send):
        mock_send.return_value = MockResponse(200, "{}")

        self.setupChannel()
        org = self.channel.org
        org.save()

        # set our very first action to be a webhook
        flow = self.get_flow('webhook_rule_first')

        # run a user through this flow
        flow.start([], [self.joe])
        event = WebHookEvent.objects.get()

        # make sure our contact still has a URN
        self.assertEqual(
            event.data['contact'], {
                'uuid': str(self.joe.uuid),
                'name': self.joe.name,
                'urn': six.text_type(self.joe.get_urn('tel'))
            })

        # make sure we don't have an input
        self.assertNotIn('input', event.data)
Ejemplo n.º 14
0
    def test_retry(self, mock_get):
        mock_get.side_effect = [
            MockResponse(429,
                         "<html>429 Too Many Requests</html>",
                         headers={"Content-Type": "text/html"}),
            MockResponse(429,
                         "<html>429 Too Many Requests</html>",
                         headers={"Content-Type": "text/html"}),
            MockResponse(429,
                         "<html>429 Too Many Requests</html>",
                         headers={"Content-Type": "text/html"}),
            MockResponse(429,
                         "<html>429 Too Many Requests</html>",
                         headers={"Content-Type": "text/html"}),
            MockResponse(200,
                         '{"count": 1, "numbers": ["12345"]}',
                         headers={"Content-Type": "application/json"}),
            MockResponse(200,
                         '{"count": 1, "numbers": ["23456"]}',
                         headers={"Content-Type": "application/json"}),
        ]

        # should retry twice and give up
        with self.assertRaises(vonage.ClientError):
            self.client.get_numbers()

        self.assertEqual(3, mock_get.call_count)
        mock_get.reset_mock()

        # should retry once and then succeed
        self.assertEqual(["12345"], self.client.get_numbers())
        self.assertEqual(2, mock_get.call_count)
        mock_get.reset_mock()

        # should succeed without any retries
        self.assertEqual(["23456"], self.client.get_numbers())
        self.assertEqual(1, mock_get.call_count)
Ejemplo n.º 15
0
    def test_claim(self):
        Channel.objects.all().delete()

        url = reverse("channels.types.whatsapp.claim")
        self.login(self.admin)

        response = self.client.get(reverse("channels.channel_claim"))
        self.assertNotContains(response, url)

        response = self.client.get(url)
        self.assertEqual(200, response.status_code)
        post_data = response.context["form"].initial

        post_data["number"] = "1234"
        post_data["username"] = "******"
        post_data["password"] = "******"
        post_data["country"] = "RW"
        post_data["base_url"] = "https://whatsapp.foo.bar"
        post_data["facebook_namespace"] = "my-custom-app"
        post_data["facebook_business_id"] = "1234"
        post_data["facebook_access_token"] = "token123"
        post_data["facebook_template_list_domain"] = "graph.facebook.com"

        # will fail with invalid phone number
        response = self.client.post(url, post_data)
        self.assertFormError(response, "form", None,
                             ["Please enter a valid phone number"])

        # valid number
        post_data["number"] = "0788123123"

        # try once with an error
        with patch("requests.post") as mock_post:
            mock_post.return_value = MockResponse(400, '{ "error": "true" }')
            response = self.client.post(url, post_data)
            self.assertEqual(200, response.status_code)
            self.assertFalse(Channel.objects.all())

            self.assertContains(response, "check username and password")

        # then FB failure
        with patch("requests.post") as mock_post:
            with patch("requests.get") as mock_get:
                mock_post.return_value = MockResponse(
                    200, '{"users": [{"token": "abc123"}]}')
                mock_get.return_value = MockResponse(400, '{"data": []}')

                response = self.client.post(url, post_data)
                self.assertEqual(200, response.status_code)
                self.assertFalse(Channel.objects.all())
                mock_get.assert_called_with(
                    "https://graph.facebook.com/v3.3/1234/message_templates",
                    params={"access_token": "token123"})

                self.assertContains(response, "check user id and access token")

        # then success
        with patch("requests.post") as mock_post:
            with patch("requests.get") as mock_get:
                mock_post.return_value = MockResponse(
                    200, '{"users": [{"token": "abc123"}]}')
                mock_get.return_value = MockResponse(200, '{"data": []}')

                response = self.client.post(url, post_data)
                self.assertEqual(302, response.status_code)

        channel = Channel.objects.get()

        self.assertEqual("temba", channel.config[Channel.CONFIG_USERNAME])
        self.assertEqual("tembapasswd",
                         channel.config[Channel.CONFIG_PASSWORD])
        self.assertEqual("abc123", channel.config[Channel.CONFIG_AUTH_TOKEN])
        self.assertEqual("https://whatsapp.foo.bar",
                         channel.config[Channel.CONFIG_BASE_URL])

        self.assertEqual("+250788123123", channel.address)
        self.assertEqual("RW", channel.country)
        self.assertEqual("WA", channel.channel_type)
        self.assertTrue(channel.get_type().has_attachment_support(channel))

        # test activating the channel
        with patch("requests.patch") as mock_patch:
            mock_patch.side_effect = [
                MockResponse(200, '{ "error": false }'),
                MockResponse(200, '{ "error": false }')
            ]
            WhatsAppType().activate(channel)
            self.assertEqual(
                mock_patch.call_args_list[0][1]["json"]["webhooks"]["url"],
                "https://%s%s" %
                (channel.org.get_brand_domain(),
                 reverse("courier.wa", args=[channel.uuid, "receive"])),
            )
            self.assertEqual(
                mock_patch.call_args_list[1][1]["json"]
                ["messaging_api_rate_limit"], ["15", "54600", "1000000"])

        with patch("requests.patch") as mock_patch:
            mock_patch.side_effect = [MockResponse(400, '{ "error": true }')]

            try:
                WhatsAppType().activate(channel)
                self.fail("Should have thrown error activating channel")
            except ValidationError:
                pass

        with patch("requests.patch") as mock_patch:
            mock_patch.side_effect = [
                MockResponse(200, '{ "error": "false" }'),
                MockResponse(400, '{ "error": "true" }'),
            ]

            try:
                WhatsAppType().activate(channel)
                self.fail("Should have thrown error activating channel")
            except ValidationError:
                pass

        # ok, test our refreshing
        refresh_url = reverse("channels.types.whatsapp.refresh",
                              args=[channel.uuid])
        resp = self.client.get(refresh_url)
        self.assertEqual(405, resp.status_code)

        with patch("requests.post") as mock_post:
            mock_post.side_effect = [MockResponse(200, '{ "error": false }')]
            self.assertFalse(
                channel.http_logs.filter(
                    log_type=HTTPLog.WHATSAPP_CONTACTS_REFRESHED,
                    is_error=False))
            self.create_contact("Joe", urn="whatsapp:250788382382")
            self.client.post(refresh_url)

            self.assertEqual(
                mock_post.call_args_list[0][1]["json"]["contacts"],
                ["+250788382382"])
            self.assertTrue(
                channel.http_logs.filter(
                    log_type=HTTPLog.WHATSAPP_CONTACTS_REFRESHED,
                    is_error=False))

        with patch("requests.post") as mock_post:
            mock_post.side_effect = [MockResponse(400, '{ "error": true }')]
            self.assertFalse(
                channel.http_logs.filter(
                    log_type=HTTPLog.WHATSAPP_CONTACTS_REFRESHED,
                    is_error=True))
            refresh_whatsapp_contacts(channel.id)
            self.assertTrue(
                channel.http_logs.filter(
                    log_type=HTTPLog.WHATSAPP_CONTACTS_REFRESHED,
                    is_error=True))

        # and fetching new tokens
        with patch("requests.post") as mock_post:
            mock_post.return_value = MockResponse(
                200, '{"users": [{"token": "abc345"}]}')
            self.assertFalse(
                channel.http_logs.filter(
                    log_type=HTTPLog.WHATSAPP_TOKENS_SYNCED, is_error=False))
            refresh_whatsapp_tokens()
            self.assertTrue(
                channel.http_logs.filter(
                    log_type=HTTPLog.WHATSAPP_TOKENS_SYNCED, is_error=False))
            channel.refresh_from_db()
            self.assertEqual("abc345",
                             channel.config[Channel.CONFIG_AUTH_TOKEN])

        with patch("requests.post") as mock_post:
            mock_post.side_effect = [MockResponse(400, '{ "error": true }')]
            self.assertFalse(
                channel.http_logs.filter(
                    log_type=HTTPLog.WHATSAPP_TOKENS_SYNCED, is_error=True))
            refresh_whatsapp_tokens()
            self.assertTrue(
                channel.http_logs.filter(
                    log_type=HTTPLog.WHATSAPP_TOKENS_SYNCED, is_error=True))
            channel.refresh_from_db()
            self.assertEqual("abc345",
                             channel.config[Channel.CONFIG_AUTH_TOKEN])

        with patch("requests.get") as mock_get:
            mock_get.side_effect = [
                MockResponse(
                    200,
                    """
            {
              "data": [
              {
                "name": "hello",
                "components": [
                  {
                    "type": "BODY",
                    "text": "Hello {{1}}"
                  }
                ],
                "language": "en",
                "status": "PENDING",
                "category": "ISSUE_RESOLUTION",
                "id": "1234"
              },
              {
                "name": "hello",
                "components": [
                  {
                    "type": "BODY",
                    "text": "Bonjour {{1}}"
                  }
                ],
                "language": "fr",
                "status": "APPROVED",
                "category": "ISSUE_RESOLUTION",
                "id": "5678"
              },
              {
                "name": "goodbye",
                "components": [
                  {
                    "type": "BODY",
                    "text": "Goodbye {{1}}, see you on {{2}}. See you later {{1}}"
                  }
                ],
                "language": "en",
                "status": "PENDING",
                "category": "ISSUE_RESOLUTION",
                "id": "9012"
              },
              {
                "name": "workout_activity",
                "components": [
                  {
                    "type": "HEADER",
                    "text": "Workout challenge week {{2}}, {{4}} extra points!"
                  },
                  {
                    "type": "BODY",
                    "text": "Hey {{1}}, Week {{2}} workout is out now. Get your discount of {{3}} for the next workout by sharing this program to 3 people."
                  },
                  {
                    "type": "FOOTER",
                    "text": "Remember to drink water."
                  }
                ],
                "language": "en",
                "status": "PENDING",
                "category": "ISSUE_RESOLUTION",
                "id": "9014"
              },
              {
                "name": "invalid_component",
                "components": [
                  {
                    "type": "RANDOM",
                    "text": "Bonjour {{1}}"
                  }
                ],
                "language": "fr",
                "status": "APPROVED",
                "category": "ISSUE_RESOLUTION",
                "id": "1233"
              },
              {
                "name": "invalid_status",
                "components": [
                  {
                    "type": "BODY",
                    "text": "This is an unknown status, it will be ignored"
                  }
                ],
                "language": "en",
                "status": "UNKNOWN",
                "category": "ISSUE_RESOLUTION",
                "id": "9012"
              },
              {
                "name": "invalid_language",
                "components": [
                  {
                    "type": "BODY",
                    "text": "This is an unknown language, it will be ignored"
                  }
                ],
                "language": "kli",
                "status": "APPROVED",
                "category": "ISSUE_RESOLUTION",
                "id": "9018"
              }
            ],
            "paging": {
              "cursors": {
              "before": "MAZDZD",
              "after": "MjQZD"
            }
            }
            }""",
                )
            ]
            refresh_whatsapp_templates()
            mock_get.assert_called_with(
                "https://graph.facebook.com/v3.3/1234/message_templates",
                params={
                    "access_token": "token123",
                    "limit": 255
                },
            )

            # should have 4 templates
            self.assertEqual(4, Template.objects.filter(org=self.org).count())
            self.assertEqual(
                5,
                TemplateTranslation.objects.filter(channel=channel).count())

            # hit our template page
            response = self.client.get(
                reverse("channels.types.whatsapp.templates",
                        args=[channel.uuid]))

            # should have our template translations
            self.assertContains(response, "Bonjour")
            self.assertContains(response, "Hello")
            self.assertContains(
                response,
                reverse("channels.types.whatsapp.sync_logs",
                        args=[channel.uuid]))

            ct = TemplateTranslation.objects.get(template__name="goodbye",
                                                 is_active=True)
            self.assertEqual(2, ct.variable_count)
            self.assertEqual(
                "Goodbye {{1}}, see you on {{2}}. See you later {{1}}",
                ct.content)
            self.assertEqual("eng", ct.language)
            self.assertEqual(TemplateTranslation.STATUS_PENDING, ct.status)
            self.assertEqual(
                "goodbye (eng) P: Goodbye {{1}}, see you on {{2}}. See you later {{1}}",
                str(ct))

            ct = TemplateTranslation.objects.get(
                template__name="workout_activity", is_active=True)
            self.assertEqual(4, ct.variable_count)
            self.assertEqual(
                "Workout challenge week {{2}}, {{4}} extra points!\n\nHey {{1}}, Week {{2}} workout is out now. Get your discount of {{3}} for the next workout by sharing this program to 3 people.\n\nRemember to drink water.",
                ct.content,
            )
            self.assertEqual("eng", ct.language)
            self.assertEqual(TemplateTranslation.STATUS_PENDING, ct.status)

            # assert that a template translation was created despite it being in an unknown language
            ct = TemplateTranslation.objects.get(
                template__name="invalid_language", is_active=True)
            self.assertEqual("kli", ct.language)
            self.assertEqual(TemplateTranslation.STATUS_UNSUPPORTED_LANGUAGE,
                             ct.status)

        # clear our FB ids, should cause refresh to be noop (but not fail)
        del channel.config[CONFIG_FB_BUSINESS_ID]
        channel.save(update_fields=["config", "modified_on"])
        refresh_whatsapp_templates()

        # deactivate our channel
        with self.settings(IS_PROD=True):
            channel.release()

        # all our templates should be inactive now
        self.assertEqual(
            5,
            TemplateTranslation.objects.filter(channel=channel,
                                               is_active=False).count())
Ejemplo n.º 16
0
    def test_nack(self):
        joe = self.create_contact("Joe", "+250788383383")
        self.create_group("Reporters", [joe])
        inbound = Msg.create_incoming(self.channel,
                                      "tel:+250788383383",
                                      "Send an inbound message",
                                      external_id='vumi-message-id')
        msg = inbound.reply("Test message", self.admin, trigger_send=False)

        # our outgoing message
        msg.refresh_from_db()
        r = get_redis_connection()

        try:
            settings.SEND_MESSAGES = True

            with patch('requests.put') as mock:
                mock.return_value = MockResponse(200,
                                                 '{ "message_id": "1515" }')

                # manually send it off
                Channel.send_message(
                    dict_to_struct('MsgStruct', msg.as_task_json()))

                # check the status of the message is now sent
                msg.refresh_from_db()
                self.assertEquals(WIRED, msg.status)
                self.assertTrue(msg.sent_on)
                self.assertEquals("1515", msg.external_id)
                self.assertEquals(1, mock.call_count)

                # should have a failsafe that it was sent
                self.assertTrue(
                    r.sismember(timezone.now().strftime(MSG_SENT_KEY),
                                str(msg.id)))

                # simulate Vumi calling back to us sending an NACK event
                data = {
                    "transport_name": "ussd_transport",
                    "event_type": "nack",
                    "nack_reason": "Unknown address.",
                    "event_id": six.text_type(uuid.uuid4()),
                    "timestamp": six.text_type(timezone.now()),
                    "message_version": "20110921",
                    "transport_metadata": {},
                    "user_message_id": msg.external_id,
                    "message_type": "event"
                }
                callback_url = reverse('handlers.vumi_handler',
                                       args=['event', self.channel.uuid])
                response = self.client.post(callback_url,
                                            json.dumps(data),
                                            content_type="application/json")

                self.assertEqual(response.status_code, 200)
                self.assertTrue(
                    self.create_contact("Joe", "+250788383383").is_stopped)

                self.clear_cache()
        finally:
            settings.SEND_MESSAGES = False
Ejemplo n.º 17
0
    def test_new_conversation_trigger(self):
        self.login(self.admin)
        flow = self.create_flow()
        flow2 = self.create_flow()

        # see if we list new conversation triggers on the trigger page
        create_trigger_url = reverse('triggers.trigger_create', args=[])
        response = self.client.get(create_trigger_url)
        self.assertNotContains(response, "conversation is started")

        # create a facebook channel
        fb_channel = Channel.add_facebook_channel(self.org, self.user, 'Temba', 1001, 'fb_token')

        # should now be able to create one
        response = self.client.get(create_trigger_url)
        self.assertContains(response, "conversation is started")

        # go create it
        with patch('requests.post') as mock_post:
            mock_post.return_value = MockResponse(200, '{"message": "Success"}')

            response = self.client.post(reverse('triggers.trigger_new_conversation', args=[]),
                                        data=dict(channel=fb_channel.id, flow=flow.id))
            self.assertEqual(response.status_code, 200)
            self.assertEqual(mock_post.call_count, 1)

        # check that it is right
        trigger = Trigger.objects.get(trigger_type=Trigger.TYPE_NEW_CONVERSATION, is_active=True, is_archived=False)
        self.assertEqual(trigger.channel, fb_channel)
        self.assertEqual(trigger.flow, flow)

        # try to create another one, fails as we already have a trigger for that channel
        response = self.client.post(reverse('triggers.trigger_new_conversation', args=[]), data=dict(channel=fb_channel.id, flow=flow2.id))
        self.assertEqual(response.status_code, 200)
        self.assertFormError(response, 'form', 'channel', 'Trigger with this Channel already exists.')

        # ok, trigger a facebook event
        data = json.loads("""{
        "object": "page",
          "entry": [
            {
              "id": "620308107999975",
              "time": 1467841778778,
              "messaging": [
                {
                  "sender":{
                    "id":"1001"
                  },
                  "recipient":{
                    "id":"%s"
                  },
                  "timestamp":1458692752478,
                  "postback":{
                    "payload":"get_started"
                  }
                }
              ]
            }
          ]
        }
        """ % fb_channel.address)

        with patch('requests.get') as mock_get:
            mock_get.return_value = MockResponse(200, '{"first_name": "Ben","last_name": "Haggerty"}')

            callback_url = reverse('handlers.facebook_handler', args=[fb_channel.uuid])
            response = self.client.post(callback_url, json.dumps(data), content_type="application/json")
            self.assertEqual(response.status_code, 200)

            # should have a new flow run for Ben
            contact = Contact.from_urn(self.org, 'facebook:1001')
            self.assertTrue(contact.name, "Ben Haggerty")

            run = FlowRun.objects.get(contact=contact)
            self.assertEqual(run.flow, flow)

        # archive our trigger, should unregister our callback
        with patch('requests.post') as mock_post:
            mock_post.return_value = MockResponse(200, '{"message": "Success"}')

            Trigger.apply_action_archive(self.admin, Trigger.objects.filter(pk=trigger.pk))
            self.assertEqual(response.status_code, 200)
            self.assertEqual(mock_post.call_count, 1)

            trigger.refresh_from_db()
            self.assertTrue(trigger.is_archived)
Ejemplo n.º 18
0
    def test_event_deliveries(self):
        sms = self.create_msg(contact=self.joe, direction='I', status='H', text="I'm gonna pop some tags")

        with patch('requests.Session.send') as mock:
            now = timezone.now()
            mock.return_value = MockResponse(200, "Hello World")

            # trigger an event, shouldnn't fire as we don't have a webhook
            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            self.assertFalse(WebHookEvent.objects.all())

        self.setupChannel()

        with patch('requests.Session.send') as mock:
            # clear out which events we listen for, we still shouldnt be notified though we have a webhook
            self.channel.org.webhook_events = 0
            self.channel.org.save()

            now = timezone.now()
            mock.return_value = MockResponse(200, "Hello World")

            # trigger an event, shouldnn't fire as we don't have a webhook
            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            self.assertFalse(WebHookEvent.objects.all())

        self.setupChannel()

        with patch('requests.Session.send') as mock:
            # remove all the org users
            self.org.administrators.clear()
            self.org.editors.clear()
            self.org.viewers.clear()

            mock.return_value = MockResponse(200, "Hello World")

            # trigger an event
            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEquals('F', event.status)
            self.assertEquals(0, event.try_count)
            self.assertFalse(event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertStringContains("No active user", result.message)
            self.assertEquals(0, result.status_code)

            self.assertFalse(mock.called)

            # what if they send weird json back?
            WebHookEvent.objects.all().delete()
            WebHookResult.objects.all().delete()

        # add ad manager back in
        self.org.administrators.add(self.admin)
        self.admin.set_org(self.org)

        with patch('requests.Session.send') as mock:
            mock.return_value = MockResponse(200, "Hello World")

            # trigger an event
            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEquals('C', event.status)
            self.assertEquals(1, event.try_count)
            self.assertFalse(event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertStringContains("Event delivered successfully", result.message)
            self.assertStringContains("not JSON", result.message)
            self.assertEquals(200, result.status_code)

            self.assertTrue(mock.called)

            WebHookEvent.objects.all().delete()
            WebHookResult.objects.all().delete()

        with patch('requests.Session.send') as mock:
            # valid json, but not our format
            bad_json = '{ "thrift_shops": ["Goodwill", "Value Village"] }'
            mock.return_value = MockResponse(200, bad_json)

            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEquals('C', event.status)
            self.assertEquals(1, event.try_count)
            self.assertFalse(event.next_attempt)

            self.assertTrue(mock.called)

            result = WebHookResult.objects.get()
            self.assertStringContains("Event delivered successfully", result.message)
            self.assertStringContains("ignoring", result.message)
            self.assertEquals(200, result.status_code)
            self.assertEquals(bad_json, result.body)

            WebHookEvent.objects.all().delete()
            WebHookResult.objects.all().delete()

        with patch('requests.Session.send') as mock:
            mock.return_value = MockResponse(200, '{ "phone": "+250788123123", "text": "I am success" }')

            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEquals('C', event.status)
            self.assertEquals(1, event.try_count)
            self.assertFalse(event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertEquals(200, result.status_code)

            self.assertTrue(mock.called)

            broadcast = Broadcast.objects.get()
            contact = Contact.get_or_create(self.org, self.admin, name=None, urns=["tel:+250788123123"], channel=self.channel)
            self.assertTrue("I am success", broadcast.text)
            self.assertTrue(contact, broadcast.contacts.all())

            self.assertTrue(mock.called)
            args = mock.call_args_list[0][0]
            prepared_request = args[0]
            self.assertEquals(self.org.get_webhook_url(), prepared_request.url)

            data = parse_qs(prepared_request.body)
            self.assertEquals(self.joe.get_urn(TEL_SCHEME).path, data['phone'][0])
            self.assertEquals(unicode(self.joe.get_urn(TEL_SCHEME)), data['urn'][0])
            self.assertEquals(self.joe.uuid, data['contact'][0])
            self.assertEquals(self.joe.name, data['contact_name'][0])
            self.assertEquals(sms.pk, int(data['sms'][0]))
            self.assertEquals(self.channel.pk, int(data['channel'][0]))
            self.assertEquals(SMS_RECEIVED, data['event'][0])
            self.assertEquals("I'm gonna pop some tags", data['text'][0])
            self.assertTrue('time' in data)

            WebHookEvent.objects.all().delete()
            WebHookResult.objects.all().delete()

        with patch('requests.Session.send') as mock:
            mock.return_value = MockResponse(500, "I am error")

            next_attempt_earliest = timezone.now() + timedelta(minutes=4)
            next_attempt_latest = timezone.now() + timedelta(minutes=6)

            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            self.assertEquals('E', event.status)
            self.assertEquals(1, event.try_count)
            self.assertTrue(event.next_attempt)
            self.assertTrue(next_attempt_earliest < event.next_attempt and next_attempt_latest > event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertStringContains("Error", result.message)
            self.assertEquals(500, result.status_code)
            self.assertEquals("I am error", result.body)

            # make sure things become failures after three retries
            event.try_count = 2
            event.deliver()
            event.save()

            self.assertTrue(mock.called)

            self.assertEquals('F', event.status)
            self.assertEquals(3, event.try_count)
            self.assertFalse(event.next_attempt)

            result = WebHookResult.objects.get()
            self.assertStringContains("Error", result.message)
            self.assertEquals(500, result.status_code)
            self.assertEquals("I am error", result.body)
            self.assertEquals("http://fake.com/webhook.php", result.url)
            self.assertTrue(result.data.find("pop+some+tags") > 0)

            # check out our api log
            response = self.client.get(reverse('api.log'))
            self.assertRedirect(response, reverse('users.user_login'))

            response = self.client.get(reverse('api.log_read', args=[event.pk]))
            self.assertRedirect(response, reverse('users.user_login'))

            WebHookEvent.objects.all().delete()
            WebHookResult.objects.all().delete()

        # add a webhook header to the org
        self.channel.org.webhook = u'{"url": "http://fake.com/webhook.php", "headers": {"X-My-Header": "foobar", "Authorization": "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="}, "method": "POST"}'
        self.channel.org.save()

        # check that our webhook settings have saved
        self.assertEquals('http://fake.com/webhook.php', self.channel.org.get_webhook_url())
        self.assertDictEqual({'X-My-Header': 'foobar', 'Authorization': 'Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=='}, self.channel.org.get_webhook_headers())

        with patch('requests.Session.send') as mock:
            mock.return_value = MockResponse(200, "Boom")
            WebHookEvent.trigger_sms_event(SMS_RECEIVED, sms, now)
            event = WebHookEvent.objects.get()

            result = WebHookResult.objects.get()
            # both headers should be in the json-encoded url string
            self.assertStringContains('X-My-Header: foobar', result.request)
            self.assertStringContains('Authorization: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==', result.request)
Ejemplo n.º 19
0
    def test_claim(self):
        Channel.objects.all().delete()

        url = reverse("channels.types.signalwire.claim")
        self.login(self.admin)

        response = self.client.get(reverse("channels.channel_claim"))
        self.assertNotContains(response, url)

        # change to LA timezone
        self.org.timezone = "America/Los_Angeles"
        self.org.save(update_fields=["timezone"])

        response = self.client.get(reverse("channels.channel_claim"))
        self.assertContains(response, url)

        response = self.client.get(url)
        self.assertEqual(200, response.status_code)
        post_data = response.context["form"].initial

        post_data["country"] = "US"
        post_data["number"] = "2065551212"
        post_data["domain"] = "temba.signalwire.com"
        post_data["project_key"] = "key123"
        post_data["api_token"] = "token123"

        # try once with an error
        with patch("requests.get") as mock_get:
            mock_get.return_value = MockResponse(400, '{ "error": "true" }')
            response = self.client.post(url, post_data)
            self.assertEqual(200, response.status_code)
            self.assertFalse(Channel.objects.all())

            self.assertContains(response, "Unable to connect to SignalWire")

        # then with missing number
        with patch("requests.get") as mock_get:
            mock_get.return_value = MockResponse(200, "{}")
            response = self.client.post(url, post_data)
            self.assertEqual(200, response.status_code)
            self.assertFalse(Channel.objects.all())
            self.assertContains(response, "Unable to find phone")

        # success this time
        with patch("requests.get") as mock_get:
            mock_get.return_value = MockResponse(
                200, '{"incoming_phone_numbers": [{"sid": "abc123", "phone_number": "+12065551212"}]}'
            )
            response = self.client.post(url, post_data)
            self.assertEqual(302, response.status_code)
            mock_get.assert_called_with(
                "https://temba.signalwire.com/api/laml/2010-04-01/Accounts/key123/IncomingPhoneNumbers.json",
                auth=("key123", "token123"),
            )

        channel = Channel.objects.get()

        self.assertEqual("https://temba.signalwire.com/api/laml", channel.config["base_url"])
        self.assertEqual("key123", channel.config["account_sid"])
        self.assertEqual("token123", channel.config["auth_token"])

        self.assertEqual("+12065551212", channel.address)
        self.assertEqual("US", channel.country)
        self.assertEqual("SW", channel.channel_type)

        # test activating the channel
        with patch("requests.get") as mock_get:
            mock_get.return_value = MockResponse(400, '{ "error": "true" }')
            with self.assertRaises(ValidationError):
                SignalWireType().activate(channel)

        # test activating the channel
        with patch("requests.get") as mock_get:
            mock_get.return_value = MockResponse(200, "{}")
            with self.assertRaises(ValidationError):
                SignalWireType().activate(channel)

        # failure registering
        with patch("requests.get") as mock_get:
            mock_get.return_value = MockResponse(
                200, '{"incoming_phone_numbers": [{"sid": "abc123", "phone_number": "+12065551212"}]}'
            )

            with patch("requests.post") as mock_post:
                mock_post.return_value = MockResponse(400, "{}")
                with self.assertRaises(ValidationError):
                    SignalWireType().activate(channel)

        # success registering
        with patch("requests.get") as mock_get:
            mock_get.return_value = MockResponse(
                200, '{"incoming_phone_numbers": [{"sid": "abc123", "phone_number": "+12065551212"}]}'
            )

            with patch("requests.post") as mock_post:
                mock_post.return_value = MockResponse(200, "{}")
                SignalWireType().activate(channel)

                self.assertEqual(
                    mock_post.mock_calls[0][1][0],
                    "https://temba.signalwire.com/api/laml/2010-04-01/Accounts/key123/IncomingPhoneNumbers/abc123.json",
                )

        # deactivate our channel
        with self.settings(IS_PROD=True):
            channel.release()
Ejemplo n.º 20
0
    def test_claim(self):
        Channel.objects.all().delete()

        url = reverse('channels.claim_whatsapp')
        self.login(self.admin)

        response = self.client.get(reverse('channels.channel_claim'))
        self.assertNotContains(response, url)

        response = self.client.get(url)
        self.assertEqual(200, response.status_code)
        post_data = response.context['form'].initial

        post_data['number'] = '1234'
        post_data['username'] = '******'
        post_data['password'] = '******'
        post_data['country'] = 'RW'
        post_data['base_url'] = 'https://whatsapp.foo.bar'

        # will fail with invalid phone number
        response = self.client.post(url, post_data)
        self.assertFormError(response, 'form', None,
                             ["Please enter a valid phone number"])

        # valid number
        post_data['number'] = '0788123123'

        # try once with an error
        with patch('requests.post') as mock_post:
            mock_post.return_value = MockResponse(400, '{ "error": "true" }')
            response = self.client.post(url, post_data)
            self.assertEqual(200, response.status_code)
            self.assertFalse(Channel.objects.all())

        # then success
        with patch('requests.post') as mock_post:
            mock_post.return_value = MockResponse(200, '{ "error": "false" }')
            response = self.client.post(url, post_data)
            self.assertEqual(302, response.status_code)

        channel = Channel.objects.get()

        self.assertEqual('temba', channel.config_json()['username'])
        self.assertEqual('tembapasswd', channel.config_json()['password'])
        self.assertEqual('https://whatsapp.foo.bar',
                         channel.config_json()['base_url'])

        self.assertEqual('+250788123123', channel.address)
        self.assertEqual('RW', channel.country)
        self.assertEqual('WA', channel.channel_type)

        # test activating the channel
        with patch('requests.post') as mock_post:
            mock_post.side_effect = [
                MockResponse(200, '{ "error": "false" }'),
                MockResponse(200, '{ "error": "false" }')
            ]
            WhatsAppType().activate(channel)
            self.assertEqual(
                mock_post.call_args_list[0][1]['json']['payload']
                ['set_settings']['webcallbacks']["1"], 'https://%s%s' %
                (channel.org.get_brand_domain(),
                 reverse('courier.wa', args=[channel.uuid, 'receive'])))
            self.assertEqual(
                mock_post.call_args_list[1][1]['json']['payload']
                ['set_allow_unsolicited_group_add'], False)

        with patch('requests.post') as mock_post:
            mock_post.side_effect = [MockResponse(400, '{ "error": "true" }')]

            try:
                WhatsAppType().activate(channel)
                self.fail("Should have thrown error activating channel")
            except ValidationError:
                pass

        with patch('requests.post') as mock_post:
            mock_post.side_effect = [
                MockResponse(200, '{ "error": "false" }'),
                MockResponse(400, '{ "error": "true" }')
            ]

            try:
                WhatsAppType().activate(channel)
                self.fail("Should have thrown error activating channel")
            except ValidationError:
                pass
Ejemplo n.º 21
0
    def test_flow_event(self, mock_send):
        self.setupChannel()

        org = self.channel.org
        org.save()

        flow = self.create_flow(definition=self.COLOR_FLOW_DEFINITION)

        # replace our uuid of 4 with the right thing
        actionset = ActionSet.objects.get(x=4)
        actionset.set_actions_dict(
            [WebhookAction(org.get_webhook_url()).as_json()])
        actionset.save()

        # run a user through this flow
        flow.start([], [self.joe])

        # have joe reply with mauve, which will put him in the other category that triggers the API Action
        sms = self.create_msg(contact=self.joe,
                              direction='I',
                              status='H',
                              text="Mauve",
                              attachments=[
                                  "image/jpeg:http://s3.com/text.jpg",
                                  "audio/mp4:http://s3.com/text.mp4"
                              ])

        mock_send.return_value = MockResponse(200, "{}")
        Flow.find_and_handle(sms)

        # should have one event created
        event = WebHookEvent.objects.get()

        self.assertEquals('C', event.status)
        self.assertEquals(1, event.try_count)
        self.assertFalse(event.next_attempt)

        result = WebHookResult.objects.get()
        self.assertIn("successfully", result.message)
        self.assertEquals(200, result.status_code)
        self.assertEqual(self.joe, result.contact)

        self.assertTrue(mock_send.called)

        args = mock_send.call_args_list[0][0]
        prepared_request = args[0]
        self.assertIn(self.channel.org.get_webhook_url(), prepared_request.url)

        data = parse_qs(prepared_request.body)

        self.assertEqual(data['channel'], [str(self.channel.id)])
        self.assertEqual(data['channel_uuid'], [self.channel.uuid])
        self.assertEqual(data['step'], [actionset.uuid])
        self.assertEqual(data['text'], ["Mauve"])
        self.assertEqual(data['attachments'],
                         ["http://s3.com/text.jpg", "http://s3.com/text.mp4"])
        self.assertEqual(data['flow'], [str(flow.id)])
        self.assertEqual(data['flow_uuid'], [flow.uuid])
        self.assertEqual(data['contact'], [self.joe.uuid])
        self.assertEqual(data['contact_name'], [self.joe.name])
        self.assertEqual(data['urn'], [six.text_type(self.joe.get_urn('tel'))])

        values = json.loads(data['values'][0])

        self.assertEquals('Other', values[0]['category']['base'])
        self.assertEquals('color', values[0]['label'])
        self.assertEquals('Mauve', values[0]['text'])
        self.assertTrue(values[0]['time'])
        self.assertTrue(data['time'])
Ejemplo n.º 22
0
 def test_settings_success(self, mock_request):
     mock_request.return_value = MockResponse(204, {})
     try:
         Client(self.secure_url, self.secret).settings("http://temba.io/mr/tickets/1234-5678")
     except ClientError:
         self.fail("The status 204 should not raise exceptions")
Ejemplo n.º 23
0
    def test_ivr_recording(self):

        # create our ivr setup
        self.org.connect_twilio("TEST_SID", "TEST_TOKEN", self.admin)
        self.org.save()
        self.import_file('capture_recording')
        flow = Flow.objects.filter(name='Capture Recording').first()

        # start our flow
        contact = self.create_contact('Chuck D', number='+13603621737')
        flow.start([], [contact])
        call = IVRCall.objects.filter(direction=OUTGOING).first()

        # after a call is picked up, twilio will call back to our server
        post_data = dict(CallSid='CallSid',
                         CallStatus='in-progress',
                         CallDuration=20)
        response = self.client.post(
            reverse('ivr.ivrcall_handle', args=[call.pk]), post_data)
        self.assertContains(
            response, '<Say>Please make a recording after the tone.</Say>')

        # simulate the caller making a recording and then hanging up, first they'll give us the
        # recording (they give us a call status of completed at the same time)
        from temba.tests import MockResponse
        with patch('requests.get') as response:
            mock1 = MockResponse(404, 'No such file')
            mock2 = MockResponse(200, 'Fake Recording Bits')
            mock2.add_header('Content-Type', 'audio/x-wav')
            response.side_effect = (mock1, mock2)

            self.client.post(
                reverse('ivr.ivrcall_handle', args=[call.pk]),
                dict(CallStatus='completed',
                     Digits='hangup',
                     RecordingUrl='http://api.twilio.com/ASID/Recordings/SID',
                     RecordingSid='FAKESID'))

        # we should have captured the recording, and ended the call
        call = IVRCall.objects.get(pk=call.pk)
        self.assertEquals(COMPLETED, call.status)

        # twilio will also send us a final completion message with the call duration (status of completed again)
        self.client.post(reverse('ivr.ivrcall_handle', args=[call.pk]),
                         dict(CallStatus='completed', CallDuration='15'))

        call = IVRCall.objects.get(pk=call.pk)
        self.assertEquals(COMPLETED, call.status)
        self.assertEquals(15, call.duration)

        messages = Msg.all_messages.filter(msg_type=IVR).order_by('pk')
        self.assertEquals(4, messages.count())
        self.assertEquals(4, self.org.get_credits_used())

        # we should have played a recording from the contact back to them
        outbound_msg = messages[1]
        self.assertTrue(outbound_msg.media.startswith('audio/x-wav:https://'))
        self.assertTrue(outbound_msg.media.endswith('.wav'))
        self.assertTrue(outbound_msg.text.startswith('https://'))
        self.assertTrue(outbound_msg.text.endswith('.wav'))

        media_msg = messages[2]
        self.assertTrue(media_msg.media.startswith('audio/x-wav:https://'))
        self.assertTrue(media_msg.media.endswith('.wav'))
        self.assertTrue(media_msg.text.startswith('https://'))
        self.assertTrue(media_msg.text.endswith('.wav'))

        (host, directory, filename) = media_msg.media.rsplit('/', 2)
        recording = '%s/%s/%s/media/%s/%s' % (settings.MEDIA_ROOT,
                                              settings.STORAGE_ROOT_DIR,
                                              self.org.pk, directory, filename)
        self.assertTrue(os.path.isfile(recording))

        from temba.flows.models import FlowStep
        steps = FlowStep.objects.all()
        self.assertEquals(4, steps.count())

        # each of our steps should have exactly one message
        for step in steps:
            self.assertEquals(
                1,
                step.messages.all().count(),
                msg="Step '%s' does not have excatly one message" % step)

        # each message should have exactly one step
        for msg in messages:
            self.assertEquals(
                1,
                msg.steps.all().count(),
                msg="Message '%s' is not attached to exaclty one step" %
                msg.text)
Ejemplo n.º 24
0
    def test_claim(self, mock_time_sleep):
        mock_time_sleep.return_value = None
        self.login(self.admin)

        # remove any existing channels
        self.org.channels.update(is_active=False, org=None)

        # make sure nexmo is on the claim page
        response = self.client.get(reverse('channels.channel_claim'))
        self.assertContains(response, "Nexmo")
        self.assertContains(response, reverse('orgs.org_nexmo_connect'))

        nexmo_config = dict(NEXMO_KEY='nexmo-key', NEXMO_SECRET='nexmo-secret', NEXMO_UUID='nexmo-uuid',
                            NEXMO_APP_ID='nexmo-app-id', NEXMO_APP_PRIVATE_KEY='nexmo-app-private-key')
        self.org.config = json.dumps(nexmo_config)
        self.org.save()

        # hit the claim page, should now have a claim nexmo link
        claim_nexmo = reverse('channels.claim_nexmo')
        response = self.client.get(reverse('channels.channel_claim'))
        self.assertContains(response, claim_nexmo)

        # try adding a shortcode
        with patch('requests.get') as nexmo_get:
            with patch('requests.post') as nexmo_post:
                nexmo_get.side_effect = [
                    MockResponse(200, '{"count":0,"numbers":[] }'),
                    MockResponse(200,
                                 '{"count":1,"numbers":[{"features": ["SMS"], "type":"mobile-lvn",'
                                 '"country":"US","msisdn":"8080"}] }'),
                    MockResponse(200,
                                 '{"count":1,"numbers":[{"features": ["SMS"], "type":"mobile-lvn",'
                                 '"country":"US","msisdn":"8080"}] }'),
                ]
                nexmo_post.return_value = MockResponse(200, '{"error-code": "200"}')
                response = self.client.post(claim_nexmo, dict(country='US', phone_number='8080'))
                self.assertRedirects(response, reverse('public.public_welcome') + "?success")
                channel = Channel.objects.filter(address='8080').first()
                self.assertTrue(Channel.ROLE_SEND in channel.role)
                self.assertTrue(Channel.ROLE_RECEIVE in channel.role)
                self.assertFalse(Channel.ROLE_ANSWER in channel.role)
                self.assertFalse(Channel.ROLE_CALL in channel.role)
                self.assertFalse(mock_time_sleep.called)
                Channel.objects.all().delete()

        # try buying a number not on the account
        with patch('requests.get') as nexmo_get:
            with patch('requests.post') as nexmo_post:
                nexmo_get.side_effect = [
                    MockResponse(200, '{"count":0,"numbers":[] }'),
                    MockResponse(200, '{"count":0,"numbers":[] }'),
                    MockResponse(200,
                                 '{"count":1,"numbers":[{"features": ["sms", "voice"], "type":"mobile",'
                                 '"country":"US","msisdn":"+12065551212"}] }'),
                ]
                nexmo_post.return_value = MockResponse(200, '{"error-code": "200"}')
                response = self.client.post(claim_nexmo, dict(country='US', phone_number='+12065551212'))
                self.assertRedirects(response, reverse('public.public_welcome') + "?success")
                channel = Channel.objects.filter(address='+12065551212').first()
                self.assertTrue(Channel.ROLE_SEND in channel.role)
                self.assertTrue(Channel.ROLE_RECEIVE in channel.role)
                self.assertTrue(Channel.ROLE_ANSWER in channel.role)
                self.assertTrue(Channel.ROLE_CALL in channel.role)
                Channel.objects.all().delete()

        # Try when we get 429 too many requests, we retry
        with patch('requests.get') as nexmo_get:
            with patch('requests.post') as nexmo_post:
                nexmo_get.side_effect = [
                    MockResponse(429, '{"error_code":429,"message":"max limit, retry later" }'),
                    MockResponse(200, '{"count":0,"numbers":[] }'),
                    MockResponse(429, '{"error_code":429,"message":"max limit, retry later" }'),
                    MockResponse(200, '{"count":0,"numbers":[] }'),
                    MockResponse(429, '{"error_code":429,"message":"max limit, retry later" }'),
                    MockResponse(200,
                                 '{"count":1,"numbers":[{"features": ["sms", "voice"], "type":"mobile",'
                                 '"country":"US","msisdn":"+12065551212"}] }'),
                ]
                nexmo_post.side_effect = [
                    MockResponse(429, '{"error_code":429,"message":"max limit, retry later" }'),
                    MockResponse(200, '{"error-code": "200"}'),
                    MockResponse(429, '{"error_code":429,"message":"max limit, retry later" }'),
                    MockResponse(200, '{"error-code": "200"}')
                ]
                response = self.client.post(claim_nexmo, dict(country='US', phone_number='+12065551212'))
                self.assertRedirects(response, reverse('public.public_welcome') + "?success")
                channel = Channel.objects.filter(address='+12065551212').first()
                self.assertTrue(Channel.ROLE_SEND in channel.role)
                self.assertTrue(Channel.ROLE_RECEIVE in channel.role)
                self.assertTrue(Channel.ROLE_ANSWER in channel.role)
                self.assertTrue(Channel.ROLE_CALL in channel.role)
                Channel.objects.all().delete()
                self.assertEqual(mock_time_sleep.call_count, 5)

        # try failing to buy a number not on the account
        with patch('requests.get') as nexmo_get:
            with patch('requests.post') as nexmo_post:
                nexmo_get.side_effect = [
                    MockResponse(200, '{"count":0,"numbers":[] }'),
                    MockResponse(200, '{"count":0,"numbers":[] }'),
                ]
                nexmo_post.side_effect = Exception('Error')
                response = self.client.post(claim_nexmo, dict(country='US', phone_number='+12065551212'))
                self.assertTrue(response.context['form'].errors)
                self.assertContains(response, "There was a problem claiming that number, "
                                              "please check the balance on your account. "
                                              "Note that you can only claim numbers after "
                                              "adding credit to your Nexmo account.")
                Channel.objects.all().delete()

        # let's add a number already connected to the account
        with patch('requests.get') as nexmo_get:
            with patch('requests.post') as nexmo_post:
                nexmo_get.return_value = MockResponse(200,
                                                      '{"count":1,"numbers":[{"features": ["SMS", "VOICE"], '
                                                      '"type":"mobile-lvn","country":"US","msisdn":"13607884540"}] }')
                nexmo_post.return_value = MockResponse(200, '{"error-code": "200"}')

                # make sure our number appears on the claim page
                response = self.client.get(claim_nexmo)
                self.assertFalse('account_trial' in response.context)
                self.assertContains(response, '360-788-4540')

                # claim it
                response = self.client.post(claim_nexmo, dict(country='US', phone_number='13607884540'))
                self.assertRedirects(response, reverse('public.public_welcome') + "?success")

                # make sure it is actually connected
                channel = Channel.objects.get(channel_type='NX', org=self.org)
                self.assertTrue(Channel.ROLE_SEND in channel.role)
                self.assertTrue(Channel.ROLE_RECEIVE in channel.role)
                self.assertTrue(Channel.ROLE_ANSWER in channel.role)
                self.assertTrue(Channel.ROLE_CALL in channel.role)

                # test the update page for nexmo
                update_url = reverse('channels.channel_update', args=[channel.pk])
                response = self.client.get(update_url)

                # try changing our address
                updated = response.context['form'].initial
                updated['address'] = 'MTN'
                updated['alert_email'] = '*****@*****.**'

                response = self.client.post(update_url, updated)
                channel = Channel.objects.get(pk=channel.id)

                self.assertEquals('MTN', channel.address)

                # add a canada number
                nexmo_get.return_value = MockResponse(200, '{"count":1,"numbers":[{"features": ["SMS", "VOICE"], "type":"mobile-lvn","country":"CA","msisdn":"15797884540"}] }')
                nexmo_post.return_value = MockResponse(200, '{"error-code": "200"}')

                # make sure our number appears on the claim page
                response = self.client.get(claim_nexmo)
                self.assertFalse('account_trial' in response.context)
                self.assertContains(response, '579-788-4540')

                # claim it
                response = self.client.post(claim_nexmo, dict(country='CA', phone_number='15797884540'))
                self.assertRedirects(response, reverse('public.public_welcome') + "?success")

                # make sure it is actually connected
                self.assertTrue(Channel.objects.filter(channel_type='NX', org=self.org, address='+15797884540').first())

                # as is our old one
                self.assertTrue(Channel.objects.filter(channel_type='NX', org=self.org, address='MTN').first())

                config_url = reverse('channels.channel_configuration', args=[channel.pk])
                response = self.client.get(config_url)
                self.assertEquals(200, response.status_code)

                self.assertContains(response, reverse('courier.nx', args=[channel.org.nexmo_uuid(), 'receive']))
                self.assertContains(response, reverse('courier.nx', args=[channel.org.nexmo_uuid(), 'status']))
                self.assertContains(response, reverse('handlers.nexmo_call_handler', args=['answer', channel.uuid]))

                call_handler_event_url = reverse('handlers.nexmo_call_handler', args=['event', channel.uuid])
                response = self.client.get(call_handler_event_url)

                self.assertEqual(response.status_code, 200)
                self.assertEqual(response.content, "")
Ejemplo n.º 25
0
    def test_send_ussd_continue_and_end_session(self):
        flow = self.get_flow('ussd_session_end')
        contact = self.create_contact("Joe", "+250788383383")

        try:
            settings.SEND_MESSAGES = True

            with patch('requests.post') as mock:
                mock.return_value = MockResponse(200, json.dumps({
                    'result': {
                        'message_id': '07033084-5cfd-4812-90a4-e4d24ffb6e3d',
                    }
                }))

                flow.start([], [contact])

                # our outgoing message
                msg = Msg.objects.filter(direction='O').order_by('id').last()

                self.assertEqual(msg.direction, 'O')
                self.assertTrue(msg.sent_on)
                self.assertEqual("07033084-5cfd-4812-90a4-e4d24ffb6e3d", msg.external_id)
                self.assertEqual(msg.connection.status, USSDSession.INITIATED)

                # reply and choose an option that doesn't have any destination thus needs to close the session
                USSDSession.handle_incoming(channel=self.channel, urn="+250788383383", content="4",
                                            date=timezone.now(), external_id="21345", message_id='vumi-message-id')
                # our outgoing message
                msg = Msg.objects.filter(direction='O').order_by('id').last()
                self.assertEqual(WIRED, msg.status)
                self.assertEqual(msg.direction, 'O')
                self.assertTrue(msg.sent_on)
                self.assertEqual("07033084-5cfd-4812-90a4-e4d24ffb6e3d", msg.external_id)
                self.assertEqual("vumi-message-id", msg.response_to.external_id)

                self.assertEqual(msg.connection.status, USSDSession.COMPLETED)
                self.assertTrue(isinstance(msg.connection.get_duration(), timedelta))

                self.assertEqual(2, mock.call_count)

                # first outbound (session continued)
                call = mock.call_args_list[0]
                (args, kwargs) = call
                payload = kwargs['json']
                self.assertIsNone(payload.get('reply_to'))
                self.assertEqual(payload.get('to'), "+250788383383")
                self.assertEqual(payload['channel_data'], {
                    'continue_session': True
                })

                # second outbound (session ended)
                call = mock.call_args_list[1]
                (args, kwargs) = call
                payload = kwargs['json']
                self.assertEqual(payload['reply_to'], 'vumi-message-id')
                self.assertEqual(payload.get('to'), None)
                self.assertEqual(payload['channel_data'], {
                    'continue_session': False
                })

                self.clear_cache()
        finally:
            settings.SEND_MESSAGES = False
Ejemplo n.º 26
0
    def test_connect(self, mock_get_host, mock_predict, mock_get_version_intents, mock_get_app):
        mock_get_host.return_value = "192.55.123.1"

        connect_url = reverse("classifiers.classifier_connect")
        response = self.client.get(connect_url)
        self.assertLoginRedirect(response)

        self.login(self.admin)
        response = self.client.get(connect_url)

        # should have url for claiming our type
        luis_url = reverse("classifiers.types.luis.connect")
        self.assertContains(response, luis_url)

        # will fail as we don't have anything filled out
        response = self.client.post(luis_url, {})
        self.assertFormError(response, "form", "name", ["This field is required."])
        self.assertFormError(response, "form", "app_id", ["This field is required."])
        self.assertFormError(response, "form", "authoring_endpoint", ["This field is required."])
        self.assertFormError(response, "form", "authoring_key", ["This field is required."])
        self.assertFormError(response, "form", "prediction_endpoint", ["This field is required."])
        self.assertFormError(response, "form", "prediction_key", ["This field is required."])
        self.assertFormError(response, "form", "slot", ["This field is required."])

        # simulate wrong authoring credentials
        mock_get_app.side_effect = RequestException(
            "Not authorized", response=MockResponse(401, '{ "error": "true" }')
        )

        response = self.client.post(
            luis_url,
            {
                "name": "Booker",
                "app_id": "12345",
                "authoring_endpoint": "http://nyaruka-authoring.luis.api",
                "authoring_key": "325253",
                "prediction_endpoint": "http://nyaruka.luis.api",
                "prediction_key": "456373",
                "slot": "staging",
            },
        )
        self.assertFormError(response, "form", "__all__", "Check authoring credentials: Not authorized")

        # simulate selected slot isn't published
        mock_get_app.side_effect = None
        mock_get_app.return_value = json.loads(GET_APP_RESPONSE)

        response = self.client.post(
            luis_url,
            {
                "name": "Booker",
                "app_id": "12345",
                "authoring_endpoint": "http://nyaruka-authoring.luis.api",
                "authoring_key": "325253",
                "prediction_endpoint": "http://nyaruka.luis.api",
                "prediction_key": "456373",
                "slot": "staging",
            },
        )
        self.assertFormError(response, "form", "__all__", "App has not yet been published to staging slot.")

        # simulate wrong prediction credentials
        mock_predict.side_effect = RequestException(
            "Not authorized", response=MockResponse(401, '{ "error": "true" }')
        )

        response = self.client.post(
            luis_url,
            {
                "name": "Booker",
                "app_id": "12345",
                "authoring_endpoint": "http://nyaruka-authoring.luis.api",
                "authoring_key": "325253",
                "prediction_endpoint": "http://nyaruka.luis.api",
                "prediction_key": "456373",
                "slot": "production",
            },
        )
        self.assertFormError(response, "form", "__all__", "Check prediction credentials: Not authorized")

        mock_get_version_intents.return_value = json.loads(GET_VERSION_INTENTS_RESPONSE)
        mock_predict.side_effect = None

        response = self.client.post(
            luis_url,
            {
                "name": "Booker",
                "app_id": "12345",
                "authoring_endpoint": "http://nyaruka-authoring.luis.api",
                "authoring_key": "325253",
                "prediction_endpoint": "http://nyaruka.luis.api",
                "prediction_key": "456373",
                "slot": "production",
            },
        )
        self.assertEqual(302, response.status_code)

        c = Classifier.objects.get()
        self.assertEqual("Booker", c.name)
        self.assertEqual("luis", c.classifier_type)
        self.assertEqual(
            {
                "app_id": "12345",
                "authoring_endpoint": "http://nyaruka-authoring.luis.api",
                "authoring_key": "325253",
                "prediction_endpoint": "http://nyaruka.luis.api",
                "prediction_key": "456373",
                "slot": "production",
            },
            c.config,
        )

        self.assertEqual(2, c.intents.all().count())
Ejemplo n.º 27
0
    def test_refresh_jiochat_tokens(self, mock_post):
        channel = Channel.create(
            self.org,
            self.user,
            None,
            "JC",
            None,
            "1212",
            config={
                "jiochat_app_id": "app-id",
                "jiochat_app_secret": "app-secret",
                "secret": Channel.generate_secret(32),
            },
            uuid="00000000-0000-0000-0000-000000001234",
        )

        mock_post.return_value = MockResponse(400, '{ "error":"Failed" }')

        self.assertFalse(ChannelLog.objects.all())
        refresh_jiochat_access_tokens()

        self.assertEqual(ChannelLog.objects.all().count(), 1)
        self.assertTrue(ChannelLog.objects.filter(is_error=True).count(), 1)

        self.assertEqual(mock_post.call_count, 1)

        channel_client = JiochatClient.from_channel(channel)

        self.assertIsNone(channel_client.get_access_token())

        mock_post.reset_mock()
        mock_post.return_value = MockResponse(200,
                                              '{ "access_token":"ABC1234" }')

        refresh_jiochat_access_tokens()

        self.assertEqual(ChannelLog.objects.all().count(), 2)
        self.assertTrue(ChannelLog.objects.filter(is_error=True).count(), 1)
        self.assertTrue(ChannelLog.objects.filter(is_error=False).count(), 1)
        self.assertEqual(mock_post.call_count, 1)

        self.assertEqual(channel_client.get_access_token(), b"ABC1234")
        self.assertEqual(
            mock_post.call_args_list[0][1]["data"],
            {
                "client_secret": u"app-secret",
                "grant_type": "client_credentials",
                "client_id": u"app-id"
            },
        )
        self.login(self.admin)
        response = self.client.get(reverse("channels.channellog_list") +
                                   "?channel=%d&others=1" % channel.id,
                                   follow=True)
        self.assertEqual(len(response.context["object_list"]), 2)

        mock_post.reset_mock()
        mock_post.return_value = MockResponse(200,
                                              '{ "access_token":"ABC1235" }')

        Channel.refresh_all_jiochat_access_token(channel.id)

        self.assertEqual(ChannelLog.objects.all().count(), 3)
        self.assertTrue(ChannelLog.objects.filter(is_error=True).count(), 1)
        self.assertTrue(ChannelLog.objects.filter(is_error=False).count(), 1)
        self.assertEqual(mock_post.call_count, 1)

        self.assertEqual(channel_client.get_access_token(), b"ABC1235")
        self.assertEqual(
            mock_post.call_args_list[0][1]["data"],
            {
                "client_secret": u"app-secret",
                "grant_type": "client_credentials",
                "client_id": u"app-id"
            },
        )
        self.login(self.admin)
        response = self.client.get(reverse("channels.channellog_list") +
                                   "?channel=%d&others=1" % channel.id,
                                   follow=True)
        self.assertEqual(len(response.context["object_list"]), 3)
Ejemplo n.º 28
0
    def test_contact_modify(self):
        with patch("requests.post") as mock_post:
            mock_post.return_value = MockResponse(
                200,
                """{
                    "1": {
                        "contact": {
                            "uuid": "6393abc0-283d-4c9b-a1b3-641a035c34bf",
                            "id": 1,
                            "name": "Frank",
                            "timezone": "America/Los_Angeles",
                            "created_on": "2018-07-06T12:30:00.123457Z"
                        },
                        "events": [
                            {
                                "type": "contact_groups_changed",
                                "created_on": "2018-07-06T12:30:03.123456789Z",
                                "groups_added": [
                                    {
                                        "uuid": "c153e265-f7c9-4539-9dbc-9b358714b638",
                                        "name": "Doctors"
                                    }
                                ]
                            }
                        ]
                    }
                }
                """,
            )

            response = get_client().contact_modify(
                1,
                1,
                [1],
                {
                    "type":
                    "groups",
                    "modification":
                    "add",
                    "groups": [{
                        "uuid": "c153e265-f7c9-4539-9dbc-9b358714b638",
                        "name": "Doctors"
                    }],
                },
            )
            self.assertEqual("6393abc0-283d-4c9b-a1b3-641a035c34bf",
                             response["1"]["contact"]["uuid"])
            mock_post.assert_called_once_with(
                "http://localhost:8090/mr/contact/modify",
                headers={"User-Agent": "Temba"},
                json={
                    "org_id": 1,
                    "user_id": 1,
                    "contact_ids": [1],
                    "modifiers": {
                        "type":
                        "groups",
                        "modification":
                        "add",
                        "groups": [{
                            "uuid": "c153e265-f7c9-4539-9dbc-9b358714b638",
                            "name": "Doctors"
                        }],
                    },
                },
            )
Ejemplo n.º 29
0
    def test_release(self, mock_post):
        mock_post.side_effect = [MockResponse(200, json.dumps({'status': 0, 'status_message': "ok"}))]
        self.channel.release()

        self.assertEqual(mock_post.call_args[0][0], 'https://chatapi.viber.com/pa/set_webhook')
Ejemplo n.º 30
0
    def test_claim_self_hosted_templates(self):
        Channel.objects.all().delete()

        url = reverse("channels.types.whatsapp.claim")
        self.login(self.admin)

        response = self.client.get(reverse("channels.channel_claim"))
        self.assertNotContains(response, url)

        response = self.client.get(url)
        self.assertEqual(200, response.status_code)
        post_data = response.context["form"].initial

        post_data["number"] = "0788123123"
        post_data["username"] = "******"
        post_data["password"] = "******"
        post_data["country"] = "RW"
        post_data["base_url"] = "https://whatsapp.foo.bar"
        post_data["facebook_namespace"] = "my-custom-app"
        post_data["facebook_business_id"] = "1234"
        post_data["facebook_access_token"] = "token123"
        post_data["facebook_template_list_domain"] = "example.org"

        # success claim
        with patch("requests.post") as mock_post:
            with patch("requests.get") as mock_get:

                mock_post.return_value = MockResponse(
                    200, '{"users": [{"token": "abc123"}]}')
                mock_get.return_value = MockResponse(200, '{"data": []}')

                response = self.client.post(url, post_data)
                self.assertEqual(302, response.status_code)
                mock_get.assert_called_with(
                    "https://example.org/v3.3/1234/message_templates",
                    params={"access_token": "token123"})

        # test the template syncing task calls the correct domain
        with patch("requests.get") as mock_get:
            mock_get.return_value = MockResponse(200, '{"data": []}')
            refresh_whatsapp_templates()
            mock_get.assert_called_with(
                "https://example.org/v3.3/1234/message_templates",
                params={
                    "access_token": "token123",
                    "limit": 255
                })

        channel = Channel.objects.get()

        self.assertEqual("example.org",
                         channel.config[CONFIG_FB_TEMPLATE_LIST_DOMAIN])
        self.assertEqual(
            1,
            channel.http_logs.filter(
                log_type=HTTPLog.WHATSAPP_TEMPLATES_SYNCED).count())
        self.assertEqual(
            1,
            channel.http_logs.filter(
                log_type=HTTPLog.WHATSAPP_TEMPLATES_SYNCED).count())

        # hit our sync logs page
        response = self.client.get(
            reverse("channels.types.whatsapp.sync_logs", args=[channel.uuid]))

        # should have our log
        self.assertContains(response, "Whatsapp Templates Synced")
        self.assertContains(
            response,
            reverse("channels.types.whatsapp.templates", args=[channel.uuid]))

        sync_log = channel.http_logs.filter(
            log_type=HTTPLog.WHATSAPP_TEMPLATES_SYNCED).first()
        log_url = reverse("request_logs.httplog_read", args=[sync_log.id])
        self.assertContains(response, log_url)

        response = self.client.get(log_url)
        self.assertContains(response, "200")
        self.assertContains(response,
                            "https://example.org/v3.3/1234/message_templates")

        with patch("requests.get") as mock_get:
            # use fake response to simulate the exception request
            # See https://github.com/psf/requests/blob/eedd67462819f8dbf8c1c32e77f9070606605231/requests/exceptions.py#L17
            mock_get.side_effect = RequestException("Network is unreachable",
                                                    response=MockResponse(
                                                        100, ""))
            refresh_whatsapp_templates()

        sync_log = channel.http_logs.filter(
            log_type=HTTPLog.WHATSAPP_TEMPLATES_SYNCED, is_error=True).first()
        log_url = reverse("request_logs.httplog_read", args=[sync_log.id])
        response = self.client.get(log_url)
        self.assertContains(response, "Connection Error")
        self.assertContains(response,
                            "https://example.org/v3.3/1234/message_templates")