# passerelle - uniform access to multiple data sources and services # Copyright (C) 2020 Entr'ouvert # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # 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 . import json import logging from unittest import mock import pytest from django.contrib.contenttypes.models import ContentType from django.urls import reverse from django.utils.translation import gettext as _ import tests.utils from passerelle.apps.choosit.models import ChoositSMSGateway from passerelle.apps.ovh.models import OVHSMSGateway from passerelle.apps.sfr_dmc.models import SfrDmcGateway from passerelle.apps.smsfactor.models import SMSFactorSMSGateway from passerelle.base.models import AccessRight, ApiUser, Job, ResourceLog from passerelle.sms.models import SMSLog, SMSResource, TrackCreditSMSResource from passerelle.utils.jsonresponse import APIError from tests.test_manager import login from tests.utils import FakedResponse pytestmark = pytest.mark.django_db klasses = [ klass for klass in SMSResource.__subclasses__() + TrackCreditSMSResource.__subclasses__() if not klass._meta.abstract ] def test_clean_numbers(): connector = OVHSMSGateway() assert connector.clean_numbers(['+ 33 12']) == ['003312'] assert connector.clean_numbers(['0 0 33 12']) == ['003312'] assert connector.clean_numbers(['0 12']) == ['003312'] connector.default_country_code = '32' connector.default_trunk_prefix = '1' connector.save() assert connector.clean_numbers(['+ 33 12']) == ['003312'] assert connector.clean_numbers(['0 0 33 12']) == ['003312'] assert connector.clean_numbers(['1 12']) == ['003212'] with pytest.raises(APIError, match='phone number %r is unsupported' % '0123'): connector.clean_numbers(['0123']) def test_authorize_numbers(): connector = OVHSMSGateway() # premium-rate assert connector.allow_premium_rate is False number = '0033' + '8' + '12345678' with pytest.raises(APIError, match='no phone number was authorized: %s' % number): connector.authorize_numbers([number]) connector.allow_premium_rate = True connector.save() assert connector.authorize_numbers([number])[0] == [number] # All country assert connector.authorized == [SMSResource.ALL] number = '0033' + '1' + '12345678' assert connector.authorize_numbers([number])[0] == [number] connector.authorized = [SMSResource.FR_METRO] connector.save() with pytest.raises(APIError, match='no phone number was authorized: %s' % number): connector.authorize_numbers([number]) # France number = '0033' + '6' + '12345678' assert connector.authorize_numbers([number])[0] == [number] connector.authorized = [SMSResource.FR_DOMTOM] connector.save() with pytest.raises(APIError, match='no phone number was authorized: %s' % number): connector.authorize_numbers([number]) # Dom-Tom number = '596596' + '123456' assert connector.authorize_numbers([number])[0] == [number] connector.authorized = [SMSResource.BE_] connector.save() with pytest.raises(APIError, match='no phone number was authorized: %s' % number): connector.authorize_numbers([number]) # Belgian number = '0032' + '45' + '1234567' assert connector.authorize_numbers([number])[0] == [number] connector.authorized = [SMSResource.FR_METRO] connector.save() with pytest.raises(APIError, match='no phone number was authorized: %s' % number): connector.authorize_numbers([number]) # Don't raise if authorized destinations are not empty connector.allow_premium_rate = False connector.authorized = [SMSResource.FR_METRO] connector.save() numbers = [ '0033' + '8' + '12345678', '0033' + '1' + '12345678', '0033' + '6' + '12345678', '596596' + '123456', '0032' + '45' + '1234567', ] authorized_numbers, warnings = connector.authorize_numbers(numbers) assert authorized_numbers == ['0033612345678'] assert warnings == { 'deny premium rate phone numbers': '0033812345678', 'deny foreign phone numbers': '0032451234567, 0033112345678, 596596123456', } @pytest.fixture(params=klasses) def connector(request, db): klass = request.param kwargs = getattr(klass, 'TEST_DEFAULTS', {}).get('create_kwargs', {}) kwargs.update( { 'title': klass.__name__, 'slug': klass.__name__.lower(), 'description': klass.__name__, } ) c = klass.objects.create(**kwargs) api = ApiUser.objects.create(username='apiuser', fullname='Api User', description='api') obj_type = ContentType.objects.get_for_model(c) # no access check AccessRight.objects.create( codename='can_send_messages', apiuser=api, resource_type=obj_type, resource_pk=c.pk ) return c def test_connectors(app, connector, freezer): path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug) result = app.post_json(path, params={}, status=400) assert result.json['err'] == 1 assert result.json['err_desc'] == "'message' is a required property" payload = { 'message': 'hello', 'from': '+33699999999', 'to': ['+33688888888', '+33677777777'], } test_vectors = getattr(connector, 'TEST_DEFAULTS', {}).get('test_vectors', []) total = len(test_vectors) nb_failed = 0 assert Job.objects.filter(method_name='send_job').count() == 0 for test_vector in test_vectors: # register job freezer.move_to('2019-01-01 00:00:00') result = app.post_json(path, params=payload) assert result.json['err'] == 0 job_id = Job.objects.get(method_name='send_job', status='registered').id # perform job freezer.move_to('2019-01-01 01:00:03') with tests.utils.mock_url( connector.URL, test_vector.get('response', ''), test_vector.get('status_code', 200) ): connector.jobs() job = Job.objects.get(id=job_id) if job.status == 'failed': assert len(job.status_details['error_summary']) > 0 assert test_vector['result']['err_desc'] in job.status_details['error_summary'] nb_failed += 1 else: assert job.status == 'completed' assert Job.objects.filter(method_name='send_job').count() == total assert SMSLog.objects.count() == total - nb_failed @pytest.mark.parametrize('connector', [OVHSMSGateway], indirect=True) def test_sms_legacy_retry_after_error(app, connector, freezer): path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug) assert not SMSLog.objects.filter(appname=connector.get_connector_slug(), slug=connector.slug).exists() payload = { 'message': 'plop', 'from': '+33699999999', 'to': ['+33688888888'], } freezer.move_to('2019-01-01 00:00:00') result = app.post_json(path, params=payload) assert result.json['err'] == 0 assert Job.objects.exists() assert Job.objects.get().status == 'registered' assert not Job.objects.get().after_timestamp with tests.utils.mock_url( url=connector.URL, response={'status': 429, 'message': 'Too much requests.\nPlease retry in 3 seconds.'}, ): connector.jobs() assert Job.objects.get().status == 'registered' assert Job.objects.get().after_timestamp with tests.utils.mock_url(url=connector.URL, response={'status': 100, 'credit_left': 22}): connector.jobs() assert Job.objects.get().status == 'registered' freezer.move_to('2019-01-01 00:00:11') with tests.utils.mock_url(url=connector.URL, response={'status': 100, 'creditLeft': 22}): connector.jobs() assert Job.objects.get().status == 'completed' assert SMSLog.objects.exists() def test_manage_views(admin_user, app, connector): url = '/%s/%s/' % (connector.get_connector_slug(), connector.slug) resp = app.get(url) assert 'Endpoints' in resp.text assert 'accessright/add' not in resp.text app = login(app) resp = app.get(url) description_fields = [ x.text.split(':')[0] for x in resp.html.find('div', {'id': 'description'}).find_all('p') ] assert 'Default country code' in description_fields assert 'Default trunk prefix' in description_fields assert 'Maximum message length' in description_fields assert 'Account' not in description_fields assert 'Username' not in description_fields assert 'Endpoints' in resp.text assert 'accessright/add' in resp.text @pytest.mark.parametrize('connector', [OVHSMSGateway], indirect=True) def test_manage_views_ovh(app, connector, admin_user): login(app) connector.default_country_code = '44' connector.account = 'secret' connector.application_key = 'secret' connector.application_secret = 'secret' connector.consumer_key = 'secret' connector.password = 'secret' connector.username = 'secret' connector.alert_emails = ['test@entrouvert.org', 'foo@example.com'] connector.save() url = '/%s/%s/' % (connector.get_connector_slug(), connector.slug) resp = app.get(url) description_fields = [x.text for x in resp.html.find('div', {'id': 'description'}).find_all('p')] assert any(x for x in description_fields if 'Default country code' in x) assert any(x for x in description_fields if '44' in x) assert not any(x for x in description_fields if 'secret' in x) alert_emails_filed = [x for x in description_fields if 'send credit alerts to' in x] assert alert_emails_filed[0].split(':')[1].strip() == 'test@entrouvert.org, foo@example.com' @pytest.mark.parametrize('connector', [OVHSMSGateway], indirect=True) def test_sms_max_message_length(app, connector): path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug) message_above_limit = 'a' * (connector.max_message_length + 1) payload = { 'message': message_above_limit, 'from': '+33699999999', 'to': ['+33688888888'], } with mock.patch.object(OVHSMSGateway, 'send_msg') as send_function: send_function.return_value = None app.post_json(path, params=payload) connector.jobs() assert send_function.call_args[1]['text'] == 'a' * connector.max_message_length @pytest.mark.parametrize('connector', [OVHSMSGateway, SMSFactorSMSGateway], indirect=True) def test_sms_log(app, connector): path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug) assert not SMSLog.objects.filter(appname=connector.get_connector_slug(), slug=connector.slug).exists() payload = { 'message': 'plop', 'from': '+33699999999', 'to': ['+33688888888'], } with mock.patch.object(connector.__class__, 'send_msg') as send_function: send_function.return_value = 1 app.post_json(path, params=payload) connector.jobs() assert SMSLog.objects.filter( appname=connector.get_connector_slug(), slug=connector.slug, credits=1 ).exists() with mock.patch.object(connector.__class__, 'send_msg') as send_function: send_function.return_value = 2 app.post_json(path, params=payload) connector.jobs() assert SMSLog.objects.filter( appname=connector.get_connector_slug(), slug=connector.slug, credits=2 ).exists() @pytest.mark.parametrize('connector', [OVHSMSGateway, SMSFactorSMSGateway], indirect=True) def test_sms_job_details_credits(admin_user, app, connector, caplog): path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug) payload = {'message': 'plop', 'from': '+33699999999', 'to': ['+33688888888']} with mock.patch.object(connector.__class__, 'send_msg') as send_function: send_function.return_value = 1 app.post_json(path, params=payload) job1_id = Job.objects.get(method_name='send_job', status='registered').id connector.jobs() send_function.return_value = 2 app.post_json(path, params=payload) job2_id = Job.objects.get(method_name='send_job', status='registered').id connector.jobs() app = login(app) resp = app.get('/manage/%s/%s/jobs/%s/' % (connector.get_connector_slug(), connector.slug, job1_id)) assert len(resp.pyquery('td:contains("credits-spent")')) == 1 assert len(resp.pyquery('td:contains("1")')) == 1 resp = app.get('/manage/%s/%s/jobs/%s/' % (connector.get_connector_slug(), connector.slug, job2_id)) assert len(resp.pyquery('td:contains("credits-spent")')) == 1 assert len(resp.pyquery('td:contains("2")')) == 1 def test_sms_nostop_parameter(app, connector): base_path = '/%s/%s/send/?nostop=1' % (connector.get_connector_slug(), connector.slug) payload = { 'message': 'not a spam', 'from': '+33699999999', 'to': ['+33688888888'], } for path in (base_path, base_path + '?nostop=1', base_path + '?nostop=foo', base_path + '?nostop'): send_patch = mock.patch( 'passerelle.apps.%s.models.%s.send_msg' % (connector.__class__._meta.app_label, connector.__class__.__name__) ) with send_patch as send_function: send_function.return_value = None app.post_json(base_path, params=payload) connector.jobs() assert send_function.call_args[1]['text'] == 'not a spam' assert send_function.call_args[1]['stop'] == ('nostop' not in path) @pytest.mark.parametrize('connector', [OVHSMSGateway, SMSFactorSMSGateway], indirect=True) @pytest.mark.parametrize( 'to, destination', [ ('06 12 34 56 78', '0033612345678'), ('06.12.34.56.78', '0033612345678'), ('06-12-34-56-78', '0033612345678'), ('+33/612345678', '0033612345678'), ], ) def test_send_schema(app, connector, to, destination): base_path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug) payload = { 'message': 'not a spam', 'from': '+33699999999', 'to': [to], } send_patch = mock.patch( 'passerelle.apps.%s.models.%s.send_msg' % (connector.__class__._meta.app_label, connector.__class__.__name__) ) with send_patch as send_function: send_function.return_value = None app.post_json(base_path, params=payload) connector.jobs() assert send_function.call_args[1]['destinations'] == [destination] def test_ovh_new_api(app, freezer): connector = OVHSMSGateway.objects.create( slug='ovh', account='sms-test42', application_key='RHrTdU2oTsrVC0pu', application_secret='CLjtS69tTcPgCKxedeoZlgMSoQGSiXMa', consumer_key='iF0zi0MJrbjNcI3hvuvwkhNk8skrigxz', ) api = ApiUser.objects.create(username='apiuser') obj_type = ContentType.objects.get_for_model(connector) # no access check AccessRight.objects.create( codename='can_send_messages', apiuser=api, resource_type=obj_type, resource_pk=connector.pk ) payload = { 'message': 'hello', 'from': '+33699999999', 'to': ['+33688888888', '+33677777777'], } # register job freezer.move_to('2019-01-01 00:00:00') path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug) result = app.post_json(path, params=payload) assert result.json['err'] == 0 job_id = Job.objects.get(method_name='send_job', status='registered').id # perform job freezer.move_to('2019-01-01 01:00:03') resp = { 'validReceivers': ['+33688888888', '+33677777777'], 'totalCreditsRemoved': 1, 'ids': [241615100], 'invalidReceivers': [], } base_url = connector.API_URL % {'serviceName': 'sms-test42'} url = base_url + 'jobs/' with tests.utils.mock_url(url, resp, 200) as mocked: connector.jobs() job = Job.objects.get(id=job_id) assert job.status == 'completed' request = mocked.handlers[0].call['requests'][0] assert 'X-Ovh-Signature' in request.headers @pytest.mark.parametrize('connector', [OVHSMSGateway], indirect=True) def test_sms_test_send(admin_user, app, connector): url = '/%s/%s/' % (connector.get_connector_slug(), connector.slug) resp = app.get(url) assert 'Send a test message' not in resp.text app = login(app) resp = app.get(url) assert 'Send a test message' in resp.text assert resp.pyquery('a#sms-test-send')[0].attrib['href'] == reverse( 'sms-test-send', kwargs={'connector': connector.get_connector_slug(), 'slug': connector.slug} ) resp = resp.click('Send a test message') resp.form['number'] = '+33688888888' resp.form['sender'] = '+33699999999' resp.form['message'] = 'hello' with mock.patch.object(OVHSMSGateway, 'send_msg') as send_function: send_function.return_value = None resp = resp.form.submit() assert send_function.call_args[1] == { 'text': 'hello', 'sender': '+33699999999', 'destinations': ['0033688888888'], 'stop': False, } assert resp.status_code == 302 assert resp.location == url def test_ovh_new_api_credit(app, freezer, admin_user): login(app) connector = OVHSMSGateway.objects.create( slug='ovh', account='sms-test42', application_key='RHrTdU2oTsrVC0pu', application_secret='CLjtS69tTcPgCKxedeoZlgMSoQGSiXMa', consumer_key='iF0zi0MJrbjNcI3hvuvwkhNk8skrigxz', ) manager_url = '/%s/%s/' % (connector.get_connector_slug(), connector.slug) resp = app.get(manager_url) assert 'no credit left' in resp.text # a job to update credit was added on connector creation resp = { 'creditsLeft': 123, } ovh_url = connector.API_URL % {'serviceName': 'sms-test42'} with tests.utils.mock_url(ovh_url, resp, 200): connector.jobs() connector.refresh_from_db() assert connector.credit_left == 123 resp = app.get(manager_url) assert '123' in resp.text # credit update when checking status resp = { 'creditsLeft': 456, } with tests.utils.mock_url(ovh_url, resp, 200): connector.check_status() assert connector.credit_left == 456 resp = { 'message': 'This credential is not valid', 'httpCode': '403 Forbidden', 'errorCode': 'INVALID_CREDENTIAL', } with tests.utils.mock_url(ovh_url, resp, 403): with pytest.raises(APIError, match='Forbidden'): connector.check_status() def test_ovh_token_request(admin_user, app): connector = OVHSMSGateway.objects.create( slug='test-ovh', title='Test OVH', account='sms-test42', application_key='RHrTdU2oTsrVC0pu', application_secret='CLjtS69tTcPgCKxedeoZlgMSoQGSiXMa', ) app = login(app) resp = app.get(connector.get_absolute_url()) assert 'not operational yet' in resp.text ovh_request_token_url = 'https://eu.api.ovh.com/1.0/auth/credential' ovh_response = { 'consumerKey': 'xyz', 'validationUrl': 'https://eu.api.ovh.com/auth/?credentialToken=iQ1joJE', } with tests.utils.mock_url(ovh_request_token_url, ovh_response, 302) as mocked: resp = resp.click('request access') assert resp.url == 'https://eu.api.ovh.com/auth/?credentialToken=iQ1joJE' request = mocked.handlers[0].call['requests'][0] body = json.loads(request.body.decode()) assert 'accessRules' in body redirect_url = body['redirection'][len('http://testserver') :] resp = app.get(redirect_url).follow() assert 'Successfuly completed connector configuration' in resp.text connector.refresh_from_db() assert connector.consumer_key == 'xyz' def test_ovh_token_request_error(admin_user, app): connector = OVHSMSGateway.objects.create( slug='test-ovh', title='Test OVH', account='sms-test42', application_key='wrong', application_secret='oups', ) app = login(app) resp = app.get(connector.get_absolute_url()) ovh_request_token_url = 'https://eu.api.ovh.com/1.0/auth/credential' ovh_response = {'message': 'Invalid application key'} with tests.utils.mock_url(ovh_request_token_url, ovh_response, 401): resp = resp.click('request access').follow() assert 'error requesting token: Invalid application key.' in resp.text ovh_response = 'not-json' with tests.utils.mock_url(ovh_request_token_url, ovh_response, 401): resp = resp.click('request access').follow() assert 'error requesting token: bad JSON response' in resp.text @pytest.mark.parametrize('connector', [ChoositSMSGateway], indirect=True) def test_manager(admin_user, app, connector): app = login(app) path = '/%s/%s/' % (connector.get_connector_slug(), connector.slug) resp = app.get(path) assert ( '33' in [ x.text for x in resp.html.find('div', {'id': 'description'}).find_all('p') if x.text.startswith(_('Default country code')) ][0] ) assert ( _('All') in [x.text for x in resp.html.find_all('p') if x.text.startswith(_('Authorized Countries'))][0] ) assert ( _('no') in [x.text for x in resp.html.find_all('p') if x.text.startswith(_('Allow premium rate numbers'))][0] ) path = '/manage/%s/%s/edit' % (connector.get_connector_slug(), connector.slug) resp = app.get(path) resp.form['authorized'] = [] resp.form['default_country_code'] = '+33' resp.form['default_trunk_prefix'] = 'x' resp = resp.form.submit() assert resp.html.find('div', {'class': 'errornotice'}).p.text == 'There were errors processing your form.' assert [x.text.strip() for x in resp.html.find_all('div', {'class': 'error'})] == [ 'The country must only contain numbers', 'The trunk prefix must only contain numbers', 'This field is required.', ] resp.form['authorized'] = [SMSResource.FR_METRO, SMSResource.FR_DOMTOM] resp.form['default_country_code'] = '33' resp.form['default_trunk_prefix'] = '0' resp = resp.form.submit() resp = resp.follow() assert ( _('France mainland (+33 [67])') in [x.text for x in resp.html.find_all('p') if x.text.startswith(_('Authorized Countries'))][0] ) path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug) payload = { 'message': 'plop', 'from': '+33699999999', 'to': ['+33688888888'], } resp = app.post_json(path, params=payload) assert resp.json['warn'] == { 'deny premium rate phone numbers': '', 'deny foreign phone numbers': '', } with mock.patch.object(type(connector), 'send_msg') as send_function: send_function.return_value = None connector.jobs() assert SMSLog.objects.count() == 1 payload['to'][0] = '+33188888888' SMSLog.objects.all().delete() app.post_json(path, params=payload) with mock.patch.object(type(connector), 'send_msg') as send_function: send_function.return_value = None connector.jobs() assert not SMSLog.objects.count() assert ResourceLog.objects.filter(levelno=logging.WARNING).count() == 1 assert ( ResourceLog.objects.filter(levelno=30)[0].extra['exception'] == 'no phone number was authorized: 0033188888888' ) @pytest.mark.parametrize('connector', [OVHSMSGateway], indirect=True) def test_api_statistics(app, freezer, connector, admin_user): resp = app.get('/api/statistics/') url = [x for x in resp.json['data'] if x['id'] == 'sms_count_ovh_ovhsmsgateway'][0]['url'] assert app.get(url, status=403) login(app) resp = app.get(url) assert len(resp.json['data']['series'][0]['data']) == 0 freezer.move_to('2021-01-01 12:00') for i in range(5): SMSLog.objects.create(appname='ovh', slug='ovhsmsgateway') freezer.move_to('2021-02-03 13:00') for i in range(3): SMSLog.objects.create(appname='ovh', slug='ovhsmsgateway') freezer.move_to('2021-02-06 13:00') SMSLog.objects.create(appname='ovh', slug='ovhsmsgateway') SMSLog.objects.create(appname='ovh', slug='other') resp = app.get(url + '?time_interval=day') assert resp.json['data'] == { 'x_labels': ['2021-01-01', '2021-02-03', '2021-02-06'], 'series': [{'label': 'SMS Count', 'data': [5, 3, 1]}], } resp = app.get(url + '?start=2021-02-04&end=2021-02-07') assert resp.json['data'] == { 'x_labels': ['2021-02-06'], 'series': [{'label': 'SMS Count', 'data': [1]}], } # invalid time_interval resp = app.get(url + '?time_interval=month') assert resp.json['err'] == 1 @pytest.mark.parametrize('connector', [SfrDmcGateway], indirect=True) def test_sfr_max_message_error(connector): with pytest.raises(APIError, match='you can\'t send more than 20 sms at once.'): connector.send_msg('hello', '0033699999999', [f'00336888888{suffix:02d}' for suffix in range(0, 21)]) @pytest.mark.parametrize('connector', [SfrDmcGateway], indirect=True) def test_sfr_prefix(connector): json_response = { 'success': True, 'response': { 0: '4500283767', 1: '4500283767', }, } with mock.patch('passerelle.utils.Request.get') as mocked_get: mocked_get.return_value = FakedResponse(content=json.dumps(json_response), status_code=200) connector.send_msg('hello', '0033699999999', ['0033688888888']) mocked_get.assert_called_once_with( 'https://www.dmc.sfr-sh.fr/DmcWS/1.5.7/JsonService/MessagesUnitairesWS/batchSingleCall', params={ 'authenticate': json.dumps( {'serviceId': '1234', 'servicePassword': 'krascuky', 'spaceId': '1234'} ), 'messageUnitaires': json.dumps( [ { 'media': 'SMSLong', 'textMsg': 'hello', 'from': '0033699999999', 'to': '+33688888888', }, ] ), }, ) @pytest.mark.parametrize('connector', [SfrDmcGateway], indirect=True) def test_sfr_unicode_message(connector): json_response = { 'success': True, 'response': { 0: '4500283767', 1: '4500283767', }, } def _check_media_type(message, expected_media_type): with mock.patch('passerelle.utils.Request.get') as mocked_get: mocked_get.return_value = FakedResponse(content=json.dumps(json_response), status_code=200) connector.send_msg(message, '0033699999999', ['0033688888888']) mocked_get.assert_called_once() messages = json.loads(mocked_get.call_args[1]['params']['messageUnitaires']) assert messages[0]['media'] == expected_media_type _check_media_type('standard GSM message', 'SMSLong') _check_media_type('usual standard GSM characters : \'"-\r\n!?éèù%à*+=€@[]|', 'SMSLong') _check_media_type('unicode message 😀', 'SMSUnicodeLong') credit_klasses = TrackCreditSMSResource.__subclasses__() @pytest.fixture(params=credit_klasses) def track_credit_connector(request, db): klass = request.param kwargs = getattr(klass, 'TEST_CREDIT_LEFT', {}).get('create_kwargs', {}) kwargs.update( { 'title': klass.__name__, 'slug': klass.__name__.lower(), 'description': klass.__name__, 'credit_threshold_alert': 100, 'credit_left': 102, 'alert_emails': ['test@entrouvert.org'], } ) c = klass.objects.create(**kwargs) api = ApiUser.objects.create(username='apiuser', fullname='Api User', description='api') obj_type = ContentType.objects.get_for_model(c) # no access check AccessRight.objects.create( codename='can_send_messages', apiuser=api, resource_type=obj_type, resource_pk=c.pk ) return c def test_send_alert_emails(track_credit_connector, app, freezer, mailoutbox): get_credit_left_payload = track_credit_connector.TEST_CREDIT_LEFT['get_credit_left_payload'] url = track_credit_connector.TEST_CREDIT_LEFT['url'] freezer.move_to('2019-01-01 00:00:00') resp = get_credit_left_payload(101) with tests.utils.mock_url(url, resp, 200): track_credit_connector.check_status() assert len(mailoutbox) == 0 resp = get_credit_left_payload(99) with tests.utils.mock_url(url, resp, 200): track_credit_connector.check_status() assert len(mailoutbox) == 1 mail = mailoutbox[0] assert mail.recipients() == ['test@entrouvert.org'] assert mail.subject == 'SMS alert: only 99 credits left' for body in (mail.body, mail.alternatives[0][0]): assert track_credit_connector.title in body assert track_credit_connector.get_absolute_url() in body mailoutbox.clear() # alert is sent again daily freezer.move_to('2019-01-01 12:00:00') resp = get_credit_left_payload(99) with tests.utils.mock_url(url, resp, 200): track_credit_connector.check_status() assert len(mailoutbox) == 0 freezer.move_to('2019-01-02 01:00:07') with tests.utils.mock_url(url, resp, 200): track_credit_connector.check_status() assert len(mailoutbox) == 1