Projet

Général

Profil

0001-orange_sms-update-connector-to-API-v1.2-41092.patch

Nicolas Roche, 12 avril 2020 13:39

Télécharger (19,4 ko)

Voir les différences:

Subject: [PATCH] orange_sms: update connector to API v1.2 (#41092)

copied from https://github.com/departement-loire-atlantique/passerelle-orangesms
 .../migrations/0008_auto_20200412_1240.py     |  47 +++++
 passerelle/apps/orange/models.py              | 116 ++++++++++--
 passerelle/apps/orange/orange.pem             |  15 --
 passerelle/apps/orange/soap.py                |  82 --------
 tests/test_orange.py                          | 175 ++++++++++++++++++
 5 files changed, 323 insertions(+), 112 deletions(-)
 create mode 100644 passerelle/apps/orange/migrations/0008_auto_20200412_1240.py
 delete mode 100644 passerelle/apps/orange/orange.pem
 delete mode 100644 passerelle/apps/orange/soap.py
 create mode 100644 tests/test_orange.py
passerelle/apps/orange/migrations/0008_auto_20200412_1240.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-04-12 10:40
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('orange', '0007_auto_20200310_1539'),
12
    ]
13

  
14
    operations = [
15
        migrations.RemoveField(
16
            model_name='orangesmsgateway',
17
            name='keystore',
18
        ),
19
        migrations.AddField(
20
            model_name='orangesmsgateway',
21
            name='default_country_code',
22
            field=models.CharField(default='33', max_length=3, verbose_name='Préfixe pays'),
23
        ),
24
        migrations.AddField(
25
            model_name='orangesmsgateway',
26
            name='default_trunk_prefix',
27
            field=models.CharField(default='0', max_length=2, verbose_name='Préfixe supprimé par défaut'),
28
        ),
29
        migrations.AddField(
30
            model_name='orangesmsgateway',
31
            name='groupname',
32
            field=models.CharField(default=None, max_length=64, verbose_name='Groupe'),
33
            preserve_default=False,
34
        ),
35
        migrations.AddField(
36
            model_name='orangesmsgateway',
37
            name='password',
38
            field=models.CharField(default=None, max_length=64, verbose_name='Mot de passe'),
39
            preserve_default=False,
40
        ),
41
        migrations.AddField(
42
            model_name='orangesmsgateway',
43
            name='username',
44
            field=models.CharField(default=None, max_length=64, verbose_name='Identifiant'),
45
            preserve_default=False,
46
        ),
47
    ]
passerelle/apps/orange/models.py
1
from django.core.files import File
2
from django.utils.translation import ugettext_lazy as _
1
# -*- coding: utf-8 -*-
2
# passerelle - uniform access to multiple data sources and services
3
#
4
# MIT License
5
# Copyright (c) 2020  departement-loire-atlantique
6
#
7
# GNU Affero General Public License
8
# Copyright (C) 2020  Entr'ouvert
9
#
10
# This program is free software: you can redistribute it and/or modify it
11
# under the terms of the GNU Affero General Public License as published
12
# by the Free Software Foundation, either version 3 of the License, or
13
# (at your option) any later version.
14
#
15
# This program is distributed in the hope that it will be useful,
16
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
# GNU Affero General Public License for more details.
19
#
20
# You should have received a copy of the GNU Affero General Public License
21
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
3 22
from django.db import models
23
from django.utils.translation import ugettext_lazy as _
4 24

  
5 25
from passerelle.base.models import SMSResource
26
from passerelle.utils.jsonresponse import APIError
27

  
28
BASE_API = 'https://contact-everyone.orange-business.com/api/v1.2/'
29
URL_TOKEN = BASE_API + 'oauth/token'
30
URL_GROUPS = BASE_API + 'groups'
31
URL_DIFFUSION = BASE_API + 'groups/%s/diffusion-requests'
32

  
33

  
34
class OrangeError(APIError):
35
    pass
6 36

  
7
from . import soap
37

  
38
def get_json(response):
39
    try:
40
        return response.json()
41
    except ValueError:
42
        raise OrangeError('Orange returned Invalid JSON content: %s' % response.text)
