Projet

Général

Profil

0001-sms-add-description-to-send-endpoint-45829.patch

Nicolas Roche, 10 septembre 2020 17:53

Télécharger (13,4 ko)

Voir les différences:

Subject: [PATCH] sms: add description to send endpoint (#45829)

 passerelle/sms/models.py                      | 50 ++++++++++++-------
 .../manage/messages_service_view.html         |  4 +-
 tests/test_api_access.py                      | 38 ++++++++------
 tests/test_sms.py                             |  4 +-
 4 files changed, 57 insertions(+), 39 deletions(-)
passerelle/sms/models.py
20 20
from django.utils import six
21 21
from django.utils.translation import ugettext_lazy as _
22 22

  
23 23
from passerelle.base.models import BaseResource
24 24
from passerelle.compat import json_loads
25 25
from passerelle.utils.api import endpoint
26 26
from passerelle.utils.jsonresponse import APIError
27 27

  
28
SEND_SCHEMA = {
29
    '$schema': 'http://json-schema.org/draft-04/schema#',
30
    "type": "object",
31
    'required': ['message', 'from', 'to'],
32
    'properties': {
33
        'message': {
34
            'description': 'String message',
35
            'type': 'string',
36
        },
37
        'from': {
38
            'description': 'Sender number',
39
            'type': 'string',
40
        },
41
        'to': {
42
            'description': 'Destination numbers',
43
            "type": "array",
44
            "items": {
45
                'type': 'string',
46
                'pattern': r'^\+?\d+$'
47
            },
48
        },
49
    }
50
}
51

  
28 52

  
29 53
class SMSResource(BaseResource):
30 54
    category = _('SMS Providers')
31 55
    documentation_url = 'https://doc-publik.entrouvert.com/admin-fonctionnel/les-tutos/configuration-envoi-sms/'
32 56

  
33 57
    _can_send_messages_description = _('Sending messages is limited to the following API users:')
34 58

  
35 59
    default_country_code = models.CharField(verbose_name=_('Default country code'), max_length=3,
......
56 80
            elif number.startswith(self.default_trunk_prefix):
57 81
                number = '00' + self.default_country_code + number[len(self.default_trunk_prefix):]
58 82
            else:
59 83
                raise APIError('phone number %r is unsupported (no international prefix, '
60 84
                               'no local trunk prefix)' % number)
61 85
            numbers.append(number)
62 86
        return numbers
63 87

  
64
    @endpoint(perm='can_send_messages', methods=['post'])
65
    def send(self, request, *args, **kwargs):
66
        try:
67
            data = json_loads(request.body)
68
            assert isinstance(data, dict), 'JSON payload is not a dict'
69
            assert 'message' in data, 'missing "message" in JSON payload'
70
            assert 'from' in data, 'missing "from" in JSON payload'
71
            assert 'to' in data, 'missing "to" in JSON payload'
72
            assert isinstance(data['message'], six.text_type), 'message is not a string'
73
            assert isinstance(data['from'], six.text_type), 'from is not a string'
74
            assert all(map(lambda x: isinstance(x, six.text_type), data['to'])), \
75
                'to is not a list of strings'
76
        except (ValueError, AssertionError) as e:
77
            raise APIError('Payload error: %s' % e)
78
        data['message'] = data['message'][:self.max_message_length]
79
        data['to'] = self.clean_numbers(data['to'])
80
        logging.info('sending SMS to %r from %r', data['to'], data['from'])
88
    @endpoint(perm='can_send_messages', methods=['post'],
89
              description=_('Send a SMS message'),
90
              post={'request_body': {'schema': {'application/json': SEND_SCHEMA}}})
91
    def send(self, request, post_data):
92
        post_data['message'] = post_data['message'][:self.max_message_length]
93
        post_data['to'] = self.clean_numbers(post_data['to'])
94
        logging.info('sending SMS to %r from %r', post_data['to'], post_data['from'])
81 95
        stop = not bool('nostop' in request.GET)
82 96
        self.add_job('send_job',
83
                     text=data['message'], sender=data['from'], destinations=data['to'],
97
                     text=post_data['message'], sender=post_data['from'], destinations=post_data['to'],
84 98
                     stop=stop)
85 99
        return {'err': 0}
86 100

  
87 101
    def send_job(self, *args, **kwargs):
88 102
        self.send_msg(**kwargs)
89 103
        SMSLog.objects.create(appname=self.get_connector_slug(), slug=self.slug)
90 104

  
91 105
    class Meta:
passerelle/sms/templates/passerelle/manage/messages_service_view.html
1 1
{% extends "passerelle/manage/service_view.html" %}
2 2
{% load i18n passerelle %}
3 3

  
4 4
{% block endpoints %}
5
<ul>
6
 <li>{% trans 'Sending a message:' %} <a href="send" >{{ site_base_uri }}{{ object.get_absolute_url }}send</a> (POST)</li>
7
</ul>
5
{{ block.super }}
8 6
{% endblock %}
tests/test_api_access.py
37 37
                    description='access for all',
38 38
                    keytype='', key='')
39 39
    obj_type = ContentType.objects.get_for_model(OxydSMSGateway)
40 40
    AccessRight.objects.create(codename='can_send_messages',
41 41
                    apiuser=api,
42 42
                    resource_type=obj_type,
43 43
                    resource_pk=oxyd.pk,
44 44
    )
45
    resp = app.post_json(endpoint_url, params={})
45
    resp = app.post_json(endpoint_url, params={}, status=400)
46 46
    # for empty payload the connector returns an APIError with
47
    # {"err_desc": "missing \"message\" in JSON payload"}
47
    # {"err_desc": "'message' is a required property"}
48 48
    assert resp.json['err'] == 1
49
    assert resp.json['err_desc'] == 'Payload error: missing "message" in JSON payload'
49
    assert resp.json['err_desc'] == "'message' is a required property"
50

  
50 51

  
51 52
def test_access_with_signature(app, oxyd):
52 53
    api = ApiUser.objects.create(username='eservices',
53 54
                    fullname='Eservices User',
54 55
                    description='eservices',
55 56
                    keytype='SIGN',
56 57
                    key='12345')
57 58
    obj_type = ContentType.objects.get_for_model(OxydSMSGateway)
......
60 61
                    apiuser=api,
61 62
                    resource_type=obj_type,
62 63
                    resource_pk=oxyd.pk,
63 64
    )
