add Isere ENS connector (#50019)

This commit is contained in:
Thomas NOËL 2021-01-10 21:04:49 +01:00
parent 2e30f462c9
commit a4bc4c2335
6 changed files with 837 additions and 0 deletions

View File

View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2021-01-19 13:09
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('base', '0028_rename_permissions'),
]
operations = [
migrations.CreateModel(
name='IsereENS',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=50, verbose_name='Title')),
('slug', models.SlugField(unique=True, verbose_name='Identifier')),
('description', models.TextField(verbose_name='Description')),
('basic_auth_username', models.CharField(blank=True, max_length=128, verbose_name='Basic authentication username')),
('basic_auth_password', models.CharField(blank=True, max_length=128, verbose_name='Basic authentication password')),
('client_certificate', models.FileField(blank=True, null=True, upload_to='', verbose_name='TLS client certificate')),
('trusted_certificate_authorities', models.FileField(blank=True, null=True, upload_to='', verbose_name='TLS trusted CAs')),
('verify_cert', models.BooleanField(default=True, verbose_name='TLS verify certificates')),
('http_proxy', models.CharField(blank=True, max_length=128, verbose_name='HTTP and HTTPS proxy')),
('base_url', models.URLField(help_text='Base API URL (before /api/...)', verbose_name='Webservice Base URL')),
('token', models.CharField(max_length=128, verbose_name='Access token')),
('users', models.ManyToManyField(blank=True, related_name='_isereens_users_+', related_query_name='+', to='base.ApiUser')),
],
options={
'verbose_name': 'Espaces naturels sensibles du CD38',
},
),
]

View File