8 43

  
9 44

  
10 45
class OrangeSMSGateway(SMSResource):
11
    keystore = models.FileField(upload_to='orange', blank=True, null=True,
12
                                verbose_name=_('Keystore'),
13
                                help_text=_('Certificate and private key in PEM format'))
14
    default_country_code = '33'
46
    username = models.CharField(verbose_name=_('Identifiant'), max_length=64)
47
    password = models.CharField(verbose_name=_('Mot de passe'), max_length=64)
48
    groupname = models.CharField(verbose_name=_('Groupe'), max_length=64)
15 49

  
16
    URL = ('https://www.api-contact-everyone.fr.orange-business.com/ContactEveryone/services'
17
           '/MultiDiffusionWS')
50
    default_country_code = models.CharField(
51
        verbose_name='Préfixe pays', max_length=3, default='33')
52
    default_trunk_prefix = models.CharField(
53
        verbose_name='Préfixe supprimé par défaut', max_length=2, default='0')
18 54

  
19 55
    manager_view_template_name = 'passerelle/manage/messages_service_view.html'
20 56

  
21 57
    class Meta:
22
        verbose_name = 'Orange'
58
        verbose_name = _('Orange')
23 59
        db_table = 'sms_orange'
24 60

  
61
    def get_access_token(self):
62
        headers = {'content-type': 'application/x-www-form-urlencoded'}
63
        params = {'username': self.username, 'password': self.password}
64
        response = self.requests.post(URL_TOKEN, data=params, headers=headers)
65
        if response.status_code != 200:
66
            raise APIError('Bad username or password: %s, %s' % (
67
                response.status_code, response.text))
68
        response_json = get_json(response)
69
        if 'access_token' not in response_json:
70
            raise OrangeError('Orange do not return access token')
71
        return response_json['access_token']
72

  
73
    def group_id_from_name(self, access_token):
74
        headers = {'authorization': 'Bearer %s' % access_token}
75
        response = self.requests.get(URL_GROUPS, headers=headers)
76
        if response.status_code != 200:
77
            raise APIError('Bad token: %s, %s' % (
78
                response.status_code, response.text))
79
        response_json = get_json(response)
80
        group_id = None
81
        for group in response_json:
82
            if group['name'] == self.groupname:
83
                group_id = group['id']
84
                break
85
        if not group_id:
86
            raise APIError('Group name not found: ' + self.groupname)
87
        return group_id
88

  
89
    def diffusion(self, access_token, group_id, destinations, message):
90
        headers = {
91
            'content-type': 'application/json',
92
            'authorization': 'Bearer %s' % access_token,
93
        }
94
        payload = {
95
            'name': 'Send a SMS from passerelle',
96
            'msisdns': destinations,
97
            'smsParam': {
98
                'encoding': 'GSM7',
99
                'body': message
100
            }
101
        }
102
        response = self.requests.post(
103
            URL_DIFFUSION % group_id, json=payload, headers=headers)
104
        if response.status_code != 201:
105
            raise OrangeError('Orange fails to send SMS: %s, %s' % (
106
                response.status_code, response.text))
107
        return get_json(response)
108

  
25 109
    def send_msg(self, text, sender, destinations, **kwargs):
26
        """Send a SMS using the Orange provider"""
27
        # unfortunately it lacks a batch API...
28
        destinations = self.clean_numbers(destinations, self.default_country_code)
29
        return soap.ContactEveryoneSoap(instance=self).send_advanced_message(destinations, sender,
30
                                                                             text)
110
        '''Send a SMS using the Orange provider'''
111
        destinations = self.clean_numbers(
112
            destinations, self.default_country_code, self.default_trunk_prefix)
113
        access_token = self.get_access_token()
114
        group_id = self.group_id_from_name(access_token)
115
        response = self.diffusion(access_token, group_id, destinations, text)
116
        return response
