Projet

Général

Profil

0001-add-Isere-ENS-connector-50019.patch

Thomas Noël, 21 janvier 2021 00:52

Télécharger (28,2 ko)

Voir les différences:

Subject: [PATCH] add Isere ENS connector (#50019)

 passerelle/contrib/isere_ens/__init__.py      |   0
 .../isere_ens/migrations/0001_initial.py      |  38 ++
 .../contrib/isere_ens/migrations/__init__.py  |   0
 passerelle/contrib/isere_ens/models.py        | 361 ++++++++++++++++++
 tests/settings.py                             |   1 +
 tests/test_isere_ens.py                       | 327 ++++++++++++++++
 6 files changed, 727 insertions(+)
 create mode 100644 passerelle/contrib/isere_ens/__init__.py
 create mode 100644 passerelle/contrib/isere_ens/migrations/0001_initial.py
 create mode 100644 passerelle/contrib/isere_ens/migrations/__init__.py
 create mode 100644 passerelle/contrib/isere_ens/models.py
 create mode 100644 tests/test_isere_ens.py
passerelle/contrib/isere_ens/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2021-01-19 13:09
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', '0028_rename_permissions'),
14
    ]
15

  
16
    operations = [
17
        migrations.CreateModel(
18
            name='IsereENS',
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
                ('basic_auth_username', models.CharField(blank=True, max_length=128, verbose_name='Basic authentication username')),
25
                ('basic_auth_password', models.CharField(blank=True, max_length=128, verbose_name='Basic authentication password')),
26
                ('client_certificate', models.FileField(blank=True, null=True, upload_to='', verbose_name='TLS client certificate')),
27
                ('trusted_certificate_authorities', models.FileField(blank=True, null=True, upload_to='', verbose_name='TLS trusted CAs')),
28
                ('verify_cert', models.BooleanField(default=True, verbose_name='TLS verify certificates')),
29
                ('http_proxy', models.CharField(blank=True, max_length=128, verbose_name='HTTP and HTTPS proxy')),
30
                ('base_url', models.URLField(help_text='Base API URL (before /api/...)', verbose_name='Webservice Base URL')),
31
                ('token', models.CharField(max_length=128, verbose_name='Access token')),
32
                ('users', models.ManyToManyField(blank=True, related_name='_isereens_users_+', related_query_name='+', to='base.ApiUser')),
33
            ],
34
            options={
35
                'verbose_name': 'Espaces naturels sensibles du CD38',
36
            },
37
        ),
38
    ]
