passerelle/tests/test_isere_ens.py

623 lines
25 KiB
Python

# 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/>.
from unittest import mock
import pytest
from django.urls import reverse
import tests.utils
from passerelle.contrib.isere_ens.models import API_VERSION, IsereENS
@pytest.fixture
def setup(db):
return tests.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"
}"""
CANCEL_RESPONSE = """{
"status": "CANCELED"
}"""
API_VERSIONS = [vers[1] for vers in API_VERSION]
@pytest.mark.parametrize('api_version', API_VERSIONS)
@mock.patch("passerelle.utils.Request.get")
def test_get_sites(mocked_get, app, setup, api_version):
setup.api_version = api_version
setup.save()
mocked_get.return_value = tests.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/" + api_version + "/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 = tests.utils.FakedResponse(content=SD29B_RESPONSE, status_code=200)
response = app.get(endpoint + "?id=SD29b")
assert mocked_get.call_args[0][0].endswith("api/" + api_version + "/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 = tests.utils.FakedResponse(content=SITE_404_RESPONSE, status_code=404)
response = app.get(endpoint + "?id=SD29x")
assert mocked_get.call_args[0][0].endswith("api/" + api_version + "/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 = tests.utils.FakedResponse(content="crash", status_code=500)
response = app.get(endpoint + "?id=foo500")
assert mocked_get.call_args[0][0].endswith("api/" + api_version + "/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 = tests.utils.FakedResponse(content=None, status_code=204)
response = app.get(endpoint + "?id=foo204")
assert mocked_get.call_args[0][0].endswith("api/" + api_version + "/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 = tests.utils.FakedResponse(content="not json", status_code=200)
response = app.get(endpoint + "?id=foo")
assert mocked_get.call_args[0][0].endswith("api/" + api_version + "/site/foo")
assert response.json["err"] == 1
assert response.json["err_class"].endswith("APIError")
assert response.json["err_desc"].startswith("invalid JSON in response:")
@pytest.mark.parametrize('api_version', API_VERSIONS)
@mock.patch("passerelle.utils.Request.get")
def test_get_animators(mocked_get, app, setup, api_version):
setup.api_version = api_version
setup.save()
mocked_get.return_value = tests.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/" + api_version + "/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"
@pytest.mark.parametrize('api_version', API_VERSIONS)
@mock.patch("passerelle.utils.Request.get")
def test_get_site_calendar(mocked_get, app, setup, freezer, api_version):
setup.api_version = api_version
setup.save()
freezer.move_to("2021-01-21 12:00:00")
mocked_get.return_value = tests.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/" + api_version + "/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
assert response.json["data"][0]["disabled"] is False
assert response.json["data"][1]["disabled"] is False
assert response.json["data"][2]["disabled"] is True
assert response.json["data"][0]["status"] == "open"
assert response.json["data"][1]["status"] == "partially-open"
assert response.json["data"][2]["status"] == "closed"
assert response.json["data"][0]["details"] == "Morning (available), Lunch (closed), Afternoon (available)"
assert response.json["data"][1]["details"] == "Morning (available), Lunch (open), Afternoon (complete)"
assert response.json["data"][2]["details"] == "Morning (complete), Lunch (closed), Afternoon (complete)"
# "2020-01-21"
assert response.json["data"][0]["date_number"] == "21"
assert response.json["data"][0]["date_weekday"] == "Tuesday"
assert response.json["data"][0]["date_weekdayindex"] == "2"
assert response.json["data"][0]["date_weeknumber"] == "4"
assert response.json["data"][0]["date_month"] == "January 2020"
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_v1(mocked_post, app, setup):
mocked_post.return_value = tests.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 mocked_post.call_args.kwargs['json']['booking']['schoolAnimator'] == 42
assert response.json["err"] == 0
assert response.json["data"]["status"] == "BOOKING"
mocked_post.return_value = tests.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 = tests.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 = tests.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 = tests.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"}
book["animator"] = ""
mocked_post.return_value = tests.utils.FakedResponse(content=BOOK_RESPONSE, status_code=200)
response = app.post_json(endpoint, params=book)
assert mocked_post.call_args[0][0].endswith("api/1.0.0/booking")
assert 'schoolAnimator' not in mocked_post.call_args.kwargs['json']['booking']
assert mocked_post.call_count == 6
assert response.json["err"] == 0
assert response.json["data"]["status"] == "BOOKING"
del book["animator"]
mocked_post.return_value = tests.utils.FakedResponse(content=BOOK_RESPONSE, status_code=200)
response = app.post_json(endpoint, params=book)
assert mocked_post.call_args[0][0].endswith("api/1.0.0/booking")
assert 'schoolAnimator' not in mocked_post.call_args.kwargs['json']['booking']
assert mocked_post.call_count == 7
assert response.json["err"] == 0
assert response.json["data"]["status"] == "BOOKING"
del book['code']
response = app.post_json(endpoint, params=book, status=400)
assert response.json['err'] == 'bad-request'
assert response.json['data'] is None
assert response.json['err_desc'] == 'code is mandatory (API v1.0.0)'
@pytest.mark.parametrize('api_version', API_VERSIONS[1:])
@mock.patch("passerelle.utils.Request.post")
def test_post_book(mocked_post, app, setup, api_version):
setup.api_version = api_version
setup.save()
mocked_post.return_value = tests.utils.FakedResponse(content=BOOK_RESPONSE, status_code=200)
endpoint = reverse(
"generic-endpoint",
kwargs={
"connector": "isere-ens",
"slug": setup.slug,
"endpoint": "site-booking",
},
)
book = {
"site": "SD29b",
"date": "2020-01-22",
"pmr": True,
"morning": True,
"lunch": False,
"afternoon": False,
"participants": "50",
"animator": "42",
"group": "3",
"grade_levels": ["CP", "CE1"],
"beneficiary_first_name": "Foo",
"beneficiary_last_name": "Bar",
"beneficiary_email": "foobar@example.net",
"beneficiary_phone": "9876",
}
response = app.post_json(endpoint, params=book)
assert mocked_post.call_args[0][0].endswith("api/" + api_version + "/site/booking/school")
assert mocked_post.call_count == 1
assert mocked_post.call_args.kwargs['json']['schoolAnimator'] == 42
assert mocked_post.call_args.kwargs['json']['participants'] == 50
assert mocked_post.call_args.kwargs['json']['beneficiary']['lastName'] == 'Bar'
assert mocked_post.call_args.kwargs['json']['beneficiary']['cellphone'] == ''
assert 'idExternal' not in mocked_post.call_args.kwargs['json']
assert 'projectCode' not in mocked_post.call_args.kwargs['json']
assert response.json["err"] == 0
assert response.json["data"]["status"] == "BOOKING"
book['external_id'] = '12-34'
book['project'] = 'pc'
response = app.post_json(endpoint, params=book)
assert mocked_post.call_args[0][0].endswith("api/" + api_version + "/site/booking/school")
assert mocked_post.call_count == 2
assert mocked_post.call_args.kwargs['json']['idExternal'] == '12-34'
assert mocked_post.call_args.kwargs['json']['projectCode'] == 'pc'
assert response.json["err"] == 0
mocked_post.return_value = tests.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/" + api_version + "/site/booking/school")
assert mocked_post.call_count == 3
assert response.json["err"] == 0
assert response.json["data"]["status"] == "OVERBOOKING"
mocked_post.return_value = tests.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/" + api_version + "/site/booking/school")
assert mocked_post.call_count == 4
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 = tests.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/" + api_version + "/site/booking/school")
assert mocked_post.call_count == 5
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 = tests.utils.FakedResponse(content="""{"foo": "bar"}""", status_code=200)
response = app.post_json(endpoint, params=book)
assert mocked_post.call_args[0][0].endswith("api/" + api_version + "/site/booking/school")
assert mocked_post.call_count == 6
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"}
book["animator"] = ""
mocked_post.return_value = tests.utils.FakedResponse(content=BOOK_RESPONSE, status_code=200)
response = app.post_json(endpoint, params=book)
assert mocked_post.call_args[0][0].endswith("api/" + api_version + "/site/booking/school")
assert 'schoolAnimator' not in mocked_post.call_args.kwargs['json']
assert mocked_post.call_count == 7
assert response.json["err"] == 0
assert response.json["data"]["status"] == "BOOKING"
del book["animator"]
mocked_post.return_value = tests.utils.FakedResponse(content=BOOK_RESPONSE, status_code=200)
response = app.post_json(endpoint, params=book)
assert mocked_post.call_args[0][0].endswith("api/" + api_version + "/site/booking/school")
assert 'schoolAnimator' not in mocked_post.call_args.kwargs['json']
assert mocked_post.call_count == 8
assert response.json["err"] == 0
assert response.json["data"]["status"] == "BOOKING"
book['group'] = 'G'
response = app.post_json(endpoint, params=book, status=400)
assert response.json['err'] == 1
assert response.json['data'] is None
assert response.json['err_desc'] == "group: 'G' does not match '^[0-9]*$'"
book['group'] = ''
response = app.post_json(endpoint, params=book, status=400)
assert response.json['err'] == 'bad-request'
assert response.json['data'] is None
assert response.json['err_desc'] == 'group or applicant are mandatory (API v2.1.0/v2.1.1)'
del book['group']
response = app.post_json(endpoint, params=book, status=400)
assert response.json['err'] == 'bad-request'
assert response.json['data'] is None
assert response.json['err_desc'] == 'group or applicant are mandatory (API v2.1.0/v2.1.1)'
book['applicant'] = 'ecole 1'
response = app.post_json(endpoint, params=book)
assert mocked_post.call_args[0][0].endswith("api/" + api_version + "/site/booking/school")
assert mocked_post.call_args.kwargs['json']['schoolGroup'] is None
assert mocked_post.call_args.kwargs['json']['applicant'] == 'ecole 1'
@pytest.mark.parametrize('api_version', API_VERSIONS)
@mock.patch("passerelle.utils.Request.get")
def test_get_booking_status(mocked_get, app, setup, api_version):
setup.api_version = api_version
setup.save()
endpoint = reverse(
"generic-endpoint",
kwargs={
"connector": "isere-ens",
"slug": setup.slug,
"endpoint": "get-site-booking",
},
)
mocked_get.return_value = tests.utils.FakedResponse(content=BOOK_RESPONSE, status_code=200)
response = app.get(endpoint + "?code=123")
if api_version == '1.0.0':
assert mocked_get.call_args[0][0].endswith("api/1.0.0/booking/123/status")
else:
assert mocked_get.call_args[0][0].endswith("api/" + api_version + "/site/booking/school/123/status/")
assert mocked_get.call_count == 1
assert response.json["err"] == 0
assert response.json["data"]["status"] == "BOOKING"
response = app.get(endpoint, status=400) # no code specified
assert mocked_get.call_count == 1
mocked_get.return_value = tests.utils.FakedResponse(content="""["not", "a", "dict"]""", status_code=200)
response = app.get(endpoint + "?code=123")
assert mocked_get.call_count == 2
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_get.return_value = tests.utils.FakedResponse(content="""{"foo": "bar"}""", status_code=200)
response = app.get(endpoint + "?code=123")
assert mocked_get.call_count == 3
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"}
@pytest.mark.parametrize('api_version', API_VERSIONS)
@mock.patch("passerelle.utils.Request.post")
def test_cancel_booking(mocked_post, app, setup, api_version):
setup.api_version = api_version
setup.save()
endpoint = reverse(
"generic-endpoint",
kwargs={
"connector": "isere-ens",
"slug": setup.slug,
"endpoint": "cancel-site-booking",
},
)
mocked_post.return_value = tests.utils.FakedResponse(content=CANCEL_RESPONSE, status_code=200)
response = app.post(endpoint + "?code=123")
if api_version == '1.0.0':
assert response.json["err"] == 1
assert response.json["data"] is None
assert response.json["err_desc"] == "not available on API v1.0.0"
return
assert mocked_post.call_args[0][0].endswith("api/" + api_version + "/site/booking/school/cancel/123")
assert mocked_post.call_count == 1
assert response.json["err"] == 0
assert response.json["data"]["status"] == "CANCELED"
response = app.post(endpoint, status=400) # no code specified
assert mocked_post.call_count == 1 # same as before
mocked_post.return_value = tests.utils.FakedResponse(content="""["not", "a", "dict"]""", status_code=200)
response = app.post(endpoint + "?code=123")
assert mocked_post.call_count == 2
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 = tests.utils.FakedResponse(content="""{"foo": "bar"}""", status_code=200)
response = app.post(endpoint + "?code=123")
assert mocked_post.call_count == 3
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"}