Projet

Général

Profil

0001-orange_sms-initial-version-41092.patch

Nicolas Roche, 06 avril 2020 07:38

Télécharger (15,1 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          | 117 ++++++++++++
 passerelle/settings.py                        |   1 +
 tests/test_orange_sms.py                      | 166 ++++++++++++++++++
 6 files changed, 321 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

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

  
29

  
30
class OrangeError(APIError):
31
    pass
32

  
33

  
34
BASE_API = 'https://contact-everyone.orange-business.com/api/v1.2/'
35
URL_TOKEN = BASE_API + 'oauth/token'
36
URL_GROUPS = BASE_API + 'groups'
37
URL_DIFFUSION = BASE_API + 'groups/%s/diffusion-requests'
38

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

  
45
def login(requests, username, password):
46
    headers = {'content-type': 'application/x-www-form-urlencoded'}
47
    params = {'username': username, 'password': password}
48
    response = requests.post(URL_TOKEN, data=params, headers=headers)
49
    if response.status_code != 200:
50
        raise APIError('Bad username or password: %s, %s' % (
51
            response.status_code, response.text))
52
    response_json = get_json(response)
53
    if 'access_token' not in response_json:
54
        raise OrangeError('Orange do not return access token')
55
    return response_json['access_token']
56

  
57
def group_id_from_name(requests, access_token, group_name):
58
    headers = {'authorization': 'Bearer %s' % access_token}
59
    response = requests.get(URL_GROUPS, headers=headers)
60
    if response.status_code != 200:
61
        raise APIError('Bad token: %s, %s' % (
62
            response.status_code, response.text))
63
    response_json = get_json(response)
64
    group_id = None
65
    for group in response_json:
66
        if group['name'] == group_name:
67
            group_id = group['id']
68
            break
69
    if not group_id:
70
        raise APIError('Group name not found: ' + group_name)
71
    return group_id
72

  
73
def send(requests, access_token, group_id, destinations, message):
74
    headers = {
75
        'content-type': 'application/json',
76
        'authorization': 'Bearer %s' % access_token,
77
    }
78
    payload = json.dumps({
79
        'name': 'Send a SMS from passerelle',
80
        'msisdns': destinations,
81
        'smsParam': {
82
            'encoding': 'GSM7',
83
            'body': message
84
        }
85
    })
86
    response = requests.post(URL_DIFFUSION % group_id, data=payload, headers=headers)
87
    if response.status_code != 201:
88
        raise OrangeError('Orange fails to send SMS: %s, %s' % (
89
            response.status_code, response.text))
90
    return get_json(response)
91

  
92

  
93
class OrangeRestSMSGateway(SMSResource):
94
    username = models.CharField(verbose_name='Identifiant', max_length=64)
95
    password = models.CharField(verbose_name='Mot de passe', max_length=64)
96
    groupname = models.CharField(verbose_name='Groupe', max_length=64)
97

  
98
    default_country_code = models.CharField(
99
        verbose_name='Préfixe pays', max_length=3, default='33')
100
    default_trunk_prefix = models.CharField(
101
        verbose_name='Préfixe supprimé par défaut', max_length=2, default='0')
102

  
103
    manager_view_template_name = 'passerelle/manage/messages_service_view.html'
104

  
105
    class Meta:
106
        verbose_name = 'Orange REST'
107
        db_table = 'sms_orangerest'
108

  
109
    def send_msg(self, text, sender, destinations, **kwargs):
110
        '''Send a SMS using the Orange REST'''
111

  
112
        destinations = self.clean_numbers(
113
            destinations, self.default_country_code, self.default_trunk_prefix)
114
        access_token = login(self.requests, self.username, self.password)
115
        group_id = group_id_from_name(self.requests, access_token, self.groupname)
116
        response = send(self.requests, access_token, group_id, destinations, text)
117
        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
import requests
19

  
20
import httmock
21
import pytest
22

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

  
25
from passerelle.apps.orange_sms.models import (
26
    OrangeRestSMSGateway, OrangeError, login, group_id_from_name, send)
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_login_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_id_from_name_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_send_ok(url, request):
57
    assert request.headers['authorization'] == 'Bearer my_token'
58
    assert json.loads(request.body)['smsParam']['body'] == PAYLOAD['message']
59
    '33688888888' in json.loads(request.body)['msisdns'][0]
60
    '33677777777' in json.loads(request.body)['msisdns'][1]
61
    content = json.dumps({'status': "I'm ok"})
62
    return httmock.response(201, content, JSON_HEADERS)
63

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

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

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

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

  
90

  
91
def test_login(app, connector):
92
    with httmock.HTTMock(response_login_ok):
93
        assert login(requests, 'jdoe', 'secret') == 'my_token'
94

  
95
    # not 200
96
    with pytest.raises(APIError, match='Bad username or password'):
97
        with httmock.HTTMock(response_500):
98
            login(requests, 'jdoe', 'secret')
99

  
100
    # not json
101
    with pytest.raises(OrangeError, match='Orange returned Invalid JSON content'):
102
        with httmock.HTTMock(response_invalid_json):
103
            login(requests, 'jdoe', 'secret')
104

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

  
110
    with pytest.raises(OrangeError, match='Orange do not return access token'):
111
        with httmock.HTTMock(mocked_response):
112
            login(requests, 'jdoe', 'secret')
113

  
114

  
115
def test_group_id_from_name(app, connector):
116
    with httmock.HTTMock(response_group_id_from_name_ok):
117
        assert group_id_from_name(requests, 'my_token', 'group2') == 'gid2'
118

  
119
    # no group
120
    with pytest.raises(APIError, match='Group name not found: group3'):
121
        with httmock.HTTMock(response_group_id_from_name_ok):
122
            group_id_from_name(requests, 'my_token', 'group3')
123

  
124
    # not 200
125
    with pytest.raises(APIError, match='Bad token'):
126
        with httmock.HTTMock(response_500):
127
            group_id_from_name(requests, 'my_token', 'group2')
128

  
129
    # not json
130
    with pytest.raises(OrangeError, match='Orange returned Invalid JSON content'):
131
        with httmock.HTTMock(response_invalid_json):
132
            group_id_from_name(requests, 'my_token', 'group2')
133

  
134

  
135
def test_send(app, connector):
136
    with httmock.HTTMock(response_send_ok):
137
        resp = send(requests, 'my_token', 'gid2', PAYLOAD['to'], PAYLOAD['message'])
138
    assert resp['status'] == "I'm ok"
139

  
140
    # not 201
141
    with pytest.raises(OrangeError, match='Orange fails to send SMS'):
142
        with httmock.HTTMock(response_500):
143
            send(requests, 'my_token', 'gid2', PAYLOAD['to'], PAYLOAD['message'])
144

  
145
    # not json
146
    @httmock.urlmatch(netloc=NETLOC)
147
    def mocked_response(url, request):
148
        return httmock.response(201, 'not a JSON content')
149

  
150
    with pytest.raises(OrangeError, match='Orange returned Invalid JSON content'):
151
        with httmock.HTTMock(mocked_response):
152
            send(requests, 'my_token', 'gid2', PAYLOAD['to'], PAYLOAD['message'])
153

  
154

  
155
def test_send_msg(app, connector):
156
    url = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug)
157
    with httmock.HTTMock(response_login_ok, response_group_id_from_name_ok, response_send_ok):
158
        resp = app.post_json(url, params=PAYLOAD, status=200)
159
    assert not resp.json['err']
160
    assert resp.json['data']['status'] == "I'm ok"
161

  
162
    # not 201
163
    with httmock.HTTMock(response_login_ok, response_group_id_from_name_ok, response_500):
164
        resp = app.post_json(url, params=PAYLOAD, status=200)
165
    assert resp.json['err']
166
    assert resp.json['err_desc'] == 'Orange fails to send SMS: 500, my_error'
0
-