passerelle/contrib/isere_ens/models.py
1
# passerelle - uniform access to multiple data sources and services
2
# Copyright (C) 2021 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
from collections import OrderedDict
18
import datetime
19

  
20
from django.core.cache import cache
21
from django.db import models
22
from django.utils.formats import date_format
23
from django.utils.six.moves.urllib import parse as urlparse
24
from django.utils.translation import ugettext_lazy as _
25

  
26
from passerelle.utils.conversion import simplify
27
from passerelle.utils.jsonresponse import APIError
28
from passerelle.utils.api import endpoint
29
from passerelle.base.models import BaseResource, HTTPResource
30

  
31

  
32
SITE_BOOKING_SCHOOL_SCHEMA = {
33
    "$schema": "http://json-schema.org/draft-04/schema#",
34
    "title": "ENS site/booking/school",
35
    "description": "",
36
    "type": "object",
37
    "required": [
38
        "site",
39
        "project",
40
        "date",
41
        "pmr",
42
        "morning",
43
        "lunch",
44
        "afternoon",
45
        "participants",
46
        "animator",
47
        "school",
48
        "grade_levels",
49
        "beneficiary_first_name",
50
        "beneficiary_last_name",
51
        "beneficiary_email",
52
        "beneficiary_phone",
53
        "beneficiary_cellphone",
54
    ],
55
    "properties": OrderedDict(
56
        {
57
            "site": {
58
                "description": "site id (code)",
59
                "type": "string",
60
            },
61
            "project": {
62
                "description": "project code",
63
                "type": "string",
64
            },
65
            "date": {
66
                "description": "booking date (format: YYYY-MM-DD)",
67
                "type": "string",
68
            },
69
            "pmr": {
70
                "description": "PMR",
71
                "type": "boolean",
72
            },
73
            "morning": {
74
                "description": "morning booking",
75
                "type": "boolean",
76
            },
77
            "lunch": {
78
                "description": "lunch booking",
79
                "type": "boolean",
80
            },
81
            "afternoon": {
82
                "description": "afternoon booking",
83
                "type": "boolean",
84
            },
85
            "participants": {
86
                "description": "number of participants",
87
                "type": "string",
88
                "pattern": "[0-9]+",
89
            },
90
            "animator": {
91
                "description": "animator id",
92
                "type": "string",
93
            },
94
            "school": {
95
                "description": "school UAI (RNE) code",
96
                "type": "string",
97
            },
98
            "grade_levels": {
99
                "description": "grade levels",
100
                "type": "array",
101
                "items": {
102
                    "type": "string",
103
                    "description": "level",
104
                },
105
            },
106
            "beneficiary_first_name": {
107
                "description": "beneficiary first name",
108
                "type": "string",
109
            },
110
            "beneficiary_last_name": {
111
                "description": "beneficiary last name",
112
                "type": "string",
113
            },
114
            "beneficiary_email": {
115
                "description": "beneficiary email",
116
                "type": "string",
117
            },
118
            "beneficiary_phone": {
119
                "description": "beneficiary phone number",
120
                "type": "string",
121
            },
122
            "beneficiary_cellphone": {
123
                "description": "beneficiary cell phone number",
124
                "type": "string",
125
            },
126
        }
127
    ),
128
}
129

  
130

  
131
class IsereENS(BaseResource, HTTPResource):
132
    category = _("Business Process Connectors")
133

  
134
    base_url = models.URLField(
135
        verbose_name=_("Webservice Base URL"),
136
        help_text=_("Base API URL (before /api/...)"),
137
    )
138
    token = models.CharField(verbose_name=_("Access token"), max_length=128)
139

  
140
    class Meta:
141
        verbose_name = _("Espaces naturels sensibles de l'Isère")
142

  
143
    def request(self, endpoint, params=None, json=None):
144
        url = urlparse.urljoin(self.base_url, endpoint)
145
        headers = {"token": self.token}
146
        if json is not None:
147
            response = self.requests.post(
148
                url, params=params, json=json, headers=headers
149
            )
150
        else:
151
            response = self.requests.get(url, params=params, headers=headers)
152

  
153
        if response.status_code // 100 != 2:
154
            try:
155
                json_content = response.json()
156
            except ValueError:
157
                json_content = None
158
            raise APIError(
159
                "error status:%s %r, content:%r"
160
                % (response.status_code, response.reason, response.content[:1024]),
161
                data={
162
                    "status_code": response.status_code,
163
                    "json_content": json_content,
164
                },
165
            )
166

  
167
        if response.status_code == 204:  # 204 No Content
168
            raise APIError('abnormal empty response')
169

  
170
        try:
171
            return response.json()
172
        except ValueError:
173
            raise APIError("invalid JSON in response: %r" % response.content[:1024])
174

  
175
    @endpoint(
176
        name="sites",
177
        description=_("Sites"),
178
        display_order=1,
179
        perm="can_access",
180
        parameters={
181
            "q": {"description": _("Search text in name field")},
182
            "id": {
183
                "description": _("Returns site with code=id"),
184
            },
185
            "kind": {
186
                "description": _("Select sites by bind: school_group or social"),
187
            },
188
        },
189
    )
190
    def sites(self, request, q=None, id=None, kind=None):
191
        if id is not None:
192
            site = self.request("api/2.0.0/site/" + id)
193
            site["id"] = site["code"]
