0001-sms-send-SMS-asynchronously-21465.patch
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 |
- |