passerelle/apps/orange/orange.pem
1
-----BEGIN CERTIFICATE-----
2
MIICWzCCAcQCAQAwDQYJKoZIhvcNAQEEBQAwdjELMAkGA1UEBhMCRlIxDzANBgNV
3
BAgTBkZyYW5jZTENMAsGA1UEBxMEQ2FlbjEXMBUGA1UEChMORnJhbmNlIFRlbGVj
4
b20xDDAKBgNVBAsTA0RQUzEMMAoGA1UEAxMDRERQMRIwEAYJKoZIhvcNAQkBFgNE
5
SU0wHhcNMDYxMTA4MjAyNDUxWhcNMTYxMTA1MjAyNDUxWjB2MQswCQYDVQQGEwJG
6
UjEPMA0GA1UECBMGRnJhbmNlMQ0wCwYDVQQHEwRDYWVuMRcwFQYDVQQKEw5GcmFu
7
Y2UgVGVsZWNvbTEMMAoGA1UECxMDRFBTMQwwCgYDVQQDEwNERFAxEjAQBgkqhkiG
8
9w0BCQEWA0RJTTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAxp5nnfYr4ZsV
9
QgIzYkz42Urc+0z49cm8JL5DQAcceIUpWYnCOSjDCEivvkYlBGEaSQhx6goLgpAk
10
6264BhFIa9tFJBz0VCbZ5erGANNFpi1zK9nglfGMkfgQmPXFcVF+hi9ztff+WHGR
11
SknuxzXAICG0/PfPy/LcpVC9E35IkG8CAwEAATANBgkqhkiG9w0BAQQFAAOBgQAk
12
plMn6da0Yu2YZ7dSP9UBrWygN3iD93Krk5H9KcJCFKXRcZsKw/871J+fFxOFxe5u
13
l/wraMBF+oo9aMBIsrHwzPkPr6/T3+cYScJAcoP0vRqGjbhio1BvoSvH4lsfmJsF
14
L9cgc58xgDNwztKHqggDtiFCWVEBpYk2jbMnoy7/xg==
15
-----END CERTIFICATE-----
passerelle/apps/orange/soap.py
1
import datetime
2
import os.path
3

  
4

  
5
from passerelle.utils.jsonresponse import APIError
6
from passerelle.soap import Soap
7
from passerelle.xml_builder import XmlBuilder
8

  
9

  
10
class ContactEveryoneSoap(Soap):
11
    WSDL_URL = ('https://www.api-contact-everyone.fr.orange-business.com/'
12
                'ContactEveryone/services/MultiDiffusionWS?wsdl')
13
    ORANGE_CERTIFICATE = os.path.join(os.path.dirname(__file__), 'orange.pem')
14

  
15
    url = WSDL_URL
16

  
17
    class ProfileListBuilder(XmlBuilder):
18
        schema = (
19
            'PROFILE_LIST',
20
            ('?loop', 'recipients',
21
             ('PROFILE',
22
              ('DEST_NAME', 'name_{to}'),
23
              ('DEST_FORENAME', 'forename_{to}'),
24
              ('DEST_ID', 'ID_{to}'),
25
              ('TERMINAL_GROUP',
26
               ('TERMINAL',
27
                ('TERMINAL_NAME', 'mobile1'),
28
                ('TERMINAL_ADDR', '{to}'),
29
                ('MEDIA_TYPE_GROUP',
30
                 ('MEDIA_TYPE', 'sms')))))))
31
        encoding = 'latin1'
32

  
33
    @property
34
    def verify(self):
35
        # Do not break if certificate is not updated
36
        if datetime.datetime.now() < datetime.datetime(2016, 11, 5):
37
            return self.ORANGE_CERTIFICATE
38
        else:
39
            return False
40

  
41
    def send_message(self, recipients, content):
42
        try:
43
            client = self.get_client()
44
        except Exception as e:
45
            raise APIError('Orange error: WSDL retrieval failed, %s' % e)
46
        message = client.factory.create('WSMessage')
47
        message.fullContenu = True
48
        message.content = content
49
        message.subject = content
50
        message.resumeContent = content
51
        message.strategy = 'sms'
52
        send_profiles = self.ProfileListBuilder().string(
53
            context={'recipients': [{'to': to} for to in recipients]})
54
        message.sendProfiles = send_profiles
55
        try:
56
            resp = client.service.sendMessage(message)
57
        except Exception as e:
58
            raise APIError('Orange error: %s' % e)