194
            site["text"] = "%(name)s (%(city)s)" % site
195
            sites = [site]
196
        else:
197
            cache_key = "isere-ens-sites-%d" % self.id
198
            sites = cache.get(cache_key)
199
            if not sites:
200
                sites = self.request("api/2.0.0/site")
201
                for site in sites:
202
                    site["id"] = site["code"]
203
                    site["text"] = "%(name)s (%(city)s)" % site
204
                cache.set(cache_key, sites, 300)
205
        if kind is not None:
206
            sites = [site for site in sites if site.get(kind)]
207
        if q is not None:
208
            q = simplify(q)
209
            sites = [site for site in sites if q in simplify(site["text"])]
210
        return {"data": sites}
211

  
212
    @endpoint(
213
        name="animators",
214
        description=_("Animators"),
215
        display_order=2,
216
        perm="can_access",
217
        parameters={
218
            "q": {"description": _("Search text in name field")},
219
            "id": {
220
                "description": _("Returns animator number id"),
221
            },
222
        },
223
    )
224
    def animators(self, request, q=None, id=None):
225
        cache_key = "isere-ens-animators-%d" % self.id
226
        animators = cache.get(cache_key)
227
        if not animators:
228
            animators = self.request("api/2.0.0/schoolAnimator")
229
            for animator in animators:
230
                animator["id"] = str(animator["id"])
231
                animator["text"] = (
232
                    "%(first_name)s %(last_name)s <%(email)s> (%(entity)s)" % animator
233
                )
234
            cache.set(cache_key, animators, 300)
235
        if id is not None:
236
            animators = [animator for animator in animators if animator["id"] == id]
237
        if q is not None:
238
            q = simplify(q)
239
            animators = [
240
                animator for animator in animators if q in simplify(animator["text"])
241
            ]
242
        return {"data": animators}
243

  
244
    @endpoint(
245
        name="site-calendar",
246
        description=_("Available bookings for a site"),
247
        display_order=3,
248
        perm="can_access",
249
        parameters={
250
            "site": {"description": _("Site code (aka id)")},
251
            "participants": {
252
                "description": _("Number of participants"),
253
            },
254
            "start_date": {
255
                "description": _(
256
                    "First date of the calendar (format: YYYY-MM-DD, default: today)"
257
                ),
258
            },
259
            "end_date": {
260
                "description": _(
261
                    "Last date of the calendar (format: YYYY-MM-DD, default: start_date + 92 days)"
262
                ),
263
            },
264
        },
265
    )
266
    def site_calendar(
267
        self, request, site, participants="1", start_date=None, end_date=None
268
    ):
269
        if start_date:
270
            try:
271
                start_date = datetime.datetime.strptime(start_date, "%Y-%m-%d").date()
272
            except ValueError:
273
                raise APIError(
274
                    "bad start_date format (%s), should be YYYY-MM-DD" % start_date,
275
                    http_status=400,
276
                )
277
        else:
278
            start_date = datetime.date.today()
279
        if end_date:
280
            try:
281
                end_date = datetime.datetime.strptime(end_date, "%Y-%m-%d").date()
282
            except ValueError:
283
                raise APIError(
284
                    "bad end_date format (%s), should be YYYY-MM-DD" % end_date,
285
                    http_status=400,
286
                )
287
        else:
288
            end_date = start_date + datetime.timedelta(days=92)
289

  
290
        params = {
291
            "site": site,
292
            "participants": participants,
293
            "start_date": start_date.strftime("%Y-%m-%d"),
294
            "end_date": end_date.strftime("%Y-%m-%d"),
295
        }
296
        dates = self.request("api/2.0.0/site/" + site + "/calendar", params=params)
297

  
298
        for date in dates:
299
            date["id"] = site + ":" + date["date"]
300
            date["site"] = site
301
            date_ = datetime.datetime.strptime(date["date"], "%Y-%m-%d").date()
302
            date["date_format"] = date_format(date_, format="DATE_FORMAT")
