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!"] } }, )
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)
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']))
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"]))
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"]))
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"]))
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)
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
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)
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" }, }, )
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)
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())
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)
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)
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())
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
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)
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)
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()
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
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'])
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")
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)
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, "")
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
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())
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)
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" }], }, }, )
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')
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")