64 65
    endpoint_url = reverse('generic-endpoint',
65 66
            kwargs={'connector': 'oxyd', 'slug': oxyd.slug, 'endpoint': 'send'})
66 67
    url = signature.sign_url(endpoint_url + '?orig=eservices', '12345')
67 68
    # for empty payload the connector returns an APIError with
68
    # {"err_desc": "missing \"message\" in JSON payload"}
69
    resp = app.post_json(url, params={})
69
    # {"err_desc": "'message' is a required property"}
70
    resp = app.post_json(url, params={}, status=400)
70 71
    assert resp.json['err'] == 1
71
    assert resp.json['err_desc'] == 'Payload error: missing "message" in JSON payload'
72
    assert resp.json['err_desc'] == "'message' is a required property"
73

  
72 74
    # bad key
73 75
    url = signature.sign_url(endpoint_url + '?orig=eservices', 'notmykey')
74 76
    resp = app.post_json(url, params={}, status=403)
75 77
    assert resp.json['err'] == 1
76 78
    assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied'
77 79
    # add garbage after signature
78 80
    url = signature.sign_url(endpoint_url + '?orig=eservices', '12345')
79 81
    url = '%s&foo=bar' % url
80 82
    resp = app.post_json(url, params={}, status=403)
81 83
    assert resp.json['err'] == 1
82 84
    assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied'
83 85

  
84 86
    # trusted user (from settings.KNOWN_SERVICES)
85 87
    url = signature.sign_url(endpoint_url + '?orig=wcs1', 'abcde')
86
    resp = app.post_json(url, params={})
88
    resp = app.post_json(url, params={}, status=400)
87 89
    assert resp.json['err'] == 1
88
    assert resp.json['err_desc'] == 'Payload error: missing "message" in JSON payload'
90
    assert resp.json['err_desc'] == "'message' is a required property"
91

  
89 92
    # bad key
90 93
    url = signature.sign_url(endpoint_url + '?orig=wcs1', 'notmykey')
91 94
    resp = app.post_json(url, params={}, status=403)
92 95
    assert resp.json['err'] == 1
93 96
    assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied'
94 97

  
95 98

  
96 99
def test_access_http_auth(app, oxyd):
......
106 109
    AccessRight.objects.create(codename='can_send_messages',
107 110
                    apiuser=api,
108 111
                    resource_type=obj_type,
109 112
                    resource_pk=oxyd.pk,
110 113
    )
111 114
    app.authorization = ('Basic', (username, password))
112 115
    endpoint_url = reverse('generic-endpoint',
113 116
            kwargs={'connector': 'oxyd', 'slug': oxyd.slug, 'endpoint': 'send'})
114
    resp = app.post_json(endpoint_url, params={})
117
    resp = app.post_json(endpoint_url, params={}, status=400)
115 118
    assert resp.json['err'] == 1
116
    assert resp.json['err_desc'] == 'Payload error: missing "message" in JSON payload'