303
            date["disabled"] = False
304
            date["color"] = "green"
305
            for period in ("morning", "afternoon"):
306
                if date[period] == "COMPLETE":
307
                    date["color"] = "orange"
308
                    date["%s_status" % period] = _("Complete")
309
                else:
310
                    date["%s_status" % period] = _("Available")
311
            if date["morning"] == "COMPLETE" and date["afternoon"] == "COMPLETE":
312
                date["disabled"] = True
313
                date["color"] = "red"
314
            if date["lunch"] == "CLOSE":
315
                date["lunch_status"] = _("Complete")
316
            else:
317
                date["lunch_status"] = _("Available")
318
            date["details"] = _(
319
                "Morning (%(morning_status)s), Lunch (%(lunch_status)s), Afternoon (%(afternoon_status)s)"
320
                % date
321
            )
322
            date["text"] = "%(date_format)s - %(details)s" % date
323
        return {"data": dates}
324

  
325
    @endpoint(
326
        name="site-booking-school",
327
        description=_("Booking a site for a school"),
328
        display_order=4,
329
        perm="can_access",
330
        methods=["post"],
331
        post={
332
            "request_body": {
333
                "schema": {
334
                    "application/json": SITE_BOOKING_SCHOOL_SCHEMA,
335
                }
336
            }
337
        },
338
    )
339
    def site_booking_school(self, request, post_data):
340
        payload = {
341
            "siteCode": post_data["site"],
342
            "projectCode": post_data["project"],
343
            "bookingDate": post_data["date"],
344
            "pmr": post_data["pmr"],
345
            "morning": post_data["morning"],
346
            "lunch": post_data["lunch"],
347
            "afternoon": post_data["afternoon"],
348
            "participants": int(post_data["participants"]),
349
            "schoolAnimator": post_data["animator"],
350
            "school": post_data["school"],
351
            "gradeLevels": post_data["grade_levels"],
352
            "beneficiary": {
353
                "firstName": post_data["beneficiary_first_name"],
354
                "lastName": post_data["beneficiary_last_name"],
355
                "email": post_data["beneficiary_email"],
356
                "phone": post_data["beneficiary_phone"],
357
                "cellphone": post_data["beneficiary_cellphone"],
358
            },
359
        }
360
        booking = self.request("api/2.0.0/site/booking/school", json=payload)
361
        return {"data": booking}
tests/settings.py
22 22
    'passerelle.contrib.grandlyon_streetsections',
23 23
    'passerelle.contrib.greco',
24 24
    'passerelle.contrib.grenoble_gru',
25
    'passerelle.contrib.isere_ens',
25 26
    'passerelle.contrib.iparapheur',
26 27
    'passerelle.contrib.iws',
27 28
    'passerelle.contrib.lille_urban_card',
tests/test_isere_ens.py
1
# -*- coding: utf-8 -*-
2
# Passerelle - uniform access to data and services
3
# Copyright (C) 2021  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; exclude 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.deepcopy of the GNU Affero General Public License
16
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17

  
18

  
19
import json
20
import os
21
import mock
22
import pytest
23

  
24
import utils
25

  
26
from django.urls import reverse
27

  
28
from passerelle.contrib.isere_ens.models import IsereENS
29
from passerelle.utils.jsonresponse import APIError
30

  
31

  
32
@pytest.fixture
33
def setup(db):
34
    return utils.setup_access_rights(
35
        IsereENS.objects.create(
36
            slug="test", base_url="https://ens38.example.net/", token="toktok"
37
        )
38
    )
