From a82c43681915e9c68f3c3b0b87d93acb0900c057 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Wed, 29 Apr 2020 11:05:38 +0200
Subject: [PATCH] sms: raise on recoverable errors (#42230)

---
 passerelle/apps/choosit/models.py |  4 ++--
 passerelle/apps/mobyt/models.py   |  4 ++--
 passerelle/apps/orange/models.py  | 13 +++++++++----
 passerelle/apps/ovh/models.py     |  4 ++--
 passerelle/apps/oxyd/models.py    |  4 ++--
 passerelle/utils/jsonresponse.py  |  5 +++++
 tests/test_orange.py              | 14 +++++++++++++-
 7 files changed, 35 insertions(+), 13 deletions(-)

diff --git a/passerelle/apps/choosit/models.py b/passerelle/apps/choosit/models.py
index a4678dcd..75843a0c 100644
--- a/passerelle/apps/choosit/models.py
+++ b/passerelle/apps/choosit/models.py
@@ -1,17 +1,17 @@
 # -*- coding: utf-8 -*-
 import json
 import requests
 
 from django.utils.six import string_types
 from django.utils.translation import ugettext_lazy as _
 from django.db import models
 
-from passerelle.utils.jsonresponse import APIError
+from passerelle.utils.jsonresponse import APIError, APIRecoverableError
 from passerelle.base.models import SMSResource
 
 
 class ChoositSMSGateway(SMSResource):
     key = models.CharField(verbose_name=_('Key'), max_length=64)
     default_country_code = models.CharField(verbose_name=_('Default country code'), max_length=3,
                                             default=u'33')
     default_trunk_prefix = models.CharField(verbose_name=_('Default trunk prefix'), max_length=2,
@@ -86,17 +86,17 @@ class ChoositSMSGateway(SMSResource):
                 'key': self.key,
                 'recipient': dest,
                 'content': text[:160],
             }
             data = {'data': json.dumps(params)}
             try:
                 r = self.requests.post(self.URL, data=data)
             except requests.RequestException as e:
-                results.append('Choosit error: %s' % e)
+                raise APIRecoverableError('Choosit error: POST failed, %s' % e)
             else:
                 try:
                     output = r.json()
                 except ValueError as e:
                     results.append('Choosit error: bad JSON response')
                 else:
                     if not isinstance(output, dict):
                         results.append('Choosit error: JSON response is not a dict %r' % output)
diff --git a/passerelle/apps/mobyt/models.py b/passerelle/apps/mobyt/models.py
index 4983b9d7..0c39b952 100644
--- a/passerelle/apps/mobyt/models.py
+++ b/passerelle/apps/mobyt/models.py
@@ -1,14 +1,14 @@
 from django.utils.translation import ugettext_lazy as _
 from django.db import models
 
 import requests
 
-from passerelle.utils.jsonresponse import APIError
+from passerelle.utils.jsonresponse import APIError, APIRecoverableError
 from passerelle.base.models import SMSResource
 
 
 class MobytSMSGateway(SMSResource):
     URL = 'http://multilevel.mobyt.fr/sms/batch.php'
     MESSAGES_QUALITIES = (
         ('l', _('sms direct')),
         ('ll', _('sms low-cost')),
@@ -61,12 +61,12 @@ class MobytSMSGateway(SMSResource):
             'rcpt': rcpt,
             'data': text,
             'sender': sender,
             'qty': self.quality,
         }
         try:
             r = self.requests.post(self.URL, data=params)
         except requests.RequestException as e:
-            raise APIError('MobyT error: POST failed, %s' % e)
+            raise APIRecoverableError('MobyT error: POST failed, %s' % e)
         if r.content[:2] != "OK":
             raise APIError('MobyT error: response is not "OK"')
         return None
diff --git a/passerelle/apps/orange/models.py b/passerelle/apps/orange/models.py
index e1b17613..1dc4f63d 100644
--- a/passerelle/apps/orange/models.py
+++ b/passerelle/apps/orange/models.py
@@ -14,21 +14,23 @@
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU Affero General Public License for more details.
 #
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
+import requests
+
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
 
 from passerelle.base.models import SMSResource
-from passerelle.utils.jsonresponse import APIError
+from passerelle.utils.jsonresponse import APIError, APIRecoverableError
 
 BASE_API = 'https://contact-everyone.orange-business.com/api/v1.2/'
 URL_TOKEN = BASE_API + 'oauth/token'
 URL_GROUPS = BASE_API + 'groups'
 URL_DIFFUSION = BASE_API + 'groups/%s/diffusion-requests'
 
 
 class OrangeError(APIError):
@@ -105,12 +107,15 @@ class OrangeSMSGateway(SMSResource):
             raise OrangeError('Orange fails to send SMS: %s, %s' % (
                 response.status_code, response.text))
         return get_json(response)
 
     def send_msg(self, text, sender, destinations, **kwargs):
         '''Send a SMS using the Orange provider'''
         destinations = self.clean_numbers(
             destinations, self.default_country_code, self.default_trunk_prefix)
-        access_token = self.get_access_token()
-        group_id = self.group_id_from_name(access_token)
-        response = self.diffusion(access_token, group_id, destinations, text)
+        try:
+            access_token = self.get_access_token()
+            group_id = self.group_id_from_name(access_token)
+            response = self.diffusion(access_token, group_id, destinations, text)
+        except requests.RequestException as exc:
+            raise APIRecoverableError('Orange error: POST failed, %s' % exc)
         return response
diff --git a/passerelle/apps/ovh/models.py b/passerelle/apps/ovh/models.py
index d9d96135..7494f247 100644
--- a/passerelle/apps/ovh/models.py
+++ b/passerelle/apps/ovh/models.py
@@ -1,15 +1,15 @@
 import requests
 
 from django.utils.translation import ugettext_lazy as _
 from django.db import models
 from django.utils.encoding import force_text
 
-from passerelle.utils.jsonresponse import APIError
+from passerelle.utils.jsonresponse import APIError, APIRecoverableError
 from passerelle.base.models import SMSResource
 
 
 class OVHSMSGateway(SMSResource):
     URL = 'https://www.ovh.com/cgi-bin/sms/http2sms.cgi'
     MESSAGES_CLASSES = (
         (0, _('Message are directly shown to users on phone screen '
               'at reception. The message is never stored, neither in the '
@@ -98,17 +98,17 @@ class OVHSMSGateway(SMSResource):
             'contentType': 'text/json',
             'class': self.msg_class,
         }
         if not kwargs['stop']:
             params.update({'noStop': 1})
         try:
             response = self.requests.post(self.URL, data=params)
         except requests.RequestException as e:
-            raise APIError('OVH error: POST failed, %s' % e)
+            raise APIRecoverableError('OVH error: POST failed, %s' % e)
         else:
             try:
                 result = response.json()
             except ValueError as e:
                 raise APIError('OVH error: bad JSON response')
             else:
                 if not isinstance(result, dict):
                     raise APIError('OVH error: bad JSON response %r, it should be a dictionnary' %
diff --git a/passerelle/apps/oxyd/models.py b/passerelle/apps/oxyd/models.py
index 5e945b16..c7215948 100644
--- a/passerelle/apps/oxyd/models.py
+++ b/passerelle/apps/oxyd/models.py
@@ -1,14 +1,14 @@
 import requests
 
 from django.db import models
 from django.utils.encoding import force_text
 
-from passerelle.utils.jsonresponse import APIError
+from passerelle.utils.jsonresponse import APIError, APIRecoverableError
 from passerelle.base.models import SMSResource
 from django.utils.translation import ugettext_lazy as _
 
 
 class OxydSMSGateway(SMSResource):
     username = models.CharField(verbose_name=_('Username'), max_length=64)
     password = models.CharField(verbose_name=_('Password'), max_length=64)
     default_country_code = models.CharField(verbose_name=_('Default country code'), max_length=3,
@@ -64,17 +64,17 @@ class OxydSMSGateway(SMSResource):
                 'pass': self.password,
                 'num': dest,
                 'sms': text.encode('utf-8'),
                 'flash': '0',
             }
             try:
                 r = self.requests.post(self.URL, params)
             except requests.RequestException as e:
-                results.append('OXYD error: POST failed, %s' % e)
+                raise APIRecoverableError('OXYD error: POST failed, %s' % e)
             else:
                 code = r.content and r.content.split()[0]
                 if force_text(code) != '200':
                     results.append('OXYD error: response is not 200')
                 else:
                     results.append(0)
         if any(results):
             raise APIError(
diff --git a/passerelle/utils/jsonresponse.py b/passerelle/utils/jsonresponse.py
index 3b9062ef..59dc1bb8 100644
--- a/passerelle/utils/jsonresponse.py
+++ b/passerelle/utils/jsonresponse.py
@@ -35,16 +35,21 @@ class APIError(RuntimeError):
     log_error = False
     http_status = 200
 
     def __init__(self, *args, **kwargs):
         self.__dict__.update(kwargs)
         super(APIError, self).__init__(*args)
 
 
+class APIRecoverableError(APIError):
+    ''''Exception to raise when call on remote application should be retry later'''
+    err = 2
+
+
 class JSONEncoder(DjangoJSONEncoder):
     def default(self, o):
         if isinstance(o, time.struct_time):
             o = datetime.datetime(*tuple(o)[:6])
         return super(JSONEncoder, self).default(o)
 
 
 class to_json(object):
diff --git a/tests/test_orange.py b/tests/test_orange.py
index 80ea6b13..e803d88f 100644
--- a/tests/test_orange.py
+++ b/tests/test_orange.py
@@ -11,26 +11,27 @@
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU Affero General Public License for more details.
 #
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import json
+import requests
 
 import httmock
 import pytest
 
 from django.contrib.contenttypes.models import ContentType
 from django.utils.encoding import force_text
 
 from passerelle.apps.orange.models import OrangeSMSGateway, OrangeError
 from passerelle.base.models import ApiUser, AccessRight
-from passerelle.utils.jsonresponse import APIError
+from passerelle.utils.jsonresponse import APIError, APIRecoverableError
 
 
 NETLOC = 'contact-everyone.orange-business.com'
 JSON_HEADERS = {'content-type': 'application/json'}
 PAYLOAD = {
     'message': 'hello',
     'from': '+33699999999',
     'to': ['+33688888888', '+33677777777'],
@@ -65,16 +66,20 @@ def response_diffusion_ok(url, request):
 @httmock.urlmatch(netloc=NETLOC)
 def response_500(url, request):
     return httmock.response(500, 'my_error')
 
 @httmock.urlmatch(netloc=NETLOC)
 def response_invalid_json(url, request):
     return httmock.response(200, 'not a JSON content')
 
+@httmock.urlmatch(netloc=NETLOC)
+def request_exception(url, request):
+    raise requests.RequestException('my_error')
+
 def setup_access_rights(obj):
     api = ApiUser.objects.create(username='all',
                                  keytype='', key='')
     obj_type = ContentType.objects.get_for_model(obj)
     AccessRight.objects.create(codename='can_send_messages', apiuser=api,
                                resource_type=obj_type, resource_pk=obj.pk)
     return obj
 
@@ -168,8 +173,15 @@ def test_send_msg(app, connector):
     assert not resp.json['err']
     assert resp.json['data']['status'] == "I'm ok"
 
     # not 201
     with httmock.HTTMock(response_token_ok, response_group_ok, response_500):
         resp = app.post_json(url, params=PAYLOAD, status=200)
     assert resp.json['err']
     assert resp.json['err_desc'] == 'Orange fails to send SMS: 500, my_error'
+
+    # RequestException
+    with httmock.HTTMock(request_exception):
+        resp = app.post_json(url, params=PAYLOAD, status=200)
+    assert resp.json['err']
+    assert resp.json['err_desc'] == 'Orange error: POST failed, my_error'
+    assert 'APIRecoverableError' in resp.json['err_class']
-- 
2.26.2

