Projet

Général

Profil

0001-sms-send-SMS-asynchronously-21465.patch

Nicolas Roche, 29 avril 2020 15:10

Télécharger (11,4 ko)

Voir les différences:

Subject: [PATCH] sms: send SMS asynchronously (#21465)

 passerelle/base/models.py | 18 +++++++++++---
 tests/test_orange.py      | 31 +++++++++++++++---------
 tests/test_sms.py         | 50 ++++++++++++++++++++++++++++++++-------
 tests/utils.py            |  4 +++-
 4 files changed, 79 insertions(+), 24 deletions(-)
passerelle/base/models.py
35 35
from model_utils.managers import InheritanceManager as ModelUtilsInheritanceManager
36 36

  
37 37
import jsonfield
38 38

  
39 39
import passerelle
40 40
import requests
41 41
from passerelle.compat import json_loads
42 42
from passerelle.utils.api import endpoint
43
from passerelle.utils.jsonresponse import APIError
43
from passerelle.utils.jsonresponse import APIError, APIRecoverableError
44 44

  
45 45
KEYTYPE_CHOICES = (
46 46
    ('API', _('API Key')),
47 47
    ('SIGN', _('HMAC Signature')),
48 48
)
49 49

  
50 50
BASE_EXPORT_FIELDS = (models.TextField, models.CharField, models.SlugField,
51 51
                      models.URLField, models.BooleanField, models.IntegerField,
......
957 957
            assert all(map(lambda x: isinstance(x, six.text_type), data['to'])), \
958 958
                'to is not a list of strings'
959 959
        except (ValueError, AssertionError) as e:
960 960
            raise APIError('Payload error: %s' % e)
961 961
        data['message'] = data['message'][:self.max_message_length]
962 962
        logging.info('sending message %r to %r with sending number %r',
963 963
                     data['message'], data['to'], data['from'])
964 964
        stop = not bool('nostop' in request.GET)
965
        result = {'data': self.send_msg(data['message'], data['from'], data['to'], stop=stop)}
965
        self.add_job('send_job',
966
                     text=data['message'], sender=data['from'], destinations=data['to'],
967
                     stop=stop)
968
        return {'err': 0}
969

  
970
    def send_job(self, *args, **kwargs):
971
        try:
972
            self.send_msg(**kwargs)
973
        except APIRecoverableError as exc:
974
            logging.info('recoverable error on message %r to %r with sending number %r: %s',
975
                         kwargs['text'], kwargs['sender'], kwargs['destinations'], exc)
976
            raise SkipJob(after_timestamp=3600 * 1)
977
        except APIError:
978
            raise  # not recoverable
966 979
        SMSLog.objects.create(appname=self.get_connector_slug(), slug=self.slug)
967
        return result
968 980

  
969 981
    class Meta:
970 982
        abstract = True
971 983

  
972 984

  
973 985
@six.python_2_unicode_compatible
974 986
class SMSLog(models.Model):
975 987
    timestamp = models.DateTimeField(auto_now_add=True)
tests/test_orange.py
20 20

  
21 21
import httmock
22 22
import pytest
23 23

  
24 24
from django.contrib.contenttypes.models import ContentType
25 25
from django.utils.encoding import force_text
26 26

  
27 27
from passerelle.apps.orange.models import OrangeSMSGateway, OrangeError
28
from passerelle.base.models import ApiUser, AccessRight
28
from passerelle.base.models import ApiUser, AccessRight, Job
29 29
from passerelle.utils.jsonresponse import APIError, APIRecoverableError
30 30

  
31 31

  
32 32
NETLOC = 'contact-everyone.orange-business.com'
33 33
JSON_HEADERS = {'content-type': 'application/json'}
34 34
PAYLOAD = {
35 35
    'message': 'hello',
36 36
    'from': '+33699999999',
......
163 163

  
164 164
    with pytest.raises(OrangeError, match='Orange returned Invalid JSON content'):
165 165
        with httmock.HTTMock(mocked_response):
166 166
            orange.diffusion('my_token', 'gid2', PAYLOAD['to'], PAYLOAD['message'])
167 167

  
168 168

  
169 169
def test_send_msg(app, connector):
170 170
    url = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug)
171
    with httmock.HTTMock(response_token_ok, response_group_ok, response_diffusion_ok):
172
        resp = app.post_json(url, params=PAYLOAD, status=200)
171
    assert Job.objects.count() == 0
172
    resp = app.post_json(url, params=PAYLOAD, status=200)
173 173
    assert not resp.json['err']
174
    assert resp.json['data']['status'] == "I'm ok"
174
    assert Job.objects.count() == 1
175
    with httmock.HTTMock(response_token_ok, response_group_ok, response_diffusion_ok):
176
        connector.jobs()
177
    assert Job.objects.all()[0].status == 'completed'
175 178

  
176 179
    # not 201
180
    resp = app.post_json(url, params=PAYLOAD, status=200)
181
    assert not resp.json['err']
182
    assert Job.objects.count() == 2
177 183
    with httmock.HTTMock(response_token_ok, response_group_ok, response_500):
178
        resp = app.post_json(url, params=PAYLOAD, status=200)
179
    assert resp.json['err']
180
    assert resp.json['err_desc'] == 'Orange fails to send SMS: 500, my_error'
184
        connector.jobs()
185
    job = Job.objects.all()[1]
186
    assert job.status == 'failed'
187
    assert 'Orange fails to send SMS: 500, my_error' in job.status_details['error_summary']
181 188

  
182 189
    # RequestException
190
    resp = app.post_json(url, params=PAYLOAD, status=200)
191
    assert not resp.json['err']
192
    assert Job.objects.count() == 3
183 193
    with httmock.HTTMock(request_exception):
184
        resp = app.post_json(url, params=PAYLOAD, status=200)
185
    assert resp.json['err']
186
    assert resp.json['err_desc'] == 'Orange error: POST failed, my_error'
187
    assert 'APIRecoverableError' in resp.json['err_class']
194
        connector.jobs()
195
    job = Job.objects.all()[2]
196
    assert job.status == 'registered'
tests/test_sms.py
1
import isodate
1 2
import mock
2 3
import pytest
4
from requests import RequestException
3 5

  
4 6
from django.contrib.contenttypes.models import ContentType
5 7

  
6 8
from passerelle.apps.ovh.models import OVHSMSGateway
7
from passerelle.base.models import ApiUser, AccessRight, SMSResource, SMSLog
9
from passerelle.base.models import ApiUser, AccessRight, Job, SMSResource, SMSLog
8 10

  
9 11
from test_manager import login, admin_user
10 12

  
11 13
import utils
12 14

  
13 15
pytestmark = pytest.mark.django_db
14 16

  
15 17
klasses = SMSResource.__subclasses__()
......
39 41
    # no access check
40 42
    AccessRight.objects.create(codename='can_send_messages',
41 43
                               apiuser=api,
42 44
                               resource_type=obj_type,
43 45
                               resource_pk=c.pk)
44 46
    return c
45 47

  
46 48

  
47
def test_connectors(app, connector):
49
def test_connectors(app, connector, freezer):
48 50
    path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug)
49 51
    result = app.post_json(path, params={})
50 52
    assert result.json['err'] == 1
51 53
    assert result.json['err_desc'].startswith('Payload error: ')
52 54

  
53 55
    payload = {
54 56
        'message': 'hello',
55 57
        'from': '+33699999999',
56 58
        'to': ['+33688888888', '+33677777777'],
57 59
    }
58
    for test_vector in getattr(connector, 'TEST_DEFAULTS', {}).get('test_vectors', []):
60
    test_vectors = getattr(connector, 'TEST_DEFAULTS', {}).get('test_vectors', [])
61
    total = len(test_vectors)
62
    nb_failed = 0
63
    assert Job.objects.count() == 0
64
    for test_vector in test_vectors:
65

  
66
        # register job
67
        freezer.move_to('2019-01-01 00:00:00')
68
        result = app.post_json(path, params=payload)
69
        assert result.json['err'] == 0
70
        job_id = Job.objects.get(status='registered').id
71

  
72
        # transport error
73
        freezer.move_to('2019-01-01 00:00:02')
74
        with utils.mock_url(connector.URL,
75
                            exception=RequestException('connection timed-out')):
76
            connector.jobs()
77
        job = Job.objects.get(id=job_id)
78
        assert job.update_timestamp == isodate.parse_datetime('2019-01-01T00:00:02+00:00')
79
        assert job.status == 'registered'
80
        assert job.status_details == {}
81

  
82
        # perform job
83
        freezer.move_to('2019-01-01 01:00:03')
59 84
        with utils.mock_url(
60 85
                connector.URL,
61 86
                test_vector.get('response', ''),
62 87
                test_vector.get('status_code', 200)):
63

  
64
            result = app.post_json(path, params=payload)
65
            for key, value in test_vector['result'].items():
66
                assert key in result.json
67
                assert result.json[key] == value
88
            connector.jobs()
89
        job = Job.objects.get(id=job_id)
90
        if job.status == 'failed':
91
            assert len(job.status_details['error_summary']) > 0
92
            assert test_vector['result']['err_desc'] in job.status_details['error_summary']
93
            nb_failed += 1
94
        else:
95
            assert job.status == 'completed'
96
    assert Job.objects.count() == total
97
    assert SMSLog.objects.count() == total - nb_failed
68 98

  
69 99

  
70 100
def test_manage_views(admin_user, app, connector):
71 101
    url = '/%s/%s/' % (connector.get_connector_slug(), connector.slug)
72 102
    resp = app.get(url)
73 103
    assert 'Endpoints' in resp.text
74 104
    assert not 'accessright/add' in resp.text
75 105
    app = login(app)
......
86 116
    payload = {
87 117
        'message': message_above_limit,
88 118
        'from': '+33699999999',
89 119
        'to': ['+33688888888'],
90 120
    }
91 121
    with mock.patch.object(OVHSMSGateway, 'send_msg') as send_function:
92 122
        send_function.return_value = {}
93 123
        result = app.post_json(path, params=payload)
94
        assert send_function.call_args[0][0] == 'a' * connector.max_message_length
124
        connector.jobs()
125
        assert send_function.call_args[1]['text'] == 'a' * connector.max_message_length
95 126

  
96 127

  
97 128
@pytest.mark.parametrize('connector', [OVHSMSGateway], indirect=True)
98 129
def test_sms_log(app, connector):
99 130
    path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug)