39

  
40

  
41
SITES_RESPONSE = """[
42
  {
43
    "name": "Save  - étangs de la Serre",
44
    "code": "SD29a",
45
    "city": "Arandon-Passins - Courtenay",
46
    "school_group": true,
47
    "social": true
48
  },
49
  {
50
    "name": "Save - étangs de Passins",
51
    "code": "SD29b",
52
    "city": "Arandon-Passins",
53
    "school_group": true,
54
    "social": true
55
  },
56
  {
57
    "name": "Save - lac de Save",
58
    "code": "SD29c",
59
    "city": "Arandon-Passins",
60
    "school_group": true,
61
    "social": false
62
  }
63
]"""
64

  
65
SD29B_RESPONSE = """{
66
  "name": "Save - étangs de Passins",
67
  "code": "SD29b",
68
  "type": "DEPARTMENTAL_ENS",
69
  "city": "Arandon-Passins",
70
  "natural_environment": "Etangs, tourbières, mares, forêts, rivière, pelouses sèches",
71
  "lunch_equipment": null,
72
  "pmr_access": "NO",
73
  "school_group": true,
74
  "social": true,
75
  "user_informations": "<p>Un album jeunesse a &eacute;t&eacute; r&eacute;alis&eacute; sur l'ENS de la Save. Il permet aux enfants de s'approprier le site en amont d'une sortie.</p>",
76
  "dogs": "LEASH",
77
  "educational_equipments": null,
78
  "eps_regulation": null,
79
  "main_manager": {
80
    "first_name": "Jo",
81
    "last_name": "Smith",
82
    "managing_entity": "Département de l'Isère",
83
    "main_phone": "01 23 45 67 89",
84
    "email": "jo.smith@example.net"
85
  }
86
}"""
87

  
88
SITE_404_RESPONSE = """{
89
  "user_message": "Impossible de trouver le site",
90
  "message": "Site not found with code SD29x"
91
}"""
92

  
93
ANIMATORS_RESPONSE = """[
94
  {
95
    "id": 1,
96
    "first_name": "Francis",
97
    "last_name": "Kuntz",
98
    "email": "fk@mail.grd",
99
    "phone": "123",
100
    "entity": "Association Nature Morte"
101
  },
102
  {
103
    "id": 2,
104
    "first_name": "Michael",
105
    "last_name": "Kael",
106
    "email": "mk@mail.grd",
107
    "phone": "456",
108
    "entity": "Association Porte de l'Enfer"
109
  }
110
]"""
111

  
112
SITE_CALENDAR_RESPONSE = """[
113
  {
114
    "date": "2020-01-21",
115
    "morning": "AVAILABLE",
116
    "lunch": "CLOSE",
117
    "afternoon": "AVAILABLE"
118
  },
119
  {
120
    "date": "2020-01-22",
121
    "morning": "AVAILABLE",
122
    "lunch": "OPEN",
123
    "afternoon": "COMPLETE"
124
  },
125
  {
126
    "date": "2020-01-23",
127
    "morning": "COMPLETE",
128
    "lunch": "CLOSE",
129
    "afternoon": "COMPLETE"
130
  }
131
]"""
132

  
133
BOOK_RESPONSE = """{
134
  "status": "OK"
135
}"""
136

  
137

  
138
@mock.patch("passerelle.utils.Request.get")
139
def test_get_sites(mocked_get, app, setup):
140
    mocked_get.return_value = utils.FakedResponse(
141
        content=SITES_RESPONSE, status_code=200
142
    )
143
    endpoint = reverse(
144
        "generic-endpoint",
145
        kwargs={"connector": "isere-ens", "slug": setup.slug, "endpoint": "sites"},
146
    )
147
    response = app.get(endpoint)
148
    assert mocked_get.call_args[0][0].endswith("api/2.0.0/site")
149
    assert mocked_get.call_args[1]["headers"]["token"] == "toktok"
150
    assert mocked_get.call_count == 1
151
    assert "data" in response.json
152
    assert response.json["err"] == 0
153
    for item in response.json["data"]:
154
        assert "id" in item
155
        assert "text" in item
156
        assert "city" in item
157
        assert "code" in item
158

  
159
    # test cache system
160
    response = app.get(endpoint)
161
    assert mocked_get.call_count == 1
162

  
163
    response = app.get(endpoint + "?q=etangs")
