From e2e382a5b57f441550ad865685efe31e3d095807 Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Fri, 4 Nov 2022 13:58:06 +0000 Subject: [PATCH 01/23] added drf-problems dependency --- answerking/settings/base.py | 12 +++--------- answerking/urls.py | 1 + pyproject.toml | 3 ++- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/answerking/settings/base.py b/answerking/settings/base.py index b1040b47..1d6b2f70 100644 --- a/answerking/settings/base.py +++ b/answerking/settings/base.py @@ -2,9 +2,7 @@ Django base settings for answerking project. """ import os - from pathlib import Path -from corsheaders.defaults import default_headers, default_methods # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -35,6 +33,7 @@ "answerking_app.apps.AnswerkingAppConfig", "rest_framework", "corsheaders", + "drf_problems", ] MIDDLEWARE = [ @@ -72,7 +71,8 @@ REST_FRAMEWORK = { "DEFAULT_PARSER_CLASSES": [ "rest_framework.parsers.JSONParser", - ] + ], + "EXCEPTION_HANDLER": "drf_problems.exceptions.exception_handler", } # Database @@ -142,9 +142,3 @@ SECURE_SSL_REDIRECT = False SESSION_COOKIE_SECURE = False CSRF_COOKIE_SECURE = False - -SECURE_HSTS_SECONDS = None -SECURE_HSTS_INCLUDE_SUBDOMAINS = False -SECURE_FRAME_DENY = False -SECURE_CONTENT_TYPE_NOSNIFF = False -SECURE_BROWSER_XSS_FILTER = False diff --git a/answerking/urls.py b/answerking/urls.py index 497a37cd..678375af 100644 --- a/answerking/urls.py +++ b/answerking/urls.py @@ -7,6 +7,7 @@ path("api/", include("answerking_app.urls.category_urls")), path("api/", include("answerking_app.urls.order_urls")), path("admin/", admin.site.urls), + path("", include("drf_problems.urls")), ] if settings.DEBUG: diff --git a/pyproject.toml b/pyproject.toml index 17ccbd7b..5720692f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "" authors = [] readme = "README.md" -packages = [{include = "answerking_python"}] +packages = [{ include = "answerking_python" }] [tool.poetry.dependencies] python = "^3.10" @@ -15,6 +15,7 @@ django-cors-headers = "^3.13.0" drf-writable-nested = "^0.7.0" django-json-404-middleware = "^0.0.1" python-dotenv = "^0.21.0" +drf-problems = "^1.0.5" [tool.poetry.group.dev.dependencies] black = "^22.10.0" From 296c169b3f3f90370ebcaf93cd9e502909790f2c Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Mon, 7 Nov 2022 11:52:34 +0000 Subject: [PATCH 02/23] fixes middleware 404, temporarly in the codebase, awaiting for the PR to be merged, fixes error codes using drf-problems --- answerking/middlewares/json404.py | 30 ++++++++++++++++ answerking/settings/base.py | 5 ++- .../utils/json404_middleware_config.py | 15 ++++++++ answerking_app/utils/mixins/ApiExceptions.py | 10 ++++++ .../utils/mixins/CategoryItemMixins.py | 6 ++-- answerking_app/utils/mixins/GenericMixins.py | 17 +++++----- answerking_app/views/category_views.py | 34 +++++++++---------- answerking_app/views/item_views.py | 12 +++---- 8 files changed, 92 insertions(+), 37 deletions(-) create mode 100644 answerking/middlewares/json404.py create mode 100644 answerking_app/utils/json404_middleware_config.py create mode 100644 answerking_app/utils/mixins/ApiExceptions.py diff --git a/answerking/middlewares/json404.py b/answerking/middlewares/json404.py new file mode 100644 index 00000000..5b46bc5b --- /dev/null +++ b/answerking/middlewares/json404.py @@ -0,0 +1,30 @@ +from django.conf import settings +from django.http import JsonResponse + + +def default_response(request): + data = {"detail": "{0} not found".format(request.path)} + return JsonResponse(data, content_type="application/json", status=404) + + +class JSON404Middleware(object): + """ + Returns JSON 404 instead of HTML + """ + + def __init__(self, get_response): + self.get_response = get_response + try: + self.data_function = settings.JSON404_DATA_FUNCTION + except AttributeError: + self.data_function = default_response + + def __call__(self, request): + response = self.get_response(request) + if ( + response.status_code == 404 + and "text/html" in response["content-type"] + ): + response = self.data_function(request) + + return response diff --git a/answerking/settings/base.py b/answerking/settings/base.py index 1d6b2f70..aab6236b 100644 --- a/answerking/settings/base.py +++ b/answerking/settings/base.py @@ -45,7 +45,8 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - "django_json_404_middleware.JSON404Middleware", + "answerking.middlewares.json404.JSON404Middleware" + # "django_json_404_middleware.JSON404Middleware", ] ROOT_URLCONF = "answerking.urls" @@ -107,6 +108,8 @@ }, ] +# JSON404 middleware +JSON404_DATA_FUNCTION = json404_response # Internationalization # https://docs.djangoproject.com/en/4.0/topics/i18n/ diff --git a/answerking_app/utils/json404_middleware_config.py b/answerking_app/utils/json404_middleware_config.py new file mode 100644 index 00000000..57547fa4 --- /dev/null +++ b/answerking_app/utils/json404_middleware_config.py @@ -0,0 +1,15 @@ +from django.http import JsonResponse + + +def json404_response(request): + + data = { + "detail": "not found", + "title": "Not found.", + "status": 404, + "type": "{}://{}/problems/not_found/", + } + data["type"] = data["type"].format(request.scheme, request.get_host()) + return JsonResponse( + data, content_type="application/problem+json", status=404 + ) diff --git a/answerking_app/utils/mixins/ApiExceptions.py b/answerking_app/utils/mixins/ApiExceptions.py new file mode 100644 index 00000000..c436a59b --- /dev/null +++ b/answerking_app/utils/mixins/ApiExceptions.py @@ -0,0 +1,10 @@ +from rest_framework import status +from rest_framework.exceptions import APIException + + +class Http400BadRequest(APIException): + status_code = status.HTTP_400_BAD_REQUEST + + +class ServiceUnavailable(APIException): + status_code = status.HTTP_503_SERVICE_UNAVAILABLE diff --git a/answerking_app/utils/mixins/CategoryItemMixins.py b/answerking_app/utils/mixins/CategoryItemMixins.py index 6e6b5a95..7009f40e 100644 --- a/answerking_app/utils/mixins/CategoryItemMixins.py +++ b/answerking_app/utils/mixins/CategoryItemMixins.py @@ -1,15 +1,13 @@ -from django.db import IntegrityError +from django.shortcuts import get_object_or_404 from rest_framework import status -from rest_framework.response import Response from rest_framework.request import Request +from rest_framework.response import Response from rest_framework.utils.serializer_helpers import ReturnDict from answerking_app.models.models import Category, Item from answerking_app.models.serializers import CategorySerializer from answerking_app.utils.ErrorType import ErrorMessage -from django.shortcuts import get_object_or_404 - class CategoryItemUpdateMixin: def update( diff --git a/answerking_app/utils/mixins/GenericMixins.py b/answerking_app/utils/mixins/GenericMixins.py index f76b6cea..defd8301 100644 --- a/answerking_app/utils/mixins/GenericMixins.py +++ b/answerking_app/utils/mixins/GenericMixins.py @@ -1,17 +1,21 @@ -from MySQLdb.constants.ER import DUP_ENTRY +from typing import NoReturn + from django.db import IntegrityError +from MySQLdb.constants.ER import DUP_ENTRY from rest_framework import status from rest_framework.mixins import CreateModelMixin, UpdateModelMixin from rest_framework.request import Request from rest_framework.response import Response +from answerking_app.utils.mixins.ApiExceptions import Http400BadRequest + class CreateMixin(CreateModelMixin): def create(self, request: Request, *args, **kwargs) -> Response: try: return super().create(request, *args, **kwargs) except IntegrityError as exc: - return duplicate_check(exc) + handle_IntegrityError(exc) class UpdateMixin(UpdateModelMixin): @@ -19,7 +23,7 @@ def update(self, request: Request, *args, **kwargs) -> Response: try: return super().update(request, *args, **kwargs) except IntegrityError as exc: - return duplicate_check(exc) + return handle_IntegrityError(exc) class RetireMixin(UpdateModelMixin): @@ -28,11 +32,8 @@ def retire(self, request: Request, *args, **kwargs) -> Response: return super().partial_update(request, *args, **kwargs) -def duplicate_check(exc: IntegrityError) -> Response: +def handle_IntegrityError(exc: IntegrityError) -> NoReturn: if exc.args[0] == DUP_ENTRY: - return Response( - {"detail": "This name already exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) + raise Http400BadRequest else: return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE) diff --git a/answerking_app/views/category_views.py b/answerking_app/views/category_views.py index 66aeeb8a..aebede2a 100644 --- a/answerking_app/views/category_views.py +++ b/answerking_app/views/category_views.py @@ -1,27 +1,23 @@ from django.db.models import QuerySet - -from rest_framework import mixins, generics +from drf_problems.mixins import AllowPermissionWithExceptionViewMixin +from rest_framework import generics, mixins from rest_framework.request import Request from rest_framework.response import Response -from answerking_app.utils.mixins.CategoryItemMixins import ( - CategoryItemUpdateMixin, - CategoryItemRemoveMixin, -) from answerking_app.models.models import Category - -from answerking_app.models.serializers import ( - CategorySerializer, -) -from answerking_app.utils.mixins.GenericMixins import ( - CreateMixin, - UpdateMixin, - RetireMixin, -) +from answerking_app.models.serializers import CategorySerializer +from answerking_app.utils.mixins.CategoryItemMixins import ( + CategoryItemRemoveMixin, CategoryItemUpdateMixin) +from answerking_app.utils.mixins.GenericMixins import (CreateMixin, + RetireMixin, + UpdateMixin) class CategoryListView( - mixins.ListModelMixin, CreateMixin, generics.GenericAPIView + mixins.ListModelMixin, + CreateMixin, + generics.GenericAPIView, + AllowPermissionWithExceptionViewMixin, ): queryset: QuerySet = Category.objects.all() serializer_class: CategorySerializer = CategorySerializer @@ -39,6 +35,7 @@ class CategoryDetailView( RetireMixin, mixins.DestroyModelMixin, generics.GenericAPIView, + AllowPermissionWithExceptionViewMixin, ): queryset: QuerySet = Category.objects.all() serializer_class: CategorySerializer = CategorySerializer @@ -54,7 +51,10 @@ def delete(self, request: Request, *args, **kwargs) -> Response: class CategoryItemListView( - CategoryItemUpdateMixin, CategoryItemRemoveMixin, generics.GenericAPIView + CategoryItemUpdateMixin, + CategoryItemRemoveMixin, + generics.GenericAPIView, + AllowPermissionWithExceptionViewMixin, ): queryset: QuerySet = Category.objects.all() serializer_class: CategorySerializer = CategorySerializer diff --git a/answerking_app/views/item_views.py b/answerking_app/views/item_views.py index 45a4eb17..403de270 100644 --- a/answerking_app/views/item_views.py +++ b/answerking_app/views/item_views.py @@ -1,16 +1,14 @@ from django.db.models import QuerySet -from rest_framework import mixins, generics +from rest_framework import generics, mixins from rest_framework.request import Request from rest_framework.response import Response -from answerking_app.utils.mixins.GenericMixins import ( - CreateMixin, - UpdateMixin, - RetireMixin, -) -from answerking_app.utils.mixins.ItemMixins import DestroyItemMixin from answerking_app.models.models import Item from answerking_app.models.serializers import ItemSerializer +from answerking_app.utils.mixins.GenericMixins import (CreateMixin, + RetireMixin, + UpdateMixin) +from answerking_app.utils.mixins.ItemMixins import DestroyItemMixin class ItemListView( From e2f5d496ce8718e5b74aeb05dc475805c14f948e Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Mon, 7 Nov 2022 12:19:52 +0000 Subject: [PATCH 03/23] fixed missing import error during rebase --- answerking/settings/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/answerking/settings/base.py b/answerking/settings/base.py index aab6236b..4205cb47 100644 --- a/answerking/settings/base.py +++ b/answerking/settings/base.py @@ -4,6 +4,10 @@ import os from pathlib import Path +from corsheaders.defaults import default_headers, default_methods + +from answerking_app.utils.json404_middleware_config import json404_response + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent From 629ff426c6158b3e723545f5e77f721113626804 Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Mon, 7 Nov 2022 13:45:47 +0000 Subject: [PATCH 04/23] replaced Responses to Raise Exception --- .../utils/mixins/CategoryItemMixins.py | 12 ++------- answerking_app/utils/mixins/GenericMixins.py | 2 +- answerking_app/utils/mixins/ItemMixins.py | 8 +++--- .../utils/mixins/OrderItemMixins.py | 26 +++++-------------- 4 files changed, 13 insertions(+), 35 deletions(-) diff --git a/answerking_app/utils/mixins/CategoryItemMixins.py b/answerking_app/utils/mixins/CategoryItemMixins.py index 7009f40e..cb5e2f88 100644 --- a/answerking_app/utils/mixins/CategoryItemMixins.py +++ b/answerking_app/utils/mixins/CategoryItemMixins.py @@ -7,6 +7,7 @@ from answerking_app.models.models import Category, Item from answerking_app.models.serializers import CategorySerializer from answerking_app.utils.ErrorType import ErrorMessage +from answerking_app.utils.mixins.ApiExceptions import Http400BadRequest class CategoryItemUpdateMixin: @@ -18,16 +19,7 @@ def update( item: Item = get_object_or_404(Item, pk=item_id) if item in category.items.all(): - error_msg: ErrorMessage = { - "error": { - "message": "Resource update failure", - "details": "Item already in category", - } - } - return Response( - error_msg, - status=status.HTTP_400_BAD_REQUEST, - ) + raise Http400BadRequest category.items.add(item) response: ReturnDict = CategorySerializer(category).data diff --git a/answerking_app/utils/mixins/GenericMixins.py b/answerking_app/utils/mixins/GenericMixins.py index defd8301..752bd9f8 100644 --- a/answerking_app/utils/mixins/GenericMixins.py +++ b/answerking_app/utils/mixins/GenericMixins.py @@ -36,4 +36,4 @@ def handle_IntegrityError(exc: IntegrityError) -> NoReturn: if exc.args[0] == DUP_ENTRY: raise Http400BadRequest else: - return Response(status=status.HTTP_503_SERVICE_UNAVAILABLE) + raise ServiceUnavailable diff --git a/answerking_app/utils/mixins/ItemMixins.py b/answerking_app/utils/mixins/ItemMixins.py index 75e97e3e..93b527cb 100644 --- a/answerking_app/utils/mixins/ItemMixins.py +++ b/answerking_app/utils/mixins/ItemMixins.py @@ -3,7 +3,8 @@ from rest_framework.request import Request from rest_framework.response import Response -from answerking_app.models.models import OrderLine, Item +from answerking_app.models.models import Item, OrderLine +from answerking_app.utils.mixins.ApiExceptions import Http400BadRequest from answerking_app.utils.model_types import OrderItemType @@ -14,10 +15,7 @@ def destroy(self, request: Request, *args, **kwargs) -> Response: item=instance.id ) if existing_orderitems: - return Response( - {"detail": "Cannot delete, item is in an order."}, - status=status.HTTP_400_BAD_REQUEST, - ) + raise Http400BadRequest self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/answerking_app/utils/mixins/OrderItemMixins.py b/answerking_app/utils/mixins/OrderItemMixins.py index e5286001..2d9dac4c 100644 --- a/answerking_app/utils/mixins/OrderItemMixins.py +++ b/answerking_app/utils/mixins/OrderItemMixins.py @@ -1,16 +1,13 @@ +from django.shortcuts import get_object_or_404 from rest_framework import status -from rest_framework.response import Response from rest_framework.request import Request +from rest_framework.response import Response from rest_framework.utils.serializer_helpers import ReturnDict -from answerking_app.models.models import Order, Item -from answerking_app.models.serializers import ( - OrderSerializer, - OrderLineSerializer, -) -from answerking_app.utils.ErrorType import ErrorMessage - -from django.shortcuts import get_object_or_404 +from answerking_app.models.models import Item, Order +from answerking_app.models.serializers import (OrderLineSerializer, + OrderSerializer) +from answerking_app.utils.mixins.ApiExceptions import Http400BadRequest class OrderItemUpdateMixin: @@ -59,16 +56,7 @@ def remove( item: Item = get_object_or_404(Item, pk=item_id) if item not in order.order_items.all(): - error_msg: ErrorMessage = { - "error": { - "message": "Resource update failure", - "details": "Item not in order", - } - } - return Response( - error_msg, - status=status.HTTP_400_BAD_REQUEST, - ) + raise Http400BadRequest updated_order: Order | None = self.remove_item(order, item) From 28d334fb561ef2a49e874b052a9af1bda6caff52 Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Mon, 7 Nov 2022 13:51:23 +0000 Subject: [PATCH 05/23] fixed rebase error --- answerking/settings/base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/answerking/settings/base.py b/answerking/settings/base.py index 4205cb47..bcaa7002 100644 --- a/answerking/settings/base.py +++ b/answerking/settings/base.py @@ -149,3 +149,9 @@ SECURE_SSL_REDIRECT = False SESSION_COOKIE_SECURE = False CSRF_COOKIE_SECURE = False + +SECURE_HSTS_SECONDS = None +SECURE_HSTS_INCLUDE_SUBDOMAINS = False +SECURE_FRAME_DENY = False +SECURE_CONTENT_TYPE_NOSNIFF = False +SECURE_BROWSER_XSS_FILTER = False From 74690f68542a513786959646034a984a4a6e5bb8 Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Mon, 7 Nov 2022 16:29:22 +0000 Subject: [PATCH 06/23] temporarly changed dependencies to forks, awaiting for merge --- answerking/middlewares/json404.py | 30 ------------------------------ answerking/settings/base.py | 3 +-- pyproject.toml | 4 ++-- 3 files changed, 3 insertions(+), 34 deletions(-) delete mode 100644 answerking/middlewares/json404.py diff --git a/answerking/middlewares/json404.py b/answerking/middlewares/json404.py deleted file mode 100644 index 5b46bc5b..00000000 --- a/answerking/middlewares/json404.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.conf import settings -from django.http import JsonResponse - - -def default_response(request): - data = {"detail": "{0} not found".format(request.path)} - return JsonResponse(data, content_type="application/json", status=404) - - -class JSON404Middleware(object): - """ - Returns JSON 404 instead of HTML - """ - - def __init__(self, get_response): - self.get_response = get_response - try: - self.data_function = settings.JSON404_DATA_FUNCTION - except AttributeError: - self.data_function = default_response - - def __call__(self, request): - response = self.get_response(request) - if ( - response.status_code == 404 - and "text/html" in response["content-type"] - ): - response = self.data_function(request) - - return response diff --git a/answerking/settings/base.py b/answerking/settings/base.py index bcaa7002..446e1961 100644 --- a/answerking/settings/base.py +++ b/answerking/settings/base.py @@ -49,8 +49,7 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - "answerking.middlewares.json404.JSON404Middleware" - # "django_json_404_middleware.JSON404Middleware", + "django_json_404_middleware.JSON404Middleware", ] ROOT_URLCONF = "answerking.urls" diff --git a/pyproject.toml b/pyproject.toml index 5720692f..47cf51e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,9 +13,9 @@ djangorestframework = "^3.14.0" mysqlclient = "^2.1.1" django-cors-headers = "^3.13.0" drf-writable-nested = "^0.7.0" -django-json-404-middleware = "^0.0.1" +django-json-404-middleware = { git = "https://github.com/Axeltherabbit/django-json-404-middleware" } python-dotenv = "^0.21.0" -drf-problems = "^1.0.5" +drf-problems = { git = "https://github.com/Axeltherabbit/drf-problems" } [tool.poetry.group.dev.dependencies] black = "^22.10.0" From ec437366289500762ce811508ad62c4c774a94be Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Tue, 8 Nov 2022 13:02:36 +0000 Subject: [PATCH 07/23] Defined generic Exception Class --- answerking_app/utils/mixins/ApiExceptions.py | 26 ++++++++++++++----- .../utils/mixins/CategoryItemMixins.py | 5 ++-- answerking_app/utils/mixins/GenericMixins.py | 6 ++--- answerking_app/utils/mixins/ItemMixins.py | 4 +-- .../utils/mixins/OrderItemMixins.py | 4 +-- 5 files changed, 28 insertions(+), 17 deletions(-) diff --git a/answerking_app/utils/mixins/ApiExceptions.py b/answerking_app/utils/mixins/ApiExceptions.py index c436a59b..6ca51f9c 100644 --- a/answerking_app/utils/mixins/ApiExceptions.py +++ b/answerking_app/utils/mixins/ApiExceptions.py @@ -1,10 +1,22 @@ -from rest_framework import status from rest_framework.exceptions import APIException -class Http400BadRequest(APIException): - status_code = status.HTTP_400_BAD_REQUEST - - -class ServiceUnavailable(APIException): - status_code = status.HTTP_503_SERVICE_UNAVAILABLE +class HttpErrorResponse(APIException): + def __init__( + self, + status: int, + detail: str | None = None, + title: str | None = None, + instance: str | None = None, + extensions: dict | None = None, + ): + super().__init__() + self.status_code = status + if detail: + self.default_detail = detail + if title: + self.title = title + if instance: + self.instance = instance + if extensions: + self.extensions = extensions diff --git a/answerking_app/utils/mixins/CategoryItemMixins.py b/answerking_app/utils/mixins/CategoryItemMixins.py index cb5e2f88..7ff7daaf 100644 --- a/answerking_app/utils/mixins/CategoryItemMixins.py +++ b/answerking_app/utils/mixins/CategoryItemMixins.py @@ -6,8 +6,7 @@ from answerking_app.models.models import Category, Item from answerking_app.models.serializers import CategorySerializer -from answerking_app.utils.ErrorType import ErrorMessage -from answerking_app.utils.mixins.ApiExceptions import Http400BadRequest +from answerking_app.utils.mixins.ApiExceptions import HttpErrorResponse class CategoryItemUpdateMixin: @@ -19,7 +18,7 @@ def update( item: Item = get_object_or_404(Item, pk=item_id) if item in category.items.all(): - raise Http400BadRequest + raise HttpErrorResponse(status=status.HTTP_400_BAD_REQUEST) category.items.add(item) response: ReturnDict = CategorySerializer(category).data diff --git a/answerking_app/utils/mixins/GenericMixins.py b/answerking_app/utils/mixins/GenericMixins.py index 752bd9f8..c394bcf1 100644 --- a/answerking_app/utils/mixins/GenericMixins.py +++ b/answerking_app/utils/mixins/GenericMixins.py @@ -7,7 +7,7 @@ from rest_framework.request import Request from rest_framework.response import Response -from answerking_app.utils.mixins.ApiExceptions import Http400BadRequest +from answerking_app.utils.mixins.ApiExceptions import HttpErrorResponse class CreateMixin(CreateModelMixin): @@ -34,6 +34,6 @@ def retire(self, request: Request, *args, **kwargs) -> Response: def handle_IntegrityError(exc: IntegrityError) -> NoReturn: if exc.args[0] == DUP_ENTRY: - raise Http400BadRequest + raise HttpErrorResponse(status=status.HTTP_400_BAD_REQUEST) else: - raise ServiceUnavailable + raise HttpErrorResponse(status=status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/answerking_app/utils/mixins/ItemMixins.py b/answerking_app/utils/mixins/ItemMixins.py index 93b527cb..dcc17f79 100644 --- a/answerking_app/utils/mixins/ItemMixins.py +++ b/answerking_app/utils/mixins/ItemMixins.py @@ -4,7 +4,7 @@ from rest_framework.response import Response from answerking_app.models.models import Item, OrderLine -from answerking_app.utils.mixins.ApiExceptions import Http400BadRequest +from answerking_app.utils.mixins.ApiExceptions import HttpErrorResponse from answerking_app.utils.model_types import OrderItemType @@ -15,7 +15,7 @@ def destroy(self, request: Request, *args, **kwargs) -> Response: item=instance.id ) if existing_orderitems: - raise Http400BadRequest + raise HttpErrorResponse(status=status.HTTP_400_BAD_REQUEST) self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/answerking_app/utils/mixins/OrderItemMixins.py b/answerking_app/utils/mixins/OrderItemMixins.py index 2d9dac4c..02699905 100644 --- a/answerking_app/utils/mixins/OrderItemMixins.py +++ b/answerking_app/utils/mixins/OrderItemMixins.py @@ -7,7 +7,7 @@ from answerking_app.models.models import Item, Order from answerking_app.models.serializers import (OrderLineSerializer, OrderSerializer) -from answerking_app.utils.mixins.ApiExceptions import Http400BadRequest +from answerking_app.utils.mixins.ApiExceptions import HttpErrorResponse class OrderItemUpdateMixin: @@ -56,7 +56,7 @@ def remove( item: Item = get_object_or_404(Item, pk=item_id) if item not in order.order_items.all(): - raise Http400BadRequest + raise HttpErrorResponse(status=status.HTTP_400_BAD_REQUEST) updated_order: Order | None = self.remove_item(order, item) From 09cd275d74f0a5773cf585245d63e2467e7e14ab Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Tue, 8 Nov 2022 13:52:37 +0000 Subject: [PATCH 08/23] Renamed integrityHandlerMixin --- .../{GenericMixins.py => IntegrityHandlerMixins.py} | 6 ------ answerking_app/utils/mixins/RetireMixin.py | 9 +++++++++ answerking_app/views/category_views.py | 12 ++++++++---- answerking_app/views/item_views.py | 12 ++++++++---- 4 files changed, 25 insertions(+), 14 deletions(-) rename answerking_app/utils/mixins/{GenericMixins.py => IntegrityHandlerMixins.py} (84%) create mode 100644 answerking_app/utils/mixins/RetireMixin.py diff --git a/answerking_app/utils/mixins/GenericMixins.py b/answerking_app/utils/mixins/IntegrityHandlerMixins.py similarity index 84% rename from answerking_app/utils/mixins/GenericMixins.py rename to answerking_app/utils/mixins/IntegrityHandlerMixins.py index c394bcf1..e3440c17 100644 --- a/answerking_app/utils/mixins/GenericMixins.py +++ b/answerking_app/utils/mixins/IntegrityHandlerMixins.py @@ -26,12 +26,6 @@ def update(self, request: Request, *args, **kwargs) -> Response: return handle_IntegrityError(exc) -class RetireMixin(UpdateModelMixin): - def retire(self, request: Request, *args, **kwargs) -> Response: - request.data["retired"] = True - return super().partial_update(request, *args, **kwargs) - - def handle_IntegrityError(exc: IntegrityError) -> NoReturn: if exc.args[0] == DUP_ENTRY: raise HttpErrorResponse(status=status.HTTP_400_BAD_REQUEST) diff --git a/answerking_app/utils/mixins/RetireMixin.py b/answerking_app/utils/mixins/RetireMixin.py new file mode 100644 index 00000000..6c560842 --- /dev/null +++ b/answerking_app/utils/mixins/RetireMixin.py @@ -0,0 +1,9 @@ +from rest_framework.mixins import UpdateModelMixin +from rest_framework.request import Request +from rest_framework.response import Response + + +class RetireMixin(UpdateModelMixin): + def retire(self, request: Request, *args, **kwargs) -> Response: + request.data["retired"] = True + return super().partial_update(request, *args, **kwargs) diff --git a/answerking_app/views/category_views.py b/answerking_app/views/category_views.py index aebede2a..44c41685 100644 --- a/answerking_app/views/category_views.py +++ b/answerking_app/views/category_views.py @@ -8,14 +8,14 @@ from answerking_app.models.serializers import CategorySerializer from answerking_app.utils.mixins.CategoryItemMixins import ( CategoryItemRemoveMixin, CategoryItemUpdateMixin) -from answerking_app.utils.mixins.GenericMixins import (CreateMixin, - RetireMixin, - UpdateMixin) +from answerking_app.utils.mixins.IntegrityHandlerMixins import ( + CreateIntegrityHandlerMixin, UpdateIntegrityHandlerMixin) +from answerking_app.utils.mixins.RetireMixin import RetireMixin class CategoryListView( mixins.ListModelMixin, - CreateMixin, + CreateIntegrityHandlerMixin, generics.GenericAPIView, AllowPermissionWithExceptionViewMixin, ): @@ -31,8 +31,12 @@ def post(self, request: Request, *args, **kwargs) -> Response: class CategoryDetailView( mixins.RetrieveModelMixin, +<<<<<<< HEAD UpdateMixin, RetireMixin, +======= + UpdateIntegrityHandlerMixin, +>>>>>>> Renamed integrityHandlerMixin mixins.DestroyModelMixin, generics.GenericAPIView, AllowPermissionWithExceptionViewMixin, diff --git a/answerking_app/views/item_views.py b/answerking_app/views/item_views.py index 403de270..49d40517 100644 --- a/answerking_app/views/item_views.py +++ b/answerking_app/views/item_views.py @@ -5,14 +5,14 @@ from answerking_app.models.models import Item from answerking_app.models.serializers import ItemSerializer -from answerking_app.utils.mixins.GenericMixins import (CreateMixin, - RetireMixin, - UpdateMixin) +from answerking_app.utils.mixins.IntegrityHandlerMixins import ( + CreateIntegrityHandlerMixin, UpdateIntegrityHandlerMixin) from answerking_app.utils.mixins.ItemMixins import DestroyItemMixin +from answerking_app.utils.mixins.RetireMixin import RetireMixin class ItemListView( - mixins.ListModelMixin, CreateMixin, generics.GenericAPIView + mixins.ListModelMixin, CreateIntegrityHandlerMixin, generics.GenericAPIView ): queryset: QuerySet = Item.objects.all() @@ -27,8 +27,12 @@ def post(self, request: Request, *args, **kwargs) -> Response: class ItemDetailView( mixins.RetrieveModelMixin, +<<<<<<< HEAD UpdateMixin, RetireMixin, +======= + UpdateIntegrityHandlerMixin, +>>>>>>> Renamed integrityHandlerMixin DestroyItemMixin, generics.GenericAPIView, ): From 9622134476f4b229cdc28f0ddd42caaf227da1d8 Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Tue, 8 Nov 2022 15:20:47 +0000 Subject: [PATCH 09/23] added detail handle for serializer Errors --- answerking_app/utils/mixins/ApiExceptions.py | 2 +- .../mixins/SerializeErrorDetailRFCMixins.py | 31 ++++++++++++++++++ answerking_app/views/category_views.py | 8 ++--- answerking_app/views/item_views.py | 12 ++++--- answerking_app/views/order_views.py | 32 +++++++------------ 5 files changed, 54 insertions(+), 31 deletions(-) create mode 100644 answerking_app/utils/mixins/SerializeErrorDetailRFCMixins.py diff --git a/answerking_app/utils/mixins/ApiExceptions.py b/answerking_app/utils/mixins/ApiExceptions.py index 6ca51f9c..d20ae5c4 100644 --- a/answerking_app/utils/mixins/ApiExceptions.py +++ b/answerking_app/utils/mixins/ApiExceptions.py @@ -13,7 +13,7 @@ def __init__( super().__init__() self.status_code = status if detail: - self.default_detail = detail + self.detail = detail if title: self.title = title if instance: diff --git a/answerking_app/utils/mixins/SerializeErrorDetailRFCMixins.py b/answerking_app/utils/mixins/SerializeErrorDetailRFCMixins.py new file mode 100644 index 00000000..d03b5191 --- /dev/null +++ b/answerking_app/utils/mixins/SerializeErrorDetailRFCMixins.py @@ -0,0 +1,31 @@ +from rest_framework.mixins import CreateModelMixin, UpdateModelMixin +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.serializers import ValidationError + +from answerking_app.utils.mixins.ApiExceptions import HttpErrorResponse + + +def ValidationErrorDetailed(exc: ValidationError): + return HttpErrorResponse( + status=400, + detail="Validation Error", + title="Invalid input.", + extensions={"errors": exc.detail}, + ) + + +class CreateErrorDetailMixin(CreateModelMixin): + def create(self, request: Request, *args, **kwargs) -> Response: + try: + return super().create(request, *args, **kwargs) + except ValidationError as exc: + raise ValidationErrorDetailed(exc) + + +class UpdateErrorDetailMixin(UpdateModelMixin): + def update(self, request: Request, *args, **kwargs) -> Response: + try: + return super().update(request, *args, **kwargs) + except ValidationError as exc: + raise ValidationErrorDetailed(exc) diff --git a/answerking_app/views/category_views.py b/answerking_app/views/category_views.py index 44c41685..d678b92b 100644 --- a/answerking_app/views/category_views.py +++ b/answerking_app/views/category_views.py @@ -11,11 +11,14 @@ from answerking_app.utils.mixins.IntegrityHandlerMixins import ( CreateIntegrityHandlerMixin, UpdateIntegrityHandlerMixin) from answerking_app.utils.mixins.RetireMixin import RetireMixin +from answerking_app.utils.mixins.SerializeErrorDetailRFCMixins import ( + CreateErrorDetailMixin, UpdateErrorDetailMixin) class CategoryListView( mixins.ListModelMixin, CreateIntegrityHandlerMixin, + CreateErrorDetailMixin, generics.GenericAPIView, AllowPermissionWithExceptionViewMixin, ): @@ -31,12 +34,9 @@ def post(self, request: Request, *args, **kwargs) -> Response: class CategoryDetailView( mixins.RetrieveModelMixin, -<<<<<<< HEAD - UpdateMixin, RetireMixin, -======= UpdateIntegrityHandlerMixin, ->>>>>>> Renamed integrityHandlerMixin + UpdateErrorDetailMixin, mixins.DestroyModelMixin, generics.GenericAPIView, AllowPermissionWithExceptionViewMixin, diff --git a/answerking_app/views/item_views.py b/answerking_app/views/item_views.py index 49d40517..e31077b2 100644 --- a/answerking_app/views/item_views.py +++ b/answerking_app/views/item_views.py @@ -9,10 +9,15 @@ CreateIntegrityHandlerMixin, UpdateIntegrityHandlerMixin) from answerking_app.utils.mixins.ItemMixins import DestroyItemMixin from answerking_app.utils.mixins.RetireMixin import RetireMixin +from answerking_app.utils.mixins.SerializeErrorDetailRFCMixins import ( + CreateErrorDetailMixin, UpdateErrorDetailMixin) class ItemListView( - mixins.ListModelMixin, CreateIntegrityHandlerMixin, generics.GenericAPIView + mixins.ListModelMixin, + CreateErrorDetailMixin, + CreateIntegrityHandlerMixin, + generics.GenericAPIView, ): queryset: QuerySet = Item.objects.all() @@ -27,12 +32,9 @@ def post(self, request: Request, *args, **kwargs) -> Response: class ItemDetailView( mixins.RetrieveModelMixin, -<<<<<<< HEAD - UpdateMixin, RetireMixin, -======= UpdateIntegrityHandlerMixin, ->>>>>>> Renamed integrityHandlerMixin + UpdateErrorDetailMixin, DestroyItemMixin, generics.GenericAPIView, ): diff --git a/answerking_app/views/order_views.py b/answerking_app/views/order_views.py index 569a5060..91c583ff 100644 --- a/answerking_app/views/order_views.py +++ b/answerking_app/views/order_views.py @@ -2,23 +2,24 @@ from django.core.exceptions import ObjectDoesNotExist from django.db.models import QuerySet - -from rest_framework import mixins, generics, status +from rest_framework import generics, mixins, status from rest_framework.request import Request from rest_framework.response import Response -from answerking_app.utils.mixins.OrderItemMixins import ( - OrderItemUpdateMixin, - OrderItemRemoveMixin, -) - from answerking_app.models.models import Order from answerking_app.models.serializers import OrderSerializer from answerking_app.utils.ErrorType import ErrorMessage +from answerking_app.utils.mixins.OrderItemMixins import (OrderItemRemoveMixin, + OrderItemUpdateMixin) +from answerking_app.utils.mixins.SerializeErrorDetailRFCMixins import ( + CreateErrorDetailMixin, UpdateErrorDetailMixin) class OrderListView( - mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView + mixins.ListModelMixin, + CreateErrorDetailMixin, + mixins.CreateModelMixin, + generics.GenericAPIView, ): queryset: QuerySet = Order.objects.all() serializer_class: OrderSerializer = OrderSerializer @@ -32,6 +33,7 @@ def post(self, request: Request, *args, **kwargs) -> Response: class OrderDetailView( mixins.RetrieveModelMixin, + UpdateErrorDetailMixin, mixins.DestroyModelMixin, mixins.UpdateModelMixin, generics.GenericAPIView, @@ -45,19 +47,7 @@ def get(self, request: Request, *args, **kwargs) -> Response: return self.retrieve(request, *args, **kwargs) def put(self, request: Request, *args, **kwargs) -> Response: - try: - return self.partial_update(request, *args, **kwargs) - except (KeyError, ObjectDoesNotExist): - error_msg: ErrorMessage = { - "error": { - "message": "Request failed", - "details": "Object could not be updated", - } - } - return Response( - error_msg, - status=status.HTTP_400_BAD_REQUEST, - ) + return self.partial_update(request, *args, **kwargs) def delete(self, request: Request, *args, **kwargs) -> Response: return self.destroy(request, *args, **kwargs) From 415e1144f3a40430e7758e83987d2b2a4af38c68 Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Tue, 8 Nov 2022 16:41:08 +0000 Subject: [PATCH 10/23] Added detail handle for 404 errors --- .../utils/json404_middleware_config.py | 8 +++-- answerking_app/utils/mixins/ApiExceptions.py | 7 +++-- .../utils/mixins/NotFoundDetailMixins.py | 31 +++++++++++++++++++ answerking_app/views/category_views.py | 5 ++- answerking_app/views/item_views.py | 5 ++- answerking_app/views/order_views.py | 10 ++++-- 6 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 answerking_app/utils/mixins/NotFoundDetailMixins.py diff --git a/answerking_app/utils/json404_middleware_config.py b/answerking_app/utils/json404_middleware_config.py index 57547fa4..d5d5d119 100644 --- a/answerking_app/utils/json404_middleware_config.py +++ b/answerking_app/utils/json404_middleware_config.py @@ -1,13 +1,15 @@ +import uuid + from django.http import JsonResponse def json404_response(request): - data = { - "detail": "not found", - "title": "Not found.", + "detail": "Not Found", + "title": "Resource not found", "status": 404, "type": "{}://{}/problems/not_found/", + "traceId": uuid.uuid4(), } data["type"] = data["type"].format(request.scheme, request.get_host()) return JsonResponse( diff --git a/answerking_app/utils/mixins/ApiExceptions.py b/answerking_app/utils/mixins/ApiExceptions.py index d20ae5c4..14853224 100644 --- a/answerking_app/utils/mixins/ApiExceptions.py +++ b/answerking_app/utils/mixins/ApiExceptions.py @@ -1,3 +1,5 @@ +import uuid + from rest_framework.exceptions import APIException @@ -8,7 +10,7 @@ def __init__( detail: str | None = None, title: str | None = None, instance: str | None = None, - extensions: dict | None = None, + extensions: dict = {}, ): super().__init__() self.status_code = status @@ -18,5 +20,4 @@ def __init__( self.title = title if instance: self.instance = instance - if extensions: - self.extensions = extensions + self.extensions = extensions | {"traceId": uuid.uuid4()} diff --git a/answerking_app/utils/mixins/NotFoundDetailMixins.py b/answerking_app/utils/mixins/NotFoundDetailMixins.py new file mode 100644 index 00000000..ba418d5b --- /dev/null +++ b/answerking_app/utils/mixins/NotFoundDetailMixins.py @@ -0,0 +1,31 @@ +from django.core.exceptions import ObjectDoesNotExist +from django.http import Http404 +from rest_framework.mixins import RetrieveModelMixin +from rest_framework.request import Request +from rest_framework.response import Response + +from answerking_app.utils.mixins.ApiExceptions import HttpErrorResponse + + +def NotFoundErrorDetailed(): + return HttpErrorResponse( + status=404, + detail="Not Found", + title="Resource not found", + ) + + +class GetNotFoundDetailMixin(RetrieveModelMixin): + def retrieve(self, request: Request, *args, **kwargs) -> Response: + try: + return super().retrieve(request, *args, **kwargs) + except (ObjectDoesNotExist, Http404): + raise NotFoundErrorDetailed() + + +class UpdateNotFoundDetailMixin(RetrieveModelMixin): + def update(self, request: Request, *args, **kwargs) -> Response: + try: + return super().update(request, *args, **kwargs) + except (ObjectDoesNotExist, Http404): + raise NotFoundErrorDetailed() diff --git a/answerking_app/views/category_views.py b/answerking_app/views/category_views.py index d678b92b..bbd12052 100644 --- a/answerking_app/views/category_views.py +++ b/answerking_app/views/category_views.py @@ -10,6 +10,8 @@ CategoryItemRemoveMixin, CategoryItemUpdateMixin) from answerking_app.utils.mixins.IntegrityHandlerMixins import ( CreateIntegrityHandlerMixin, UpdateIntegrityHandlerMixin) +from answerking_app.utils.mixins.NotFoundDetailMixins import ( + GetNotFoundDetailMixin, UpdateNotFoundDetailMixin) from answerking_app.utils.mixins.RetireMixin import RetireMixin from answerking_app.utils.mixins.SerializeErrorDetailRFCMixins import ( CreateErrorDetailMixin, UpdateErrorDetailMixin) @@ -33,8 +35,9 @@ def post(self, request: Request, *args, **kwargs) -> Response: class CategoryDetailView( - mixins.RetrieveModelMixin, RetireMixin, + GetNotFoundDetailMixin, + UpdateNotFoundDetailMixin, UpdateIntegrityHandlerMixin, UpdateErrorDetailMixin, mixins.DestroyModelMixin, diff --git a/answerking_app/views/item_views.py b/answerking_app/views/item_views.py index e31077b2..f22c0724 100644 --- a/answerking_app/views/item_views.py +++ b/answerking_app/views/item_views.py @@ -8,6 +8,8 @@ from answerking_app.utils.mixins.IntegrityHandlerMixins import ( CreateIntegrityHandlerMixin, UpdateIntegrityHandlerMixin) from answerking_app.utils.mixins.ItemMixins import DestroyItemMixin +from answerking_app.utils.mixins.NotFoundDetailMixins import ( + GetNotFoundDetailMixin, UpdateNotFoundDetailMixin) from answerking_app.utils.mixins.RetireMixin import RetireMixin from answerking_app.utils.mixins.SerializeErrorDetailRFCMixins import ( CreateErrorDetailMixin, UpdateErrorDetailMixin) @@ -31,8 +33,9 @@ def post(self, request: Request, *args, **kwargs) -> Response: class ItemDetailView( - mixins.RetrieveModelMixin, RetireMixin, + GetNotFoundDetailMixin, + UpdateNotFoundDetailMixin, UpdateIntegrityHandlerMixin, UpdateErrorDetailMixin, DestroyItemMixin, diff --git a/answerking_app/views/order_views.py b/answerking_app/views/order_views.py index 91c583ff..37ccb688 100644 --- a/answerking_app/views/order_views.py +++ b/answerking_app/views/order_views.py @@ -9,6 +9,8 @@ from answerking_app.models.models import Order from answerking_app.models.serializers import OrderSerializer from answerking_app.utils.ErrorType import ErrorMessage +from answerking_app.utils.mixins.NotFoundDetailMixins import ( + GetNotFoundDetailMixin, UpdateNotFoundDetailMixin) from answerking_app.utils.mixins.OrderItemMixins import (OrderItemRemoveMixin, OrderItemUpdateMixin) from answerking_app.utils.mixins.SerializeErrorDetailRFCMixins import ( @@ -32,7 +34,8 @@ def post(self, request: Request, *args, **kwargs) -> Response: class OrderDetailView( - mixins.RetrieveModelMixin, + GetNotFoundDetailMixin, + UpdateNotFoundDetailMixin, UpdateErrorDetailMixin, mixins.DestroyModelMixin, mixins.UpdateModelMixin, @@ -54,7 +57,10 @@ def delete(self, request: Request, *args, **kwargs) -> Response: class OrderItemListView( - OrderItemUpdateMixin, OrderItemRemoveMixin, generics.GenericAPIView + UpdateNotFoundDetailMixin, + OrderItemUpdateMixin, + OrderItemRemoveMixin, + generics.GenericAPIView, ): serializer_class: OrderSerializer = OrderSerializer From 084190dba7f675bcd6deac67e1344ad8a43b1098 Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Tue, 8 Nov 2022 16:48:04 +0000 Subject: [PATCH 11/23] fixed error detail string --- answerking_app/utils/mixins/IntegrityHandlerMixins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/answerking_app/utils/mixins/IntegrityHandlerMixins.py b/answerking_app/utils/mixins/IntegrityHandlerMixins.py index e3440c17..44adeb21 100644 --- a/answerking_app/utils/mixins/IntegrityHandlerMixins.py +++ b/answerking_app/utils/mixins/IntegrityHandlerMixins.py @@ -28,6 +28,9 @@ def update(self, request: Request, *args, **kwargs) -> Response: def handle_IntegrityError(exc: IntegrityError) -> NoReturn: if exc.args[0] == DUP_ENTRY: - raise HttpErrorResponse(status=status.HTTP_400_BAD_REQUEST) + raise HttpErrorResponse( + status=status.HTTP_400_BAD_REQUEST, + detail="This name already exists", + ) else: raise HttpErrorResponse(status=status.HTTP_500_INTERNAL_SERVER_ERROR) From c2fc0f57e2bf92203fd330176ec7058f666f78ad Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Wed, 9 Nov 2022 17:29:47 +0000 Subject: [PATCH 12/23] all fixed tests and various bugs in the mixins --- answerking_app/tests/test_categories.py | 172 +++++++++----- answerking_app/tests/test_items.py | 219 ++++++++++++------ answerking_app/tests/test_orderlines.py | 119 ++++++++-- answerking_app/tests/test_orders.py | 116 ++++++---- .../utils/mixins/NotFoundDetailMixins.py | 32 ++- .../utils/mixins/OrderItemMixins.py | 5 +- .../mixins/SerializeErrorDetailRFCMixins.py | 27 ++- answerking_app/utils/model_types.py | 79 +++---- answerking_app/views/order_views.py | 5 +- 9 files changed, 501 insertions(+), 273 deletions(-) diff --git a/answerking_app/tests/test_categories.py b/answerking_app/tests/test_categories.py index 069485d2..791c01e4 100644 --- a/answerking_app/tests/test_categories.py +++ b/answerking_app/tests/test_categories.py @@ -1,17 +1,9 @@ from django.db.models.query import QuerySet from django.test import Client, TestCase, TransactionTestCase -from rest_framework.exceptions import ParseError from answerking_app.models.models import Category, Item -from answerking_app.utils.ErrorType import ErrorMessage -from answerking_app.utils.model_types import ( - CategoryType, - DetailError, - IDType, - ItemType, - NewCategoryName, - NewCategoryType, -) +from answerking_app.utils.model_types import (CategoryType, DetailError, + ItemType) client = Client() @@ -108,7 +100,7 @@ def test_get_all_with_categories_returns_ok(self): self.assertEqual(expected, actual) self.assertEqual(response.status_code, 200) - def test_get_id_valid_returns_ok(self): + def test_get_valid_id_returns_ok(self): # Arrange expected: CategoryType = { "id": self.test_cat_1.id, @@ -139,20 +131,25 @@ def test_get_id_valid_returns_ok(self): # Act response = client.get(f"/api/categories/{self.test_cat_1.id}") actual = response.json() - # Assert self.assertEqual(expected, actual) self.assertEqual(response.status_code, 200) - def test_get_id_invalid_returns_not_found(self): + def test_get_invalid_id_returns_not_found(self): # Arrange - expected: DetailError = {"detail": "/api/categories/f not found"} + expected: DetailError = { + "detail": "Not Found", + "status": 404, + "title": "Resource not found", + "type": "http://testserver/problems/not_found/", + } # Act response = client.get("/api/categories/f") actual = response.json() # Assert + self.assertIsInstance(actual.pop("traceId"), str) self.assertEqual(expected, actual) self.assertEqual(response.status_code, 404) @@ -169,10 +166,13 @@ def test_post_valid_with_items_returns_ok(self): "calories": self.test_item_3.calories, "retired": False, } - post_data: NewCategoryType = {"name": "Vegetarian", "items": [item]} + post_data: CategoryType = {"name": "Vegetarian", "items": [item]} - expected_id: IDType = {"id": self.test_cat_2.id + 1} - expected: CategoryType = {**post_data, **expected_id, "retired": False} + expected: CategoryType = { + **post_data, + "id": self.test_cat_2.id + 1, + "retired": False, + } # Act response = client.post( @@ -196,9 +196,13 @@ def test_post_valid_with_items_returns_ok(self): def test_post_valid_without_items_returns_ok(self): # Arrange old_list = client.get("/api/categories").json() - post_data: NewCategoryType = {"name": "Gluten Free", "items": []} - expected_id: IDType = {"id": self.test_cat_2.id + 1} - expected: CategoryType = {**post_data, **expected_id, "retired": False} + post_data: CategoryType = {"name": "Gluten Free", "items": []} + + expected: CategoryType = { + **post_data, + "id": self.test_cat_2.id + 1, + "retired": False, + } # Act response = client.post( @@ -218,7 +222,13 @@ def test_post_valid_without_items_returns_ok(self): def test_post_invalid_json_returns_bad_request(self): # Arrange invalid_json_data: str = '{"invalid": }' - expected_json_error: str = "JSON parse error -" + expected: DetailError = { + "detail": "Parsing JSON Error", + "errors": "JSON parse error - Expecting value: line 1 column 13 (char 12)", + "status": 400, + "title": "Invalid input json.", + "type": "http://testserver/problems/error/", + } # Act response = client.post( @@ -229,17 +239,23 @@ def test_post_invalid_json_returns_bad_request(self): actual = response.json() # Assert - self.assertRaises(ParseError) - self.assertIn(expected_json_error, actual["detail"]) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_post_invalid_details_returns_bad_request(self): # Arrange - invalid_post_data: NewCategoryType = { + invalid_post_data: CategoryType = { "name": "Vegetarian%", "items": [], } - expected_failure_error: dict = {"name": ["Enter a valid value."]} + expected: DetailError = { + "detail": "Validation Error", + "errors": {"name": ["Enter a valid value."]}, + "status": 400, + "title": "Invalid input.", + "type": "http://testserver/problems/error/", + } # Act response = client.post( @@ -250,7 +266,8 @@ def test_post_invalid_details_returns_bad_request(self): actual = response.json() # Assert - self.assertEqual(expected_failure_error, actual) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_put_valid_without_items_returns_no_items(self): @@ -259,15 +276,14 @@ def test_put_valid_without_items_returns_no_items(self): f"/api/categories/{self.test_cat_1.id}" ).json() - post_data: NewCategoryName = {"name": "New Name"} - expected_id: IDType = {"id": self.test_cat_1.id} + post_data: CategoryType = {"name": "New Name"} + expected: CategoryType = { **post_data, - **expected_id, + "id": self.test_cat_1.id, "items": [], "retired": False, } - # Act response = client.put( f"/api/categories/{self.test_cat_1.id}", @@ -287,22 +303,34 @@ def test_put_valid_without_items_returns_no_items(self): self.assertEqual(expected, actual) self.assertEqual(response.status_code, 200) - def test_put_invalid_id_returns_bad_request(self): + def test_put_invalid_id_returns_not_found(self): # Arrange - expected: DetailError = {"detail": "/api/categories/f not found"} + expected: DetailError = { + "detail": "Not Found", + "status": 404, + "title": "Resource not found", + "type": "http://testserver/problems/not_found/", + } # Act response = client.get("/api/categories/f") actual = response.json() # Assert + self.assertIsInstance(actual.pop("traceId"), str) self.assertEqual(expected, actual) self.assertEqual(response.status_code, 404) def test_put_invalid_json_returns_bad_request(self): # Arrange invalid_json_data: str = '{"invalid": }' - expected_json_error: str = "JSON parse error -" + expected: DetailError = { + "detail": "Parsing JSON Error", + "errors": "JSON parse error - Expecting value: line 1 column 13 (char 12)", + "status": 400, + "title": "Invalid input json.", + "type": "http://testserver/problems/error/", + } # Act response = client.put( @@ -313,17 +341,23 @@ def test_put_invalid_json_returns_bad_request(self): actual = response.json() # Assert - self.assertRaises(ParseError) - self.assertIn(expected_json_error, actual["detail"]) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_put_invalid_name_returns_bad_request(self): # Arrange - invalid_post_data: NewCategoryType = { + invalid_post_data: CategoryType = { "name": "New Name*", "items": [], } - expected_failure_error: dict = {"name": ["Enter a valid value."]} + expected: DetailError = { + "detail": "Validation Error", + "errors": {"name": ["Enter a valid value."]}, + "status": 400, + "title": "Invalid input.", + "type": "http://testserver/problems/error/", + } # Act response = client.put( @@ -334,10 +368,11 @@ def test_put_invalid_name_returns_bad_request(self): actual = response.json() # Assert - self.assertEqual(expected_failure_error, actual) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) - def test_put_invalid_item_returns_bad_request(self): + def test_put_not_existing_item_returns_not_found(self): # Arrange item: ItemType = { "id": -1, @@ -348,11 +383,17 @@ def test_put_invalid_item_returns_bad_request(self): "calories": self.test_item_1.calories, "retired": False, } - invalid_post_data: NewCategoryType = { + invalid_post_data: CategoryType = { "name": "New Name", "items": [item], } - expected_failure_error: dict = {"detail": "Not found."} + expected: DetailError = { + "detail": "Object was not Found", + "errors": ["Item matching query does not exist."], + "status": 404, + "title": "Resource not found", + "type": "http://testserver/problems/error/", + } # Act response = client.put( @@ -363,7 +404,8 @@ def test_put_invalid_item_returns_bad_request(self): actual = response.json() # Assert - self.assertEqual(expected_failure_error, actual) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 404) def test_put_wrong_item_except_id_returns_correct_item_details(self): @@ -377,7 +419,7 @@ def test_put_wrong_item_except_id_returns_correct_item_details(self): "calories": 666, "retired": False, } - invalid_post_data: NewCategoryType = { + invalid_post_data: CategoryType = { "name": "Burgers", "items": [item_with_wrong_info], } @@ -425,13 +467,18 @@ def test_delete_valid_returns_ok(self): def test_delete_invalid_id_returns_not_found(self): # Arrange - expected: DetailError = {"detail": "/api/categories/f not found"} - + expected: DetailError = { + "detail": "Not Found", + "status": 404, + "title": "Resource not found", + "type": "http://testserver/problems/not_found/", + } # Act response = client.delete("/api/categories/f") actual = response.json() # Assert + self.assertIsInstance(actual.pop("traceId"), str) self.assertEqual(expected, actual) self.assertEqual(response.status_code, 404) @@ -477,15 +524,17 @@ def test_put_add_duplicated_item_in_url_to_category_return_400(self): content_type="application/json", ) actual = response.json() - error_msg: ErrorMessage = { - "error": { - "message": "Resource update failure", - "details": "Item already in category", - } + expected: DetailError = { + "detail": "A server error occurred.", + "status": 400, + "title": "A server error occurred.", + "type": "http://testserver/problems/error/", } + # Assert + self.assertIsInstance(actual.pop("traceId"), str) self.assertEqual(response.status_code, 400) - self.assertEqual(actual, error_msg) + self.assertEqual(actual, expected) class CategoryTestsDB(TransactionTestCase): @@ -524,7 +573,7 @@ def tearDown(self): def test_post_duplicated_name_returns_400(self): # Arrange - post_data: NewCategoryType = {"name": "Vegan", "items": []} + post_data: CategoryType = {"name": "Vegan", "items": []} client.post( "/api/categories", post_data, content_type="application/json" ) @@ -534,16 +583,23 @@ def test_post_duplicated_name_returns_400(self): "/api/categories", post_data, content_type="application/json" ) - expected: DetailError = {"detail": "This name already exists"} + expected: DetailError = { + "detail": "This name already exists", + "status": 400, + "title": "A server error occurred.", + "type": "http://testserver/problems/error/", + } + actual = response.json() # Assert + self.assertIsInstance(actual.pop("traceId"), str) self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_put_duplicated_name_returns_400(self): # Arrange - post_data: NewCategoryType = {"name": "Vegan", "items": []} + post_data: CategoryType = {"name": "Vegan", "items": []} client.post( "/api/categories", post_data, content_type="application/json" @@ -556,8 +612,14 @@ def test_put_duplicated_name_returns_400(self): content_type="application/json", ) actual = response.json() - expected: DetailError = {"detail": "This name already exists"} + expected: DetailError = { + "detail": "This name already exists", + "status": 400, + "title": "A server error occurred.", + "type": "http://testserver/problems/error/", + } # Assert + self.assertIsInstance(actual.pop("traceId"), str) self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) diff --git a/answerking_app/tests/test_items.py b/answerking_app/tests/test_items.py index 6694be88..4729dfba 100644 --- a/answerking_app/tests/test_items.py +++ b/answerking_app/tests/test_items.py @@ -3,12 +3,7 @@ from rest_framework.exceptions import ParseError from answerking_app.models.models import Item -from answerking_app.utils.model_types import ( - DetailError, - IDType, - ItemType, - NewItemType, -) +from answerking_app.utils.model_types import DetailError, ItemType client = Client() @@ -97,30 +92,40 @@ def test_get_id_valid_returns_ok(self): self.assertEqual(expected, actual) self.assertEqual(response.status_code, 200) - def test_get_id_invalid_returns_not_found(self): + def test_get_invalid_id_returns_not_found(self): # Arrange - expected: DetailError = {"detail": "/api/items/f not found"} + expected: DetailError = { + "detail": "Not Found", + "status": 404, + "title": "Resource not found", + "type": "http://testserver/problems/not_found/", + } # Act response = client.get("/api/items/f") actual = response.json() # Assert + self.assertIsInstance(actual.pop("traceId"), str) self.assertEqual(expected, actual) self.assertEqual(response.status_code, 404) def test_post_valid_returns_ok(self): # Arrange old_list = client.get("/api/items").json() - post_data: NewItemType = { + post_data: ItemType = { "name": "Whopper", "price": "1.50", "description": "desc", "stock": 100, "calories": 100, } - expected_id: IDType = {"id": self.test_item_2.id + 1} - expected: ItemType = {**expected_id, **post_data, "retired": False} + + expected: ItemType = { + "id": self.test_item_2.id + 1, + **post_data, + "retired": False, + } # Act response = client.post( @@ -140,8 +145,12 @@ def test_post_valid_returns_ok(self): def test_post_invalid_json_returns_bad_request(self): # Arrange invalid_json_data: str = '{"invalid": }' - expected_json_error: DetailError = { - "detail": "JSON parse error - Expecting value: line 1 column 13 (char 12)" + expected: DetailError = { + "detail": "Parsing JSON Error", + "errors": "JSON parse error - Expecting value: line 1 column 13 (char 12)", + "status": 400, + "title": "Invalid input json.", + "type": "http://testserver/problems/error/", } # Act @@ -151,21 +160,25 @@ def test_post_invalid_json_returns_bad_request(self): actual = response.json() # Assert - self.assertRaises(ParseError) - self.assertEqual(expected_json_error, actual) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_post_invalid_name_returns_bad_request(self): # Arrange - invalid_post_data: NewItemType = { + invalid_post_data: ItemType = { "name": "Bad data£", "price": "1.50", "description": "desc", "stock": 100, "calories": 100, } - expected_failure_error: dict[str, list[str]] = { - "name": ["Enter a valid value."] + expected: DetailError = { + "detail": "Validation Error", + "errors": {"name": ["Enter a valid value."]}, + "status": 400, + "title": "Invalid input.", + "type": "http://testserver/problems/error/", } # Act @@ -175,20 +188,25 @@ def test_post_invalid_name_returns_bad_request(self): actual = response.json() # Assert - self.assertEqual(expected_failure_error, actual) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_post_invalid_price_returns_bad_request(self): # Arrange - invalid_post_data: NewItemType = { + invalid_post_data: ItemType = { "name": "Bad data", "price": "1.50f", "description": "desc", "stock": 100, "calories": 100, } - expected_failure_error: dict[str, list[str]] = { - "price": ["A valid number is required."] + expected: DetailError = { + "detail": "Validation Error", + "errors": {"price": ["A valid number is required."]}, + "status": 400, + "title": "Invalid input.", + "type": "http://testserver/problems/error/", } # Act @@ -198,20 +216,25 @@ def test_post_invalid_price_returns_bad_request(self): actual = response.json() # Assert - self.assertEqual(expected_failure_error, actual) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_post_invalid_description_returns_bad_request(self): # Arrange - invalid_post_data: NewItemType = { + invalid_post_data: ItemType = { "name": "Bad data", "price": "1.50", "description": "desc&", "stock": 100, "calories": 100, } - expected_failure_error: dict[str, list[str]] = { - "description": ["Enter a valid value."] + expected: DetailError = { + "detail": "Validation Error", + "errors": {"description": ["Enter a valid value."]}, + "status": 400, + "title": "Invalid input.", + "type": "http://testserver/problems/error/", } # Act @@ -221,20 +244,25 @@ def test_post_invalid_description_returns_bad_request(self): actual = response.json() # Assert - self.assertEqual(expected_failure_error, actual) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_post_invalid_stock_returns_bad_request(self): # Arrange - invalid_post_data: NewItemType = { + invalid_post_data: ItemType = { "name": "Bad data", "price": "1.50", "description": "desc", "stock": "f100", # type: ignore "calories": 100, } - expected_failure_error: dict[str, list[str]] = { - "stock": ["A valid integer is required."] + expected: DetailError = { + "detail": "Validation Error", + "errors": {"stock": ["A valid integer is required."]}, + "status": 400, + "title": "Invalid input.", + "type": "http://testserver/problems/error/", } # Act @@ -244,20 +272,27 @@ def test_post_invalid_stock_returns_bad_request(self): actual = response.json() # Assert - self.assertEqual(expected_failure_error, actual) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_post_negative_stock_returns_bad_request(self): # Arrange - invalid_post_data: NewItemType = { + invalid_post_data: ItemType = { "name": "Bad data", "price": "1.50", "description": "desc", "stock": -100, "calories": 100, } - expected_failure_error: dict[str, list[str]] = { - "stock": ["Ensure this value is greater than or equal to 0."] + expected: DetailError = { + "detail": "Validation Error", + "errors": { + "stock": ["Ensure this value is greater than or equal to 0."] + }, + "status": 400, + "title": "Invalid input.", + "type": "http://testserver/problems/error/", } # Act @@ -267,20 +302,25 @@ def test_post_negative_stock_returns_bad_request(self): actual = response.json() # Assert - self.assertEqual(expected_failure_error, actual) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_post_invalid_calories_returns_bad_request(self): # Arrange - invalid_post_data: NewItemType = { + invalid_post_data: ItemType = { "name": "Bad data", "price": "1.50", "description": "desc", "stock": 100, "calories": "100f", # type: ignore } - expected_failure_error: dict[str, list[str]] = { - "calories": ["A valid integer is required."] + expected: DetailError = { + "detail": "Validation Error", + "errors": {"calories": ["A valid integer is required."]}, + "status": 400, + "title": "Invalid input.", + "type": "http://testserver/problems/error/", } # Act @@ -290,20 +330,29 @@ def test_post_invalid_calories_returns_bad_request(self): actual = response.json() # Assert - self.assertEqual(expected_failure_error, actual) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_post_negative_calories_returns_bad_request(self): # Arrange - invalid_post_data: NewItemType = { + invalid_post_data: ItemType = { "name": "Bad data", "price": "1.50", "description": "desc", "stock": 100, "calories": -100, } - expected_failure_error: dict[str, list[str]] = { - "calories": ["Ensure this value is greater than or equal to 0."] + expected: DetailError = { + "detail": "Validation Error", + "errors": { + "calories": [ + "Ensure this value is greater than or equal to 0." + ] + }, + "status": 400, + "title": "Invalid input.", + "type": "http://testserver/problems/error/", } # Act @@ -313,21 +362,25 @@ def test_post_negative_calories_returns_bad_request(self): actual = response.json() # Assert - self.assertEqual(expected_failure_error, actual) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_put_valid_returns_ok(self): # Arrange old_item = client.get(f"/api/items/{self.test_item_1.id}").json() - post_data: NewItemType = { + post_data: ItemType = { "name": "New Burger", "price": "1.75", "description": "new desc", "stock": 0, "calories": 200, } - expected_id: IDType = {"id": self.test_item_1.id} - expected: ItemType = {**expected_id, **post_data, "retired": False} + expected: ItemType = { + "id": self.test_item_1.id, + **post_data, + "retired": False, + } # Act response = client.put( @@ -346,23 +399,15 @@ def test_put_valid_returns_ok(self): self.assertEqual(expected, actual) self.assertEqual(response.status_code, 200) - def test_put_invalid_id_returns_bad_request(self): - # Arrange - expected: DetailError = {"detail": "/api/items/f not found"} - - # Act - response = client.get("/api/items/f") - actual = response.json() - - # Assert - self.assertEqual(expected, actual) - self.assertEqual(response.status_code, 404) - def test_put_invalid_json_returns_bad_request(self): # Arrange invalid_json_data: str = '{"invalid": }' - expected_json_error: DetailError = { - "detail": "JSON parse error - Expecting value: line 1 column 13 (char 12)" + expected: DetailError = { + "detail": "Parsing JSON Error", + "errors": "JSON parse error - Expecting value: line 1 column 13 (char 12)", + "status": 400, + "title": "Invalid input json.", + "type": "http://testserver/problems/error/", } # Act @@ -374,22 +419,28 @@ def test_put_invalid_json_returns_bad_request(self): actual = response.json() # Assert - self.assertRaises(ParseError) - self.assertEqual(expected_json_error, actual) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_put_invalid_details_returns_bad_request(self): # Arrange - invalid_post_data: NewItemType = { + invalid_post_data: ItemType = { "name": "Bad data£", "price": "1.50", "description": "*", "stock": 100, "calories": 100, } - expected_failure_error: dict[str, list[str]] = { - "description": ["Enter a valid value."], - "name": ["Enter a valid value."], + expected: DetailError = { + "detail": "Validation Error", + "errors": { + "description": ["Enter a valid value."], + "name": ["Enter a valid value."], + }, + "status": 400, + "title": "Invalid input.", + "type": "http://testserver/problems/error/", } # Act @@ -401,7 +452,8 @@ def test_put_invalid_details_returns_bad_request(self): actual = response.json() # Assert - self.assertEqual(expected_failure_error, actual) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_delete_valid_returns_retired_true(self): @@ -430,13 +482,19 @@ def test_delete_valid_returns_retired_true(self): def test_delete_invalid_id_returns_not_found(self): # Arrange - expected: DetailError = {"detail": "/api/items/f not found"} + expected: DetailError = { + "detail": "Not Found", + "status": 404, + "title": "Resource not found", + "type": "http://testserver/problems/not_found/", + } # Act response = client.delete("/api/items/f") actual = response.json() # Assert + self.assertIsInstance(actual.pop("traceId"), str) self.assertEqual(expected, actual) self.assertEqual(response.status_code, 404) @@ -463,7 +521,7 @@ def tearDown(self): def test_post_duplicated_name_returns_400(self): # Arrange - post_data: NewItemType = { + post_data: ItemType = { "name": "Whopper", "price": "1.50", "description": "desc", @@ -477,17 +535,24 @@ def test_post_duplicated_name_returns_400(self): "/api/items", post_data, content_type="application/json" ) - expected: DetailError = {"detail": "This name already exists"} + expected: DetailError = { + "detail": "This name already exists", + "status": 400, + "title": "A server error occurred.", + "type": "http://testserver/problems/error/", + } + actual = response.json() # Assert + self.assertIsInstance(actual.pop("traceId"), str) self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) - def test_put_duplicated_name_returns_404(self): + def test_put_duplicated_name_returns_400(self): # Arrange old_item = client.get(f"/api/items/{self.test_item_1.id}").json() - post_data: NewItemType = { + post_data: ItemType = { "name": "New Burger", "price": "1.75", "description": "new desc", @@ -495,7 +560,7 @@ def test_put_duplicated_name_returns_404(self): "calories": 200, } - post_data_different_name: NewItemType = { + post_data_different_name: ItemType = { **post_data, "name": "Different Name", } @@ -514,8 +579,14 @@ def test_put_duplicated_name_returns_404(self): content_type="application/json", ) actual = response.json() - expected: DetailError = {"detail": "This name already exists"} + expected: DetailError = { + "detail": "This name already exists", + "status": 400, + "title": "A server error occurred.", + "type": "http://testserver/problems/error/", + } # Assert + self.assertIsInstance(actual.pop("traceId"), str) self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) diff --git a/answerking_app/tests/test_orderlines.py b/answerking_app/tests/test_orderlines.py index fe46c69b..5eb3cfb7 100644 --- a/answerking_app/tests/test_orderlines.py +++ b/answerking_app/tests/test_orderlines.py @@ -2,11 +2,7 @@ from answerking_app.models.models import Item, Order, Status from answerking_app.utils.ErrorType import ErrorMessage -from answerking_app.utils.model_types import ( - DetailError, - OrderItemQtyType, - OrderType, -) +from answerking_app.utils.model_types import DetailError, OrderType client = Client() @@ -82,7 +78,7 @@ def test_add_new_orderline_valid_returns_ok(self): ], "total": "6.50", } - post_data: OrderItemQtyType = {"quantity": 1} + post_data: OrderType = {"quantity": 1} # Act response = client.put( @@ -113,7 +109,7 @@ def test_update_existing_orderline_valid_returns_ok(self): ], "total": "2.50", } - post_data: OrderItemQtyType = {"quantity": 1} + post_data: OrderType = {"quantity": 1} # Act response = client.put( @@ -136,7 +132,7 @@ def test_update_existing_orderline_zero_quantity_returns_ok(self): "order_items": [], "total": "0.00", } - post_data: OrderItemQtyType = {"quantity": 0} + post_data: OrderType = {"quantity": 0} # Act response = client.put( @@ -152,10 +148,15 @@ def test_update_existing_orderline_zero_quantity_returns_ok(self): def test_update_existing_orderline_invalid_returns_bad_request(self): # Arrange - expected_failure_error: dict[str, list[str]] = { - "quantity": ["A valid integer is required."] + expected: DetailError = { + "detail": "Validation Error", + "errors": {"quantity": ["A valid integer is required."]}, + "status": 400, + "title": "Invalid input.", + "type": "http://testserver/problems/error/", } - post_data: OrderItemQtyType = {"quantity": "f"} # type: ignore + + post_data: OrderType = {"quantity": "f"} # type: ignore # Act response = client.put( @@ -166,15 +167,24 @@ def test_update_existing_orderline_invalid_returns_bad_request(self): actual = response.json() # Assert - self.assertEqual(expected_failure_error, actual) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_update_existing_orderline_negative_returns_bad_request(self): # Arrange - expected_failure_error: dict[str, list[str]] = { - "quantity": ["Ensure this value is greater than or equal to 0."] + expected: DetailError = { + "detail": "Validation Error", + "errors": { + "quantity": [ + "Ensure this value is greater than or equal to 0." + ] + }, + "status": 400, + "title": "Invalid input.", + "type": "http://testserver/problems/error/", } - post_data: OrderItemQtyType = {"quantity": -1} # type: ignore + post_data: OrderType = {"quantity": -1} # type: ignore # Act response = client.put( @@ -185,12 +195,18 @@ def test_update_existing_orderline_negative_returns_bad_request(self): actual = response.json() # Assert - self.assertEqual(expected_failure_error, actual) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_nonexistant_orderid_returns_not_found(self): # Arrange - expected: DetailError = {"detail": "Not found."} + expected: DetailError = { + "detail": "Not Found", + "status": 404, + "title": "Resource not found", + "type": "http://testserver/problems/error/", + } # Act response = client.put( @@ -201,12 +217,18 @@ def test_nonexistant_orderid_returns_not_found(self): actual = response.json() # Assert + self.assertIsInstance(actual.pop("traceId"), str) self.assertEqual(expected, actual) self.assertEqual(response.status_code, 404) def test_nonexistant_itemid_returns_not_found(self): # Arrange - expected: DetailError = {"detail": "Not found."} + expected: DetailError = { + "detail": "Not Found", + "status": 404, + "title": "Resource not found", + "type": "http://testserver/problems/error/", + } # Act response = client.put( @@ -217,13 +239,17 @@ def test_nonexistant_itemid_returns_not_found(self): actual = response.json() # Assert + self.assertIsInstance(actual.pop("traceId"), str) self.assertEqual(expected, actual) self.assertEqual(response.status_code, 404) def test_invalid_orderid_returns_not_found(self): # Arrange expected: DetailError = { - "detail": f"/api/orders/f/orderline/{self.test_item_2.id} not found" + "detail": "Not Found", + "status": 404, + "title": "Resource not found", + "type": "http://testserver/problems/not_found/", } # Act @@ -235,13 +261,17 @@ def test_invalid_orderid_returns_not_found(self): actual = response.json() # Assert + self.assertIsInstance(actual.pop("traceId"), str) self.assertEqual(expected, actual) self.assertEqual(response.status_code, 404) def test_invalid_itemid_returns_not_found(self): # Arrange expected: DetailError = { - "detail": f"/api/orders/{self.test_order_1.id}/orderline/f not found" + "detail": "Not Found", + "status": 404, + "title": "Resource not found", + "type": "http://testserver/problems/not_found/", } # Act @@ -253,6 +283,7 @@ def test_invalid_itemid_returns_not_found(self): actual = response.json() # Assert + self.assertIsInstance(actual.pop("traceId"), str) self.assertEqual(expected, actual) self.assertEqual(response.status_code, 404) @@ -278,11 +309,11 @@ def test_delete_valid_returns_ok(self): def test_delete_nonexistant_id_returns_not_found(self): # Arrange - expected: ErrorMessage = { - "error": { - "message": "Resource update failure", - "details": "Item not in order", - } + expected: DetailError = { + "detail": "A server error occurred.", + "status": 404, + "title": "A server error occurred.", + "type": "http://testserver/problems/error/", } # Act @@ -292,5 +323,43 @@ def test_delete_nonexistant_id_returns_not_found(self): actual = response.json() # Assert + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) + self.assertEqual(response.status_code, 404) + + def test_delete_invalid_id_returns_not_found(self): + # Arrange + expected: DetailError = { + "detail": "Not found.", + "status": 404, + "title": "Not found.", + "type": "http://testserver/problems/not_found/", + } + + # Act + response = client.delete( + f"/api/orders/{self.test_order_1.id}/orderline/100000" + ) + actual = response.json() + + # Assert + self.assertEqual(expected, actual) + self.assertEqual(response.status_code, 404) + + def test_delete_item_when_in_order_returns_bad_request(self): + # Arrange + expected: DetailError = { + "detail": "A server error occurred.", + "status": 400, + "title": "A server error occurred.", + "type": "http://testserver/problems/error/", + } + + # Act + response = client.delete(f"/api/items/{self.test_item_1.id}") + actual = response.json() + + # Assert + self.assertIsInstance(actual.pop("traceId"), str) self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) diff --git a/answerking_app/tests/test_orders.py b/answerking_app/tests/test_orders.py index 1c4c065d..4aa072d7 100644 --- a/answerking_app/tests/test_orders.py +++ b/answerking_app/tests/test_orders.py @@ -1,18 +1,11 @@ from django.db.models import QuerySet -from django.test import TestCase, Client +from django.test import Client, TestCase from rest_framework.exceptions import ParseError -from answerking_app.models.models import Item, Order, Status, OrderLine -from answerking_app.utils.model_types import ( - OrderType, - NewOrderAddressType, - OrderItemType, - NewOrderType, - UpdateOrderType, - NewStatusType, - DetailError, -) +from answerking_app.models.models import Item, Order, OrderLine, Status from answerking_app.utils.ErrorType import ErrorMessage +from answerking_app.utils.model_types import (DetailError, OrderItemType, + OrderType) client = Client() @@ -159,13 +152,19 @@ def test_get_id_valid_returns_ok(self): def test_get_id_invalid_returns_not_found(self): # Arrange - expected: DetailError = {"detail": "/api/orders/f not found"} + expected: DetailError = { + "detail": "Not Found", + "status": 404, + "title": "Resource not found", + "type": "http://testserver/problems/not_found/", + } # Act response = client.get("/api/orders/f") actual = response.json() # Assert + self.assertIsInstance(actual.pop("traceId"), str) self.assertEqual(expected, actual) self.assertEqual(response.status_code, 404) @@ -173,7 +172,7 @@ def test_post_valid_without_items_returns_ok(self): # Arrange old_list = client.get("/api/orders").json() - post_data: NewOrderAddressType = {"address": "test street 123"} + post_data: OrderType = {"address": "test street 123"} expected: OrderType = { "id": self.test_order_2.id + 1, "status": self.status_pending.status, @@ -242,7 +241,7 @@ def test_post_valid_with_items_returns_ok(self): "quantity": 1, "sub_total": f"{self.test_item_3.price:.2f}", } - post_data: NewOrderType = { + post_data: OrderType = { "address": "test street 123", "order_items": [order_item], } @@ -274,7 +273,13 @@ def test_post_valid_with_items_returns_ok(self): def test_post_invalid_json_returns_bad_request(self): # Arrange invalid_json_data: str = '{"invalid": }' - expected_json_error: str = "JSON parse error -" + expected: DetailError = { + "detail": "Parsing JSON Error", + "errors": "JSON parse error - Expecting value: line 1 column 13 (char 12)", + "status": 400, + "title": "Invalid input json.", + "type": "http://testserver/problems/error/", + } # Act response = client.post( @@ -283,15 +288,19 @@ def test_post_invalid_json_returns_bad_request(self): actual = response.json() # Assert - self.assertRaises(ParseError) - self.assertIn(expected_json_error, actual["detail"]) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_post_invalid_details_returns_bad_request(self): # Arrange - invalid_post_data: NewOrderAddressType = {"address": "test%"} - expected_failure_error: dict[str, list[str]] = { - "address": ["Enter a valid value."] + invalid_post_data: OrderType = {"address": "test%"} + expected: DetailError = { + "detail": "Validation Error", + "errors": {"address": ["Enter a valid value."]}, + "status": 400, + "title": "Invalid input.", + "type": "http://testserver/problems/error/", } # Act @@ -301,17 +310,24 @@ def test_post_invalid_details_returns_bad_request(self): actual = response.json() # Assert - self.assertEqual(expected_failure_error, actual) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_post_invalid_items_returns_bad_request(self): # Arrange - invalid_post_data: NewOrderType = { + invalid_post_data: OrderType = { "address": "test", "order_items": [{"values": "invalid"}], # type: ignore } - expected_failure_error: dict[str, list[dict[str, list[str]]]] = { - "order_items": [{"quantity": ["This field is required."]}] + expected: DetailError = { + "detail": "Validation Error", + "errors": { + "order_items": [{"quantity": ["This field is required."]}] + }, + "status": 400, + "title": "Invalid input.", + "type": "http://testserver/problems/error/", } # Act @@ -321,13 +337,14 @@ def test_post_invalid_items_returns_bad_request(self): actual = response.json() # Assert - self.assertEqual(expected_failure_error, actual) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) def test_put_valid_address_and_status_returns_ok(self): # Arrange old_order = client.get(f"/api/orders/{self.test_order_1.id}").json() - post_data: UpdateOrderType = { + post_data: OrderType = { "address": "test", "status": self.status_complete.status, } @@ -373,7 +390,7 @@ def test_put_valid_address_and_status_returns_ok(self): def test_put_valid_address_returns_ok(self): # Arrange old_order = client.get(f"/api/orders/{self.test_order_1.id}").json() - post_data: NewOrderAddressType = {"address": "test"} + post_data: OrderType = {"address": "test"} expected: OrderType = { "id": self.test_order_1.id, **post_data, @@ -417,7 +434,9 @@ def test_put_valid_address_returns_ok(self): def test_put_valid_status_returns_ok(self): # Arrange old_order = client.get(f"/api/orders/{self.test_order_1.id}").json() - post_data: NewStatusType = {"status": self.status_complete.status} + post_data: OrderType = { + "status": self.status_complete.status, + } expected: OrderType = { "id": self.test_order_1.id, "address": self.test_order_1.address, @@ -460,9 +479,13 @@ def test_put_valid_status_returns_ok(self): def test_put_invalid_address_returns_bad_request(self): # Arrange - invalid_post_data: NewOrderAddressType = {"address": "test&"} - expected_failure_error: dict[str, list[str]] = { - "address": ["Enter a valid value."] + invalid_post_data: OrderType = {"address": "test&"} + expected: DetailError = { + "detail": "Validation Error", + "errors": {"address": ["Enter a valid value."]}, + "status": 400, + "title": "Invalid input.", + "type": "http://testserver/problems/error/", } # Act @@ -474,20 +497,22 @@ def test_put_invalid_address_returns_bad_request(self): actual = response.json() # Assert - self.assertEqual(expected_failure_error, actual) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) - def test_put_invalid_status_returns_bad_request(self): + def test_put_invalid_status_returns_not_found(self): # Arrange - invalid_post_data: UpdateOrderType = { + invalid_post_data: OrderType = { "address": "test", "status": "invalid", } - expected_failure_error: ErrorMessage = { - "error": { - "message": "Request failed", - "details": "Object could not be updated", - } + expected: DetailError = { + "detail": "Object was not Found", + "errors": ["Status matching query does not exist."], + "status": 404, + "title": "Resource not found", + "type": "http://testserver/problems/error/", } # Act @@ -499,8 +524,9 @@ def test_put_invalid_status_returns_bad_request(self): actual = response.json() # Assert - self.assertEqual(expected_failure_error, actual) - self.assertEqual(response.status_code, 400) + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) + self.assertEqual(response.status_code, 404) def test_delete_valid_returns_ok(self): # Arrange @@ -516,12 +542,18 @@ def test_delete_valid_returns_ok(self): def test_delete_invalid_id_returns_not_found(self): # Arrange - expected: DetailError = {"detail": "/api/orders/f not found"} + expected: DetailError = { + "detail": "Not Found", + "status": 404, + "title": "Resource not found", + "type": "http://testserver/problems/not_found/", + } # Act response = client.delete("/api/orders/f") actual = response.json() # Assert + self.assertIsInstance(actual.pop("traceId"), str) self.assertEqual(expected, actual) self.assertEqual(response.status_code, 404) diff --git a/answerking_app/utils/mixins/NotFoundDetailMixins.py b/answerking_app/utils/mixins/NotFoundDetailMixins.py index ba418d5b..70bf7ceb 100644 --- a/answerking_app/utils/mixins/NotFoundDetailMixins.py +++ b/answerking_app/utils/mixins/NotFoundDetailMixins.py @@ -1,31 +1,39 @@ from django.core.exceptions import ObjectDoesNotExist from django.http import Http404 -from rest_framework.mixins import RetrieveModelMixin +from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin from rest_framework.request import Request from rest_framework.response import Response from answerking_app.utils.mixins.ApiExceptions import HttpErrorResponse -def NotFoundErrorDetailed(): - return HttpErrorResponse( - status=404, - detail="Not Found", - title="Resource not found", - ) +def NotFoundErrorDetailed(exc: Http404 | ObjectDoesNotExist): + if isinstance(exc, ObjectDoesNotExist): + return HttpErrorResponse( + status=404, + detail="Object was not Found", + title="Resource not found", + extensions={"errors": exc.args}, + ) + if isinstance(exc, Http404): + return HttpErrorResponse( + status=404, + detail="Not Found", + title="Resource not found", + ) class GetNotFoundDetailMixin(RetrieveModelMixin): def retrieve(self, request: Request, *args, **kwargs) -> Response: try: return super().retrieve(request, *args, **kwargs) - except (ObjectDoesNotExist, Http404): - raise NotFoundErrorDetailed() + except (ObjectDoesNotExist, Http404) as exc: + raise NotFoundErrorDetailed(exc) -class UpdateNotFoundDetailMixin(RetrieveModelMixin): +class UpdateNotFoundDetailMixin(UpdateModelMixin): def update(self, request: Request, *args, **kwargs) -> Response: try: return super().update(request, *args, **kwargs) - except (ObjectDoesNotExist, Http404): - raise NotFoundErrorDetailed() + except (ObjectDoesNotExist, Http404) as exc: + raise NotFoundErrorDetailed(exc) diff --git a/answerking_app/utils/mixins/OrderItemMixins.py b/answerking_app/utils/mixins/OrderItemMixins.py index 02699905..7c832e52 100644 --- a/answerking_app/utils/mixins/OrderItemMixins.py +++ b/answerking_app/utils/mixins/OrderItemMixins.py @@ -1,5 +1,6 @@ from django.shortcuts import get_object_or_404 from rest_framework import status +from rest_framework.mixins import UpdateModelMixin from rest_framework.request import Request from rest_framework.response import Response from rest_framework.utils.serializer_helpers import ReturnDict @@ -10,7 +11,7 @@ from answerking_app.utils.mixins.ApiExceptions import HttpErrorResponse -class OrderItemUpdateMixin: +class OrderItemUpdateMixin(UpdateModelMixin): def update( self, request: Request, order_id: int, item_id: int, *args, **kwargs ) -> Response | None: @@ -56,7 +57,7 @@ def remove( item: Item = get_object_or_404(Item, pk=item_id) if item not in order.order_items.all(): - raise HttpErrorResponse(status=status.HTTP_400_BAD_REQUEST) + raise HttpErrorResponse(status=status.HTTP_404_NOT_FOUND) updated_order: Order | None = self.remove_item(order, item) diff --git a/answerking_app/utils/mixins/SerializeErrorDetailRFCMixins.py b/answerking_app/utils/mixins/SerializeErrorDetailRFCMixins.py index d03b5191..c00c279b 100644 --- a/answerking_app/utils/mixins/SerializeErrorDetailRFCMixins.py +++ b/answerking_app/utils/mixins/SerializeErrorDetailRFCMixins.py @@ -1,3 +1,4 @@ +from rest_framework.exceptions import ParseError from rest_framework.mixins import CreateModelMixin, UpdateModelMixin from rest_framework.request import Request from rest_framework.response import Response @@ -6,20 +7,28 @@ from answerking_app.utils.mixins.ApiExceptions import HttpErrorResponse -def ValidationErrorDetailed(exc: ValidationError): - return HttpErrorResponse( - status=400, - detail="Validation Error", - title="Invalid input.", - extensions={"errors": exc.detail}, - ) +def ValidationErrorDetailed(exc: ValidationError | ParseError): + if isinstance(exc, ValidationError): + return HttpErrorResponse( + status=400, + detail="Validation Error", + title="Invalid input.", + extensions={"errors": exc.detail}, + ) + elif isinstance(exc, ParseError): + return HttpErrorResponse( + status=400, + detail="Parsing JSON Error", + title="Invalid input json.", + extensions={"errors": exc.detail}, + ) class CreateErrorDetailMixin(CreateModelMixin): def create(self, request: Request, *args, **kwargs) -> Response: try: return super().create(request, *args, **kwargs) - except ValidationError as exc: + except (ValidationError, ParseError) as exc: raise ValidationErrorDetailed(exc) @@ -27,5 +36,5 @@ class UpdateErrorDetailMixin(UpdateModelMixin): def update(self, request: Request, *args, **kwargs) -> Response: try: return super().update(request, *args, **kwargs) - except ValidationError as exc: + except (ValidationError, ParseError) as exc: raise ValidationErrorDetailed(exc) diff --git a/answerking_app/utils/model_types.py b/answerking_app/utils/model_types.py index 62d82c3b..eefa0156 100644 --- a/answerking_app/utils/model_types.py +++ b/answerking_app/utils/model_types.py @@ -1,73 +1,46 @@ -from typing import TypedDict, Any +from typing import Any - -class IDType(TypedDict): - id: int +from typing_extensions import ( # for Python <3.11 with (Not)Required + NotRequired, TypedDict) -class NewItemType(TypedDict): +class ItemType(TypedDict): + id: NotRequired[int] name: str price: str description: str + retired: NotRequired[bool] stock: int calories: int -class ItemType(IDType, NewItemType): - retired: bool - - -class NewCategoryName(TypedDict): - name: str - - -class NewCategoryItems(TypedDict): - items: list[ItemType] - - -class NewCategoryType(NewCategoryName, NewCategoryItems): - pass - - -class CategoryType(IDType, NewCategoryType): - retired: bool +class CategoryType(TypedDict): + id: NotRequired[int] + name: NotRequired[str] + retired: NotRequired[bool] + items: "NotRequired[list[ItemType]]" class OrderItemType(TypedDict): id: int - name: str - price: str + name: NotRequired[str] + price: NotRequired[str] quantity: int - sub_total: str + sub_total: NotRequired[str] -class NewOrderAddressType(TypedDict): - address: str - - -class NewOrderType(NewOrderAddressType, TypedDict): - order_items: list[OrderItemType] - - -class NewStatusType(TypedDict): - status: str - - -class UpdateOrderType(NewOrderAddressType, NewStatusType): - pass - - -class OrderType(IDType, NewOrderType, NewStatusType, TypedDict): - total: str - - -class OrderItemQtyType(TypedDict): - quantity: int +class OrderType(TypedDict): + address: NotRequired[str] + order_items: "NotRequired[list[OrderItemType]]" + status: NotRequired[str] + total: NotRequired[str] class DetailError(TypedDict): - detail: str - - -class QuantityError(TypedDict): - quantity: list + detail: NotRequired[str] + type: str + title: str + instance: NotRequired[str] + errors: "NotRequired[str | list[Any] | dict[Any, Any]]" + status: NotRequired[int] + traceID: NotRequired[str] diff --git a/answerking_app/views/order_views.py b/answerking_app/views/order_views.py index 37ccb688..3f67c22a 100644 --- a/answerking_app/views/order_views.py +++ b/answerking_app/views/order_views.py @@ -38,7 +38,6 @@ class OrderDetailView( UpdateNotFoundDetailMixin, UpdateErrorDetailMixin, mixins.DestroyModelMixin, - mixins.UpdateModelMixin, generics.GenericAPIView, ): @@ -57,12 +56,16 @@ def delete(self, request: Request, *args, **kwargs) -> Response: class OrderItemListView( + GetNotFoundDetailMixin, + UpdateErrorDetailMixin, UpdateNotFoundDetailMixin, OrderItemUpdateMixin, OrderItemRemoveMixin, generics.GenericAPIView, ): serializer_class: OrderSerializer = OrderSerializer + serializer_class: OrderSerializer = OrderSerializer + lookup_url_kwarg: Literal["order_id"] = "order_id" def put( self, request: Request, order_id: int, item_id: int, *args, **kwargs From 47046dcb660c289024047a2d0651e5b02867426f Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Thu, 10 Nov 2022 11:19:19 +0000 Subject: [PATCH 13/23] formatting and typing --- answerking_app/models/serializers.py | 18 +++++------------- answerking_app/tests/test_categories.py | 1 - answerking_app/tests/test_orderlines.py | 13 ++++++++----- answerking_app/tests/test_orders.py | 9 ++++++--- .../utils/mixins/NotFoundDetailMixins.py | 9 ++++++--- answerking_app/utils/mixins/OrderItemMixins.py | 6 ++++-- .../mixins/SerializeErrorDetailRFCMixins.py | 7 +++++-- answerking_app/utils/model_types.py | 9 +++++---- answerking_app/views/item_views.py | 5 +++-- answerking_app/views/order_views.py | 14 ++++++++++---- 10 files changed, 52 insertions(+), 39 deletions(-) diff --git a/answerking_app/models/serializers.py b/answerking_app/models/serializers.py index da323518..c9f9599d 100644 --- a/answerking_app/models/serializers.py +++ b/answerking_app/models/serializers.py @@ -1,21 +1,13 @@ import re from typing import OrderedDict -from django.core.validators import ( - MaxValueValidator, - MinValueValidator, - RegexValidator, -) +from django.core.validators import (MaxValueValidator, MinValueValidator, + RegexValidator) from rest_framework import serializers from rest_framework.generics import get_object_or_404 -from answerking_app.models.models import ( - Category, - Item, - Order, - OrderLine, - Status, -) +from answerking_app.models.models import (Category, Item, Order, OrderLine, + Status) from answerking_app.utils.model_types import ItemType MAXNUMBERSIZE = 2147483647 @@ -151,7 +143,7 @@ def create(self, validated_data: dict) -> Order: ) for order_item in order_items_data: item_data: ItemType = order_item.pop("item") - item: Item = get_object_or_404(Item, pk=item_data["id"]) + item: Item = get_object_or_404(Item, pk=item_data["id"]) # type: ignore[reportTypedDictNotRequiredAccess] if item.retired: continue OrderLine.objects.create(order=order, item=item, **order_item) diff --git a/answerking_app/tests/test_categories.py b/answerking_app/tests/test_categories.py index 791c01e4..17e3bafb 100644 --- a/answerking_app/tests/test_categories.py +++ b/answerking_app/tests/test_categories.py @@ -51,7 +51,6 @@ def test_get_all_without_categories_returns_no_content(self): response = client.get("/api/categories") actual = response.json() - # Assert # Assert self.assertEqual(expected, actual) self.assertEqual(response.status_code, 200) diff --git a/answerking_app/tests/test_orderlines.py b/answerking_app/tests/test_orderlines.py index 5eb3cfb7..9525dc88 100644 --- a/answerking_app/tests/test_orderlines.py +++ b/answerking_app/tests/test_orderlines.py @@ -1,8 +1,11 @@ from django.test import Client, TestCase from answerking_app.models.models import Item, Order, Status -from answerking_app.utils.ErrorType import ErrorMessage -from answerking_app.utils.model_types import DetailError, OrderType +from answerking_app.utils.model_types import ( + DetailError, + OrderItemType, + OrderType, +) client = Client() @@ -78,7 +81,7 @@ def test_add_new_orderline_valid_returns_ok(self): ], "total": "6.50", } - post_data: OrderType = {"quantity": 1} + post_data: OrderItemType = {"quantity": 1} # Act response = client.put( @@ -109,7 +112,7 @@ def test_update_existing_orderline_valid_returns_ok(self): ], "total": "2.50", } - post_data: OrderType = {"quantity": 1} + post_data: OrderItemType = {"quantity": 1} # Act response = client.put( @@ -132,7 +135,7 @@ def test_update_existing_orderline_zero_quantity_returns_ok(self): "order_items": [], "total": "0.00", } - post_data: OrderType = {"quantity": 0} + post_data: OrderItemType = {"quantity": 0} # Act response = client.put( diff --git a/answerking_app/tests/test_orders.py b/answerking_app/tests/test_orders.py index 4aa072d7..8687a868 100644 --- a/answerking_app/tests/test_orders.py +++ b/answerking_app/tests/test_orders.py @@ -4,8 +4,11 @@ from answerking_app.models.models import Item, Order, OrderLine, Status from answerking_app.utils.ErrorType import ErrorMessage -from answerking_app.utils.model_types import (DetailError, OrderItemType, - OrderType) +from answerking_app.utils.model_types import ( + DetailError, + OrderItemType, + OrderType, +) client = Client() @@ -202,7 +205,7 @@ def test_post_valid_with_empty_items_returns_ok(self): # Arrange old_list = client.get("/api/orders").json() - post_data: NewOrderType = { + post_data: OrderType = { "address": "test street 123", "order_items": [], } diff --git a/answerking_app/utils/mixins/NotFoundDetailMixins.py b/answerking_app/utils/mixins/NotFoundDetailMixins.py index 70bf7ceb..66fa89fd 100644 --- a/answerking_app/utils/mixins/NotFoundDetailMixins.py +++ b/answerking_app/utils/mixins/NotFoundDetailMixins.py @@ -1,5 +1,6 @@ from django.core.exceptions import ObjectDoesNotExist from django.http import Http404 +from rest_framework import status from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin from rest_framework.request import Request from rest_framework.response import Response @@ -10,17 +11,19 @@ def NotFoundErrorDetailed(exc: Http404 | ObjectDoesNotExist): if isinstance(exc, ObjectDoesNotExist): return HttpErrorResponse( - status=404, + status=status.HTTP_404_NOT_FOUND, detail="Object was not Found", title="Resource not found", extensions={"errors": exc.args}, ) - if isinstance(exc, Http404): + elif isinstance(exc, Http404): return HttpErrorResponse( - status=404, + status=status.HTTP_404_NOT_FOUND, detail="Not Found", title="Resource not found", ) + else: + return HttpErrorResponse(status=status.HTTP_500_INTERNAL_SERVER_ERROR) class GetNotFoundDetailMixin(RetrieveModelMixin): diff --git a/answerking_app/utils/mixins/OrderItemMixins.py b/answerking_app/utils/mixins/OrderItemMixins.py index 7c832e52..dfcbac9f 100644 --- a/answerking_app/utils/mixins/OrderItemMixins.py +++ b/answerking_app/utils/mixins/OrderItemMixins.py @@ -6,8 +6,10 @@ from rest_framework.utils.serializer_helpers import ReturnDict from answerking_app.models.models import Item, Order -from answerking_app.models.serializers import (OrderLineSerializer, - OrderSerializer) +from answerking_app.models.serializers import ( + OrderLineSerializer, + OrderSerializer, +) from answerking_app.utils.mixins.ApiExceptions import HttpErrorResponse diff --git a/answerking_app/utils/mixins/SerializeErrorDetailRFCMixins.py b/answerking_app/utils/mixins/SerializeErrorDetailRFCMixins.py index c00c279b..c9ab5885 100644 --- a/answerking_app/utils/mixins/SerializeErrorDetailRFCMixins.py +++ b/answerking_app/utils/mixins/SerializeErrorDetailRFCMixins.py @@ -1,3 +1,4 @@ +from rest_framework import status from rest_framework.exceptions import ParseError from rest_framework.mixins import CreateModelMixin, UpdateModelMixin from rest_framework.request import Request @@ -10,18 +11,20 @@ def ValidationErrorDetailed(exc: ValidationError | ParseError): if isinstance(exc, ValidationError): return HttpErrorResponse( - status=400, + status=status.HTTP_404_NOT_FOUND, detail="Validation Error", title="Invalid input.", extensions={"errors": exc.detail}, ) elif isinstance(exc, ParseError): return HttpErrorResponse( - status=400, + status=status.HTTP_404_NOT_FOUND, detail="Parsing JSON Error", title="Invalid input json.", extensions={"errors": exc.detail}, ) + else: + return HttpErrorResponse(status=status.HTTP_500_INTERNAL_SERVER_ERROR) class CreateErrorDetailMixin(CreateModelMixin): diff --git a/answerking_app/utils/model_types.py b/answerking_app/utils/model_types.py index eefa0156..f4230916 100644 --- a/answerking_app/utils/model_types.py +++ b/answerking_app/utils/model_types.py @@ -18,11 +18,11 @@ class CategoryType(TypedDict): id: NotRequired[int] name: NotRequired[str] retired: NotRequired[bool] - items: "NotRequired[list[ItemType]]" + items: NotRequired["list[ItemType]"] class OrderItemType(TypedDict): - id: int + id: NotRequired[int] name: NotRequired[str] price: NotRequired[str] quantity: int @@ -30,8 +30,9 @@ class OrderItemType(TypedDict): class OrderType(TypedDict): + id: NotRequired[int] address: NotRequired[str] - order_items: "NotRequired[list[OrderItemType]]" + order_items: NotRequired["list[OrderItemType]"] status: NotRequired[str] total: NotRequired[str] @@ -41,6 +42,6 @@ class DetailError(TypedDict): type: str title: str instance: NotRequired[str] - errors: "NotRequired[str | list[Any] | dict[Any, Any]]" + errors: NotRequired["str | list[Any] | dict[Any, Any]"] status: NotRequired[int] traceID: NotRequired[str] diff --git a/answerking_app/views/item_views.py b/answerking_app/views/item_views.py index f22c0724..9d540250 100644 --- a/answerking_app/views/item_views.py +++ b/answerking_app/views/item_views.py @@ -11,8 +11,9 @@ from answerking_app.utils.mixins.NotFoundDetailMixins import ( GetNotFoundDetailMixin, UpdateNotFoundDetailMixin) from answerking_app.utils.mixins.RetireMixin import RetireMixin -from answerking_app.utils.mixins.SerializeErrorDetailRFCMixins import ( - CreateErrorDetailMixin, UpdateErrorDetailMixin) + + UpdateErrorDetailMixin, +) class ItemListView( diff --git a/answerking_app/views/order_views.py b/answerking_app/views/order_views.py index 3f67c22a..2826942c 100644 --- a/answerking_app/views/order_views.py +++ b/answerking_app/views/order_views.py @@ -10,11 +10,17 @@ from answerking_app.models.serializers import OrderSerializer from answerking_app.utils.ErrorType import ErrorMessage from answerking_app.utils.mixins.NotFoundDetailMixins import ( - GetNotFoundDetailMixin, UpdateNotFoundDetailMixin) -from answerking_app.utils.mixins.OrderItemMixins import (OrderItemRemoveMixin, - OrderItemUpdateMixin) + GetNotFoundDetailMixin, + UpdateNotFoundDetailMixin, +) +from answerking_app.utils.mixins.OrderItemMixins import ( + OrderItemRemoveMixin, + OrderItemUpdateMixin, +) from answerking_app.utils.mixins.SerializeErrorDetailRFCMixins import ( - CreateErrorDetailMixin, UpdateErrorDetailMixin) + CreateErrorDetailMixin, + UpdateErrorDetailMixin, +) class OrderListView( From 9d745ee248cf93af4e3875da1ad83b8964203677 Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Thu, 10 Nov 2022 12:05:35 +0000 Subject: [PATCH 14/23] fixed rebase error --- answerking_app/utils/mixins/IntegrityHandlerMixins.py | 4 ++-- answerking_app/views/item_views.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/answerking_app/utils/mixins/IntegrityHandlerMixins.py b/answerking_app/utils/mixins/IntegrityHandlerMixins.py index 44adeb21..a9443804 100644 --- a/answerking_app/utils/mixins/IntegrityHandlerMixins.py +++ b/answerking_app/utils/mixins/IntegrityHandlerMixins.py @@ -10,7 +10,7 @@ from answerking_app.utils.mixins.ApiExceptions import HttpErrorResponse -class CreateMixin(CreateModelMixin): +class CreateIntegrityHandlerMixin(CreateModelMixin): def create(self, request: Request, *args, **kwargs) -> Response: try: return super().create(request, *args, **kwargs) @@ -18,7 +18,7 @@ def create(self, request: Request, *args, **kwargs) -> Response: handle_IntegrityError(exc) -class UpdateMixin(UpdateModelMixin): +class UpdateIntegrityHandlerMixin(UpdateModelMixin): def update(self, request: Request, *args, **kwargs) -> Response: try: return super().update(request, *args, **kwargs) diff --git a/answerking_app/views/item_views.py b/answerking_app/views/item_views.py index 9d540250..f22c0724 100644 --- a/answerking_app/views/item_views.py +++ b/answerking_app/views/item_views.py @@ -11,9 +11,8 @@ from answerking_app.utils.mixins.NotFoundDetailMixins import ( GetNotFoundDetailMixin, UpdateNotFoundDetailMixin) from answerking_app.utils.mixins.RetireMixin import RetireMixin - - UpdateErrorDetailMixin, -) +from answerking_app.utils.mixins.SerializeErrorDetailRFCMixins import ( + CreateErrorDetailMixin, UpdateErrorDetailMixin) class ItemListView( From ff91a4fccc2afbe7b437ab57fa3581bad9b497f5 Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Thu, 10 Nov 2022 12:20:29 +0000 Subject: [PATCH 15/23] fixed rebase error --- answerking_app/tests/test_categories.py | 3 +- answerking_app/tests/test_orderlines.py | 44 +------------------ .../mixins/SerializeErrorDetailRFCMixins.py | 4 +- 3 files changed, 5 insertions(+), 46 deletions(-) diff --git a/answerking_app/tests/test_categories.py b/answerking_app/tests/test_categories.py index 17e3bafb..60de4833 100644 --- a/answerking_app/tests/test_categories.py +++ b/answerking_app/tests/test_categories.py @@ -387,8 +387,7 @@ def test_put_not_existing_item_returns_not_found(self): "items": [item], } expected: DetailError = { - "detail": "Object was not Found", - "errors": ["Item matching query does not exist."], + "detail": "Not Found", "status": 404, "title": "Resource not found", "type": "http://testserver/problems/error/", diff --git a/answerking_app/tests/test_orderlines.py b/answerking_app/tests/test_orderlines.py index 9525dc88..217d4410 100644 --- a/answerking_app/tests/test_orderlines.py +++ b/answerking_app/tests/test_orderlines.py @@ -1,11 +1,8 @@ from django.test import Client, TestCase from answerking_app.models.models import Item, Order, Status -from answerking_app.utils.model_types import ( - DetailError, - OrderItemType, - OrderType, -) +from answerking_app.utils.model_types import (DetailError, OrderItemType, + OrderType) client = Client() @@ -329,40 +326,3 @@ def test_delete_nonexistant_id_returns_not_found(self): self.assertIsInstance(actual.pop("traceId"), str) self.assertEqual(expected, actual) self.assertEqual(response.status_code, 404) - - def test_delete_invalid_id_returns_not_found(self): - # Arrange - expected: DetailError = { - "detail": "Not found.", - "status": 404, - "title": "Not found.", - "type": "http://testserver/problems/not_found/", - } - - # Act - response = client.delete( - f"/api/orders/{self.test_order_1.id}/orderline/100000" - ) - actual = response.json() - - # Assert - self.assertEqual(expected, actual) - self.assertEqual(response.status_code, 404) - - def test_delete_item_when_in_order_returns_bad_request(self): - # Arrange - expected: DetailError = { - "detail": "A server error occurred.", - "status": 400, - "title": "A server error occurred.", - "type": "http://testserver/problems/error/", - } - - # Act - response = client.delete(f"/api/items/{self.test_item_1.id}") - actual = response.json() - - # Assert - self.assertIsInstance(actual.pop("traceId"), str) - self.assertEqual(expected, actual) - self.assertEqual(response.status_code, 400) diff --git a/answerking_app/utils/mixins/SerializeErrorDetailRFCMixins.py b/answerking_app/utils/mixins/SerializeErrorDetailRFCMixins.py index c9ab5885..72363ed0 100644 --- a/answerking_app/utils/mixins/SerializeErrorDetailRFCMixins.py +++ b/answerking_app/utils/mixins/SerializeErrorDetailRFCMixins.py @@ -11,14 +11,14 @@ def ValidationErrorDetailed(exc: ValidationError | ParseError): if isinstance(exc, ValidationError): return HttpErrorResponse( - status=status.HTTP_404_NOT_FOUND, + status=status.HTTP_400_BAD_REQUEST, detail="Validation Error", title="Invalid input.", extensions={"errors": exc.detail}, ) elif isinstance(exc, ParseError): return HttpErrorResponse( - status=status.HTTP_404_NOT_FOUND, + status=status.HTTP_400_BAD_REQUEST, detail="Parsing JSON Error", title="Invalid input json.", extensions={"errors": exc.detail}, From bc6fb1e455779a2e53ec74bc47f8d117302ac36f Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Thu, 10 Nov 2022 12:21:03 +0000 Subject: [PATCH 16/23] formatting --- answerking_app/models/serializers.py | 16 ++++++++++++---- answerking_app/tests/test_categories.py | 7 +++++-- answerking_app/tests/test_orderlines.py | 7 +++++-- answerking_app/utils/model_types.py | 4 +++- answerking_app/views/category_views.py | 16 ++++++++++++---- answerking_app/views/item_views.py | 12 +++++++++--- 6 files changed, 46 insertions(+), 16 deletions(-) diff --git a/answerking_app/models/serializers.py b/answerking_app/models/serializers.py index c9f9599d..1edc1df8 100644 --- a/answerking_app/models/serializers.py +++ b/answerking_app/models/serializers.py @@ -1,13 +1,21 @@ import re from typing import OrderedDict -from django.core.validators import (MaxValueValidator, MinValueValidator, - RegexValidator) +from django.core.validators import ( + MaxValueValidator, + MinValueValidator, + RegexValidator, +) from rest_framework import serializers from rest_framework.generics import get_object_or_404 -from answerking_app.models.models import (Category, Item, Order, OrderLine, - Status) +from answerking_app.models.models import ( + Category, + Item, + Order, + OrderLine, + Status, +) from answerking_app.utils.model_types import ItemType MAXNUMBERSIZE = 2147483647 diff --git a/answerking_app/tests/test_categories.py b/answerking_app/tests/test_categories.py index 60de4833..444a6f37 100644 --- a/answerking_app/tests/test_categories.py +++ b/answerking_app/tests/test_categories.py @@ -2,8 +2,11 @@ from django.test import Client, TestCase, TransactionTestCase from answerking_app.models.models import Category, Item -from answerking_app.utils.model_types import (CategoryType, DetailError, - ItemType) +from answerking_app.utils.model_types import ( + CategoryType, + DetailError, + ItemType, +) client = Client() diff --git a/answerking_app/tests/test_orderlines.py b/answerking_app/tests/test_orderlines.py index 217d4410..f39f1d1a 100644 --- a/answerking_app/tests/test_orderlines.py +++ b/answerking_app/tests/test_orderlines.py @@ -1,8 +1,11 @@ from django.test import Client, TestCase from answerking_app.models.models import Item, Order, Status -from answerking_app.utils.model_types import (DetailError, OrderItemType, - OrderType) +from answerking_app.utils.model_types import ( + DetailError, + OrderItemType, + OrderType, +) client = Client() diff --git a/answerking_app/utils/model_types.py b/answerking_app/utils/model_types.py index f4230916..cf9b2028 100644 --- a/answerking_app/utils/model_types.py +++ b/answerking_app/utils/model_types.py @@ -1,7 +1,9 @@ from typing import Any from typing_extensions import ( # for Python <3.11 with (Not)Required - NotRequired, TypedDict) + NotRequired, + TypedDict, +) class ItemType(TypedDict): diff --git a/answerking_app/views/category_views.py b/answerking_app/views/category_views.py index bbd12052..22cb588c 100644 --- a/answerking_app/views/category_views.py +++ b/answerking_app/views/category_views.py @@ -7,14 +7,22 @@ from answerking_app.models.models import Category from answerking_app.models.serializers import CategorySerializer from answerking_app.utils.mixins.CategoryItemMixins import ( - CategoryItemRemoveMixin, CategoryItemUpdateMixin) + CategoryItemRemoveMixin, + CategoryItemUpdateMixin, +) from answerking_app.utils.mixins.IntegrityHandlerMixins import ( - CreateIntegrityHandlerMixin, UpdateIntegrityHandlerMixin) + CreateIntegrityHandlerMixin, + UpdateIntegrityHandlerMixin, +) from answerking_app.utils.mixins.NotFoundDetailMixins import ( - GetNotFoundDetailMixin, UpdateNotFoundDetailMixin) + GetNotFoundDetailMixin, + UpdateNotFoundDetailMixin, +) from answerking_app.utils.mixins.RetireMixin import RetireMixin from answerking_app.utils.mixins.SerializeErrorDetailRFCMixins import ( - CreateErrorDetailMixin, UpdateErrorDetailMixin) + CreateErrorDetailMixin, + UpdateErrorDetailMixin, +) class CategoryListView( diff --git a/answerking_app/views/item_views.py b/answerking_app/views/item_views.py index f22c0724..578f740d 100644 --- a/answerking_app/views/item_views.py +++ b/answerking_app/views/item_views.py @@ -6,13 +6,19 @@ from answerking_app.models.models import Item from answerking_app.models.serializers import ItemSerializer from answerking_app.utils.mixins.IntegrityHandlerMixins import ( - CreateIntegrityHandlerMixin, UpdateIntegrityHandlerMixin) + CreateIntegrityHandlerMixin, + UpdateIntegrityHandlerMixin, +) from answerking_app.utils.mixins.ItemMixins import DestroyItemMixin from answerking_app.utils.mixins.NotFoundDetailMixins import ( - GetNotFoundDetailMixin, UpdateNotFoundDetailMixin) + GetNotFoundDetailMixin, + UpdateNotFoundDetailMixin, +) from answerking_app.utils.mixins.RetireMixin import RetireMixin from answerking_app.utils.mixins.SerializeErrorDetailRFCMixins import ( - CreateErrorDetailMixin, UpdateErrorDetailMixin) + CreateErrorDetailMixin, + UpdateErrorDetailMixin, +) class ItemListView( From e83e1e1eede345d7c3755a8f977a250b12e6ba1d Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Thu, 10 Nov 2022 12:29:31 +0000 Subject: [PATCH 17/23] addes typing_extensions to dependencies --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 47cf51e4..579f1cd8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ drf-writable-nested = "^0.7.0" django-json-404-middleware = { git = "https://github.com/Axeltherabbit/django-json-404-middleware" } python-dotenv = "^0.21.0" drf-problems = { git = "https://github.com/Axeltherabbit/drf-problems" } +typing-extensions = "^4.4.0" [tool.poetry.group.dev.dependencies] black = "^22.10.0" From 8631f2f09291a8e46a3a36216d95d77e4ca9c080 Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Thu, 10 Nov 2022 12:45:38 +0000 Subject: [PATCH 18/23] removed unused import --- answerking_app/views/category_views.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/answerking_app/views/category_views.py b/answerking_app/views/category_views.py index 22cb588c..6fd9c951 100644 --- a/answerking_app/views/category_views.py +++ b/answerking_app/views/category_views.py @@ -1,5 +1,4 @@ from django.db.models import QuerySet -from drf_problems.mixins import AllowPermissionWithExceptionViewMixin from rest_framework import generics, mixins from rest_framework.request import Request from rest_framework.response import Response @@ -30,7 +29,6 @@ class CategoryListView( CreateIntegrityHandlerMixin, CreateErrorDetailMixin, generics.GenericAPIView, - AllowPermissionWithExceptionViewMixin, ): queryset: QuerySet = Category.objects.all() serializer_class: CategorySerializer = CategorySerializer @@ -50,7 +48,6 @@ class CategoryDetailView( UpdateErrorDetailMixin, mixins.DestroyModelMixin, generics.GenericAPIView, - AllowPermissionWithExceptionViewMixin, ): queryset: QuerySet = Category.objects.all() serializer_class: CategorySerializer = CategorySerializer @@ -69,7 +66,6 @@ class CategoryItemListView( CategoryItemUpdateMixin, CategoryItemRemoveMixin, generics.GenericAPIView, - AllowPermissionWithExceptionViewMixin, ): queryset: QuerySet = Category.objects.all() serializer_class: CategorySerializer = CategorySerializer From 26aee0f0f8aa77a45e3369708c020ba85580aa8f Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Thu, 10 Nov 2022 12:52:04 +0000 Subject: [PATCH 19/23] removed unused import --- answerking_app/tests/test_orders.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/answerking_app/tests/test_orders.py b/answerking_app/tests/test_orders.py index 8687a868..73642cbe 100644 --- a/answerking_app/tests/test_orders.py +++ b/answerking_app/tests/test_orders.py @@ -1,9 +1,7 @@ from django.db.models import QuerySet from django.test import Client, TestCase -from rest_framework.exceptions import ParseError from answerking_app.models.models import Item, Order, OrderLine, Status -from answerking_app.utils.ErrorType import ErrorMessage from answerking_app.utils.model_types import ( DetailError, OrderItemType, From fff5ca6f780804328a4ee2cf357c45f04e372b94 Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Thu, 10 Nov 2022 13:01:22 +0000 Subject: [PATCH 20/23] added specific type ignore --- answerking_app/tests/test_items.py | 4 ++-- answerking_app/tests/test_orderlines.py | 4 ++-- answerking_app/tests/test_orders.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/answerking_app/tests/test_items.py b/answerking_app/tests/test_items.py index 4729dfba..9cee0c2f 100644 --- a/answerking_app/tests/test_items.py +++ b/answerking_app/tests/test_items.py @@ -254,7 +254,7 @@ def test_post_invalid_stock_returns_bad_request(self): "name": "Bad data", "price": "1.50", "description": "desc", - "stock": "f100", # type: ignore + "stock": "f100", # type: ignore[reportGeneralTypeIssues] "calories": 100, } expected: DetailError = { @@ -313,7 +313,7 @@ def test_post_invalid_calories_returns_bad_request(self): "price": "1.50", "description": "desc", "stock": 100, - "calories": "100f", # type: ignore + "calories": "100f", # type: ignore[reportGeneralTypeIssues] } expected: DetailError = { "detail": "Validation Error", diff --git a/answerking_app/tests/test_orderlines.py b/answerking_app/tests/test_orderlines.py index f39f1d1a..3473ac10 100644 --- a/answerking_app/tests/test_orderlines.py +++ b/answerking_app/tests/test_orderlines.py @@ -159,7 +159,7 @@ def test_update_existing_orderline_invalid_returns_bad_request(self): "type": "http://testserver/problems/error/", } - post_data: OrderType = {"quantity": "f"} # type: ignore + post_data: OrderItemType = {"quantity": "f"} # type: ignore[reportGeneralTypeIssues] # Act response = client.put( @@ -187,7 +187,7 @@ def test_update_existing_orderline_negative_returns_bad_request(self): "title": "Invalid input.", "type": "http://testserver/problems/error/", } - post_data: OrderType = {"quantity": -1} # type: ignore + post_data: OrderItemType = {"quantity": -1} # Act response = client.put( diff --git a/answerking_app/tests/test_orders.py b/answerking_app/tests/test_orders.py index 73642cbe..c898f5ab 100644 --- a/answerking_app/tests/test_orders.py +++ b/answerking_app/tests/test_orders.py @@ -319,7 +319,7 @@ def test_post_invalid_items_returns_bad_request(self): # Arrange invalid_post_data: OrderType = { "address": "test", - "order_items": [{"values": "invalid"}], # type: ignore + "order_items": [{"values": "invalid"}], # type: ignore[reportGeneralTypeIssues] } expected: DetailError = { "detail": "Validation Error", From d347b9e15736fd419668231e79adc6a95b412cb7 Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Thu, 10 Nov 2022 15:04:52 +0000 Subject: [PATCH 21/23] added test with multiple errors --- answerking_app/tests/test_items.py | 31 ++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/answerking_app/tests/test_items.py b/answerking_app/tests/test_items.py index 9cee0c2f..28e5061d 100644 --- a/answerking_app/tests/test_items.py +++ b/answerking_app/tests/test_items.py @@ -248,6 +248,37 @@ def test_post_invalid_description_returns_bad_request(self): self.assertEqual(expected, actual) self.assertEqual(response.status_code, 400) + def test_post_invalid_stock_and_price_returns_bad_request(self): + # Arrange + invalid_post_data: ItemType = { + "name": "Bad data", + "price": "1.50asd", # type: ignore[reportGeneralTypeIssues] + "description": "desc", + "stock": "f100", # type: ignore[reportGeneralTypeIssues] + "calories": 100, + } + expected: DetailError = { + "detail": "Validation Error", + "errors": { + "stock": ["A valid integer is required."], + "price": ["A valid number is required."], + }, + "status": 400, + "title": "Invalid input.", + "type": "http://testserver/problems/error/", + } + + # Act + response = client.post( + "/api/items", invalid_post_data, content_type="application/json" + ) + actual = response.json() + + # Assert + self.assertIsInstance(actual.pop("traceId"), str) + self.assertEqual(expected, actual) + self.assertEqual(response.status_code, 400) + def test_post_invalid_stock_returns_bad_request(self): # Arrange invalid_post_data: ItemType = { From f977c8928585357910602062edea16e527c44813 Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Thu, 10 Nov 2022 15:06:23 +0000 Subject: [PATCH 22/23] removed duplicated line --- answerking_app/views/order_views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/answerking_app/views/order_views.py b/answerking_app/views/order_views.py index 2826942c..94afab4d 100644 --- a/answerking_app/views/order_views.py +++ b/answerking_app/views/order_views.py @@ -69,7 +69,6 @@ class OrderItemListView( OrderItemRemoveMixin, generics.GenericAPIView, ): - serializer_class: OrderSerializer = OrderSerializer serializer_class: OrderSerializer = OrderSerializer lookup_url_kwarg: Literal["order_id"] = "order_id" From 18f724c3b328f077caf3a83912b2cb58371689da Mon Sep 17 00:00:00 2001 From: pietro convalle Date: Fri, 11 Nov 2022 12:08:55 +0000 Subject: [PATCH 23/23] Added detail string for duplicated item in category error --- answerking_app/tests/test_categories.py | 2 +- answerking_app/utils/mixins/CategoryItemMixins.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/answerking_app/tests/test_categories.py b/answerking_app/tests/test_categories.py index 444a6f37..9e1bcf6c 100644 --- a/answerking_app/tests/test_categories.py +++ b/answerking_app/tests/test_categories.py @@ -526,7 +526,7 @@ def test_put_add_duplicated_item_in_url_to_category_return_400(self): ) actual = response.json() expected: DetailError = { - "detail": "A server error occurred.", + "detail": "Item is already in the category", "status": 400, "title": "A server error occurred.", "type": "http://testserver/problems/error/", diff --git a/answerking_app/utils/mixins/CategoryItemMixins.py b/answerking_app/utils/mixins/CategoryItemMixins.py index 7ff7daaf..0e36abb8 100644 --- a/answerking_app/utils/mixins/CategoryItemMixins.py +++ b/answerking_app/utils/mixins/CategoryItemMixins.py @@ -18,7 +18,10 @@ def update( item: Item = get_object_or_404(Item, pk=item_id) if item in category.items.all(): - raise HttpErrorResponse(status=status.HTTP_400_BAD_REQUEST) + raise HttpErrorResponse( + status=status.HTTP_400_BAD_REQUEST, + detail="Item is already in the category", + ) category.items.add(item) response: ReturnDict = CategorySerializer(category).data