59
        else:
60
            return {'msg_ids': [msg_id for msg_id in resp.msgId]}
61

  
62
    def send_advanced_message(self, recipients, sender, content):
63
        try:
64
            client = self.get_client()
65
        except Exception as e:
66
            raise APIError('Orange error: WSDL retrieval failed, %s' % e)
67
        message = client.factory.create('WSAdvancedMessage')
68
        message.fullContenu = True
69
        message.content = content
70
        message.subject = content
71
        message.resumeContent = content
72
        message.strategy = 'sms'
73
        message.smsReplyTo = sender
74
        send_profiles = self.ProfileListBuilder().string(
75
            context={'recipients': [{'to': to} for to in recipients]})
76
        message.sendProfiles = send_profiles
77
        try:
78
            resp = client.service.sendAdvancedMessage(message)
79
        except Exception as e:
80
            raise APIError('Orange error: %s' % e)
81
        else:
82
            return {'msg_ids': [msg_id for msg_id in resp.msgId]}
tests/test_orange.py
1
# -*- coding: utf-8 -*-
2
# passerelle - uniform access to multiple data sources and services
3
# Copyright (C) 2020  Entr'ouvert
4
#
5
# This program is free software: you can redistribute it and/or modify it
6
# under the terms of the GNU Affero General Public License as published
7
# by the Free Software Foundation, either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU Affero General Public License for more details.
14
#
15
# You should have received a copy of the GNU Affero General Public License
16
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17

  
18
import json
19

  
20
import httmock
21
import pytest
22

  
23
from django.contrib.contenttypes.models import ContentType
24
from django.utils.encoding import force_text
25

  
26
from passerelle.apps.orange.models import OrangeSMSGateway, OrangeError
27
from passerelle.base.models import ApiUser, AccessRight
28
from passerelle.utils.jsonresponse import APIError
29

  
30

  
31
NETLOC = 'contact-everyone.orange-business.com'
32
JSON_HEADERS = {'content-type': 'application/json'}
33
PAYLOAD = {
34
    'message': 'hello',
35
    'from': '+33699999999',
36
    'to': ['+33688888888', '+33677777777'],
37
}
38

  
39

  
40
@httmock.urlmatch(netloc=NETLOC, path='/api/v1.2/oauth/token', method='POST')
41
def response_token_ok(url, request):
42
    assert 'username=jdoe' in request.body
43
    assert 'password=secret' in request.body
44
    content = json.dumps({'access_token': 'my_token'})
45
    return httmock.response(200, content, JSON_HEADERS)
46

  
47
@httmock.urlmatch(netloc=NETLOC, path='/api/v1.2/groups', method='GET')
48
def response_group_ok(url, request):
49
    content = json.dumps([
50
        {'name': 'group1', 'id': 'gid1'},
51
        {'name': 'group2', 'id': 'gid2'},
52
    ])
53
    return httmock.response(200, content, JSON_HEADERS)
54

  
55
@httmock.urlmatch(netloc=NETLOC, path='/api/v1.2/groups/gid2/diffusion-requests', method='POST')
56
def response_diffusion_ok(url, request):
57
    assert request.headers['authorization'] == 'Bearer my_token'
58
    request_body = json.loads(force_text(request.body))
59
    assert request_body['smsParam']['body'] == PAYLOAD['message']
60
    '33688888888' in request_body['msisdns'][0]
61
    '33677777777' in request_body['msisdns'][1]
62
    content = json.dumps({'status': "I'm ok"})
63
    return httmock.response(201, content, JSON_HEADERS)
64

  
65
@httmock.urlmatch(netloc=NETLOC)
66
def response_500(url, request):
67
    return httmock.response(500, 'my_error')
68

  
69
@httmock.urlmatch(netloc=NETLOC)
70
def response_invalid_json(url, request):
71
    return httmock.response(200, 'not a JSON content')
72

  
73
def setup_access_rights(obj):
74
    api = ApiUser.objects.create(username='all',
75
                                 keytype='', key='')
76
    obj_type = ContentType.objects.get_for_model(obj)