119
    assert resp.json['err_desc'] == "'message' is a required property"
120

  
117 121

  
118 122
def test_access_apikey(app, oxyd):
119 123
    password = 'apiuser_12345'
120 124
    api = ApiUser.objects.create(username='apiuser',
121 125
            fullname='Api User',
122 126
            description='api',
123 127
            keytype='API',
124 128
            key=password)
......
127 131
    AccessRight.objects.create(codename='can_send_messages',
128 132
                    apiuser=api,
129 133
                    resource_type=obj_type,
130 134
                    resource_pk=oxyd.pk,
131 135
    )
132 136
    params = {'message': 'test'}
133 137
    endpoint_url = reverse('generic-endpoint',
134 138
            kwargs={'connector': 'oxyd', 'slug': oxyd.slug, 'endpoint': 'send'})
135
    resp = app.post_json(endpoint_url + '?apikey=' + password , params=params)
139
    resp = app.post_json(endpoint_url + '?apikey=' + password , params=params, status=400)
136 140
    resp.json['err'] == 1
137
    assert resp.json['err_desc'] == 'Payload error: missing "from" in JSON payload'
141
    assert resp.json['err_desc'] == "'from' is a required property"
138 142
    resp = app.post_json(endpoint_url + '?apikey=' + password[:3] , params=params, status=403)
139 143
    resp.json['err'] == 1
140 144
    assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied'
141 145

  
146

  
142 147
def test_access_apiuser_with_no_key(app, oxyd):
143 148
    api = ApiUser.objects.create(username='apiuser',
144 149
            fullname='Api User',
145 150
            description='api')
146 151
    obj_type = ContentType.objects.get_for_model(OxydSMSGateway)
147 152

  
148 153
    AccessRight.objects.create(codename='can_send_messages',
149 154
                    apiuser=api,
150 155
                    resource_type=obj_type,
151 156
                    resource_pk=oxyd.pk,
152 157
    )
153 158
    params = {'message': 'test', 'from': 'test api'}
154 159
    endpoint_url = reverse('generic-endpoint',
155 160
            kwargs={'connector': 'oxyd', 'slug': oxyd.slug, 'endpoint': 'send'})
156
    resp = app.post_json(endpoint_url, params=params)
161
    resp = app.post_json(endpoint_url, params=params, status=400)
157 162
    assert resp.json['err'] == 1
158
    assert resp.json['err_desc'] == 'Payload error: missing "to" in JSON payload'
163
    assert resp.json['err_desc'] == "'to' is a required property"
164

  
159 165

  
160 166
def test_access_apiuser_with_ip_restriction(app, oxyd):
161 167
    authorized_ip = '176.31.123.109'
162 168
    api = ApiUser.objects.create(username='apiuser',
163 169
            fullname='Api User',
164 170
            description='api',
165 171
            ipsource=authorized_ip
166 172
    )
......
176 182
    resp = app.post_json(endpoint_url, params={}, extra_environ={'REMOTE_ADDR': '127.0.0.1'},
177 183
                         status=403)
178 184
    assert resp.json['err'] == 1
179 185
    assert resp.json['err_class'] == 'django.core.exceptions.PermissionDenied'
180 186

  
181 187
    endpoint_url = reverse('generic-endpoint',
182 188
            kwargs={'connector': 'oxyd', 'slug': oxyd.slug, 'endpoint': 'send'})
183 189
    resp = app.post_json(endpoint_url, params={},
184
                         extra_environ={'REMOTE_ADDR': authorized_ip})
190
                         extra_environ={'REMOTE_ADDR': authorized_ip}, status=400)
185 191
    assert resp.json['err'] == 1
186
    assert resp.json['err_desc'] == 'Payload error: missing "message" in JSON payload'
192
    assert resp.json['err_desc'] == "'message' is a required property"
tests/test_sms.py
51 51
                               apiuser=api,
52 52
                               resource_type=obj_type,
53 53
                               resource_pk=c.pk)
54 54
    return c
55 55

  
56 56

  
57 57
def test_connectors(app, connector, freezer):
58 58
    path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug)
59
    result = app.post_json(path, params={})
59
    result = app.post_json(path, params={}, status=400)
60 60
    assert result.json['err'] == 1
61
    assert result.json['err_desc'].startswith('Payload error: ')
61
    assert result.json['err_desc'] == "'message' is a required property"
62 62

  
63 63
    payload = {
64 64
        'message': 'hello',
65 65
        'from': '+33699999999',
66 66
        'to': ['+33688888888', '+33677777777'],
67 67
    }
68 68
    test_vectors = getattr(connector, 'TEST_DEFAULTS', {}).get('test_vectors', [])
69 69
    total = len(test_vectors)
70
-