Projet

Général

Profil

0001-orange_sms-initial-version-41092.patch

Nicolas Roche, 06 avril 2020 10:22

Télécharger (15,4 ko)

Voir les différences:

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

copied from https://github.com/departement-loire-atlantique/passerelle-orangesms
 passerelle/apps/orange_sms/__init__.py        |   0
 .../orange_sms/migrations/0001_initial.py     |  37 ++++
 .../apps/orange_sms/migrations/__init__.py    |   0
 passerelle/apps/orange_sms/models.py          | 118 ++++++++++++
 passerelle/settings.py                        |   1 +
 tests/test_orange_sms.py                      | 172 ++++++++++++++++++
 6 files changed, 328 insertions(+)
 create mode 100644 passerelle/apps/orange_sms/__init__.py
 create mode 100644 passerelle/apps/orange_sms/migrations/0001_initial.py
 create mode 100644 passerelle/apps/orange_sms/migrations/__init__.py
 create mode 100644 passerelle/apps/orange_sms/models.py
 create mode 100644 tests/test_orange_sms.py
passerelle/apps/orange_sms/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-04-04 11:32
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 SMS',
34
                'db_table': 'sms_orangerest',
35
            },
36
        ),
37
    ]
passerelle/apps/orange_sms/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
import json
23

  
24
from django.db import models
25
from django.utils.translation import ugettext_lazy as _
26

  
27
from passerelle.base.models import SMSResource
28
from passerelle.utils.jsonresponse import APIError
29

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

  
35

  
36
class OrangeError(APIError):
37
    pass
38

  
39

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

  
46

  
47
class OrangeRestSMSGateway(SMSResource):
48
    username = models.CharField(verbose_name=_('Identifiant'), max_length=64)
49
    password = models.CharField(verbose_name=_('Mot de passe'), max_length=64)
50
    groupname = models.CharField(verbose_name=_('Groupe'), max_length=64)
51

  
52
    default_country_code = models.CharField(
53
        verbose_name='Préfixe pays', max_length=3, default='33')
54
    default_trunk_prefix = models.CharField(
55
        verbose_name='Préfixe supprimé par défaut', max_length=2, default='0')
56

  
57
    manager_view_template_name = 'passerelle/manage/messages_service_view.html'
58

  
59
    class Meta:
60
        verbose_name = 'Orange REST'
61
        db_table = 'sms_orangerest'
62

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

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

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

  
111
    def send_msg(self, text, sender, destinations, **kwargs):
112
        '''Send a SMS using the Orange REST'''
113
        destinations = self.clean_numbers(
114
            destinations, self.default_country_code, self.default_trunk_prefix)
115
        access_token = self.get_access_token()
116
        group_id = self.group_id_from_name(access_token)
117
        response = self.diffusion(access_token, group_id, destinations, text)
118
        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.orange_sms',
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_orange_sms.py
1
# passerelle - uniform access to multiple data sources and services
2
# Copyright (C) 2020  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import json
18

  
19
import httmock
20
import pytest
21

  
22
from django.contrib.contenttypes.models import ContentType
23

  
24
from passerelle.apps.orange_sms.models import OrangeRestSMSGateway, OrangeError
25
from passerelle.base.models import ApiUser, AccessRight
26
from passerelle.utils.jsonresponse import APIError
27

  
28

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

  
37

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

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

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

  
62
@httmock.urlmatch(netloc=NETLOC)
63
def response_500(url, request):
64
    return httmock.response(500, 'my_error')
65

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

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

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

  
88

  
89
def test_get_access_token(app, connector):
90
    orange = OrangeRestSMSGateway()
91
    orange.username = 'jdoe'
92
    orange.password = 'secret'
93
    with httmock.HTTMock(response_token_ok):
94
        assert orange.get_access_token() == 'my_token'
95

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

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

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

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

  
115

  
116
def test_group_id_from_name(app, connector):
117
    orange = OrangeRestSMSGateway()
118
    orange.groupname = 'group2'
119
    with httmock.HTTMock(response_group_ok):
120
        assert orange.group_id_from_name('my_token') == 'gid2'
121

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

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

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

  
139

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

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

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

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

  
160

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

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