From 9f290e42370681a6d7c6b251b8117208a66efc47 Mon Sep 17 00:00:00 2001 From: Oliver Sauder Date: Tue, 16 Mar 2021 17:17:37 +0400 Subject: [PATCH] Convert parsers test to pytest style --- example/tests/test_parsers.py | 145 ---------------------------------- tests/conftest.py | 6 ++ tests/test_parsers.py | 130 ++++++++++++++++++++++++++++++ tests/test_relations.py | 8 +- tests/test_views.py | 117 +++++++++++++++++++-------- tests/views.py | 10 +++ 6 files changed, 234 insertions(+), 182 deletions(-) delete mode 100644 example/tests/test_parsers.py create mode 100644 tests/test_parsers.py create mode 100644 tests/views.py diff --git a/example/tests/test_parsers.py b/example/tests/test_parsers.py deleted file mode 100644 index b83f70a7..00000000 --- a/example/tests/test_parsers.py +++ /dev/null @@ -1,145 +0,0 @@ -import json -from io import BytesIO - -from django.test import TestCase, override_settings -from django.urls import path, reverse -from rest_framework import status, views -from rest_framework.exceptions import ParseError -from rest_framework.response import Response -from rest_framework.test import APITestCase - -from rest_framework_json_api import serializers -from rest_framework_json_api.parsers import JSONParser -from rest_framework_json_api.renderers import JSONRenderer - - -class TestJSONParser(TestCase): - def setUp(self): - class MockRequest(object): - def __init__(self): - self.method = "GET" - - request = MockRequest() - - self.parser_context = {"request": request, "kwargs": {}, "view": "BlogViewSet"} - - data = { - "data": { - "id": 123, - "type": "Blog", - "attributes": {"json-value": {"JsonKey": "JsonValue"}}, - }, - "meta": {"random_key": "random_value"}, - } - - self.string = json.dumps(data) - - @override_settings(JSON_API_FORMAT_FIELD_NAMES="dasherize") - def test_parse_include_metadata_format_field_names(self): - parser = JSONParser() - - stream = BytesIO(self.string.encode("utf-8")) - data = parser.parse(stream, None, self.parser_context) - - self.assertEqual(data["_meta"], {"random_key": "random_value"}) - self.assertEqual(data["json_value"], {"JsonKey": "JsonValue"}) - - def test_parse_invalid_data(self): - parser = JSONParser() - - string = json.dumps([]) - stream = BytesIO(string.encode("utf-8")) - - with self.assertRaises(ParseError): - parser.parse(stream, None, self.parser_context) - - def test_parse_invalid_data_key(self): - parser = JSONParser() - - string = json.dumps( - { - "data": [ - { - "id": 123, - "type": "Blog", - "attributes": {"json-value": {"JsonKey": "JsonValue"}}, - } - ] - } - ) - stream = BytesIO(string.encode("utf-8")) - - with self.assertRaises(ParseError): - parser.parse(stream, None, self.parser_context) - - -class DummyDTO: - def __init__(self, response_dict): - for k, v in response_dict.items(): - setattr(self, k, v) - - @property - def pk(self): - return self.id if hasattr(self, "id") else None - - -class DummySerializer(serializers.Serializer): - body = serializers.CharField() - id = serializers.IntegerField() - - -class DummyAPIView(views.APIView): - parser_classes = [JSONParser] - renderer_classes = [JSONRenderer] - resource_name = "dummy" - - def patch(self, request, *args, **kwargs): - serializer = DummySerializer(DummyDTO(request.data)) - return Response(status=status.HTTP_200_OK, data=serializer.data) - - -urlpatterns = [ - path("repeater", DummyAPIView.as_view(), name="repeater"), -] - - -class TestParserOnAPIView(APITestCase): - def setUp(self): - class MockRequest(object): - def __init__(self): - self.method = "PATCH" - - request = MockRequest() - # To be honest view string isn't resolved into actual view - self.parser_context = {"request": request, "kwargs": {}, "view": "DummyAPIView"} - - self.data = { - "data": { - "id": 123, - "type": "strs", - "attributes": {"body": "hello"}, - } - } - - self.string = json.dumps(self.data) - - def test_patch_doesnt_raise_attribute_error(self): - parser = JSONParser() - - stream = BytesIO(self.string.encode("utf-8")) - - data = parser.parse(stream, None, self.parser_context) - - assert data["id"] == 123 - assert data["body"] == "hello" - - @override_settings(ROOT_URLCONF=__name__) - def test_patch_request(self): - url = reverse("repeater") - data = self.data - data["data"]["type"] = "dummy" - response = self.client.patch(url, data=data) - data = response.json() - - assert data["data"]["id"] == str(123) - assert data["data"]["attributes"]["body"] == "hello" diff --git a/tests/conftest.py b/tests/conftest.py index 22be93a1..ebdf5348 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import pytest +from rest_framework.test import APIClient from tests.models import ( BasicModel, @@ -51,3 +52,8 @@ def many_to_many_targets(db): ManyToManyTarget.objects.create(name="Target1"), ManyToManyTarget.objects.create(name="Target2"), ] + + +@pytest.fixture +def client(): + return APIClient() diff --git a/tests/test_parsers.py b/tests/test_parsers.py new file mode 100644 index 00000000..907d1eb6 --- /dev/null +++ b/tests/test_parsers.py @@ -0,0 +1,130 @@ +import json +from io import BytesIO + +import pytest +from rest_framework.exceptions import ParseError + +from rest_framework_json_api.parsers import JSONParser +from rest_framework_json_api.utils import format_value +from tests.views import BasicModelViewSet + + +class TestJSONParser: + @pytest.fixture + def parser(self): + return JSONParser() + + @pytest.fixture + def parse(self, parser, parser_context): + def parse_wrapper(data): + stream = BytesIO(json.dumps(data).encode("utf-8")) + return parser.parse(stream, None, parser_context) + + return parse_wrapper + + @pytest.fixture + def parser_context(self, rf): + return {"request": rf.post("/"), "kwargs": {}, "view": BasicModelViewSet()} + + @pytest.mark.parametrize( + "format_field_names", + [ + None, + "dasherize", + "camelize", + "capitalize", + "underscore", + ], + ) + def test_parse_formats_field_names( + self, + settings, + format_field_names, + parse, + ): + settings.JSON_API_FORMAT_FIELD_NAMES = format_field_names + + data = { + "data": { + "id": "123", + "type": "BasicModel", + "attributes": { + format_value("test_attribute", format_field_names): "test-value" + }, + "relationships": { + format_value("test_relationship", format_field_names): { + "data": {"type": "TestRelationship", "id": "123"} + } + }, + } + } + + result = parse(data) + assert result == { + "id": "123", + "test_attribute": "test-value", + "test_relationship": {"id": "123", "type": "TestRelationship"}, + } + + def test_parse_extracts_meta(self, parse): + data = { + "data": { + "type": "BasicModel", + }, + "meta": {"random_key": "random_value"}, + } + + result = parse(data) + assert result["_meta"] == data["meta"] + + def test_parse_preserves_json_value_field_names(self, settings, parse): + settings.JSON_API_FORMAT_FIELD_NAMES = "dasherize" + + data = { + "data": { + "type": "BasicModel", + "attributes": {"json-value": {"JsonKey": "JsonValue"}}, + }, + } + + result = parse(data) + assert result["json_value"] == {"JsonKey": "JsonValue"} + + def test_parse_raises_error_on_empty_data(self, parse): + data = [] + + with pytest.raises(ParseError) as excinfo: + parse(data) + assert "Received document does not contain primary data" == str(excinfo.value) + + def test_parse_fails_on_list_of_objects(self, parse): + data = { + "data": [ + { + "type": "BasicModel", + "attributes": {"json-value": {"JsonKey": "JsonValue"}}, + } + ], + } + + with pytest.raises(ParseError) as excinfo: + parse(data) + + assert "Received data is not a valid JSONAPI Resource Identifier Object" == str( + excinfo.value + ) + + def test_parse_fails_when_id_is_missing_on_patch(self, rf, parse, parser_context): + parser_context["request"] = rf.patch("/") + data = { + "data": { + "type": "BasicModel", + }, + } + + with pytest.raises(ParseError) as excinfo: + parse(data) + + assert "The resource identifier object must contain an 'id' member" == str( + excinfo.value + ) diff --git a/tests/test_relations.py b/tests/test_relations.py index d66c602f..1baafdd0 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -11,13 +11,14 @@ SerializerMethodHyperlinkedRelatedField, ) from rest_framework_json_api.utils import format_link_segment -from rest_framework_json_api.views import ModelViewSet, RelationshipView +from rest_framework_json_api.views import RelationshipView from tests.models import BasicModel from tests.serializers import ( ForeignKeySourceSerializer, ManyToManySourceReadOnlySerializer, ManyToManySourceSerializer, ) +from tests.views import BasicModelViewSet @pytest.mark.django_db @@ -266,11 +267,6 @@ def test_get_links( # Routing setup -class BasicModelViewSet(ModelViewSet): - class Meta: - model = BasicModel - - class BasicModelRelationshipView(RelationshipView): queryset = BasicModel.objects diff --git a/tests/test_views.py b/tests/test_views.py index 8241a10e..e419ea0f 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,49 +1,104 @@ import pytest +from django.urls import path, reverse +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView -from rest_framework_json_api import serializers, views +from rest_framework_json_api import serializers +from rest_framework_json_api.parsers import JSONParser from rest_framework_json_api.relations import ResourceRelatedField +from rest_framework_json_api.renderers import JSONRenderer from rest_framework_json_api.utils import format_value +from rest_framework_json_api.views import ModelViewSet +from tests.models import BasicModel -from .models import BasicModel -related_model_field_name = "related_field_model" +class TestModelViewSet: + @pytest.mark.parametrize( + "format_links", + [ + None, + "dasherize", + "camelize", + "capitalize", + "underscore", + ], + ) + def test_get_related_field_name_handles_formatted_link_segments( + self, format_links, rf + ): + # use field name which actually gets formatted + related_model_field_name = "related_field_model" + class RelatedFieldNameSerializer(serializers.ModelSerializer): + related_model_field = ResourceRelatedField(queryset=BasicModel.objects) -@pytest.mark.parametrize( - "format_links", - [ - None, - "dasherize", - "camelize", - "capitalize", - "underscore", - ], -) -def test_get_related_field_name_handles_formatted_link_segments(format_links, rf): - url_segment = format_value(related_model_field_name, format_links) + def __init__(self, *args, **kwargs): + self.related_model_field.field_name = related_model_field_name + super().__init(*args, **kwargs) - request = rf.get(f"/basic_models/1/{url_segment}") + class Meta: + model = BasicModel - view = BasicModelFakeViewSet() - view.setup(request, related_field=url_segment) + class RelatedFieldNameView(ModelViewSet): + serializer_class = RelatedFieldNameSerializer - assert view.get_related_field_name() == related_model_field_name + url_segment = format_value(related_model_field_name, format_links) + request = rf.get(f"/basic_models/1/{url_segment}") -class BasicModelSerializer(serializers.ModelSerializer): - related_model_field = ResourceRelatedField(queryset=BasicModel.objects) + view = RelatedFieldNameView() + view.setup(request, related_field=url_segment) - def __init__(self, *args, **kwargs): - # Intentionally setting field_name property to something that matches no format - self.related_model_field.field_name = related_model_field_name - super(BasicModelSerializer, self).__init(*args, **kwargs) + assert view.get_related_field_name() == related_model_field_name - class Meta: - model = BasicModel +class TestAPIView: + @pytest.mark.urls(__name__) + def test_patch(self, client): + data = { + "data": { + "id": 123, + "type": "custom", + "attributes": {"body": "hello"}, + } + } -class BasicModelFakeViewSet(views.ModelViewSet): - serializer_class = BasicModelSerializer + url = reverse("custom") - def retrieve(self, request, *args, **kwargs): - pass + response = client.patch(url, data=data) + result = response.json() + + assert result["data"]["id"] == str(123) + assert result["data"]["type"] == "custom" + assert result["data"]["attributes"]["body"] == "hello" + + +class CustomModel: + def __init__(self, response_dict): + for k, v in response_dict.items(): + setattr(self, k, v) + + @property + def pk(self): + return self.id if hasattr(self, "id") else None + + +class CustomModelSerializer(serializers.Serializer): + body = serializers.CharField() + id = serializers.IntegerField() + + +class CustomAPIView(APIView): + parser_classes = [JSONParser] + renderer_classes = [JSONRenderer] + resource_name = "custom" + + def patch(self, request, *args, **kwargs): + serializer = CustomModelSerializer(CustomModel(request.data)) + return Response(status=status.HTTP_200_OK, data=serializer.data) + + +urlpatterns = [ + path("custom", CustomAPIView.as_view(), name="custom"), +] diff --git a/tests/views.py b/tests/views.py new file mode 100644 index 00000000..e7046a52 --- /dev/null +++ b/tests/views.py @@ -0,0 +1,10 @@ +from rest_framework_json_api.views import ModelViewSet +from tests.models import BasicModel +from tests.serializers import BasicModelSerializer + + +class BasicModelViewSet(ModelViewSet): + serializer_class = BasicModelSerializer + + class Meta: + model = BasicModel