diff --git a/api/requirements.txt b/api/requirements.txt index 319048b3611..85917f8905f 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -64,7 +64,7 @@ schwifty==2022.9.0 semver==2.13.0 sentry-sdk==1.14.0 sib-api-v3-sdk -spectree==0.7.2 +spectree==1.2.* # FIXME (dbaty, 2023-01-04): do not use 1.4.46 that has a new # deprecation warning for which we're not ready # (https://docs.sqlalchemy.org/en/20/changelog/changelog_14.html#change-e67bfa1efbe52ae40aa842124bc40c51). diff --git a/api/src/pcapi/routes/public/collective/endpoints/categories.py b/api/src/pcapi/routes/public/collective/endpoints/categories.py index ef3a4d628bd..179e79e50bb 100644 --- a/api/src/pcapi/routes/public/collective/endpoints/categories.py +++ b/api/src/pcapi/routes/public/collective/endpoints/categories.py @@ -1,11 +1,9 @@ from collections import defaultdict -from typing import cast from pcapi.core.categories import categories from pcapi.core.categories import subcategories_v2 from pcapi.routes.public import blueprints from pcapi.routes.public.collective.serialization import offers as offers_serialization -from pcapi.routes.serialization import BaseModel from pcapi.serialization.decorator import spectree_serialize from pcapi.serialization.spec_tree import ExtendResponse as SpectreeResponse from pcapi.validation.routes.users_authentifications import api_key_required @@ -16,18 +14,14 @@ api=blueprints.v2_prefixed_public_api_schema, tags=["API offres collectives"], resp=SpectreeResponse( - **( - { - "HTTP_200": ( - offers_serialization.CollectiveOffersListCategoriesResponseModel, - "La liste des catégories éligibles existantes.", - ), - "HTTP_401": ( - cast(BaseModel, offers_serialization.AuthErrorResponseModel), - "Authentification nécessaire", - ), - } - ) + HTTP_200=( + offers_serialization.CollectiveOffersListCategoriesResponseModel, + "La liste des catégories éligibles existantes.", + ), + HTTP_401=( + offers_serialization.AuthErrorResponseModel, + "Authentification nécessaire", + ), ), ) @api_key_required @@ -48,18 +42,14 @@ def list_categories() -> offers_serialization.CollectiveOffersListCategoriesResp api=blueprints.v2_prefixed_public_api_schema, tags=["API offres collectives"], resp=SpectreeResponse( - **( - { - "HTTP_200": ( - offers_serialization.CollectiveOffersListSubCategoriesResponseModel, - "La liste des sous-catégories éligibles existantes.", - ), - "HTTP_401": ( - cast(BaseModel, offers_serialization.AuthErrorResponseModel), - "Authentification nécessaire", - ), - } - ) + HTTP_200=( + offers_serialization.CollectiveOffersListSubCategoriesResponseModel, + "La liste des sous-catégories éligibles existantes.", + ), + HTTP_401=( + offers_serialization.AuthErrorResponseModel, + "Authentification nécessaire", + ), ), ) @api_key_required diff --git a/api/src/pcapi/routes/public/collective/endpoints/domains.py b/api/src/pcapi/routes/public/collective/endpoints/domains.py index e28acf2c853..a17f18a6cfb 100644 --- a/api/src/pcapi/routes/public/collective/endpoints/domains.py +++ b/api/src/pcapi/routes/public/collective/endpoints/domains.py @@ -1,10 +1,7 @@ -from typing import cast - from pcapi.core.educational import repository as educational_repository from pcapi.routes.public import blueprints from pcapi.routes.public.collective.serialization import domains as domains_serialization from pcapi.routes.public.collective.serialization import offers as offers_serialization -from pcapi.routes.serialization import BaseModel from pcapi.serialization.decorator import spectree_serialize from pcapi.serialization.spec_tree import ExtendResponse as SpectreeResponse from pcapi.utils.cache import cached_view @@ -16,18 +13,14 @@ api=blueprints.v2_prefixed_public_api_schema, tags=["API offres collectives"], resp=SpectreeResponse( - **( - { - "HTTP_200": ( - domains_serialization.CollectiveOffersListDomainsResponseModel, - "La liste des domaines d'éducation.", - ), - "HTTP_401": ( - cast(BaseModel, offers_serialization.AuthErrorResponseModel), - "Authentification nécessaire", - ), - } - ) + HTTP_200=( + domains_serialization.CollectiveOffersListDomainsResponseModel, + "La liste des domaines d'éducation.", + ), + HTTP_401=( + offers_serialization.AuthErrorResponseModel, + "Authentification nécessaire", + ), ), ) @api_key_required diff --git a/api/src/pcapi/routes/public/collective/endpoints/educational_institutions.py b/api/src/pcapi/routes/public/collective/endpoints/educational_institutions.py index 14632ed7881..4fb36e76401 100644 --- a/api/src/pcapi/routes/public/collective/endpoints/educational_institutions.py +++ b/api/src/pcapi/routes/public/collective/endpoints/educational_institutions.py @@ -1,10 +1,7 @@ -from typing import cast - from pcapi.core.educational.api.institution import search_educational_institution from pcapi.routes.public import blueprints from pcapi.routes.public.collective.serialization import institutions as institutions_serialization from pcapi.routes.public.collective.serialization import offers as offers_serialization -from pcapi.routes.serialization import BaseModel from pcapi.serialization.decorator import spectree_serialize from pcapi.serialization.spec_tree import ExtendResponse as SpectreeResponse from pcapi.validation.routes.users_authentifications import api_key_required @@ -15,22 +12,18 @@ api=blueprints.v2_prefixed_public_api_schema, tags=["API offres collectives"], resp=SpectreeResponse( - **( - { - "HTTP_200": ( - institutions_serialization.CollectiveOffersListEducationalInstitutionResponseModel, - "La liste des établissement scolaires éligibles.", - ), - "HTTP_400": ( - cast(BaseModel, offers_serialization.ErrorResponseModel), - "Requête malformée", - ), - "HTTP_401": ( - cast(BaseModel, offers_serialization.AuthErrorResponseModel), - "Authentification nécessaire", - ), - } - ) + HTTP_200=( + institutions_serialization.CollectiveOffersListEducationalInstitutionResponseModel, + "La liste des établissement scolaires éligibles.", + ), + HTTP_400=( + offers_serialization.ErrorResponseModel, + "Requête malformée", + ), + HTTP_401=( + offers_serialization.AuthErrorResponseModel, + "Authentification nécessaire", + ), ), ) @api_key_required diff --git a/api/src/pcapi/routes/public/collective/endpoints/offers.py b/api/src/pcapi/routes/public/collective/endpoints/offers.py index 16f3593461c..a3bcda385e5 100644 --- a/api/src/pcapi/routes/public/collective/endpoints/offers.py +++ b/api/src/pcapi/routes/public/collective/endpoints/offers.py @@ -35,15 +35,11 @@ api=blueprints.v2_prefixed_public_api_schema, tags=["API offres collectives"], resp=SpectreeResponse( - **( - BASE_CODE_DESCRIPTIONS - | { - "HTTP_200": ( - offers_serialization.CollectiveOffersListResponseModel, - "L'offre collective existe", - ), - } - ) + **(BASE_CODE_DESCRIPTIONS), + HTTP_200=( + offers_serialization.CollectiveOffersListResponseModel, + "L'offre collective existe", + ), ), ) @api_key_required @@ -76,15 +72,11 @@ def get_collective_offers_public( api=blueprints.v2_prefixed_public_api_schema, tags=["API offres collectives"], resp=SpectreeResponse( - **( - BASE_CODE_DESCRIPTIONS - | { - "HTTP_200": ( - offers_serialization.GetPublicCollectiveOfferResponseModel, - "L'offre collective existe", - ), - } - ) + **(BASE_CODE_DESCRIPTIONS), + HTTP_200=( + offers_serialization.GetPublicCollectiveOfferResponseModel, + "L'offre collective existe", + ), ), ) @api_key_required diff --git a/api/src/pcapi/routes/public/collective/endpoints/students_levels.py b/api/src/pcapi/routes/public/collective/endpoints/students_levels.py index 14a81cd8d92..e5c9c043e69 100644 --- a/api/src/pcapi/routes/public/collective/endpoints/students_levels.py +++ b/api/src/pcapi/routes/public/collective/endpoints/students_levels.py @@ -1,10 +1,7 @@ -from typing import cast - from pcapi.core.educational import models as educational_models from pcapi.routes.public import blueprints from pcapi.routes.public.collective.serialization import offers as offers_serialization from pcapi.routes.public.collective.serialization import students_levels as students_levels_serialization -from pcapi.routes.serialization import BaseModel from pcapi.serialization.decorator import spectree_serialize from pcapi.serialization.spec_tree import ExtendResponse as SpectreeResponse from pcapi.validation.routes.users_authentifications import api_key_required @@ -15,18 +12,14 @@ api=blueprints.v2_prefixed_public_api_schema, tags=["API offres collectives"], resp=SpectreeResponse( - **( - { - "HTTP_200": ( - students_levels_serialization.CollectiveOffersListStudentLevelsResponseModel, - "La liste des domaines d'éducation.", - ), - "HTTP_401": ( - cast(BaseModel, offers_serialization.AuthErrorResponseModel), - "Authentification nécessaire", - ), - } - ) + HTTP_200=( + students_levels_serialization.CollectiveOffersListStudentLevelsResponseModel, + "La liste des domaines d'éducation.", + ), + HTTP_401=( + offers_serialization.AuthErrorResponseModel, + "Authentification nécessaire", + ), ), ) @api_key_required diff --git a/api/src/pcapi/routes/public/collective/endpoints/venues.py b/api/src/pcapi/routes/public/collective/endpoints/venues.py index 1dabf99c8bb..939b18281eb 100644 --- a/api/src/pcapi/routes/public/collective/endpoints/venues.py +++ b/api/src/pcapi/routes/public/collective/endpoints/venues.py @@ -1,12 +1,9 @@ -from typing import cast - from pcapi.core.offerers import repository as offerers_repository from pcapi.core.providers import repository as providers_repository from pcapi.models.feature import FeatureToggle from pcapi.routes.public import blueprints from pcapi.routes.public.collective.serialization import offers as offers_serialization from pcapi.routes.public.collective.serialization import venues as venues_serialization -from pcapi.routes.serialization import BaseModel from pcapi.serialization.decorator import spectree_serialize from pcapi.serialization.spec_tree import ExtendResponse as SpectreeResponse from pcapi.validation.routes.users_authentifications import api_key_required @@ -18,18 +15,14 @@ api=blueprints.v2_prefixed_public_api_schema, tags=["API offres collectives"], resp=SpectreeResponse( - **( - { - "HTTP_200": ( - venues_serialization.CollectiveOffersListVenuesResponseModel, - "La liste des lieux ou vous pouvez créer une offre.", - ), - "HTTP_401": ( - cast(BaseModel, offers_serialization.AuthErrorResponseModel), - "Authentification nécessaire", - ), - } - ) + HTTP_200=( + venues_serialization.CollectiveOffersListVenuesResponseModel, + "La liste des lieux ou vous pouvez créer une offre.", + ), + HTTP_401=( + offers_serialization.AuthErrorResponseModel, + "Authentification nécessaire", + ), ), ) @api_key_required diff --git a/api/src/pcapi/serialization/decorator.py b/api/src/pcapi/serialization/decorator.py index c84a80a8906..68dff27f118 100644 --- a/api/src/pcapi/serialization/decorator.py +++ b/api/src/pcapi/serialization/decorator.py @@ -3,20 +3,19 @@ import logging from typing import Any from typing import Callable -from typing import Iterable +from typing import Sequence from typing import Type from flask import Response from flask import make_response from flask import request -import pydantic +import spectree from werkzeug.exceptions import BadRequest from pcapi.models.api_errors import ApiErrors from pcapi.routes.apis import api as default_api from pcapi.routes.serialization import BaseModel from pcapi.serialization.spec_tree import ExtendResponse as SpectreeResponse -from pcapi.serialization.spec_tree import ExtendedSpecTree logger = logging.getLogger(__name__) @@ -64,7 +63,7 @@ def spectree_serialize( headers: Type[BaseModel] = None, cookies: Type[BaseModel] = None, response_model: Type[BaseModel] = None, - tags: Iterable = (), + tags: Sequence = (), before: Callable = None, after: Callable = None, response_by_alias: bool = True, @@ -72,7 +71,7 @@ def spectree_serialize( on_success_status: int = 200, on_empty_status: int | None = None, on_error_statuses: list[int] | None = None, - api: ExtendedSpecTree = default_api, + api: spectree.SpecTree = default_api, json_format: bool = True, response_headers: dict[str, str] | None = None, resp: SpectreeResponse | None = None, @@ -123,15 +122,16 @@ def decorate_validation(route: Callable[..., Any]) -> Callable[[Any], Any]: @wraps(route) @api.validate( - query=query_in_kwargs, - headers=headers, - cookies=cookies, - resp=spectree_response, - tags=tags, - before=before, after=after, + before=before, + cookies=cookies, + form=form_in_kwargs, + headers=headers, json=body_in_kwargs, + query=query_in_kwargs, + resp=spectree_response, security=security, + tags=tags, ) def sync_validate(*args: Any, **kwargs: Any) -> Response: try: @@ -142,8 +142,9 @@ def sync_validate(*args: Any, **kwargs: Any) -> Response: # not throw error when some invalid json in provided for V2 bookings api. body_params = None else: - # Since pydantic validator is applied before this method and use a silent json parser, # the only case we should end here is with an PATCH/POST with no validator for body params - # or a GET request with a invalid body. raise + # Since pydantic validator is applied before this method and use a silent json parser, + # the only case we should end here is with an PATCH/POST with no validator for body params + # or a GET request with a invalid body. raise query_params = request.args form = request.form @@ -152,13 +153,7 @@ def sync_validate(*args: Any, **kwargs: Any) -> Response: if query_in_kwargs: kwargs["query"] = query_in_kwargs(**query_params) if form_in_kwargs: - try: - kwargs["form"] = form_in_kwargs(**form) - except pydantic.ValidationError as validation_errors: - error_dict = {} - for errors in validation_errors.errors(): - error_dict[errors["loc"][0]] = errors["msg"] - raise ApiErrors(error_dict) + kwargs["form"] = form_in_kwargs(**form) result = route(*args, **kwargs) if json_format: diff --git a/api/src/pcapi/serialization/spec_tree.py b/api/src/pcapi/serialization/spec_tree.py index e5baf818c29..53c75f3a5eb 100644 --- a/api/src/pcapi/serialization/spec_tree.py +++ b/api/src/pcapi/serialization/spec_tree.py @@ -7,7 +7,6 @@ from pydantic import BaseModel # pylint: disable=wrong-pydantic-base-model-import from spectree import Response from spectree import SpecTree -from spectree.utils import parse_code from pcapi import settings @@ -71,14 +70,5 @@ def _get_model_definitions(self) -> dict: class ExtendResponse(Response): - def generate_spec(self) -> Dict[str, Any]: - responses: Dict[str, Any] = {} - for code in self.codes: - responses[parse_code(code)] = {"description": self.get_code_description(code)} - for code, model in self.code_models.items(): - model_name = get_model_key(model=model) - responses[parse_code(code)] = { - "description": self.get_code_description(code), - "content": {"application/json": {"schema": {"$ref": f"#/components/schemas/{model_name}"}}}, - } - return responses + def generate_spec(self, _naming_strategy: Callable[[Type[BaseModel]], str] = None) -> Dict[str, Any]: + return super().generate_spec(naming_strategy=get_model_key) diff --git a/api/tests/routes/external/user_subscription_test.py b/api/tests/routes/external/user_subscription_test.py index ce7f633d3b0..adff989819a 100644 --- a/api/tests/routes/external/user_subscription_test.py +++ b/api/tests/routes/external/user_subscription_test.py @@ -47,7 +47,10 @@ def test_dms_request_no_token(self, client): assert response.status_code == 403 def test_dms_request_no_params_with_token(self, client): - response = client.post(f"/webhooks/dms/application_status?token={settings.DMS_WEBHOOK_TOKEN}") + response = client.post( + f"/webhooks/dms/application_status?token={settings.DMS_WEBHOOK_TOKEN}", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) assert response.status_code == 400 diff --git a/api/tests/routes/native/v1/authentication_test.py b/api/tests/routes/native/v1/authentication_test.py index fbcda74620d..3193beeb353 100644 --- a/api/tests/routes/native/v1/authentication_test.py +++ b/api/tests/routes/native/v1/authentication_test.py @@ -367,7 +367,10 @@ def should_not_send_email_when_feature_flag_is_active_but_email_is_inactive(self def test_send_reset_password_email_without_email(client): - response = client.post("/native/v1/request_password_reset") + response = client.post( + "/native/v1/request_password_reset", + headers={"Content-Type": "application/json"}, + ) assert response.status_code == 400 assert response.json["email"] == ["Ce champ est obligatoire"] diff --git a/api/tests/routes/native/v1/cookies_consent_test.py b/api/tests/routes/native/v1/cookies_consent_test.py index ab667667635..dfea534c4e8 100644 --- a/api/tests/routes/native/v1/cookies_consent_test.py +++ b/api/tests/routes/native/v1/cookies_consent_test.py @@ -81,7 +81,7 @@ def test_log_data(self, client, caplog, body): def test_invalid_data_structure(self, client): body = None - response = client.post("/native/v1/cookies_consent", json=body) + response = client.post("/native/v1/cookies_consent", json=body, headers={"Content-Type": "application/json"}) assert response.status_code == 400 diff --git a/api/tests/routes/native/v1/openapi_test.py b/api/tests/routes/native/v1/openapi_test.py index 57b50fe1401..5c25791af33 100644 --- a/api/tests/routes/native/v1/openapi_test.py +++ b/api/tests/routes/native/v1/openapi_test.py @@ -253,11 +253,7 @@ def test_public_api(client): "title": "Beginningdatetime", "type": "string", }, - "features": { - "items": {"type": "string"}, - "title": "Features", - "type": "array", - }, + "features": {"items": {"type": "string"}, "title": "Features", "type": "array"}, "id": {"title": "Id", "type": "integer"}, "offer": {"$ref": "#/components/schemas/BookingOfferResponse"}, "price": {"title": "Price", "type": "integer"}, @@ -2030,7 +2026,7 @@ def test_public_api(client): "/native/v1/account": { "post": { "description": "", - "operationId": "post_/native/v1/account", + "operationId": "post__native_v1_account", "parameters": [], "requestBody": { "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AccountRequest"}}} @@ -2053,7 +2049,7 @@ def test_public_api(client): "/native/v1/account/suspend": { "post": { "description": "", - "operationId": "post_/native/v1/account/suspend", + "operationId": "post__native_v1_account_suspend", "parameters": [], "responses": { "204": {"description": "No Content"}, @@ -2073,7 +2069,7 @@ def test_public_api(client): "/native/v1/account/suspend/token_validation/{token}": { "get": { "description": "", - "operationId": "get_/native/v1/account/suspend/token_validation/{token}", + "operationId": "get__native_v1_account_suspend_token_validation_{token}", "parameters": [ { "description": "", @@ -2102,7 +2098,7 @@ def test_public_api(client): "/native/v1/account/suspend_for_suspicious_login": { "post": { "description": "", - "operationId": "post_/native/v1/account/suspend_for_suspicious_login", + "operationId": "post__native_v1_account_suspend_for_suspicious_login", "parameters": [], "requestBody": { "content": { @@ -2131,7 +2127,7 @@ def test_public_api(client): "/native/v1/account/suspension_date": { "get": { "description": "", - "operationId": "get_/native/v1/account/suspension_date", + "operationId": "get__native_v1_account_suspension_date", "parameters": [], "responses": { "200": { @@ -2158,7 +2154,7 @@ def test_public_api(client): "/native/v1/account/suspension_status": { "get": { "description": "", - "operationId": "get_/native/v1/account/suspension_status", + "operationId": "get__native_v1_account_suspension_status", "parameters": [], "responses": { "200": { @@ -2185,7 +2181,7 @@ def test_public_api(client): "/native/v1/account/unsuspend": { "post": { "description": "", - "operationId": "post_/native/v1/account/unsuspend", + "operationId": "post__native_v1_account_unsuspend", "parameters": [], "responses": { "204": {"description": "No Content"}, @@ -2205,7 +2201,7 @@ def test_public_api(client): "/native/v1/banner": { "get": { "description": "", - "operationId": "get_/native/v1/banner", + "operationId": "get__native_v1_banner", "parameters": [ { "description": "", @@ -2238,7 +2234,7 @@ def test_public_api(client): "/native/v1/bookings": { "get": { "description": "", - "operationId": "get_/native/v1/bookings", + "operationId": "get__native_v1_bookings", "parameters": [], "responses": { "200": { @@ -2261,7 +2257,7 @@ def test_public_api(client): }, "post": { "description": "", - "operationId": "post_/native/v1/bookings", + "operationId": "post__native_v1_bookings", "parameters": [], "requestBody": { "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BookOfferRequest"}}} @@ -2290,7 +2286,7 @@ def test_public_api(client): "/native/v1/bookings/{booking_id}/cancel": { "post": { "description": "", - "operationId": "post_/native/v1/bookings/{booking_id}/cancel", + "operationId": "post__native_v1_bookings_{booking_id}_cancel", "parameters": [ { "description": "", @@ -2320,7 +2316,7 @@ def test_public_api(client): "/native/v1/bookings/{booking_id}/toggle_display": { "post": { "description": "", - "operationId": "post_/native/v1/bookings/{booking_id}/toggle_display", + "operationId": "post__native_v1_bookings_{booking_id}_toggle_display", "parameters": [ { "description": "", @@ -2354,7 +2350,7 @@ def test_public_api(client): "/native/v1/change_password": { "post": { "description": "", - "operationId": "post_/native/v1/change_password", + "operationId": "post__native_v1_change_password", "parameters": [], "requestBody": { "content": { @@ -2380,7 +2376,7 @@ def test_public_api(client): "/native/v1/cookies_consent": { "post": { "description": "", - "operationId": "post_/native/v1/cookies_consent", + "operationId": "post__native_v1_cookies_consent", "parameters": [], "requestBody": { "content": { @@ -2405,7 +2401,7 @@ def test_public_api(client): "/native/v1/cultural_survey/answers": { "post": { "description": "", - "operationId": "post_/native/v1/cultural_survey/answers", + "operationId": "post__native_v1_cultural_survey_answers", "parameters": [], "requestBody": { "content": { @@ -2433,7 +2429,7 @@ def test_public_api(client): "/native/v1/cultural_survey/questions": { "get": { "description": "", - "operationId": "get_/native/v1/cultural_survey/questions", + "operationId": "get__native_v1_cultural_survey_questions", "parameters": [], "responses": { "200": { @@ -2460,7 +2456,7 @@ def test_public_api(client): "/native/v1/me": { "get": { "description": "", - "operationId": "get_/native/v1/me", + "operationId": "get__native_v1_me", "parameters": [], "responses": { "200": { @@ -2485,7 +2481,7 @@ def test_public_api(client): "/native/v1/me/favorites": { "get": { "description": "", - "operationId": "get_/native/v1/me/favorites", + "operationId": "get__native_v1_me_favorites", "parameters": [], "responses": { "200": { @@ -2510,7 +2506,7 @@ def test_public_api(client): }, "post": { "description": "", - "operationId": "post_/native/v1/me/favorites", + "operationId": "post__native_v1_me_favorites", "parameters": [], "requestBody": { "content": {"application/json": {"schema": {"$ref": "#/components/schemas/FavoriteRequest"}}} @@ -2539,7 +2535,7 @@ def test_public_api(client): "/native/v1/me/favorites/count": { "get": { "description": "", - "operationId": "get_/native/v1/me/favorites/count", + "operationId": "get__native_v1_me_favorites_count", "parameters": [], "responses": { "200": { @@ -2564,7 +2560,7 @@ def test_public_api(client): "/native/v1/me/favorites/{favorite_id}": { "delete": { "description": "", - "operationId": "delete_/native/v1/me/favorites/{favorite_id}", + "operationId": "delete__native_v1_me_favorites_{favorite_id}", "parameters": [ { "description": "", @@ -2592,7 +2588,7 @@ def test_public_api(client): "/native/v1/offer/report/reasons": { "get": { "description": "", - "operationId": "get_/native/v1/offer/report/reasons", + "operationId": "get__native_v1_offer_report_reasons", "parameters": [], "responses": { "200": { @@ -2617,7 +2613,7 @@ def test_public_api(client): "/native/v1/offer/{offer_id}": { "get": { "description": "", - "operationId": "get_/native/v1/offer/{offer_id}", + "operationId": "get__native_v1_offer_{offer_id}", "parameters": [ { "description": "", @@ -2648,7 +2644,7 @@ def test_public_api(client): "/native/v1/offer/{offer_id}/report": { "post": { "description": "", - "operationId": "post_/native/v1/offer/{offer_id}/report", + "operationId": "post__native_v1_offer_{offer_id}_report", "parameters": [ { "description": "", @@ -2679,7 +2675,7 @@ def test_public_api(client): "/native/v1/offers/reports": { "get": { "description": "", - "operationId": "get_/native/v1/offers/reports", + "operationId": "get__native_v1_offers_reports", "parameters": [], "responses": { "200": { @@ -2706,7 +2702,7 @@ def test_public_api(client): "/native/v1/phone_validation/remaining_attempts": { "get": { "description": "", - "operationId": "get_/native/v1/phone_validation/remaining_attempts", + "operationId": "get__native_v1_phone_validation_remaining_attempts", "parameters": [], "responses": { "200": { @@ -2733,7 +2729,7 @@ def test_public_api(client): "/native/v1/profile": { "post": { "description": "", - "operationId": "post_/native/v1/profile", + "operationId": "post__native_v1_profile", "parameters": [], "requestBody": { "content": { @@ -2760,47 +2756,42 @@ def test_public_api(client): "tags": [], } }, - "/native/v1/profile/email_update/status": { - "get": { + "/native/v1/profile/email_update/cancel": { + "post": { "description": "", - "operationId": "get_/native/v1/profile/email_update/status", + "operationId": "post__native_v1_profile_email_update_cancel", "parameters": [], + "requestBody": { + "content": { + "application/json": {"schema": {"$ref": "#/components/schemas/ChangeBeneficiaryEmailBody"}} + } + }, "responses": { - "200": { - "content": { - "application/json": {"schema": {"$ref": "#/components/schemas/EmailUpdateStatus"}} - }, - "description": "OK", - }, + "204": {"description": "No Content"}, "403": {"description": "Forbidden"}, "422": { "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ValidationError"}, - }, + "application/json": {"schema": {"$ref": "#/components/schemas/ValidationError"}} }, "description": "Unprocessable Entity", }, }, - "security": [{"JWTAuth": []}], - "summary": "get_email_update_status ", + "summary": "cancel_email_update ", "tags": [], - }, + } }, - "/native/v1/profile/token_expiration": { - "get": { + "/native/v1/profile/email_update/confirm": { + "post": { "description": "", - "operationId": "get_/native/v1/profile/token_expiration", + "operationId": "post__native_v1_profile_email_update_confirm", "parameters": [], + "requestBody": { + "content": { + "application/json": {"schema": {"$ref": "#/components/schemas/ChangeBeneficiaryEmailBody"}} + } + }, "responses": { - "200": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UpdateEmailTokenExpiration"} - } - }, - "description": "OK", - }, + "204": {"description": "No Content"}, "403": {"description": "Forbidden"}, "422": { "content": { @@ -2809,23 +2800,22 @@ def test_public_api(client): "description": "Unprocessable Entity", }, }, - "security": [{"JWTAuth": []}], - "summary": "get_email_update_token_expiration_date ", + "summary": "confirm_email_update ", "tags": [], } }, - "/native/v1/profile/update_email": { - "post": { + "/native/v1/profile/email_update/status": { + "get": { "description": "", - "operationId": "post_/native/v1/profile/update_email", + "operationId": "get__native_v1_profile_email_update_status", "parameters": [], - "requestBody": { - "content": { - "application/json": {"schema": {"$ref": "#/components/schemas/UserProfileEmailUpdate"}} - } - }, "responses": { - "204": {"description": "No Content"}, + "200": { + "content": { + "application/json": {"schema": {"$ref": "#/components/schemas/EmailUpdateStatus"}} + }, + "description": "OK", + }, "403": {"description": "Forbidden"}, "422": { "content": { @@ -2835,14 +2825,14 @@ def test_public_api(client): }, }, "security": [{"JWTAuth": []}], - "summary": "update_user_email ", + "summary": "get_email_update_status ", "tags": [], } }, - "/native/v1/profile/email_update/cancel": { - "post": { + "/native/v1/profile/email_update/validate": { + "put": { "description": "", - "operationId": "post_/native/v1/profile/email_update/cancel", + "operationId": "put__native_v1_profile_email_update_validate", "parameters": [], "requestBody": { "content": { @@ -2859,22 +2849,24 @@ def test_public_api(client): "description": "Unprocessable Entity", }, }, - "summary": "cancel_email_update ", + "summary": "validate_user_email ", "tags": [], } }, - "/native/v1/profile/email_update/confirm": { - "post": { + "/native/v1/profile/token_expiration": { + "get": { "description": "", - "operationId": "post_/native/v1/profile/email_update/confirm", + "operationId": "get__native_v1_profile_token_expiration", "parameters": [], - "requestBody": { - "content": { - "application/json": {"schema": {"$ref": "#/components/schemas/ChangeBeneficiaryEmailBody"}} - } - }, "responses": { - "204": {"description": "No Content"}, + "200": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UpdateEmailTokenExpiration"} + } + }, + "description": "OK", + }, "403": {"description": "Forbidden"}, "422": { "content": { @@ -2883,18 +2875,19 @@ def test_public_api(client): "description": "Unprocessable Entity", }, }, - "summary": "confirm_email_update ", + "security": [{"JWTAuth": []}], + "summary": "get_email_update_token_expiration_date ", "tags": [], } }, - "/native/v1/profile/email_update/validate": { - "put": { + "/native/v1/profile/update_email": { + "post": { "description": "", - "operationId": "put_/native/v1/profile/email_update/validate", + "operationId": "post__native_v1_profile_update_email", "parameters": [], "requestBody": { "content": { - "application/json": {"schema": {"$ref": "#/components/schemas/ChangeBeneficiaryEmailBody"}} + "application/json": {"schema": {"$ref": "#/components/schemas/UserProfileEmailUpdate"}} } }, "responses": { @@ -2907,14 +2900,15 @@ def test_public_api(client): "description": "Unprocessable Entity", }, }, - "summary": "validate_user_email ", + "security": [{"JWTAuth": []}], + "summary": "update_user_email ", "tags": [], } }, "/native/v1/refresh_access_token": { "post": { "description": "", - "operationId": "post_/native/v1/refresh_access_token", + "operationId": "post__native_v1_refresh_access_token", "parameters": [], "responses": { "200": { @@ -2939,7 +2933,7 @@ def test_public_api(client): "/native/v1/request_password_reset": { "post": { "description": "", - "operationId": "post_/native/v1/request_password_reset", + "operationId": "post__native_v1_request_password_reset", "parameters": [], "requestBody": { "content": { @@ -2964,7 +2958,7 @@ def test_public_api(client): "/native/v1/resend_email_validation": { "post": { "description": "", - "operationId": "post_/native/v1/resend_email_validation", + "operationId": "post__native_v1_resend_email_validation", "parameters": [], "requestBody": { "content": { @@ -2990,7 +2984,7 @@ def test_public_api(client): "/native/v1/reset_password": { "post": { "description": "", - "operationId": "post_/native/v1/reset_password", + "operationId": "post__native_v1_reset_password", "parameters": [], "requestBody": { "content": { @@ -3020,7 +3014,7 @@ def test_public_api(client): "/native/v1/reset_recredit_amount_to_show": { "post": { "description": "", - "operationId": "post_/native/v1/reset_recredit_amount_to_show", + "operationId": "post__native_v1_reset_recredit_amount_to_show", "parameters": [], "responses": { "200": { @@ -3045,7 +3039,7 @@ def test_public_api(client): "/native/v1/send_offer_link_by_push/{offer_id}": { "post": { "description": "", - "operationId": "post_/native/v1/send_offer_link_by_push/{offer_id}", + "operationId": "post__native_v1_send_offer_link_by_push_{offer_id}", "parameters": [ { "description": "", @@ -3073,7 +3067,7 @@ def test_public_api(client): "/native/v1/send_offer_webapp_link_by_email/{offer_id}": { "post": { "description": "", - "operationId": "post_/native/v1/send_offer_webapp_link_by_email/{offer_id}", + "operationId": "post__native_v1_send_offer_webapp_link_by_email_{offer_id}", "parameters": [ { "description": "", @@ -3101,7 +3095,7 @@ def test_public_api(client): "/native/v1/send_phone_validation_code": { "post": { "description": "", - "operationId": "post_/native/v1/send_phone_validation_code", + "operationId": "post__native_v1_send_phone_validation_code", "parameters": [], "requestBody": { "content": { @@ -3126,7 +3120,7 @@ def test_public_api(client): "/native/v1/settings": { "get": { "description": "", - "operationId": "get_/native/v1/settings", + "operationId": "get__native_v1_settings", "parameters": [], "responses": { "200": { @@ -3150,7 +3144,7 @@ def test_public_api(client): "/native/v1/signin": { "post": { "description": "", - "operationId": "post_/native/v1/signin", + "operationId": "post__native_v1_signin", "parameters": [], "requestBody": { "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SigninRequest"}}} @@ -3177,7 +3171,7 @@ def test_public_api(client): "/native/v1/subcategories": { "get": { "description": "", - "operationId": "get_/native/v1/subcategories", + "operationId": "get__native_v1_subcategories", "parameters": [], "responses": { "200": { @@ -3203,7 +3197,7 @@ def test_public_api(client): "/native/v1/subcategories/v2": { "get": { "description": "", - "operationId": "get_/native/v1/subcategories/v2", + "operationId": "get__native_v1_subcategories_v2", "parameters": [], "responses": { "200": { @@ -3229,7 +3223,7 @@ def test_public_api(client): "/native/v1/subscription/honor_statement": { "post": { "description": "", - "operationId": "post_/native/v1/subscription/honor_statement", + "operationId": "post__native_v1_subscription_honor_statement", "parameters": [], "responses": { "204": {"description": "No Content"}, @@ -3249,7 +3243,7 @@ def test_public_api(client): "/native/v1/subscription/next_step": { "get": { "description": "", - "operationId": "get_/native/v1/subscription/next_step", + "operationId": "get__native_v1_subscription_next_step", "parameters": [], "responses": { "200": { @@ -3276,7 +3270,7 @@ def test_public_api(client): "/native/v1/subscription/profile": { "get": { "description": "", - "operationId": "get_/native/v1/subscription/profile", + "operationId": "get__native_v1_subscription_profile", "parameters": [], "responses": { "200": {"description": "OK"}, @@ -3295,7 +3289,7 @@ def test_public_api(client): }, "post": { "description": "", - "operationId": "post_/native/v1/subscription/profile", + "operationId": "post__native_v1_subscription_profile", "parameters": [], "requestBody": { "content": { @@ -3320,7 +3314,7 @@ def test_public_api(client): "/native/v1/subscription/profile_options": { "get": { "description": "", - "operationId": "get_/native/v1/subscription/profile_options", + "operationId": "get__native_v1_subscription_profile_options", "parameters": [], "responses": { "200": { @@ -3344,7 +3338,7 @@ def test_public_api(client): "/native/v1/subscription/stepper": { "get": { "description": "", - "operationId": "get_/native/v1/subscription/stepper", + "operationId": "get__native_v1_subscription_stepper", "parameters": [], "responses": { "200": { @@ -3371,7 +3365,7 @@ def test_public_api(client): "/native/v1/ubble_identification": { "post": { "description": "", - "operationId": "post_/native/v1/ubble_identification", + "operationId": "post__native_v1_ubble_identification", "parameters": [], "requestBody": { "content": { @@ -3405,7 +3399,7 @@ def test_public_api(client): "/native/v1/ubble_identification/e2e": { "post": { "description": "", - "operationId": "post_/native/v1/ubble_identification/e2e", + "operationId": "post__native_v1_ubble_identification_e2e", "parameters": [], "requestBody": { "content": {"application/json": {"schema": {"$ref": "#/components/schemas/E2EUbbleIdCheck"}}} @@ -3428,7 +3422,7 @@ def test_public_api(client): "/native/v1/validate_email": { "post": { "description": "", - "operationId": "post_/native/v1/validate_email", + "operationId": "post__native_v1_validate_email", "parameters": [], "requestBody": { "content": { @@ -3457,7 +3451,7 @@ def test_public_api(client): "/native/v1/validate_phone_number": { "post": { "description": "", - "operationId": "post_/native/v1/validate_phone_number", + "operationId": "post__native_v1_validate_phone_number", "parameters": [], "requestBody": { "content": { @@ -3482,7 +3476,7 @@ def test_public_api(client): "/native/v1/venue/{venue_id}": { "get": { "description": "", - "operationId": "get_/native/v1/venue/{venue_id}", + "operationId": "get__native_v1_venue_{venue_id}", "parameters": [ { "description": "", diff --git a/api/tests/serialization/serialization_decorator_test.py b/api/tests/serialization/serialization_decorator_test.py index fd2a61aa589..e24d60690eb 100644 --- a/api/tests/serialization/serialization_decorator_test.py +++ b/api/tests/serialization/serialization_decorator_test.py @@ -3,9 +3,7 @@ from unittest.mock import patch from flask.blueprints import Blueprint -import pytest -from pcapi.models import api_errors from pcapi.routes.serialization import BaseModel from pcapi.serialization.decorator import spectree_serialize @@ -42,6 +40,12 @@ def spectree_post_test_endpoint(): endpoint_method() +@test_blueprint.route("/validation", methods=["POST"]) +@spectree_serialize(on_success_status=206) +def spectree_empty_form_validation(form: TestQueryModel): + return + + @test_bookings_blueprint.route("/bookings", methods=["GET"]) @spectree_serialize(on_success_status=204) def spectree_get_booking_test_endpoint(): @@ -143,12 +147,12 @@ def test_post_with_content_type_with_invalid_body_throw_error(self, client, capl ) assert response.status_code == 400 - def test_http_form_validation(self): - @spectree_serialize(response_model=TestResponseModel, on_success_status=206) - def mock_func(form: TestQueryModel): - return - - with pytest.raises(api_errors.ApiErrors) as exc_info: - mock_func(form=None) + def test_http_form_validation(self, client): + response = client.post( + "/test-blueprint/validation", form=None, headers={"Content-Type": "application/x-www-form-urlencoded"} + ) - assert exc_info.value.errors == {"compulsory_int_query": "field required"} + assert response.status_code == 400 + assert response.json == { + "compulsory_int_query": ["Ce champ est obligatoire"], + } diff --git a/pro/src/apiClient/v1/index.ts b/pro/src/apiClient/v1/index.ts index c020b149ff2..1ff64353671 100644 --- a/pro/src/apiClient/v1/index.ts +++ b/pro/src/apiClient/v1/index.ts @@ -13,6 +13,7 @@ export type { OpenAPIConfig } from './core/OpenAPI'; export type { AdageCulturalPartnerResponseModel } from './models/AdageCulturalPartnerResponseModel'; export type { AdageCulturalPartnersResponseModel } from './models/AdageCulturalPartnersResponseModel'; export type { Address } from './models/Address'; +export type { AttachImageFormModel } from './models/AttachImageFormModel'; export type { AttachImageResponseModel } from './models/AttachImageResponseModel'; export type { BannerMetaModel } from './models/BannerMetaModel'; export { BookingExportType } from './models/BookingExportType'; @@ -53,6 +54,7 @@ export type { Consent } from './models/Consent'; export type { CookieConsentRequest } from './models/CookieConsentRequest'; export type { CreateOffererQueryModel } from './models/CreateOffererQueryModel'; export type { CreatePriceCategoryModel } from './models/CreatePriceCategoryModel'; +export type { CreateThumbnailBodyModel } from './models/CreateThumbnailBodyModel'; export type { CreateThumbnailResponseModel } from './models/CreateThumbnailResponseModel'; export type { CropParams } from './models/CropParams'; export type { CulturalPartner } from './models/CulturalPartner'; diff --git a/pro/src/apiClient/v1/models/AttachImageFormModel.ts b/pro/src/apiClient/v1/models/AttachImageFormModel.ts new file mode 100644 index 00000000000..a7b0b2e447a --- /dev/null +++ b/pro/src/apiClient/v1/models/AttachImageFormModel.ts @@ -0,0 +1,13 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type AttachImageFormModel = { + credit: string; + croppingRectHeight: number; + croppingRectWidth: number; + croppingRectX: number; + croppingRectY: number; +}; + diff --git a/pro/src/apiClient/v1/models/CreateThumbnailBodyModel.ts b/pro/src/apiClient/v1/models/CreateThumbnailBodyModel.ts new file mode 100644 index 00000000000..dc9f53d0a2f --- /dev/null +++ b/pro/src/apiClient/v1/models/CreateThumbnailBodyModel.ts @@ -0,0 +1,14 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type CreateThumbnailBodyModel = { + credit?: string | null; + croppingRectHeight?: number | null; + croppingRectWidth?: number | null; + croppingRectX?: number | null; + croppingRectY?: number | null; + offerId: number; +}; + diff --git a/pro/src/apiClient/v1/services/DefaultService.ts b/pro/src/apiClient/v1/services/DefaultService.ts index 734cb93427d..8c4a0607639 100644 --- a/pro/src/apiClient/v1/services/DefaultService.ts +++ b/pro/src/apiClient/v1/services/DefaultService.ts @@ -4,6 +4,7 @@ /* eslint-disable */ import type { AdageCulturalPartnerResponseModel } from '../models/AdageCulturalPartnerResponseModel'; import type { AdageCulturalPartnersResponseModel } from '../models/AdageCulturalPartnersResponseModel'; +import type { AttachImageFormModel } from '../models/AttachImageFormModel'; import type { AttachImageResponseModel } from '../models/AttachImageResponseModel'; import type { BookingExportType } from '../models/BookingExportType'; import type { BookingStatusFilter } from '../models/BookingStatusFilter'; @@ -21,6 +22,7 @@ import type { CollectiveStockEditionBodyModel } from '../models/CollectiveStockE import type { CollectiveStockResponseModel } from '../models/CollectiveStockResponseModel'; import type { CookieConsentRequest } from '../models/CookieConsentRequest'; import type { CreateOffererQueryModel } from '../models/CreateOffererQueryModel'; +import type { CreateThumbnailBodyModel } from '../models/CreateThumbnailBodyModel'; import type { CreateThumbnailResponseModel } from '../models/CreateThumbnailResponseModel'; import type { DeleteOfferRequestBody } from '../models/DeleteOfferRequestBody'; import type { EditVenueBodyModel } from '../models/EditVenueBodyModel'; @@ -493,11 +495,13 @@ export class DefaultService { /** * attach_offer_template_image * @param offerId + * @param formData * @returns AttachImageResponseModel OK * @throws ApiError */ public attachOfferTemplateImage( offerId: string, + formData?: AttachImageFormModel, ): CancelablePromise { return this.httpRequest.request({ method: 'POST', @@ -505,6 +509,8 @@ export class DefaultService { path: { 'offer_id': offerId, }, + formData: formData, + mediaType: 'multipart/form-data', errors: { 403: `Forbidden`, 422: `Unprocessable Entity`, @@ -748,11 +754,13 @@ export class DefaultService { /** * attach_offer_image * @param offerId + * @param formData * @returns AttachImageResponseModel OK * @throws ApiError */ public attachOfferImage( offerId: number, + formData?: AttachImageFormModel, ): CancelablePromise { return this.httpRequest.request({ method: 'POST', @@ -760,6 +768,8 @@ export class DefaultService { path: { 'offer_id': offerId, }, + formData: formData, + mediaType: 'multipart/form-data', errors: { 403: `Forbidden`, 422: `Unprocessable Entity`, @@ -1425,13 +1435,18 @@ export class DefaultService { /** * create_thumbnail + * @param formData * @returns CreateThumbnailResponseModel Created * @throws ApiError */ - public createThumbnail(): CancelablePromise { + public createThumbnail( + formData?: CreateThumbnailBodyModel, + ): CancelablePromise { return this.httpRequest.request({ method: 'POST', url: '/offers/thumbnails/', + formData: formData, + mediaType: 'multipart/form-data', errors: { 403: `Forbidden`, 422: `Unprocessable Entity`,