@ -0,0 +1,414 @@
# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2021 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from collections import OrderedDict
import datetime
from django.core.cache import cache
from django.db import models
from django.utils.formats import date_format
from django.utils.six.moves.urllib import parse as urlparse
from django.utils.translation import ugettext_lazy as _
from passerelle.utils.conversion import simplify
from passerelle.utils.jsonresponse import APIError
from passerelle.utils.api import endpoint
from passerelle.base.models import BaseResource, HTTPResource
SITE_BOOKING_SCHOOL_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "ENS site/booking/school",
"description": "",
"type": "object",
"required": [
"code",
"status",
"beneficiary_id",
"beneficiary_first_name",
"beneficiary_last_name",
"beneficiary_email",
"beneficiary_phone",
"beneficiary_cellphone",
"entity_id",
"entity_name",
"entity_type",
"project",
"site",
"applicant",
"public",
"date",
"participants",
"morning",
"lunch",
"afternoon",
"pmr",
"grade_levels",
"animator",
],
"properties": OrderedDict(
{
"code": {
"description": "booking code",
"type": "string",
},
"status": {
"description": "booking status",
"type": "string",
},
"beneficiary_id": {
"description": "beneficiary id",
"type": "string",
},
"beneficiary_first_name": {
"description": "beneficiary first name",
"type": "string",
},
"beneficiary_last_name": {
"description": "beneficiary last name",
"type": "string",
},
"beneficiary_email": {
"description": "beneficiary email",
"type": "string",
},
"beneficiary_phone": {
"description": "beneficiary phone number",
"type": "string",
},
"beneficiary_cellphone": {
"description": "beneficiary cell phone number",
"type": "string",
},
"entity_id": {
"description": "entity/school id (UAI/RNE)",
"type": "string",
},
"entity_name": {
"description": "entity/school name",
"type": "string",
},
"entity_type": {
"description": "entity/school type",
"type": "string",
},
"project": {
"description": "project code",
"type": "string",
},
"site": {
"description": "site id (code)",
"type": "string",
},
"applicant": {
"description": "applicant",
"type": "string",
},
"public": {
"description": "public",
"type": "string",
},
"date": {
"description": "booking date (format: YYYY-MM-DD)",
"type": "string",
},
"participants": {
"description": "number of participants",
"type": "string",
"pattern": "[0-9]+",
},
"morning": {
"description": "morning booking",
"type": "boolean",
},
"lunch": {
"description": "lunch booking",
"type": "boolean",
},
"afternoon": {
"description": "afternoon booking",
"type": "boolean",
},
"pmr": {
"description": "PMR",
"type": "boolean",
},
"grade_levels": {
"description": "grade levels",
"type": "array",
"items": {
"type": "string",
"description": "level",
},
},
"animator": {
"description": "animator id",
"type": "string",
"pattern": "[0-9]+",
},
}
),
}
class IsereENS(BaseResource, HTTPResource):
category = _("Business Process Connectors")
base_url = models.URLField(
verbose_name=_("Webservice Base URL"),
help_text=_("Base API URL (before /api/...)"),
)
token = models.CharField(verbose_name=_("Access token"), max_length=128)
class Meta:
verbose_name = _("Espaces naturels sensibles de l'Isère")
def request(self, endpoint, params=None, json=None):
url = urlparse.urljoin(self.base_url, endpoint)
headers = {"token": self.token}
if json is not None:
response = self.requests.post(
url, params=params, json=json, headers=headers
)
else:
response = self.requests.get(url, params=params, headers=headers)
if response.status_code // 100 != 2:
try:
json_content = response.json()
except ValueError:
json_content = None
raise APIError(
"error status:%s %r, content:%r"
% (response.status_code, response.reason, response.content[:1024]),
data={
"status_code": response.status_code,
"json_content": json_content,
},
)
if response.status_code == 204: # 204 No Content
raise APIError("abnormal empty response")
try:
return response.json()
except ValueError:
raise APIError("invalid JSON in response: %r" % response.content[:1024])
@endpoint(
name="sites",
description=_("Sites"),
display_order=1,
perm="can_access",
parameters={
"q": {"description": _("Search text in name field")},
"id": {
"description": _("Returns site with code=id"),
},
"kind": {
"description": _("Select sites by bind: school_group or social"),
},
},
)
def sites(self, request, q=None, id=None, kind=None):
if id is not None:
site = self.request("api/1.0.0/site/" + id)
site["id"] = site["code"]
site["text"] = "%(name)s (%(city)s)" % site
sites = [site]
else:
cache_key = "isere-ens-sites-%d" % self.id
sites = cache.get(cache_key)
if not sites:
sites = self.request("api/1.0.0/site")
for site in sites:
site["id"] = site["code"]
site["text"] = "%(name)s (%(city)s)" % site
cache.set(cache_key, sites, 300)
if kind is not None:
sites = [site for site in sites if site.get(kind)]
if q is not None:
q = simplify(q)
sites = [site for site in sites if q in simplify(site["text"])]
return {"data": sites}
@endpoint(
name="animators",
description=_("Animators"),
display_order=2,
perm="can_access",
parameters={
"q": {"description": _("Search text in name field")},
"id": {
"description": _("Returns animator number id"),
},
},
)
def animators(self, request, q=None, id=None):
cache_key = "isere-ens-animators-%d" % self.id
animators = cache.get(cache_key)
if not animators:
animators = self.request("api/1.0.0/schoolAnimator")
for animator in animators:
animator["id"] = str(animator["id"])
animator["text"] = (
"%(first_name)s %(last_name)s <%(email)s> (%(entity)s)" % animator
)
cache.set(cache_key, animators, 300)
if id is not None:
animators = [animator for animator in animators if animator["id"] == id]
if q is not None:
q = simplify(q)
animators = [
animator for animator in animators if q in simplify(animator["text"])
]
return {"data": animators}
@endpoint(
name="site-calendar",
description=_("Available bookings for a site"),
display_order=3,
perm="can_access",
parameters={
"site": {"description": _("Site code (aka id)")},
"participants": {
"description": _("Number of participants"),
},
"start_date": {
"description": _(
"First date of the calendar (format: YYYY-MM-DD, default: today)"
),
},
"end_date": {
"description": _(
"Last date of the calendar (format: YYYY-MM-DD, default: start_date + 92 days)"
),
},
},
)
def site_calendar(
self, request, site, participants="1", start_date=None, end_date=None
):
if start_date:
try:
start_date = datetime.datetime.strptime(start_date, "%Y-%m-%d").date()
except ValueError:
raise APIError(
"bad start_date format (%s), should be YYYY-MM-DD" % start_date,
http_status=400,
)
else:
start_date = datetime.date.today()
if end_date:
try:
end_date = datetime.datetime.strptime(end_date, "%Y-%m-%d").date()
except ValueError:
raise APIError(
"bad end_date format (%s), should be YYYY-MM-DD" % end_date,
http_status=400,
)
else:
end_date = start_date + datetime.timedelta(days=92)
params = {
"site": site,
"participants": participants,
"start_date": start_date.strftime("%Y-%m-%d"),
"end_date": end_date.strftime("%Y-%m-%d"),
}
dates = self.request("api/1.0.0/site/" + site + "/calendar", params=params)
for date in dates:
date["id"] = site + ":" + date["date"]
date["site"] = site
date_ = datetime.datetime.strptime(date["date"], "%Y-%m-%d").date()
date["date_format"] = date_format(date_, format="DATE_FORMAT")
date["disabled"] = False
date["color"] = "green"
for period in ("morning", "afternoon"):
if date[period] == "COMPLETE":
date["color"] = "orange"
date["%s_status" % period] = _("Complete")
else:
date["%s_status" % period] = _("Available")
if date["morning"] == "COMPLETE" and date["afternoon"] == "COMPLETE":
date["disabled"] = True
date["color"] = "red"
if date["lunch"] == "CLOSE":
date["lunch_status"] = _("Complete")
else:
date["lunch_status"] = _("Available")
date["details"] = _(
"Morning (%(morning_status)s), Lunch (%(lunch_status)s), Afternoon (%(afternoon_status)s)"
% date
)
date["text"] = "%(date_format)s - %(details)s" % date
return {"data": dates}
@endpoint(
name="site-booking",
description=_("Book a site for an entity (school)"),
display_order=4,
perm="can_access",
methods=["post"],
post={
"request_body": {
"schema": {
"application/json": SITE_BOOKING_SCHOOL_SCHEMA,
}
}
},
)
def site_booking(self, request, post_data):
payload = {
"code": post_data["code"],
"status": post_data["status"],
"beneficiary": {
"id": post_data["beneficiary_id"],
"firstName": post_data["beneficiary_first_name"],
"lastName": post_data["beneficiary_last_name"],
"email": post_data["beneficiary_email"],
"phone": post_data["beneficiary_phone"],
"cellphone": post_data["beneficiary_cellphone"],
},
"entity": {
"id": post_data["entity_id"],
"name": post_data["entity_name"],
"type": post_data["entity_type"],
},
"booking": {
"projectCode": post_data.get("project"),
"siteCode": post_data["site"],
"applicant": post_data["applicant"],
"public": post_data["public"],
"bookingDate": post_data["date"],
"participants": int(post_data["participants"]),
"morning": post_data["morning"],
"lunch": post_data["lunch"],
"afternoon": post_data["afternoon"],
"pmr": post_data["pmr"],
"gradeLevels": post_data["grade_levels"],
"schoolAnimator": int(post_data["animator"]),
},
}
booking = self.request("api/1.0.0/booking", json=payload)
if not isinstance(booking, dict):
raise APIError("response is not a dict", data=booking)
if not "status" in booking:
raise APIError("no status in response", data=booking)
if booking["status"] not in ("BOOKING", "OVERBOOKING"):
raise APIError("booking status is %s" % booking["status"], data=booking)
return {"data": booking}