100 131
    assert not SMSLog.objects.filter(appname=connector.get_connector_slug(), slug=connector.slug).exists()
101 132

  
102 133
    payload = {
103 134
        'message': 'plop',
104 135
        'from': '+33699999999',
105 136
        'to': ['+33688888888'],
106 137
    }
107 138
    with mock.patch.object(OVHSMSGateway, 'send_msg') as send_function:
108 139
        send_function.return_value = {}
109 140
        result = app.post_json(path, params=payload)
141
        connector.jobs()
110 142
        assert SMSLog.objects.filter(appname=connector.get_connector_slug(), slug=connector.slug).exists()
tests/utils.py
27 27

  
28 28
class FakedResponse(mock.Mock):
29 29
    headers = {}
30 30

  
31 31
    def json(self):
32 32
        return json_loads(self.content)
33 33

  
34 34

  
35
def mock_url(url=None, response='', status_code=200, headers=None):
35
def mock_url(url=None, response='', status_code=200, headers=None, exception=None):
36 36
    urlmatch_kwargs = {}
37 37
    if url:
38 38
        parsed = urlparse.urlparse(url)
39 39
        if parsed.netloc:
40 40
            urlmatch_kwargs['netloc'] = parsed.netloc
41 41
        if parsed.path:
42 42
            urlmatch_kwargs['path'] = parsed.path
43 43

  
44 44
    if not isinstance(response, str):
45 45
        response = json.dumps(response)
46 46

  
47 47
    @httmock.urlmatch(**urlmatch_kwargs)
48 48
    def mocked(url, request):
49
        if exception:
50
            raise exception
49 51
        return httmock.response(status_code, response, headers, request=request)
50 52
    return httmock.HTTMock(mocked)
51 53

  
52 54

  
53 55
def make_resource(model_class, **kwargs):
54 56
    resource = model_class.objects.create(**kwargs)
55 57
    setup_access_rights(resource)
56 58
    return resource
57
-