164
    assert len(response.json["data"]) == 2
165
    response = app.get(endpoint + "?q=CourTe")
166
    assert len(response.json["data"]) == 1
167
    response = app.get(endpoint + "?kind=social")
168
    assert len(response.json["data"]) == 2
169

  
170
    mocked_get.return_value = utils.FakedResponse(
171
        content=SD29B_RESPONSE, status_code=200
172
    )
173
    response = app.get(endpoint + "?id=SD29b")
174
    assert mocked_get.call_args[0][0].endswith("api/2.0.0/site/SD29b")
175
    assert len(response.json["data"]) == 1
176
    assert response.json["data"][0]["id"] == "SD29b"
177
    assert response.json["data"][0]["dogs"] == "LEASH"
178

  
179
    # bad response for ENS API
180
    mocked_get.return_value = utils.FakedResponse(
181
        content=SITE_404_RESPONSE, status_code=404
182
    )
183
    response = app.get(endpoint + "?id=SD29x")
184
    assert mocked_get.call_args[0][0].endswith("api/2.0.0/site/SD29x")
185
    assert response.json["err"] == 1
186
    assert response.json["err_class"].endswith("APIError")
187
    assert response.json["err_desc"].startswith("error status:404")
188
    assert response.json["data"]["status_code"] == 404
189
    assert (
190
        response.json["data"]["json_content"]["message"]
191
        == "Site not found with code SD29x"
192
    )
193
    mocked_get.return_value = utils.FakedResponse(content="crash", status_code=500)
194
    response = app.get(endpoint + "?id=foo500")
195
    assert mocked_get.call_args[0][0].endswith("api/2.0.0/site/foo500")
196
    assert response.json["err"] == 1
197
    assert response.json["err_desc"].startswith("error status:500")
198
    assert response.json["err_class"].endswith("APIError")
199
    assert response.json["data"]["status_code"] == 500
200
    assert response.json["data"]["json_content"] is None
201
    mocked_get.return_value = utils.FakedResponse(content=None, status_code=204)
202
    response = app.get(endpoint + "?id=foo204")
203
    assert mocked_get.call_args[0][0].endswith("api/2.0.0/site/foo204")
204
    assert response.json["err"] == 1
205
    assert response.json["err_class"].endswith("APIError")
206
    assert response.json["err_desc"] == "abnormal empty response"
207
    mocked_get.return_value = utils.FakedResponse(content="not json", status_code=200)
208
    response = app.get(endpoint + "?id=foo")
209
    assert mocked_get.call_args[0][0].endswith("api/2.0.0/site/foo")
210
    assert response.json["err"] == 1
211
    assert response.json["err_class"].endswith("APIError")
212
    assert response.json["err_desc"].startswith("invalid JSON in response:")
213

  
214

  
215
@mock.patch("passerelle.utils.Request.get")
216
def test_get_animators(mocked_get, app, setup):
217
    mocked_get.return_value = utils.FakedResponse(
218
        content=ANIMATORS_RESPONSE, status_code=200
219
    )
220
    endpoint = reverse(
221
        "generic-endpoint",
222
        kwargs={"connector": "isere-ens", "slug": setup.slug, "endpoint": "animators"},
223
    )
224
    response = app.get(endpoint)
225
    assert mocked_get.call_args[0][0].endswith("api/2.0.0/schoolAnimator")
226
    assert mocked_get.call_count == 1
227
    assert "data" in response.json
228
    assert response.json["err"] == 0
229
    for item in response.json["data"]:
230
        assert "id" in item
231
        assert "text" in item
232
        assert "first_name" in item
233
        assert "email" in item
234

  
235
    # test cache system
236
    response = app.get(endpoint)
237
    assert mocked_get.call_count == 1
238

  
239
    response = app.get(endpoint + "?q=Kael")
240
    assert len(response.json["data"]) == 1
241
    response = app.get(endpoint + "?q=association")
242
    assert len(response.json["data"]) == 2