View File

@ -22,6 +22,7 @@ INSTALLED_APPS += (
'passerelle.contrib.grandlyon_streetsections',
'passerelle.contrib.greco',
'passerelle.contrib.grenoble_gru',
'passerelle.contrib.isere_ens',
'passerelle.contrib.iparapheur',
'passerelle.contrib.iws',
'passerelle.contrib.lille_urban_card',

384
tests/test_isere_ens.py Normal file
View File

@ -0,0 +1,384 @@
# -*- coding: utf-8 -*-
# Passerelle - uniform access to data and services
# Copyright (C) 2021 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; exclude even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a.deepcopy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
import os
import mock
import pytest
import utils
from django.urls import reverse
from passerelle.contrib.isere_ens.models import IsereENS
from passerelle.utils.jsonresponse import APIError
@pytest.fixture
def setup(db):
return utils.setup_access_rights(
IsereENS.objects.create(
slug="test", base_url="https://ens38.example.net/", token="toktok"
)
)
SITES_RESPONSE = """[
{
"name": "Save - étangs de la Serre",
"code": "SD29a",
"city": "Arandon-Passins - Courtenay",
"school_group": true,
"social": true
},
{
"name": "Save - étangs de Passins",
"code": "SD29b",
"city": "Arandon-Passins",
"school_group": true,
"social": true
},
{
"name": "Save - lac de Save",
"code": "SD29c",
"city": "Arandon-Passins",
"school_group": true,
"social": false
}
]"""
SD29B_RESPONSE = """{
"name": "Save - étangs de Passins",
"code": "SD29b",
"type": "DEPARTMENTAL_ENS",
"city": "Arandon-Passins",
"natural_environment": "Etangs, tourbières, mares, forêts, rivière, pelouses sèches",
"lunch_equipment": null,
"pmr_access": "NO",
"school_group": true,
"social": true,
"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>",
"dogs": "LEASH",
"educational_equipments": null,
"eps_regulation": null,
"main_manager": {
"first_name": "Jo",
"last_name": "Smith",
"managing_entity": "Département de l'Isère",
"main_phone": "01 23 45 67 89",
"email": "jo.smith@example.net"
}
}"""
SITE_404_RESPONSE = """{
"user_message": "Impossible de trouver le site",
"message": "Site not found with code SD29x"
}"""
ANIMATORS_RESPONSE = """[
{
"id": 1,
"first_name": "Francis",
"last_name": "Kuntz",
"email": "fk@mail.grd",
"phone": "123",
"entity": "Association Nature Morte"
},
{
"id": 2,
"first_name": "Michael",
"last_name": "Kael",
"email": "mk@mail.grd",
"phone": "456",
"entity": "Association Porte de l'Enfer"
}
]"""
SITE_CALENDAR_RESPONSE = """[
{
"date": "2020-01-21",
"morning": "AVAILABLE",
"lunch": "CLOSE",
"afternoon": "AVAILABLE"
},
{
"date": "2020-01-22",
"morning": "AVAILABLE",
"lunch": "OPEN",
"afternoon": "COMPLETE"
},
{
"date": "2020-01-23",
"morning": "COMPLETE",
"lunch": "CLOSE",
"afternoon": "COMPLETE"
}
]"""
BOOK_RESPONSE = """{
"status": "BOOKING"
}"""
BOOK_RESPONSE_OVERBOOKING = """{
"status": "OVERBOOKING"
}"""
BOOK_RESPONSE_REFUSED = """{
"status": "REFUSED"
}"""
@mock.patch("passerelle.utils.Request.get")
def test_get_sites(mocked_get, app, setup):
mocked_get.return_value = utils.FakedResponse(
content=SITES_RESPONSE, status_code=200
)
endpoint = reverse(
"generic-endpoint",
kwargs={"connector": "isere-ens", "slug": setup.slug, "endpoint": "sites"},
)
response = app.get(endpoint)
assert mocked_get.call_args[0][0].endswith("api/1.0.0/site")
assert mocked_get.call_args[1]["headers"]["token"] == "toktok"
assert mocked_get.call_count == 1
assert "data" in response.json
assert response.json["err"] == 0
for item in response.json["data"]:
assert "id" in item
assert "text" in item
assert "city" in item
assert "code" in item
# test cache system
response = app.get(endpoint)
assert mocked_get.call_count == 1
response = app.get(endpoint + "?q=etangs")
assert len(response.json["data"]) == 2
response = app.get(endpoint + "?q=CourTe")
assert len(response.json["data"]) == 1
response = app.get(endpoint + "?kind=social")
assert len(response.json["data"]) == 2
mocked_get.return_value = utils.FakedResponse(
content=SD29B_RESPONSE, status_code=200
)
response = app.get(endpoint + "?id=SD29b")
assert mocked_get.call_args[0][0].endswith("api/1.0.0/site/SD29b")
assert len(response.json["data"]) == 1
assert response.json["data"][0]["id"] == "SD29b"
assert response.json["data"][0]["dogs"] == "LEASH"
# bad response for ENS API
mocked_get.return_value = utils.FakedResponse(
content=SITE_404_RESPONSE, status_code=404
)
response = app.get(endpoint + "?id=SD29x")
assert mocked_get.call_args[0][0].endswith("api/1.0.0/site/SD29x")
assert response.json["err"] == 1
assert response.json["err_class"].endswith("APIError")
assert response.json["err_desc"].startswith("error status:404")
assert response.json["data"]["status_code"] == 404
assert (
response.json["data"]["json_content"]["message"]
== "Site not found with code SD29x"
)
mocked_get.return_value = utils.FakedResponse(content="crash", status_code=500)
response = app.get(endpoint + "?id=foo500")
assert mocked_get.call_args[0][0].endswith("api/1.0.0/site/foo500")
assert response.json["err"] == 1
assert response.json["err_desc"].startswith("error status:500")
assert response.json["err_class"].endswith("APIError")
assert response.json["data"]["status_code"] == 500
assert response.json["data"]["json_content"] is None
mocked_get.return_value = utils.FakedResponse(content=None, status_code=204)
response = app.get(endpoint + "?id=foo204")
assert mocked_get.call_args[0][0].endswith("api/1.0.0/site/foo204")
assert response.json["err"] == 1
assert response.json["err_class"].endswith("APIError")
assert response.json["err_desc"] == "abnormal empty response"
mocked_get.return_value = utils.FakedResponse(content="not json", status_code=200)
response = app.get(endpoint + "?id=foo")
assert mocked_get.call_args[0][0].endswith("api/1.0.0/site/foo")
assert response.json["err"] == 1
assert response.json["err_class"].endswith("APIError")
assert response.json["err_desc"].startswith("invalid JSON in response:")
@mock.patch("passerelle.utils.Request.get")
def test_get_animators(mocked_get, app, setup):
mocked_get.return_value = utils.FakedResponse(
content=ANIMATORS_RESPONSE, status_code=200
)
endpoint = reverse(
"generic-endpoint",
kwargs={"connector": "isere-ens", "slug": setup.slug, "endpoint": "animators"},
)
response = app.get(endpoint)
assert mocked_get.call_args[0][0].endswith("api/1.0.0/schoolAnimator")
assert mocked_get.call_count == 1
assert "data" in response.json
assert response.json["err"] == 0
for item in response.json["data"]:
assert "id" in item
assert "text" in item
assert "first_name" in item
assert "email" in item
# test cache system
response = app.get(endpoint)
assert mocked_get.call_count == 1
response = app.get(endpoint + "?q=Kael")
assert len(response.json["data"]) == 1
response = app.get(endpoint + "?q=association")
assert len(response.json["data"]) == 2
response = app.get(endpoint + "?q=mail.grd")
assert len(response.json["data"]) == 2
response = app.get(endpoint + "?id=2")
assert len(response.json["data"]) == 1
assert response.json["data"][0]["first_name"] == "Michael"
@mock.patch("passerelle.utils.Request.get")
def test_get_site_calendar(mocked_get, app, setup, freezer):
freezer.move_to("2021-01-21 12:00:00")
mocked_get.return_value = utils.FakedResponse(
content=SITE_CALENDAR_RESPONSE, status_code=200
)
endpoint = reverse(
"generic-endpoint",
kwargs={
"connector": "isere-ens",
"slug": setup.slug,
"endpoint": "site-calendar",
},
)
response = app.get(endpoint + "?site=SD29b")
assert mocked_get.call_args[0][0].endswith("api/1.0.0/site/SD29b/calendar")
assert mocked_get.call_args[1]["params"]["start_date"] == "2021-01-21"
assert mocked_get.call_args[1]["params"]["end_date"] == "2021-04-23"
assert response.json["err"] == 0
assert len(response.json["data"]) == 3
response = app.get(endpoint + "?site=SD29b&start_date=2021-01-22")
assert mocked_get.call_args[1]["params"]["start_date"] == "2021-01-22"
assert mocked_get.call_args[1]["params"]["end_date"] == "2021-04-24"
assert response.json["err"] == 0
response = app.get(
endpoint + "?site=SD29b&start_date=2021-01-22&end_date=2021-01-30"
)
assert mocked_get.call_args[1]["params"]["start_date"] == "2021-01-22"
assert mocked_get.call_args[1]["params"]["end_date"] == "2021-01-30"
assert response.json["err"] == 0
response = app.get(endpoint + "?site=SD29b&start_date=foo", status=400)
assert response.json["err"] == 1
assert response.json["err_class"].endswith("APIError")
assert (
response.json["err_desc"] == "bad start_date format (foo), should be YYYY-MM-DD"
)
response = app.get(endpoint + "?site=SD29b&end_date=bar", status=400)
assert response.json["err"] == 1
assert response.json["err_class"].endswith("APIError")
assert (
response.json["err_desc"] == "bad end_date format (bar), should be YYYY-MM-DD"
)
@mock.patch("passerelle.utils.Request.post")
def test_post_book(mocked_post, app, setup):
mocked_post.return_value = utils.FakedResponse(
content=BOOK_RESPONSE, status_code=200
)
endpoint = reverse(
"generic-endpoint",
kwargs={
"connector": "isere-ens",
"slug": setup.slug,
"endpoint": "site-booking",
},
)
book = {
"code": "resa",
"status": "OK",
"beneficiary_id": "42",
"beneficiary_first_name": "Foo",
"beneficiary_last_name": "Bar",
"beneficiary_email": "foobar@example.net",
"beneficiary_phone": "9876",
"beneficiary_cellphone": "06",
"entity_id": "38420D",
"entity_name": "Ecole FooBar",
"entity_type": "school",
"project": "Publik",
"site": "SD29b",
"applicant": "app",
"public": "GS",
"date": "2020-01-22",
"participants": "50",
"morning": True,
"lunch": False,
"afternoon": False,
"pmr": True,
"grade_levels": ["CP", "CE1"],
"animator": "42",
}
response = app.post_json(endpoint, params=book)
assert mocked_post.call_args[0][0].endswith("api/1.0.0/booking")
assert mocked_post.call_count == 1
assert response.json["err"] == 0
assert response.json["data"]["status"] == "BOOKING"
mocked_post.return_value = utils.FakedResponse(
content=BOOK_RESPONSE_OVERBOOKING, status_code=200
)
response = app.post_json(endpoint, params=book)
assert mocked_post.call_args[0][0].endswith("api/1.0.0/booking")
assert mocked_post.call_count == 2
assert response.json["err"] == 0
assert response.json["data"]["status"] == "OVERBOOKING"
mocked_post.return_value = utils.FakedResponse(
content=BOOK_RESPONSE_REFUSED, status_code=200
)
response = app.post_json(endpoint, params=book)
assert mocked_post.call_args[0][0].endswith("api/1.0.0/booking")
assert mocked_post.call_count == 3
assert response.json["err"] == 1
assert response.json["err_class"].endswith("APIError")
assert response.json["err_desc"] == "booking status is REFUSED"
assert response.json["data"]["status"] == "REFUSED"
mocked_post.return_value = utils.FakedResponse(
content="""["not", "a", "dict"]""", status_code=200
)
response = app.post_json(endpoint, params=book)
assert mocked_post.call_args[0][0].endswith("api/1.0.0/booking")
assert mocked_post.call_count == 4
assert response.json["err"] == 1
assert response.json["err_class"].endswith("APIError")
assert response.json["err_desc"] == "response is not a dict"
assert response.json["data"] == ["not", "a", "dict"]
mocked_post.return_value = utils.FakedResponse(
content="""{"foo": "bar"}""", status_code=200
)
response = app.post_json(endpoint, params=book)
assert mocked_post.call_args[0][0].endswith("api/1.0.0/booking")
assert mocked_post.call_count == 5
assert response.json["err"] == 1
assert response.json["err_class"].endswith("APIError")
assert response.json["err_desc"] == "no status in response"
assert response.json["data"] == {"foo": "bar"}