0003-sms-add-endpoint-to-send-sms-asynchronously-21465.patch
passerelle/apps/choosit/models.py | ||
---|---|---|
104 | 104 |
results.append(u'Choosit error: %s' % output['error']) |
105 | 105 |
else: |
106 | 106 |
results.append(output) |
107 | 107 |
if any(isinstance(result, string_types) for result in results): |
108 | 108 |
raise APIError('Choosit error: some destinations failed', |
109 | 109 |
data=list(zip(destinations, results))) |
110 | 110 |
return list(zip(destinations, results)) |
111 | 111 | |
112 |
def send_one_msg(self, text, sender, destination, **kwargs): |
|
113 |
"""Send a uniq SMS using the Twilio provider""" |
|
114 |
destination = self.clean_number(destination, |
|
115 |
self.default_country_code, |
|
116 |
self.default_trunk_prefix) |
|
117 |
params = { |
|
118 |
'key': self.key, |
|
119 |
'recipient': destination, |
|
120 |
'content': text[:160], |
|
121 |
} |
|
122 |
data = {'data': json.dumps(params)} |
|
123 |
r = self.requests.post(self.URL, data=data) |
|
124 |
try: |
|
125 |
output = r.json() |
|
126 |
except ValueError: |
|
127 |
raise APIError('Choosit error: bad JSON response') |
|
128 |
else: |
|
129 |
if not isinstance(output, dict): |
|
130 |
raise APIError('Choosit error: JSON response is not a dict %r' % output) |
|
131 |
elif 'error' in output: |
|
132 |
raise APIError('Choosit error: %s' % output['error']) |
|
133 | ||
112 | 134 | |
113 | 135 |
class ChoositSMS(SMS): |
114 | 136 |
gateway = models.ForeignKey(ChoositSMSGateway, related_name='sms_set') |
115 | 137 | |
116 | 138 |
class Meta: |
117 | 139 |
verbose_name = _('Choosit SMS') |
118 | 140 |
db_table = 'sms_choosit_sms' |
passerelle/apps/mobyt/models.py | ||
---|---|---|
64 | 64 |
try: |
65 | 65 |
r = self.requests.post(self.URL, data=params) |
66 | 66 |
except requests.RequestException as e: |
67 | 67 |
raise APIError('MobyT error: POST failed, %s' % e) |
68 | 68 |
if r.content[:2] != "OK": |
69 | 69 |
raise APIError('MobyT error: response is not "OK"') |
70 | 70 |
return None |
71 | 71 | |
72 |
def send_one_msg(self, text, sender, destination, **kwargs): |
|
73 |
"""Send a uniq SMS using the Mobyt provider""" |
|
74 |
destination = self.clean_number(destination, |
|
75 |
self.default_country_code, |
|
76 |
self.default_trunk_prefix) |
|
77 |
params = { |
|
78 |
'user': self.username, |
|
79 |
'pass': self.password, |
|
80 |
'rcpt': destination, |
|
81 |
'data': text, |
|
82 |
'sender': sender, |
|
83 |
'qty': self.quality, |
|
84 |
} |
|
85 |
r = self.requests.post(self.URL, data=params) |
|
86 |
if r.content[:2] != "OK": |
|
87 |
raise APIError('MobyT error: response is not "OK"') |
|
88 | ||
72 | 89 | |
73 | 90 |
class MobytSMS(SMS): |
74 | 91 |
gateway = models.ForeignKey(MobytSMSGateway, related_name='sms_set') |
75 | 92 | |
76 | 93 |
class Meta: |
77 | 94 |
verbose_name = _('Mobyt SMS') |
78 | 95 |
db_table = 'sms_mobyt_sms' |
passerelle/apps/orange/models.py | ||
---|---|---|
53 | 53 |
verbose_name=_('Default trunk prefix'), max_length=2, default='0') |
54 | 54 | |
55 | 55 |
manager_view_template_name = 'passerelle/manage/messages_service_view.html' |
56 | 56 | |
57 | 57 |
class Meta: |
58 | 58 |
verbose_name = _('Orange') |
59 | 59 |
db_table = 'sms_orange' |
60 | 60 | |
61 |
TEST_DEFAULTS = { |
|
62 |
'create_kwargs': { |
|
63 |
'username': 'jdoe', |
|
64 |
'password': 'xxxx', |
|
65 |
'groupname': 'group1', |
|
66 |
}, |
|
67 |
'test_vectors': [], |
|
68 |
} |
|
69 | ||
61 | 70 |
def get_access_token(self): |
62 | 71 |
headers = {'content-type': 'application/x-www-form-urlencoded'} |
63 | 72 |
params = {'username': self.username, 'password': self.password} |
64 | 73 |
response = self.requests.post(URL_TOKEN, data=params, headers=headers) |
65 | 74 |
if response.status_code != 200: |
66 | 75 |
raise APIError('Bad username or password: %s, %s' % ( |
67 | 76 |
response.status_code, response.text)) |
68 | 77 |
response_json = get_json(response) |
... | ... | |
110 | 119 |
'''Send a SMS using the Orange provider''' |
111 | 120 |
destinations = self.clean_numbers( |
112 | 121 |
destinations, self.default_country_code, self.default_trunk_prefix) |
113 | 122 |
access_token = self.get_access_token() |
114 | 123 |
group_id = self.group_id_from_name(access_token) |
115 | 124 |
response = self.diffusion(access_token, group_id, destinations, text) |
116 | 125 |
return response |
117 | 126 | |
127 |
def send_one_msg(self, text, sender, destination, **kwargs): |
|
128 |
'''Send a SMS using the Orange provider''' |
|
129 |
destination = self.clean_number( |
|
130 |
destination, self.default_country_code, self.default_trunk_prefix) |
|
131 |
access_token = self.get_access_token() |
|
132 |
group_id = self.group_id_from_name(access_token) |
|
133 |
response = self.diffusion(access_token, group_id, [destination], text) |
|
134 |
return response |
|
135 | ||
118 | 136 | |
119 | 137 |
class OrangeSMS(SMS): |
120 | 138 |
gateway = models.ForeignKey(OrangeSMSGateway, related_name='sms_set') |
121 | 139 | |
122 | 140 |
class Meta: |
123 | 141 |
verbose_name = _('Orange SMS') |
124 | 142 |
db_table = 'sms_orange_sms' |
passerelle/apps/ovh/models.py | ||
---|---|---|
123 | 123 |
(self.slug, credit_left, self.credit_threshold_alert)) |
124 | 124 |
ret['credit_left'] = credit_left |
125 | 125 |
ret['ovh_result'] = result |
126 | 126 |
ret['sms_ids'] = result.get('SmsIds', []) |
127 | 127 |
return ret |
128 | 128 |
else: |
129 | 129 |
raise APIError('OVH error: %r' % result) |
130 | 130 | |
131 |
def send_one_msg(self, text, sender, destination, **kwargs): |
|
132 |
"""Send a uniq SMS using the OVH provider""" |
|
133 | ||
134 |
destination = self.clean_number(destination, |
|
135 |
self.default_country_code, |
|
136 |
self.default_trunk_prefix) |
|
137 |
text = force_text(text).encode('utf-8') |
|
138 |
to = destination |
|
139 |
params = { |
|
140 |
'account': self.account.encode('utf-8'), |
|
141 |
'login': self.username.encode('utf-8'), |
|
142 |
'password': self.password.encode('utf-8'), |
|
143 |
'from': sender.encode('utf-8'), |
|
144 |
'to': to, |
|
145 |
'message': text, |
|
146 |
'contentType': 'text/json', |
|
147 |
'class': self.msg_class, |
|
148 |
} |
|
149 |
if not kwargs['stop']: |
|
150 |
params.update({'noStop': 1}) |
|
151 |
response = self.requests.post(self.URL, data=params) |
|
152 |
try: |
|
153 |
result = response.json() |
|
154 |
except ValueError: |
|
155 |
raise APIError('OVH error: bad JSON response') |
|
156 |
else: |
|
157 |
if not isinstance(result, dict): |
|
158 |
raise APIError('OVH error: bad JSON response %r, it should be a dictionnary' % |
|
159 |
result) |
|
160 |
if 100 <= result['status'] < 200: |
|
161 |
ret = {} |
|
162 |
credit_left = float(result['creditLeft']) |
|
163 |
# update credit left |
|
164 |
OVHSMSGateway.objects.filter(id=self.id).update(credit_left=credit_left) |
|
165 |
if credit_left < self.credit_threshold_alert: |
|
166 |
ret['warning'] = ('credit level too low for %s: %s (threshold %s)' % |
|
167 |
(self.slug, credit_left, self.credit_threshold_alert)) |
|
168 |
ret['credit_left'] = credit_left |
|
169 |
ret['ovh_result'] = result |
|
170 |
ret['sms_ids'] = result.get('SmsIds', []) |
|
171 |
return ret |
|
172 |
else: |
|
173 |
raise APIError('OVH error: %r' % result) |
|
174 | ||
131 | 175 | |
132 | 176 |
class OVHSMS(SMS): |
133 | 177 |
gateway = models.ForeignKey(OVHSMSGateway, related_name='sms_set') |
134 | 178 | |
135 | 179 |
class Meta: |
136 | 180 |
verbose_name = _('OVH SMS') |
137 | 181 |
db_table = 'sms_ovh_sms' |
passerelle/apps/oxyd/models.py | ||
---|---|---|
82 | 82 |
return None |
83 | 83 | |
84 | 84 |
def get_sms_left(self, type='standard'): |
85 | 85 |
raise NotImplementedError |
86 | 86 | |
87 | 87 |
def get_money_left(self): |
88 | 88 |
raise NotImplementedError |
89 | 89 | |
90 |
def send_one_msg(self, text, sender, destination, **kwargs): |
|
91 |
"""Send a uniq SMS using the Oxyd provider""" |
|
92 |
# unfortunately it lacks a batch API... |
|
93 |
destination = self.clean_number( |
|
94 |
destination, self.default_country_code, self.default_trunk_prefix) |
|
95 |
params = { |
|
96 |
'id': self.username, |
|
97 |
'pass': self.password, |
|
98 |
'num': destination, |
|
99 |
'sms': text.encode('utf-8'), |
|
100 |
'flash': '0', |
|
101 |
} |
|
102 |
r = self.requests.post(self.URL, params) |
|
103 |
code = r.content and r.content.split()[0] |
|
104 |
if force_text(code) != '200': |
|
105 |
APIError('OXYD error: response is not 200') |
|
106 | ||
90 | 107 | |
91 | 108 |
class OxydSMS(SMS): |
92 | 109 |
gateway = models.ForeignKey(OxydSMSGateway, related_name='sms_set') |
93 | 110 | |
94 | 111 |
class Meta: |
95 | 112 |
verbose_name = _('Oxyd SMS') |
96 | 113 |
db_table = 'sms_oxyd_sms' |
passerelle/apps/twilio/models.py | ||
---|---|---|
84 | 84 |
else: |
85 | 85 |
results.append(0) |
86 | 86 |
if any(results): |
87 | 87 |
raise APIError( |
88 | 88 |
'Twilio error: some destinations failed', |
89 | 89 |
data=list(zip(destinations, results))) |
90 | 90 |
return None |
91 | 91 | |
92 |
def send_one_msg(self, text, sender, destination, **kwargs): |
|
93 |
"""Send a uniq SMS using the Twilio provider""" |
|
94 | ||
95 |
url = '%s/%s/Messages.json' % (TwilioSMSGateway.URL, self.account_sid) |
|
96 |
auth = HTTPBasicAuth(self.account_sid, self.auth_token) |
|
97 |
params = { |
|
98 |
'Body': text, |
|
99 |
'From': sender, |
|
100 |
'To': destination |
|
101 |
} |
|
102 |
resp = self.requests.post(url, params, auth=auth) |
|
103 |
if resp.status_code != 201: |
|
104 |
raise APIError('Twilio error: %s' % resp.text) |
|
105 | ||
92 | 106 | |
93 | 107 |
class TwilioSMS(SMS): |
94 | 108 |
gateway = models.ForeignKey(TwilioSMSGateway, related_name='sms_set') |
95 | 109 | |
96 | 110 |
class Meta: |
97 | 111 |
verbose_name = _('Twilio SMS') |
98 | 112 |
db_table = 'sms_twilio_sms' |
passerelle/base/models.py | ||
---|---|---|
976 | 976 | |
977 | 977 |
@endpoint(perm='can_send_messages', methods=['post']) |
978 | 978 |
def send(self, request, *args, **kwargs): |
979 | 979 |
parameters = self.get_parameters(request, *args, **kwargs) |
980 | 980 |
result = {'data': self.send_msg(**parameters)} |
981 | 981 |
SMSLog.objects.create(appname=self.get_connector_slug(), slug=self.slug) |
982 | 982 |
return result |
983 | 983 | |
984 |
def send_one_msg_wrapper(self, sms_id): |
|
985 |
sms = self.sms_set.get(id=sms_id) |
|
986 |
try: |
|
987 |
self.send_one_msg(sms.text, sms.sender, sms.destination, stop=sms.stop) |
|
988 |
except requests.RequestException: |
|
989 |
self.add_job('send_one_msg_wrapper', after_timestamp=5 * 60, sms_id=sms.id) |
|
990 |
raise |
|
991 |
except APIError as exc: |
|
992 |
sms.status = SMS.STATUS_ERROR |
|
993 |
sms.status_details = str(exc) |
|
994 |
else: |
|
995 |
sms.status = SMS.STATUS_PUSHED |
|
996 |
sms.save() |
|
997 | ||
998 |
@endpoint(perm='can_send_messages', methods=['post']) |
|
999 |
def send_async(self, request, *args, **kwargs): |
|
1000 |
parameters = self.get_parameters(request, *args, **kwargs) |
|
1001 |
for destination in parameters['destinations']: |
|
1002 |
sms = self.sms_set.create( |
|
1003 |
text=parameters['text'], |
|
1004 |
sender=parameters['sender'], |
|
1005 |
destination=destination, |
|
1006 |
stop=parameters['stop'], |
|
1007 |
status=SMS.STATUS_PENDING |
|
1008 |
) |
|
1009 |
self.add_job('send_one_msg_wrapper', sms_id=sms.id) |
|
1010 |
SMSLog.objects.create(appname=self.get_connector_slug(), slug=self.slug) |
|
1011 | ||
984 | 1012 |
class Meta: |
985 | 1013 |
abstract = True |
986 | 1014 | |
987 | 1015 | |
988 | 1016 |
class SMS(models.Model): |
989 | 1017 |
STATUS_PENDING = 'pending' |
990 | 1018 |
STATUS_PUSHED = 'pushed' |
991 | 1019 |
STATUS_ERROR = 'error' |
tests/test_sms.py | ||
---|---|---|
1 | 1 |
import mock |
2 | 2 |
import pytest |
3 |
from requests import RequestException |
|
3 | 4 | |
4 | 5 |
from django.contrib.contenttypes.models import ContentType |
5 | 6 | |
6 | 7 |
from passerelle.apps.ovh.models import OVHSMSGateway |
7 |
from passerelle.base.models import ApiUser, AccessRight, SMSResource, SMSLog
|
|
8 |
from passerelle.base.models import ApiUser, AccessRight, Job, SMSResource, SMS, SMSLog
|
|
8 | 9 | |
9 | 10 |
from test_manager import login, admin_user |
10 | 11 | |
11 | 12 |
import utils |
12 | 13 | |
13 | 14 |
pytestmark = pytest.mark.django_db |
14 | 15 | |
15 | 16 |
klasses = SMSResource.__subclasses__() |
... | ... | |
61 | 62 |
test_vector.get('response', ''), |
62 | 63 |
test_vector.get('status_code', 200)): |
63 | 64 | |
64 | 65 |
result = app.post_json(path, params=payload) |
65 | 66 |
for key, value in test_vector['result'].items(): |
66 | 67 |
assert key in result.json |
67 | 68 |
assert result.json[key] == value |
68 | 69 | |
70 |
def test_send_async(app, connector, freezer): |
|
71 |
path = '/%s/%s/send_async/' % (connector.get_connector_slug(), connector.slug) |
|
72 |
result = app.post_json(path, params={}) |
|
73 |
assert result.json['err'] == 1 |
|
74 |
assert result.json['err_desc'].startswith('Payload error: ') |
|
75 | ||
76 |
payload = { |
|
77 |
'message': 'hello', |
|
78 |
'from': '+33699999999', |
|
79 |
'to': ['+33688888888', '+33677777777'], |
|
80 |
} |
|
81 |
test_vectors = getattr(connector, 'TEST_DEFAULTS', {}).get('test_vectors', []) |
|
82 |
total = len(test_vectors) * len(payload['to']) |
|
83 |
assert Job.objects.count() == 0 |
|
84 |
assert connector.sms_set.count() == 0 |
|
85 |
for test_vector in test_vectors: |
|
86 | ||
87 |
# register job |
|
88 |
freezer.move_to('2019-01-01 00:00:00') |
|
89 |
result = app.post_json(path, params=payload) |
|
90 |
assert len(Job.objects.filter(status='registered')) == len(payload['to']) |
|
91 |
new_job_ids = [x.id for x in Job.objects.filter(status='registered')] |
|
92 | ||
93 |
# transport error |
|
94 |
with utils.mock_url(connector.URL, exception=RequestException): |
|
95 |
connector.jobs() |
|
96 |
for job_id in new_job_ids: |
|
97 |
job = Job.objects.get(id=job_id) |
|
98 |
assert job.status == 'failed' |
|
99 |
assert 'RequestException' in job.status_details['error_summary'] |
|
100 |
assert len(Job.objects.filter(status='registered')) == len(payload['to']) |
|
101 |
new_job_ids = [x.id for x in Job.objects.filter(status='registered')] |
|
102 | ||
103 |
# perform jobs |
|
104 |
freezer.move_to('2019-01-01 00:05:01') |
|
105 |
with utils.mock_url( |
|
106 |
connector.URL, |
|
107 |
test_vector.get('response', ''), |
|
108 |
test_vector.get('status_code', 200)): |
|
109 |
connector.jobs() |
|
110 |
for job_id in new_job_ids: |
|
111 |
job = Job.objects.get(id=job_id) |
|
112 |
assert job.status == 'completed' |
|
113 |
sms_id = job.parameters['sms_id'] |
|
114 |
sms = connector.sms_set.get(id=sms_id) |
|
115 |
if sms.status == SMS.STATUS_ERROR: |
|
116 |
if test_vector['result'].get('data'): |
|
117 |
assert sms.status_details == test_vector['result']['data'][0][1] |
|
118 |
else: |
|
119 |
assert sms.status == SMS.STATUS_PUSHED |
|
120 | ||
121 |
assert Job.objects.count() == total * 2 # transport error + perform jobs |
|
122 |
assert connector.sms_set.count() == total |
|
123 |
assert SMSLog.objects.count() == total / len(payload['to']) |
|
69 | 124 | |
70 | 125 |
def test_manage_views(admin_user, app, connector): |
71 | 126 |
url = '/%s/%s/' % (connector.get_connector_slug(), connector.slug) |
72 | 127 |
resp = app.get(url) |
73 | 128 |
assert 'Endpoints' in resp.text |
74 | 129 |
assert not 'accessright/add' in resp.text |
75 | 130 |
app = login(app) |
76 | 131 |
resp = app.get(url) |
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 |
- |