824 lines
30 KiB
Python
824 lines
30 KiB
Python
# 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 <http://www.gnu.org/licenses/>.
|
|
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
|
|
from passerelle.utils.jsonresponse import APIError
|
|
from tests.test_manager import login
|
|
from tests.utils import FakedResponse
|
|
|
|
pytestmark = pytest.mark.django_db
|
|
|
|
klasses = SMSResource.__subclasses__()
|
|
|
|
|
|
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
|
|
|
|
# hourly update
|
|
resp = {
|
|
'creditsLeft': 456,
|
|
}
|
|
with tests.utils.mock_url(ovh_url, resp, 200):
|
|
connector.hourly()
|
|
assert connector.credit_left == 456
|
|
|
|
|
|
def test_ovh_alert_emails(app, freezer, mailoutbox):
|
|
connector = OVHSMSGateway.objects.create(
|
|
slug='test-ovh',
|
|
title='Test OVH',
|
|
account='sms-test42',
|
|
application_key='RHrTdU2oTsrVC0pu',
|
|
application_secret='CLjtS69tTcPgCKxedeoZlgMSoQGSiXMa',
|
|
consumer_key='iF0zi0MJrbjNcI3hvuvwkhNk8skrigxz',
|
|
credit_threshold_alert=100,
|
|
credit_left=102,
|
|
alert_emails=['test@entrouvert.org'],
|
|
)
|
|
api = ApiUser.objects.create(username='apiuser')
|
|
obj_type = ContentType.objects.get_for_model(connector)
|
|
AccessRight.objects.create(
|
|
codename='can_send_messages', apiuser=api, resource_type=obj_type, resource_pk=connector.pk
|
|
)
|
|
|
|
freezer.move_to('2019-01-01 00:00:00')
|
|
resp = {'creditsLeft': 101}
|
|
ovh_url = connector.API_URL % {'serviceName': 'sms-test42'}
|
|
with tests.utils.mock_url(ovh_url, resp, 200):
|
|
connector.hourly()
|
|
assert len(mailoutbox) == 0
|
|
|
|
resp = {'creditsLeft': 99}
|
|
ovh_url = connector.API_URL % {'serviceName': 'sms-test42'}
|
|
with tests.utils.mock_url(ovh_url, resp, 200):
|
|
connector.hourly()
|
|
assert len(mailoutbox) == 1
|
|
|
|
mail = mailoutbox[0]
|
|
assert mail.recipients() == ['test@entrouvert.org']
|
|
assert mail.subject == 'OVH SMS alert: only 99 credits left'
|
|
for body in (mail.body, mail.alternatives[0][0]):
|
|
assert connector.account in body
|
|
assert connector.title in body
|
|
assert 'http://localhost/ovh/test-ovh/' in body
|
|
mailoutbox.clear()
|
|
|
|
# alert is sent again daily
|
|
freezer.move_to('2019-01-01 12:00:00')
|
|
resp = {'creditsLeft': 99}
|
|
ovh_url = connector.API_URL % {'serviceName': 'sms-test42'}
|
|
with tests.utils.mock_url(ovh_url, resp, 200):
|
|
connector.hourly()
|
|
assert len(mailoutbox) == 0
|
|
|
|
freezer.move_to('2019-01-02 01:00:07')
|
|
with tests.utils.mock_url(ovh_url, resp, 200):
|
|
connector.hourly()
|
|
assert len(mailoutbox) == 1
|
|
|
|
|
|
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')
|
|
|
|
|
|
def test_sms_factor_alert_emails(app, freezer, mailoutbox):
|
|
connector = SMSFactorSMSGateway.objects.create(
|
|
slug='test-sms-factor',
|
|
title='Test SMS Factor',
|
|
auth_token='foo',
|
|
credit_threshold_alert=100,
|
|
credit_left=102,
|
|
alert_emails=['test@entrouvert.org'],
|
|
)
|
|
api = ApiUser.objects.create(username='apiuser')
|
|
obj_type = ContentType.objects.get_for_model(connector)
|
|
AccessRight.objects.create(
|
|
codename='can_send_messages', apiuser=api, resource_type=obj_type, resource_pk=connector.pk
|
|
)
|
|
|
|
freezer.move_to('2019-01-01 00:00:00')
|
|
resp = {'credits': "101"}
|
|
url = connector.URL
|
|
with tests.utils.mock_url(url, resp, 200):
|
|
connector.hourly()
|
|
assert len(mailoutbox) == 0
|
|
|
|
resp = {'credits': "99"}
|
|
url = connector.URL
|
|
with tests.utils.mock_url(url, resp, 200):
|
|
connector.hourly()
|
|
assert len(mailoutbox) == 1
|
|
|
|
mail = mailoutbox[0]
|
|
assert mail.recipients() == ['test@entrouvert.org']
|
|
assert mail.subject == 'SMS Factor alert: only 99 credits left'
|
|
for body in (mail.body, mail.alternatives[0][0]):
|
|
assert "SMS Factor" in body
|
|
assert connector.title in body
|
|
assert 'http://localhost/smsfactor/test-sms-factor/' in body
|
|
mailoutbox.clear()
|
|
|
|
# alert is sent again daily
|
|
freezer.move_to('2019-01-01 12:00:00')
|
|
resp = {'credits': 99}
|
|
url = connector.URL
|
|
with tests.utils.mock_url(url, resp, 200):
|
|
connector.hourly()
|
|
assert len(mailoutbox) == 0
|
|
|
|
freezer.move_to('2019-01-02 01:00:07')
|
|
with tests.utils.mock_url(url, resp, 200):
|
|
connector.hourly()
|
|
assert len(mailoutbox) == 1
|