243
    response = app.get(endpoint + "?q=mail.grd")
244
    assert len(response.json["data"]) == 2
245
    response = app.get(endpoint + "?id=2")
246
    assert len(response.json["data"]) == 1
247
    assert response.json["data"][0]["first_name"] == "Michael"
248

  
249

  
250
@mock.patch("passerelle.utils.Request.get")
251
def test_get_site_calendar(mocked_get, app, setup, freezer):
252
    freezer.move_to("2021-01-21 12:00:00")
253
    mocked_get.return_value = utils.FakedResponse(
254
        content=SITE_CALENDAR_RESPONSE, status_code=200
255
    )
256
    endpoint = reverse(
257
        "generic-endpoint",
258
        kwargs={
259
            "connector": "isere-ens",
260
            "slug": setup.slug,
261
            "endpoint": "site-calendar",
262
        },
263
    )
264
    response = app.get(endpoint + "?site=SD29b")
265
    assert mocked_get.call_args[0][0].endswith("api/2.0.0/site/SD29b/calendar")
266
    assert mocked_get.call_args[1]["params"]["start_date"] == "2021-01-21"
267
    assert mocked_get.call_args[1]["params"]["end_date"] == "2021-04-23"
268
    assert response.json["err"] == 0
269
    assert len(response.json["data"]) == 3
270
    response = app.get(endpoint + "?site=SD29b&start_date=2021-01-22")
271
    assert mocked_get.call_args[1]["params"]["start_date"] == "2021-01-22"
272
    assert mocked_get.call_args[1]["params"]["end_date"] == "2021-04-24"
273
    assert response.json["err"] == 0
274
    response = app.get(
275
        endpoint + "?site=SD29b&start_date=2021-01-22&end_date=2021-01-30"
276
    )
277
    assert mocked_get.call_args[1]["params"]["start_date"] == "2021-01-22"
278
    assert mocked_get.call_args[1]["params"]["end_date"] == "2021-01-30"
279
    assert response.json["err"] == 0
280
    response = app.get(endpoint + "?site=SD29b&start_date=foo", status=400)
281
    assert response.json["err"] == 1
282
    assert response.json["err_class"].endswith("APIError")
283
    assert (
284
        response.json["err_desc"] == "bad start_date format (foo), should be YYYY-MM-DD"
285
    )
286
    response = app.get(endpoint + "?site=SD29b&end_date=bar", status=400)
287
    assert response.json["err"] == 1
288
    assert response.json["err_class"].endswith("APIError")
289
    assert (
290
        response.json["err_desc"] == "bad end_date format (bar), should be YYYY-MM-DD"
291
    )
292

  
293

  
294
@mock.patch("passerelle.utils.Request.post")
295
def test_post_book(mocked_post, app, setup):
296
    mocked_post.return_value = utils.FakedResponse(
297
        content=BOOK_RESPONSE, status_code=200
298
    )
299
    endpoint = reverse(
300
        "generic-endpoint",
301
        kwargs={
302
            "connector": "isere-ens",
303
            "slug": setup.slug,
304
            "endpoint": "site-booking-school",
305
        },
306
    )
307
    book = {
308
        "site": "SD29b",
309
        "project": "Publik",
310
        "date": "2020-01-22",
311
        "pmr": True,
312
        "morning": True,
313
        "lunch": False,
314
        "afternoon": False,
315
        "participants": "50",
316
        "animator": "2",
317
        "school": "1234567X",
318
        "grade_levels": ["CP", "CE1"],
319
        "beneficiary_first_name": "Foo",
320
        "beneficiary_last_name": "Bar",
321
        "beneficiary_email": "foobar@example.net",
322
        "beneficiary_phone": "9876",
323
        "beneficiary_cellphone": "06",
324
    }
325
    response = app.post_json(endpoint, params=book)
326
    assert mocked_post.call_args[0][0].endswith("api/2.0.0/site/booking/school")
327
    assert mocked_post.call_count == 1
0
-