From 00f7a1ebc7350e5ee904667854c3fe25c18d28a7 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Tue, 2 Feb 2021 12:20:55 +0100
Subject: [PATCH] sms: filter authorized numbers (#39650)

---
 .../migrations/0010_auto_20210202_1304.py     |  26 ++++
 .../migrations/0009_auto_20210202_1304.py     |  26 ++++
 .../migrations/0009_auto_20210202_1304.py     |  26 ++++
 .../ovh/migrations/0013_auto_20210202_1304.py |  31 +++++
 .../migrations/0009_auto_20210202_1304.py     |  26 ++++
 .../migrations/0002_auto_20210202_1304.py     |  26 ++++
 passerelle/sms/forms.py                       |  31 +++++
 passerelle/sms/models.py                      |  69 +++++++++-
 passerelle/sms/views.py                       |   1 +
 tests/test_sms.py                             | 122 +++++++++++++++++-
 10 files changed, 382 insertions(+), 2 deletions(-)
 create mode 100644 passerelle/apps/choosit/migrations/0010_auto_20210202_1304.py
 create mode 100644 passerelle/apps/mobyt/migrations/0009_auto_20210202_1304.py
 create mode 100644 passerelle/apps/orange/migrations/0009_auto_20210202_1304.py
 create mode 100644 passerelle/apps/ovh/migrations/0013_auto_20210202_1304.py
 create mode 100644 passerelle/apps/oxyd/migrations/0009_auto_20210202_1304.py
 create mode 100644 passerelle/apps/twilio/migrations/0002_auto_20210202_1304.py

diff --git a/passerelle/apps/choosit/migrations/0010_auto_20210202_1304.py b/passerelle/apps/choosit/migrations/0010_auto_20210202_1304.py
new file mode 100644
index 00000000..04c9593d
--- /dev/null
+++ b/passerelle/apps/choosit/migrations/0010_auto_20210202_1304.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2021-02-02 12:04
+from __future__ import unicode_literals
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('choosit', '0009_choositsmsgateway_max_message_length'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='choositsmsgateway',
+            name='allow_premium_rate',
+            field=models.BooleanField(default=False, help_text='This option is only applyed to France mainland', verbose_name='Allow premium rate numbers'),
+        ),
+        migrations.AddField(
+            model_name='choositsmsgateway',
+            name='authorized',
+            field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('fr-metro', 'France mainland (+33 [67])'), ('fr-domtom', 'France DOM/TOM (+262, etc.)'), ('be', 'Belgian (+32 4[5-9]) '), ('all', 'All')], max_length=32, null=True), default=['all'], size=None, verbose_name='Authorized Countries'),
+        ),
+    ]
diff --git a/passerelle/apps/mobyt/migrations/0009_auto_20210202_1304.py b/passerelle/apps/mobyt/migrations/0009_auto_20210202_1304.py
new file mode 100644
index 00000000..f4c748f5
--- /dev/null
+++ b/passerelle/apps/mobyt/migrations/0009_auto_20210202_1304.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2021-02-02 12:04
+from __future__ import unicode_literals
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('mobyt', '0008_auto_20200310_1539'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='mobytsmsgateway',
+            name='allow_premium_rate',
+            field=models.BooleanField(default=False, help_text='This option is only applyed to France mainland', verbose_name='Allow premium rate numbers'),
+        ),
+        migrations.AddField(
+            model_name='mobytsmsgateway',
+            name='authorized',
+            field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('fr-metro', 'France mainland (+33 [67])'), ('fr-domtom', 'France DOM/TOM (+262, etc.)'), ('be', 'Belgian (+32 4[5-9]) '), ('all', 'All')], max_length=32, null=True), default=['all'], size=None, verbose_name='Authorized Countries'),
+        ),
+    ]
diff --git a/passerelle/apps/orange/migrations/0009_auto_20210202_1304.py b/passerelle/apps/orange/migrations/0009_auto_20210202_1304.py
new file mode 100644
index 00000000..3275d7df
--- /dev/null
+++ b/passerelle/apps/orange/migrations/0009_auto_20210202_1304.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2021-02-02 12:04
+from __future__ import unicode_literals
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('orange', '0008_auto_20200412_1240'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='orangesmsgateway',
+            name='allow_premium_rate',
+            field=models.BooleanField(default=False, help_text='This option is only applyed to France mainland', verbose_name='Allow premium rate numbers'),
+        ),
+        migrations.AddField(
+            model_name='orangesmsgateway',
+            name='authorized',
+            field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('fr-metro', 'France mainland (+33 [67])'), ('fr-domtom', 'France DOM/TOM (+262, etc.)'), ('be', 'Belgian (+32 4[5-9]) '), ('all', 'All')], max_length=32, null=True), default=['all'], size=None, verbose_name='Authorized Countries'),
+        ),
+    ]
diff --git a/passerelle/apps/ovh/migrations/0013_auto_20210202_1304.py b/passerelle/apps/ovh/migrations/0013_auto_20210202_1304.py
new file mode 100644
index 00000000..8b7004a1
--- /dev/null
+++ b/passerelle/apps/ovh/migrations/0013_auto_20210202_1304.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2021-02-02 12:04
+from __future__ import unicode_literals
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('ovh', '0012_auto_20201027_1121'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='ovhsmsgateway',
+            name='allow_premium_rate',
+            field=models.BooleanField(default=False, help_text='This option is only applyed to France mainland', verbose_name='Allow premium rate numbers'),
+        ),
+        migrations.AddField(
+            model_name='ovhsmsgateway',
+            name='authorized',
+            field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('fr-metro', 'France mainland (+33 [67])'), ('fr-domtom', 'France DOM/TOM (+262, etc.)'), ('be', 'Belgian (+32 4[5-9]) '), ('all', 'All')], max_length=32, null=True), default=['all'], size=None, verbose_name='Authorized Countries'),
+        ),
+        migrations.AlterField(
+            model_name='ovhsmsgateway',
+            name='alert_emails',
+            field=django.contrib.postgres.fields.ArrayField(base_field=models.EmailField(blank=True, max_length=254), blank=True, null=True, size=None, verbose_name='Email addresses list to send credit alerts to, separated by comma'),
+        ),
+    ]
diff --git a/passerelle/apps/oxyd/migrations/0009_auto_20210202_1304.py b/passerelle/apps/oxyd/migrations/0009_auto_20210202_1304.py
new file mode 100644
index 00000000..7e98a6a7
--- /dev/null
+++ b/passerelle/apps/oxyd/migrations/0009_auto_20210202_1304.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2021-02-02 12:04
+from __future__ import unicode_literals
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('oxyd', '0008_oxydsmsgateway_max_message_length'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='oxydsmsgateway',
+            name='allow_premium_rate',
+            field=models.BooleanField(default=False, help_text='This option is only applyed to France mainland', verbose_name='Allow premium rate numbers'),
+        ),
+        migrations.AddField(
+            model_name='oxydsmsgateway',
+            name='authorized',
+            field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('fr-metro', 'France mainland (+33 [67])'), ('fr-domtom', 'France DOM/TOM (+262, etc.)'), ('be', 'Belgian (+32 4[5-9]) '), ('all', 'All')], max_length=32, null=True), default=['all'], size=None, verbose_name='Authorized Countries'),
+        ),
+    ]
diff --git a/passerelle/apps/twilio/migrations/0002_auto_20210202_1304.py b/passerelle/apps/twilio/migrations/0002_auto_20210202_1304.py
new file mode 100644
index 00000000..4e631985
--- /dev/null
+++ b/passerelle/apps/twilio/migrations/0002_auto_20210202_1304.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2021-02-02 12:04
+from __future__ import unicode_literals
+
+import django.contrib.postgres.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('twilio', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='twiliosmsgateway',
+            name='allow_premium_rate',
+            field=models.BooleanField(default=False, help_text='This option is only applyed to France mainland', verbose_name='Allow premium rate numbers'),
+        ),
+        migrations.AddField(
+            model_name='twiliosmsgateway',
+            name='authorized',
+            field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('fr-metro', 'France mainland (+33 [67])'), ('fr-domtom', 'France DOM/TOM (+262, etc.)'), ('be', 'Belgian (+32 4[5-9]) '), ('all', 'All')], max_length=32, null=True), default=['all'], size=None, verbose_name='Authorized Countries'),
+        ),
+    ]
diff --git a/passerelle/sms/forms.py b/passerelle/sms/forms.py
index dba06a09..39519634 100644
--- a/passerelle/sms/forms.py
+++ b/passerelle/sms/forms.py
@@ -1,7 +1,38 @@
+# 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/>.
 from django import forms
 from django.utils.translation import ugettext_lazy as _
 