77
    AccessRight.objects.create(codename='can_send_messages', apiuser=api,
78
                               resource_type=obj_type, resource_pk=obj.pk)
79
    return obj
80

  
81
@pytest.fixture
82
def connector(db):
83
    return setup_access_rights(
84
        OrangeSMSGateway.objects.create(
85
            slug='my_connector',
86
            username='jdoe',
87
            password='secret',
88
            groupname='group2'
89
        ))
90

  
91

  
92
def test_get_access_token(app, connector):
93
    orange = OrangeSMSGateway()
94
    orange.username = 'jdoe'
95
    orange.password = 'secret'
96
    with httmock.HTTMock(response_token_ok):
97
        assert orange.get_access_token() == 'my_token'
98

  
99
    # not 200
100
    with pytest.raises(APIError, match='Bad username or password'):
101
        with httmock.HTTMock(response_500):
102
            orange.get_access_token()
103

  
104
    # not json
105
    with pytest.raises(OrangeError, match='Orange returned Invalid JSON content'):
106
        with httmock.HTTMock(response_invalid_json):
107
            orange.get_access_token()
108

  
109
    # no token
110
    @httmock.urlmatch(netloc=NETLOC, path='/api/v1.2/oauth/token', method='POST')
111
    def mocked_response(url, request):
112
        return httmock.response(200, '{}')
113

  
114
    with pytest.raises(OrangeError, match='Orange do not return access token'):
115
        with httmock.HTTMock(mocked_response):
116
            orange.get_access_token()
117

  
118

  
119
def test_group_id_from_name(app, connector):
120
    orange = OrangeSMSGateway()
121
    orange.groupname = 'group2'
122
    with httmock.HTTMock(response_group_ok):
123
        assert orange.group_id_from_name('my_token') == 'gid2'
124

  
125
    # no group
126
    orange.groupname = 'group3'
127
    with pytest.raises(APIError, match='Group name not found: group3'):
128
        with httmock.HTTMock(response_group_ok):
129
            orange.group_id_from_name('my_token')
130

  
131
    # not 200
132
    orange.groupname = 'group2'
133
    with pytest.raises(APIError, match='Bad token'):
134
        with httmock.HTTMock(response_500):
135
            orange.group_id_from_name('my_token')
136

  
137
    # not json
138
    with pytest.raises(OrangeError, match='Orange returned Invalid JSON content'):
139
        with httmock.HTTMock(response_invalid_json):
140
            orange.group_id_from_name('my_token')
141

  
142

  
143
def test_diffusion(app, connector):
144
    orange = OrangeSMSGateway()
145
    with httmock.HTTMock(response_diffusion_ok):
146
        resp = orange.diffusion('my_token', 'gid2', PAYLOAD['to'], PAYLOAD['message'])
147
    assert resp['status'] == "I'm ok"
148

  
149
    # not 201
150
    with pytest.raises(OrangeError, match='Orange fails to send SMS'):
151
        with httmock.HTTMock(response_500):
152
            orange.diffusion('my_token', 'gid2', PAYLOAD['to'], PAYLOAD['message'])
153

  
154
    # not json
155
    @httmock.urlmatch(netloc=NETLOC)
156
    def mocked_response(url, request):
157
        return httmock.response(201, 'not a JSON content')
158

  
159
    with pytest.raises(OrangeError, match='Orange returned Invalid JSON content'):
160
        with httmock.HTTMock(mocked_response):
161
            orange.diffusion('my_token', 'gid2', PAYLOAD['to'], PAYLOAD['message'])
162

  
163

  
164
def test_send_msg(app, connector):
165
    url = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug)
166
    with httmock.HTTMock(response_token_ok, response_group_ok, response_diffusion_ok):
167
        resp = app.post_json(url, params=PAYLOAD, status=200)
168
    assert not resp.json['err']
169
    assert resp.json['data']['status'] == "I'm ok"
170

  
171
    # not 201
172
    with httmock.HTTMock(response_token_ok, response_group_ok, response_500):
173
        resp = app.post_json(url, params=PAYLOAD, status=200)
174
    assert resp.json['err']
175
    assert resp.json['err_desc'] == 'Orange fails to send SMS: 500, my_error'
0
-