Projet

Général

Profil

0001-orange_sms-initial-version-41092.patch

Nicolas Roche, 10 avril 2020 11:31

Télécharger (15,5 ko)

Voir les différences:

Subject: [PATCH] orange_sms: initial version (#41092)

copied from https://github.com/departement-loire-atlantique/passerelle-orangesms
 passerelle/apps/orangerest/__init__.py        |   0
 .../orangerest/migrations/0001_initial.py     |  37 ++++
 .../apps/orangerest/migrations/__init__.py    |   0
 passerelle/apps/orangerest/models.py          | 116 ++++++++++++
 passerelle/settings.py                        |   1 +
 tests/test_orangerest.py                      | 175 ++++++++++++++++++
 6 files changed, 329 insertions(+)
 create mode 100644 passerelle/apps/orangerest/__init__.py
 create mode 100644 passerelle/apps/orangerest/migrations/0001_initial.py
 create mode 100644 passerelle/apps/orangerest/migrations/__init__.py
 create mode 100644 passerelle/apps/orangerest/models.py
 create mode 100644 tests/test_orangerest.py
passerelle/apps/orangerest/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-04-10 07:23
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    initial = True
11

  
12
    dependencies = [
13
        ('base', '0018_smslog'),
14
    ]
15

  
16
    operations = [
17
        migrations.CreateModel(
18
            name='OrangeRestSMSGateway',
19
            fields=[
20
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
                ('title', models.CharField(max_length=50, verbose_name='Title')),
22
                ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
23
                ('description', models.TextField(verbose_name='Description')),
24
                ('max_message_length', models.IntegerField(default=160, verbose_name='Maximum message length')),
25
                ('username', models.CharField(max_length=64, verbose_name='Identifiant')),
26
                ('password', models.CharField(max_length=64, verbose_name='Mot de passe')),
27
                ('groupname', models.CharField(max_length=64, verbose_name='Groupe')),
28
                ('default_country_code', models.CharField(default='33', max_length=3, verbose_name='Préfixe pays')),
29
                ('default_trunk_prefix', models.CharField(default='0', max_length=2, verbose_name='Préfixe supprimé par défaut')),
30
                ('users', models.ManyToManyField(blank=True, related_name='_orangerestsmsgateway_users_+', related_query_name='+', to='base.ApiUser')),
31
            ],
32
            options={
33
                'verbose_name': 'Orange REST',
34
                'db_table': 'sms_orangerest',
35
            },
36
        ),
37
    ]
passerelle/apps/orangerest/models.py
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/>.
22
from django.db import models
23
from django.utils.translation import ugettext_lazy as _
24

  
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 OrangeRestError(APIError):
35
    pass
36

  
37

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

  
44

  
45
class OrangeRestSMSGateway(SMSResource):
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)
49

  
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')
54

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

  
57
    class Meta:
58
        verbose_name = _('Orange REST')
59
        db_table = 'sms_orangerest'
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 OrangeRestError('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 OrangeRestError('Orange fails to send SMS: %s, %s' % (
106
                response.status_code, response.text))
107
        return get_json(response)
108

  
109
    def send_msg(self, text, sender, destinations, **kwargs):
110
        '''Send a SMS using the Orange REST'''
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/settings.py
149 149
    'passerelle.apps.opengis',
150 150
    'passerelle.apps.orange',
151 151
    'passerelle.apps.ovh',
152 152
    'passerelle.apps.oxyd',
153 153
    'passerelle.apps.pastell',
154 154
    'passerelle.apps.phonecalls',
155 155
    'passerelle.apps.solis',
156 156
    'passerelle.apps.vivaticket',
157
    'passerelle.apps.orangerest',
157 158
    # backoffice templates and static
158 159
    'gadjo',
159 160
)
160 161

  
161 162
# disable some applications for now
162 163
PASSERELLE_APP_BDP_ENABLED = False
163 164
PASSERELLE_APP_GDC_ENABLED = False
164 165
PASSERELLE_APP_PASTELL_ENABLED = False
tests/test_orangerest.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.orangerest.models import OrangeRestSMSGateway, OrangeRestError
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
        OrangeRestSMSGateway.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 = OrangeRestSMSGateway()
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(OrangeRestError, 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(OrangeRestError, 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 = OrangeRestSMSGateway()
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(OrangeRestError, 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 = OrangeRestSMSGateway()
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(OrangeRestError, 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(OrangeRestError, 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
-