+from passerelle.forms import GenericConnectorForm
+
+
 class SmsTestSendForm(forms.Form):
     number = forms.CharField(label=_('To'), max_length=12)
     sender = forms.CharField(label=_('From'), max_length=12)
     message = forms.CharField(label=_('Message'), max_length=128)
+
+
+class SMSConnectorForm(GenericConnectorForm):
+    def __init__(self, *args, **kwargs):
+        from passerelle.sms.models import SMSResource
+
+        super(SMSConnectorForm, self).__init__(*args, **kwargs)
+        self.fields['authorized'] = forms.MultipleChoiceField(
+            choices=SMSResource.AUTHORIZED,
+            widget=forms.CheckboxSelectMultiple,
+            initial=[SMSResource.ALL],
+            label=_('Authorized Countries'),
+        )
diff --git a/passerelle/sms/models.py b/passerelle/sms/models.py
index 2461a249..622077cd 100644
--- a/passerelle/sms/models.py
+++ b/passerelle/sms/models.py
@@ -11,26 +11,29 @@
 # 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 logging
 import re
 
+from django.contrib.postgres.fields import ArrayField
 from django.db import models
 from django.utils import six
 from django.utils.module_loading import import_string
 from django.utils.translation import ugettext_lazy as _
 
 from passerelle.base.models import BaseResource
 from passerelle.compat import json_loads
