0001-smsfactor-initial-implementation-69363.patch
passerelle/apps/smsfactor/migrations/0001_initial.py | ||
---|---|---|
1 |
# Generated by Django 2.2.26 on 2022-09-21 08:39 |
|
2 | ||
3 |
import django.contrib.postgres.fields |
|
4 |
import django.core.validators |
|
5 |
from django.db import migrations, models |
|
6 | ||
7 |
import passerelle.sms.models |
|
8 | ||
9 | ||
10 |
class Migration(migrations.Migration): |
|
11 | ||
12 |
initial = True |
|
13 | ||
14 |
dependencies = [ |
|
15 |
('base', '0029_auto_20210202_1627'), |
|
16 |
] |
|
17 | ||
18 |
operations = [ |
|
19 |
migrations.CreateModel( |
|
20 |
name='SMSFactorSMSGateway', |
|
21 |
fields=[ |
|
22 |
( |
|
23 |
'id', |
|
24 |
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), |
|
25 |
), |
|
26 |
('title', models.CharField(max_length=50, verbose_name='Title')), |
|
27 |
('slug', models.SlugField(unique=True, verbose_name='Identifier')), |
|
28 |
('description', models.TextField(verbose_name='Description')), |
|
29 |
( |
|
30 |
'default_country_code', |
|
31 |
models.CharField( |
|
32 |
default='33', |
|
33 |
max_length=3, |
|
34 |
validators=[ |
|
35 |
django.core.validators.RegexValidator( |
|
36 |
'^[0-9]*$', 'The country must only contain numbers' |
|
37 |
) |
|
38 |
], |
|
39 |
verbose_name='Default country code', |
|
40 |
), |
|
41 |
), |
|
42 |
( |
|
43 |
'default_trunk_prefix', |
|
44 |
models.CharField( |
|
45 |
default='0', |
|
46 |
max_length=2, |
|
47 |
validators=[ |
|
48 |
django.core.validators.RegexValidator( |
|
49 |
'^[0-9]*$', 'The trunk prefix must only contain numbers' |
|
50 |
) |
|
51 |
], |
|
52 |
verbose_name='Default trunk prefix', |
|
53 |
), |
|
54 |
), |
|
55 |
( |
|
56 |
'max_message_length', |
|
57 |
models.IntegerField( |
|
58 |
default=2000, |
|
59 |
help_text='Messages over this limit will be truncated.', |
|
60 |
verbose_name='Maximum message length', |
|
61 |
), |
|
62 |
), |
|
63 |
( |
|
64 |
'authorized', |
|
65 |
django.contrib.postgres.fields.ArrayField( |
|
66 |
base_field=models.CharField( |
|
67 |
choices=[ |
|
68 |
('fr-metro', 'France mainland (+33 [67])'), |
|
69 |
('fr-domtom', 'France DOM/TOM (+262, etc.)'), |
|
70 |
('be', 'Belgian (+32 4[5-9]) '), |
|
71 |
('all', 'All'), |
|
72 |
], |
|
73 |
max_length=32, |
|
74 |
null=True, |
|
75 |
), |
|
76 |
default=passerelle.sms.models.authorized_default, |
|
77 |
size=None, |
|
78 |
verbose_name='Authorized Countries', |
|
79 |
), |
|
80 |
), |
|
81 |
('auth_token', models.CharField(max_length=255, verbose_name='Auth Token')), |
|
82 |
( |
|
83 |
'credit_threshold_alert', |
|
84 |
models.PositiveIntegerField(default=500, verbose_name='Credit alert threshold'), |
|
85 |
), |
|
86 |
( |
|
87 |
'credit_left', |
|
88 |
models.PositiveIntegerField(default=0, editable=False, verbose_name='Credit left'), |
|
89 |
), |
|
90 |
( |
|
91 |
'alert_emails', |
|
92 |
django.contrib.postgres.fields.ArrayField( |
|
93 |
base_field=models.EmailField(blank=True, max_length=254), |
|
94 |
blank=True, |
|
95 |
null=True, |
|
96 |
size=None, |
|
97 |
verbose_name='Email addresses list to send credit alerts to, separated by comma', |
|
98 |
), |
|
99 |
), |
|
100 |
('credit_alert_timestamp', models.DateTimeField(editable=False, null=True)), |
|
101 |
( |
|
102 |
'users', |
|
103 |
models.ManyToManyField( |
|
104 |
blank=True, |
|
105 |
related_name='_smsfactorsmsgateway_users_+', |
|
106 |
related_query_name='+', |
|
107 |
to='base.ApiUser', |
|
108 |
), |
|
109 |
), |
|
110 |
], |
|
111 |
options={ |
|
112 |
'verbose_name': 'SMS Factor', |
|
113 |
'db_table': 'sms_factor', |
|
114 |
}, |
|
115 |
), |
|
116 |
] |
passerelle/apps/smsfactor/models.py | ||
---|---|---|
1 |
# passerelle - uniform access to multiple data sources and services |
|
2 |
# Copyright (C) 2020 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 |
import datetime |
|
17 |
import logging |
|
18 |
import urllib.parse |
|
19 | ||
20 |
import requests |
|
21 |
from django.conf import settings |
|
22 |
from django.contrib.postgres.fields import ArrayField |
|
23 |
from django.core.mail import send_mail |
|
24 |
from django.db import models |
|
25 |
from django.template.loader import render_to_string |
|
26 |
from django.utils import timezone |
|
27 |
from django.utils.translation import gettext_lazy as _ |
|
28 | ||
29 |
from passerelle.sms.models import SMSResource |
|
30 |
from passerelle.utils.api import endpoint |
|
31 |
from passerelle.utils.jsonresponse import APIError |
|
32 | ||
33 |
SEND_SCHEMA = { |
|
34 |
'$schema': 'http://json-schema.org/draft-04/schema#', |
|
35 |
"type": "object", |
|
36 |
'required': ['message', 'to'], |
|
37 |
'properties': { |
|
38 |
'message': { |
|
39 |
'description': 'String message', |
|
40 |
'type': 'string', |
|
41 |
'examples': ['Votre demande a été mise à jour.'], |
|
42 |
}, |
|
43 |
'from': {'description': 'Sender identifier', 'type': 'string', 'examples': ['Mairie de Marseille']}, |
|
44 |
'to': { |
|
45 |
'description': 'Destination numbers', |
|
46 |
"type": "array", |
|
47 |
"items": {'type': 'string', 'pattern': r'^\+?[-.\s/\d]+$'}, |
|
48 |
"examples": [["0612345678"]], |
|
49 |
}, |
|
50 |
}, |
|
51 |
} |
|
52 | ||
53 | ||
54 |
class SMSFactorSMSGateway(SMSResource): |
|
55 |
auth_token = models.CharField(verbose_name=_('Auth Token'), max_length=255) |
|
56 |
credit_threshold_alert = models.PositiveIntegerField( |
|
57 |
verbose_name=_('Credit alert threshold'), default=500 |
|
58 |
) |
|
59 |
credit_left = models.PositiveIntegerField(verbose_name=_('Credit left'), default=0, editable=False) |
|
60 |
alert_emails = ArrayField( |
|
61 |
models.EmailField(blank=True), |
|
62 |
blank=True, |
|
63 |
null=True, |
|
64 |
verbose_name=_('Email addresses list to send credit alerts to, separated by comma'), |
|
65 |
) |
|
66 |
credit_alert_timestamp = models.DateTimeField(null=True, editable=False) |
|
67 | ||
68 |
# unecessary field |
|
69 |
allow_premium_rate = None |
|
70 | ||
71 |
class Meta: |
|
72 |
verbose_name = 'SMS Factor' |
|
73 |
db_table = 'sms_factor' |
|
74 | ||
75 |
TEST_DEFAULTS = { |
|
76 |
'create_kwargs': { |
|
77 |
'auth_token': 'yyy', |
|
78 |
'credit_threshold_alert': 1000, |
|
79 |
}, |
|
80 |
'test_vectors': [ |
|
81 |
{ |
|
82 |
'status_code': 200, |
|
83 |
'response': { |
|
84 |
"status": -7, |
|
85 |
"message": "Erreur de données", |
|
86 |
"details": "Texte du message introuvable", |
|
87 |
}, |
|
88 |
'result': { |
|
89 |
'err': 1, |
|
90 |
'err_desc': 'SMS Factor error: some destinations failed', |
|
91 |
'data': [ |
|
92 |
['33688888888', "Texte du message introuvable"], |
|
93 |
['33677777777', "Texte du message introuvable"], |
|
94 |
], |
|
95 |
}, |
|
96 |
}, |
|
97 |
{ |
|
98 |
'status_code': 200, |
|
99 |
'response': { |
|
100 |
"status": 1, |
|
101 |
"message": "OK", |
|
102 |
"ticket": "14672468", |
|
103 |
"cost": 2, |
|
104 |
"credits": 642, |
|
105 |
"total": 2, |
|
106 |
"sent": 2, |
|
107 |
"blacklisted": 0, |
|
108 |
"duplicated": 0, |
|
109 |
"invalid": 0, |
|
110 |
"npai": 0, |
|
111 |
}, |
|
112 |
'result': { |
|
113 |
'err': 0, |
|
114 |
'data': { |
|
115 |
"status": 1, |
|
116 |
"message": "OK", |
|
117 |
"ticket": "14672468", |
|
118 |
"cost": 2, |
|
119 |
"credits": 642, |
|
120 |
"total": 2, |
|
121 |
"sent": 2, |
|
122 |
"blacklisted": 0, |
|
123 |
"duplicated": 0, |
|
124 |
"invalid": 0, |
|
125 |
"npai": 0, |
|
126 |
}, |
|
127 |
}, |
|
128 |
}, |
|
129 |
], |
|
130 |
} |
|
131 |
URL = 'https://api.smsfactor.com' |
|
132 | ||
133 |
@endpoint( |
|
134 |
perm='can_send_messages', |
|
135 |
methods=['post'], |
|
136 |
description=_('Send a SMS message'), |
|
137 |
parameters={ |
|
138 |
'simulate': { |
|
139 |
'description': _('Do not actually send a SMS to the recipient, for testing purpose.'), |
|
140 |
'example_value': 'true', |
|
141 |
} |
|
142 |
}, |
|
143 |
post={'request_body': {'schema': {'application/json': SEND_SCHEMA}}}, |
|
144 |
) |
|
145 |
def send(self, request, post_data, simulate=False): |
|
146 |
post_data['message'] = post_data['message'][: self.max_message_length] |
|
147 |
post_data['to'] = self.clean_numbers(post_data['to']) |
|
148 |
post_data['to'], warnings = self.authorize_numbers(post_data['to']) |
|
149 |
logging.info('sending SMS to %r from %r', post_data['to'], post_data['from']) |
|
150 |
self.add_job( |
|
151 |
'send_job', |
|
152 |
text=post_data['message'], |
|
153 |
sender=post_data['from'], |
|
154 |
destinations=post_data['to'], |
|
155 |
simulate=simulate, |
|
156 |
) |
|
157 |
return {'err': 0, 'warn': warnings} |
|
158 | ||
159 |
def request(self, method, endpoint, **kwargs): |
|
160 |
url = urllib.parse.urljoin(self.URL, endpoint) |
|
161 | ||
162 |
headers = { |
|
163 |
"Authorization": f"Bearer {self.auth_token}", |
|
164 |
"Accept": "application/json", |
|
165 |
} |
|
166 | ||
167 |
try: |
|
168 |
response = self.requests.request(method, url, headers=headers, **kwargs) |
|
169 |
except requests.RequestException as e: |
|
170 |
raise APIError('SMS Factor: request failed, %s' % e) |
|
171 |
else: |
|
172 |
try: |
|
173 |
result = response.json() |
|
174 |
except ValueError: |
|
175 |
raise APIError('SMS Factor: bad JSON response') |
|
176 |
try: |
|
177 |
response.raise_for_status() |
|
178 |
except requests.RequestException as e: |
|
179 |
raise APIError('SMS Factor: %s "%s"' % (e, result)) |
|
180 |
return result |
|
181 | ||
182 |
def send_msg(self, text, sender, destinations, **kwargs): |
|
183 |
"""Send a SMS using the SMS Factor provider""" |
|
184 |
# from https://dev.smsfactor.com/en/api/sms/send/send-single |
|
185 |
# and https://dev.smsfactor.com/en/api/sms/send/send-simulate |
|
186 |
# set destinations phone number in E.164 format (without the + prefix) |
|
187 |
# [country code][phone number including area code] |
|
188 |
endpoint = 'send/simulate' if kwargs.get('simulate', False) else 'send' |
|
189 |
numbers = [] |
|
190 |
for dest in destinations: |
|
191 |
numbers.append(dest[2:]) |
|
192 |
destinations = numbers |
|
193 | ||
194 |
results = [] |
|
195 | ||
196 |
for dest in destinations: |
|
197 |
params = {'text': text, 'to': dest, 'pushtype': 'alert'} |
|
198 |
data = self.request('get', endpoint, params=params) |
|
199 |
logging.info('SMS Factor answered with %s', data) |
|
200 |
results.append(data) |
|
201 | ||
202 |
errors = [f'SMS Factor error: {r["status"]}: {r["message"]}' for r in results if r['status'] != 1] |
|
203 | ||
204 |
consumed_credits = None |
|
205 |
try: |
|
206 |
self.credit_left = results[-1]['credits'] |
|
207 |
consumed_credits = sum([r['cost'] for r in results]) |
|
208 |
self.save(update_fields=['credit_left']) |
|
209 |
except KeyError: |
|
210 |
# no credits key, there was probably an error with the request |
|
211 |
pass |
|
212 |
if any(errors): |
|
213 |
raise APIError('SMS Factor error: some destinations failed', data=errors) |
|
214 |
return consumed_credits |
|
215 | ||
216 |
def update_credit_left(self): |
|
217 |
result = self.request('get', endpoint='credits') |
|
218 |
self.credit_left = result['credits'] |
|
219 |
self.save(update_fields=['credit_left']) |
|
220 | ||
221 |
def send_credit_alert_if_needed(self): |
|
222 |
if self.credit_left >= self.credit_threshold_alert: |
|
223 |
return |
|
224 |
if self.credit_alert_timestamp and self.credit_alert_timestamp > timezone.now() - datetime.timedelta( |
|
225 |
days=1 |
|
226 |
): |
|
227 |
return # alerts are sent daily |
|
228 |
ctx = { |
|
229 |
'connector': self, |
|
230 |
'connector_url': urllib.parse.urljoin(settings.SITE_BASE_URL, self.get_absolute_url()), |
|
231 |
} |
|
232 |
subject = render_to_string('smsfactor/credit_alert_subject.txt', ctx).strip() |
|
233 |
body = render_to_string('smsfactor/credit_alert_body.txt', ctx) |
|
234 |
html_body = render_to_string('smsfactor/credit_alert_body.html', ctx) |
|
235 |
send_mail( |
|
236 |
subject, |
|
237 |
body, |
|
238 |
settings.DEFAULT_FROM_EMAIL, |
|
239 |
self.alert_emails, |
|
240 |
html_message=html_body, |
|
241 |
) |
|
242 |
self.credit_alert_timestamp = timezone.now() |
|
243 |
self.save() |
|
244 |
self.logger.warning('credit is too low, alerts were sent to %s', self.alert_emails) |
|
245 | ||
246 |
def hourly(self): |
|
247 |
super().hourly() |
|
248 |
self.update_credit_left() |
|
249 |
self.send_credit_alert_if_needed() |
passerelle/apps/smsfactor/templates/smsfactor/credit_alert_body.html | ||
---|---|---|
1 |
{% extends "emails/body_base.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block content %} |
|
5 |
<p>{% trans "Hi," %}</p> |
|
6 | ||
7 |
<p> |
|
8 |
{% blocktrans trimmed with name=connector.title credit_left=connector.credit_left %} |
|
9 |
There are only {{ credit_left }} credits left for connector {{ name }}. |
|
10 |
{% endblocktrans %} |
|
11 |
</p> |
|
12 | ||
13 |
<p> |
|
14 |
{% blocktrans trimmed with account=connector.account %} |
|
15 |
Please add more credit as soon as possible for your SMS Factor account. |
|
16 |
{% endblocktrans %} |
|
17 |
</p> |
|
18 | ||
19 |
{% with _("View connector page") as button_label %} |
|
20 |
{% include "emails/button-link.html" with url=connector_url label=button_label %} |
|
21 |
{% endwith %} |
|
22 |
{% endblock %} |
passerelle/apps/smsfactor/templates/smsfactor/credit_alert_body.txt | ||
---|---|---|
1 |
{% extends "emails/body_base.txt" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block content %}{% autoescape off %}{% trans "Hi," %} |
|
5 | ||
6 |
{% blocktrans trimmed with name=connector.title credit_left=connector.credit_left %} |
|
7 |
There are only {{ credit_left }} credits left for connector {{ name }}. |
|
8 |
{% endblocktrans %} |
|
9 | ||
10 |
{% blocktrans trimmed with account=connector.account %} |
|
11 |
Please add more credit as soon as possible for your SMS Factor account.. |
|
12 |
{% endblocktrans %} |
|
13 | ||
14 |
{% trans "View connector page:" %} {{ connector_url }} |
|
15 | ||
16 |
{% endautoescape %} |
|
17 |
{% endblock %} |
passerelle/apps/smsfactor/templates/smsfactor/credit_alert_subject.txt | ||
---|---|---|
1 |
{% extends "emails/subject.txt" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block email-subject %}{% autoescape off %}{% blocktrans trimmed with credit_left=connector.credit_left %} |
|
5 |
SMS Factor alert: only {{ credit_left }} credits left |
|
6 |
{% endblocktrans %}{% endautoescape %}{% endblock %} |
passerelle/settings.py | ||
---|---|---|
168 | 168 |
'passerelle.apps.sfr_dmc', |
169 | 169 |
'passerelle.apps.signal_arretes', |
170 | 170 |
'passerelle.apps.sivin', |
171 |
'passerelle.apps.smsfactor', |
|
171 | 172 |
'passerelle.apps.soap', |
172 | 173 |
'passerelle.apps.solis', |
173 | 174 |
'passerelle.apps.sp_fr', |
tests/test_sms.py | ||
---|---|---|
26 | 26 |
from passerelle.apps.choosit.models import ChoositSMSGateway |
27 | 27 |
from passerelle.apps.ovh.models import OVHSMSGateway |
28 | 28 |
from passerelle.apps.sfr_dmc.models import SfrDmcGateway |
29 |
from passerelle.apps.smsfactor.models import SMSFactorSMSGateway |
|
29 | 30 |
from passerelle.base.models import AccessRight, ApiUser, Job, ResourceLog |
30 | 31 |
from passerelle.sms.models import SMSLog, SMSResource |
31 | 32 |
from passerelle.utils.jsonresponse import APIError |
... | ... | |
270 | 271 |
assert send_function.call_args[1]['text'] == 'a' * connector.max_message_length |
271 | 272 | |
272 | 273 | |
273 |
@pytest.mark.parametrize('connector', [OVHSMSGateway], indirect=True) |
|
274 |
@pytest.mark.parametrize('connector', [OVHSMSGateway, SMSFactorSMSGateway], indirect=True)
|
|
274 | 275 |
def test_sms_log(app, connector): |
275 | 276 |
path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug) |
276 | 277 |
assert not SMSLog.objects.filter(appname=connector.get_connector_slug(), slug=connector.slug).exists() |
... | ... | |
280 | 281 |
'from': '+33699999999', |
281 | 282 |
'to': ['+33688888888'], |
282 | 283 |
} |
283 |
with mock.patch.object(OVHSMSGateway, 'send_msg') as send_function: |
|
284 | ||
285 |
with mock.patch.object(connector.__class__, 'send_msg') as send_function: |
|
284 | 286 |
send_function.return_value = 1 |
285 | 287 |
app.post_json(path, params=payload) |
286 | 288 |
connector.jobs() |
... | ... | |
288 | 290 |
appname=connector.get_connector_slug(), slug=connector.slug, credits=1 |
289 | 291 |
).exists() |
290 | 292 | |
291 |
with mock.patch.object(OVHSMSGateway, 'send_msg') as send_function:
|
|
293 |
with mock.patch.object(connector.__class__, 'send_msg') as send_function:
|
|
292 | 294 |
send_function.return_value = 2 |
293 | 295 |
app.post_json(path, params=payload) |
294 | 296 |
connector.jobs() |
... | ... | |
297 | 299 |
).exists() |
298 | 300 | |
299 | 301 | |
300 |
@pytest.mark.parametrize('connector', [OVHSMSGateway], indirect=True) |
|
302 |
@pytest.mark.parametrize('connector', [OVHSMSGateway, SMSFactorSMSGateway], indirect=True)
|
|
301 | 303 |
def test_sms_job_details_credits(admin_user, app, connector, caplog): |
302 | 304 |
path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug) |
303 | 305 |
payload = {'message': 'plop', 'from': '+33699999999', 'to': ['+33688888888']} |
304 |
with mock.patch.object(OVHSMSGateway, 'send_msg') as send_function:
|
|
306 |
with mock.patch.object(connector.__class__, 'send_msg') as send_function:
|
|
305 | 307 |
send_function.return_value = 1 |
306 | 308 |
app.post_json(path, params=payload) |
307 | 309 |
job1_id = Job.objects.get(method_name='send_job', status='registered').id |
... | ... | |
323 | 325 | |
324 | 326 | |
325 | 327 |
def test_sms_nostop_parameter(app, connector): |
328 |
if isinstance(connector, SMSFactorSMSGateway): |
|
329 |
pytest.skip(f"nostop parameter not supported for {connector}") |
|
326 | 330 |
base_path = '/%s/%s/send/?nostop=1' % (connector.get_connector_slug(), connector.slug) |
327 | 331 |
payload = { |
328 | 332 |
'message': 'not a spam', |
... | ... | |
342 | 346 |
assert send_function.call_args[1]['stop'] == ('nostop' not in path) |
343 | 347 | |
344 | 348 | |
345 |
@pytest.mark.parametrize('connector', [OVHSMSGateway], indirect=True) |
|
349 |
@pytest.mark.parametrize('connector', [OVHSMSGateway, SMSFactorSMSGateway], indirect=True)
|
|
346 | 350 |
@pytest.mark.parametrize( |
347 | 351 |
'to, destination', |
348 | 352 |
[ |
... | ... | |
768 | 772 |
_check_media_type('standard GSM message', 'SMSLong') |
769 | 773 |
_check_media_type('usual standard GSM characters : \'"-\r\n!?éèù%à*+=€@[]|', 'SMSLong') |
770 | 774 |
_check_media_type('unicode message 😀', 'SMSUnicodeLong') |
775 | ||
776 | ||
777 |
def test_sms_factor_alert_emails(app, freezer, mailoutbox): |
|
778 |
connector = SMSFactorSMSGateway.objects.create( |
|
779 |
slug='test-sms-factor', |
|
780 |
title='Test SMS Factor', |
|
781 |
auth_token='foo', |
|
782 |
credit_threshold_alert=100, |
|
783 |
credit_left=102, |
|
784 |
alert_emails=['test@entrouvert.org'], |
|
785 |
) |
|
786 |
api = ApiUser.objects.create(username='apiuser') |
|
787 |
obj_type = ContentType.objects.get_for_model(connector) |
|
788 |
AccessRight.objects.create( |
|
789 |
codename='can_send_messages', apiuser=api, resource_type=obj_type, resource_pk=connector.pk |
|
790 |
) |
|
791 | ||
792 |
freezer.move_to('2019-01-01 00:00:00') |
|
793 |
resp = {'credits': 101} |
|
794 |
url = connector.URL |
|
795 |
with tests.utils.mock_url(url, resp, 200): |
|
796 |
connector.hourly() |
|
797 |
assert len(mailoutbox) == 0 |
|
798 | ||
799 |
resp = {'credits': 99} |
|
800 |
url = connector.URL |
|
801 |
with tests.utils.mock_url(url, resp, 200): |
|
802 |
connector.hourly() |
|
803 |
assert len(mailoutbox) == 1 |
|
804 | ||
805 |
mail = mailoutbox[0] |
|
806 |
assert mail.recipients() == ['test@entrouvert.org'] |
|
807 |
assert mail.subject == 'SMS Factor alert: only 99 credits left' |
|
808 |
for body in (mail.body, mail.alternatives[0][0]): |
|
809 |
assert "SMS Factor" in body |
|
810 |
assert connector.title in body |
|
811 |
assert 'http://localhost/smsfactor/test-sms-factor/' in body |
|
812 |
mailoutbox.clear() |
|
813 | ||
814 |
# alert is sent again daily |
|
815 |
freezer.move_to('2019-01-01 12:00:00') |
|
816 |
resp = {'credits': 99} |
|
817 |
url = connector.URL |
|
818 |
with tests.utils.mock_url(url, resp, 200): |
|
819 |
connector.hourly() |
|
820 |
assert len(mailoutbox) == 0 |
|
821 | ||
822 |
freezer.move_to('2019-01-02 01:00:07') |
|
823 |
with tests.utils.mock_url(url, resp, 200): |
|
824 |
connector.hourly() |
|
825 |
assert len(mailoutbox) == 1 |
|
771 |
- |