+from passerelle.sms.forms import SMSConnectorForm
 from passerelle.utils.api import endpoint
 from passerelle.utils.jsonresponse import APIError
 
+
 SEND_SCHEMA = {
     '$schema': 'http://json-schema.org/draft-04/schema#',
     "type": "object",
     'required': ['message', 'from', 'to'],
     'properties': {
         'message': {
             'description': 'String message',
             'type': 'string',
@@ -47,34 +50,65 @@ SEND_SCHEMA = {
                 'pattern': r'^\+?[-.\s/\d]+$'
             },
         },
     }
 }
 
 
 class SMSResource(BaseResource):
+    manager_form_base_class = SMSConnectorForm
     category = _('SMS Providers')
     documentation_url = 'https://doc-publik.entrouvert.com/admin-fonctionnel/les-tutos/configuration-envoi-sms/'
 
     _can_send_messages_description = _('Sending messages is limited to the following API users:')
 
     default_country_code = models.CharField(verbose_name=_('Default country code'), max_length=3,
                                             default=u'33')
     default_trunk_prefix = models.CharField(verbose_name=_('Default trunk prefix'), max_length=2,
                                             default=u'0')  # Yeah France first !
-    # FIXME: add regexp field, to check destination and from format
     max_message_length = models.IntegerField(_('Maximum message length'), default=160)
 
     manager_view_template_name = 'passerelle/manage/messages_service_view.html'
 
+    FR_METRO = 'fr-metro'
+    FR_DOMTOM = 'fr-domtom'
+    BE_ = 'be'
+    ALL = 'all'
+    AUTHORIZED = [
+        (FR_METRO, _('France mainland (+33 [67])')),
+        (FR_DOMTOM, _('France DOM/TOM (+262, etc.)')),
+        (BE_, _('Belgian (+32 4[5-9]) ')),
+        (ALL, _('All')),
+    ]
+    authorized = ArrayField(
+        models.CharField(max_length=32, null=True, choices=AUTHORIZED),
+        verbose_name=_('Authorized Countries'),
+        default=[ALL])
+
+    allow_premium_rate = models.BooleanField(
+        _('Allow premium rate numbers'), default=False,
+        help_text=_('This option is only applyed to France mainland')
+    )
+
     @classmethod
     def get_management_urls(cls):
         return import_string('passerelle.sms.urls.management_urlpatterns')
 
+    def _get_authorized_display(self):
+        result = []
+        for key, value in self.AUTHORIZED:
+            if key in self.authorized:
+                result.append(str(value))
+        return ', '.join(result)
+
+    def __init__(self, *args, **kwargs):
+        super(SMSResource, self).__init__(*args, **kwargs)
+        self.get_authorized_display = self._get_authorized_display
+
     def clean_numbers(self, destinations):
         numbers = []
         for dest in destinations:
             # most gateways needs the number prefixed by the country code, this is
             # really unfortunate.
             dest = dest.strip()
             number = ''.join(re.findall('[0-9]', dest))
             if dest.startswith('+'):
@@ -85,23 +119,56 @@ class SMSResource(BaseResource):
             elif number.startswith(self.default_trunk_prefix):
                 number = '00' + self.default_country_code + number[len(self.default_trunk_prefix):]
             else:
                 raise APIError('phone number %r is unsupported (no international prefix, '
                                'no local trunk prefix)' % number)
             numbers.append(number)
         return numbers
 
+    def authorize_numbers(self, destinations):
+        foreign_numbers = set()
+        premium_numbers = set()
+        premium_regex = re.compile(r'^0033[8]\d{8}$')
+        regexes = []
+        if SMSResource.ALL not in self.authorized:
+            if SMSResource.FR_METRO in self.authorized:
+                regexes.append(r'^0033[67]\d{8}$')    # France
+            if SMSResource.FR_DOMTOM in self.authorized:
+                regexes.append(r'^00262262\d{6}$')    # Réunion, Mayotte, Terres australe/antarctiques
+                regexes.append(r'^508508\d{6}$')      # Saint-Pierre-et-Miquelon
+                regexes.append(r'^590590\d{6}$')      # Guadeloupe, Saint-Barthélemy, Saint-Martin
+                regexes.append(r'^594594\d{6}$')      # Guyane
+                regexes.append(r'^596596\d{6}$')      # Martinique
+                regexes.append(r'^00687[67]\d{8}$')   # Nouvelle-Calédonie
+            if SMSResource.BE_ in self.authorized:
+                regexes.append(r'^00324[5-9]\d{7}$')  # Belgian
+        country_regex = re.compile('|'.join(regexes))
+
+        if not self.allow_premium_rate:
+            premium_numbers = set(dest for dest in destinations if premium_regex.match(dest))
+        if SMSResource.ALL not in self.authorized:
+            foreign_numbers = set(dest for dest in destinations if not country_regex.match(dest))
+        for number in list(premium_numbers):
+            logging.warning('unauthorized premium rate phone number: %s', number)
+        for number in list(foreign_numbers - premium_numbers):
+            logging.warning('unauthorized foreign phone number: %s', number)
+        authorized_numbers = list(set(destinations) - foreign_numbers - premium_numbers)
+        if len(authorized_numbers) == 0:
+            raise APIError('no phone number was authorized: %s' % ', '.join(destinations))
+        return authorized_numbers
+
     @endpoint(perm='can_send_messages', methods=['post'],
               description=_('Send a SMS message'),
               parameters={'nostop': {'description': _('Do not send STOP instruction'), 'example_value': '1'}},
               post={'request_body': {'schema': {'application/json': SEND_SCHEMA}}})
     def send(self, request, post_data, nostop=None):
         post_data['message'] = post_data['message'][:self.max_message_length]
         post_data['to'] = self.clean_numbers(post_data['to'])
+        post_data['to'] = self.authorize_numbers(post_data['to'])
         logging.info('sending SMS to %r from %r', post_data['to'], post_data['from'])
         stop = nostop is None  # ?nostop in not in query string
         self.add_job('send_job',
                      text=post_data['message'], sender=post_data['from'], destinations=post_data['to'],
                      stop=stop)
         return {'err': 0}
 
     def send_job(self, *args, **kwargs):
diff --git a/passerelle/sms/views.py b/passerelle/sms/views.py
index d4e4edad..0739892f 100644
--- a/passerelle/sms/views.py
+++ b/passerelle/sms/views.py
@@ -24,15 +24,16 @@ class SmsTestSendView(GenericConnectorMixin, FormView):
 
     def form_valid(self, form):
         number = form.cleaned_data['number']
         sender = form.cleaned_data['sender']
         message = form.cleaned_data['message']
         connector = self.get_object()
         try:
             number = connector.clean_numbers([number])[0]
+            number = connector.authorize_numbers([number])[0]
             connector.send_msg(
                 text=message, sender=sender, destinations=[number], stop=False)
         except APIError as exc:
             messages.error(self.request, _('Sending SMS fails: %s' % exc))
         else:
             messages.success(self.request, _('An SMS was just sent'))
         return super(SmsTestSendView, self).form_valid(form)
diff --git a/tests/test_sms.py b/tests/test_sms.py
index 59063f04..7be95d34 100644
--- a/tests/test_sms.py
+++ b/tests/test_sms.py
@@ -1,20 +1,38 @@
+# 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 isodate
 import json
+import logging
 import mock
 import pytest
 from requests import RequestException
 
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.urls import reverse
+from django.utils.translation import ugettext as _
 
+from passerelle.apps.choosit.models import ChoositSMSGateway
 from passerelle.apps.ovh.models import OVHSMSGateway
-from passerelle.base.models import ApiUser, AccessRight, Job
+from passerelle.base.models import ApiUser, AccessRight, Job, ResourceLog
 from passerelle.sms.models import SMSResource, SMSLog
 from passerelle.utils.jsonresponse import APIError
 
 from test_manager import login
 
 import utils
 
 pytestmark = pytest.mark.django_db
@@ -32,16 +50,65 @@ def test_clean_numbers():
     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 == False
+    number = '0033' + '8' + '12345678'
+    with pytest.raises(APIError, match='no phone number was authorized: %s' % number):
+        assert connector.authorize_numbers([number])
+    connector.allow_premium_rate = True
+    connector.save()
+
+    # All country
+    assert connector.authorized == [SMSResource.ALL]
+    number = '0033' + '1' + '12345678'
+    assert connector.authorize_numbers([number]) == [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]) == [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]) == [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]) == [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 filtered destination is not empty
+    numbers = ['0033' + '6' + '12345678', '0032' + '45' + '1234567']
+    assert len(connector.authorize_numbers(numbers)) == 1
+
+
 @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__,
@@ -393,8 +460,61 @@ def test_ovh_token_request(admin_user, app):
     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'
+
+
+@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 = resp.form.submit()
+    assert resp.html.find('div', {'class': 'errornotice'}).p.text == \
+        'There were errors processing your form.'
+    assert resp.html.find('div', {'class': 'error'}).text.strip() == 'This field is required.'
+    resp.form['authorized'] = [SMSResource.FR_METRO, SMSResource.FR_DOMTOM]
+    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'],
+    }
+    app.post_json(path, params=payload)
+    with mock.patch.object(type(connector), 'send_msg') as send_function:
+        send_function.return_value = {}
+        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 = {}
+        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'
-- 
2.29.2

