Skip to content

Commit

Permalink
feat: adding django ninja support
Browse files Browse the repository at this point in the history
  • Loading branch information
Matias Cardenas authored and maticardenas committed Feb 19, 2024
1 parent 608f111 commit 7ee42ce
Show file tree
Hide file tree
Showing 11 changed files with 847 additions and 442 deletions.
7 changes: 3 additions & 4 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@ jobs:
path: |
.venv
~/.cache/pre-commit
key: venv-3 # increment to reset
key: ${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}-6
- run: |
python -m venv .venv --upgrade-deps
source .venv/bin/activate
pip install pre-commit
if: steps.cache.outputs.cache-hit != 'true'
- run: |
source .venv/bin/activate
pre-commit run --all-files
Expand All @@ -35,7 +34,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [ "3.7.14", "3.8.14" , "3.9.14", "3.10.7", "3.11.2" ]
python-version: [ "3.8.14" , "3.9.14", "3.10.7", "3.11.2" ]
django-version: [ "3.2", "4.0", "4.1" ]
exclude:
# Django v4 dropped 3.7 support
Expand All @@ -61,7 +60,7 @@ jobs:
python -m venv .venv
source .venv/bin/activate
pip install wheel setuptools pip -U
poetry install --no-interaction --no-root --extras drf-spectacular --extras drf-yasg
poetry install --no-interaction --no-root --extras drf-spectacular --extras drf-yasg --extras django-ninja
if: steps.cache-venv.outputs.cache-hit != 'true'
- run: |
source .venv/bin/activate
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

# Django Contract Tester

This is a test utility to validate DRF (Django REST Framework) test requests & responses against OpenAPI 2 and 3 schema.
This is a test utility to validate DRF (Django REST Framework) and Django Ninja test requests & responses against OpenAPI 2 and 3 schema.

It has built-in support for:

Expand Down
54 changes: 46 additions & 8 deletions openapi_tester/schema_tester.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
""" Schema Tester """
from __future__ import annotations

import json
import re
from dataclasses import dataclass
from itertools import chain
Expand All @@ -9,6 +10,7 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.validators import URLValidator
from rest_framework.response import Response

from openapi_tester.constants import (
INIT_ERROR,
Expand Down Expand Up @@ -48,7 +50,7 @@
if TYPE_CHECKING:
from typing import Optional

from rest_framework.response import Response
from django.http.response import HttpResponse


@dataclass
Expand All @@ -62,6 +64,41 @@ class OpenAPITestConfig:
http_message: str = "response"


class ResponseHandler:
"""
Response Handler: this class is used to handle the response data for both
DRF and Django HTTP (used in Django Ninja) responses.
"""

def __init__(self, response: Response | HttpResponse):
self._response = response

@property
def data(self) -> dict | None:
if isinstance(self._response, Response):
return self._drf_response_data()
return self._django_http_response_data()

@property
def request_data(self) -> dict | None:
if isinstance(self._response, Response):
return self._drf_request_data()
return self._django_http_request_data()

def _drf_request_data(self) -> dict | None:
return self._response.renderer_context["request"].data # type: ignore[union-attr]

def _drf_response_data(self) -> dict | None:
return self._response.json() if self._response.data is not None else None # type: ignore[union-attr]

def _django_http_request_data(self) -> dict | None:
response_body = self._response.wsgi_request.body # type: ignore[union-attr]
return json.loads(response_body) if response_body else None

def _django_http_response_data(self) -> dict | None:
return self._response.json() if self._response.content else None # type: ignore[attr-defined]


class SchemaTester:
"""Schema Tester: this is the base class of the library."""

Expand Down Expand Up @@ -139,7 +176,7 @@ def get_schema_type(schema: dict[str, str]) -> str | None:
return "object"
return None

def get_response_schema_section(self, response: Response) -> dict[str, Any]:
def get_response_schema_section(self, response: Response | HttpResponse) -> dict[str, Any]:
"""
Fetches the response section of a schema, wrt. the route, method, status code, and schema version.
Expand Down Expand Up @@ -200,7 +237,7 @@ def get_response_schema_section(self, response: Response) -> dict[str, Any]:
)
return self.get_key_value(json_object, "schema")

if response.data and response.json(): # type: ignore
if ResponseHandler(response).data:
raise UndocumentedSchemaSectionError(
UNDOCUMENTED_SCHEMA_SECTION_ERROR.format(
key="content",
Expand Down Expand Up @@ -361,7 +398,7 @@ def test_schema_section(
"""
test_config = test_config or OpenAPITestConfig()
if data is None:
if self.test_is_nullable(schema_section):
if self.test_is_nullable(schema_section) or not schema_section:
# If data is None and nullable, we return early
return
raise DocumentationError(
Expand Down Expand Up @@ -488,7 +525,7 @@ def test_openapi_array(self, schema_section: dict[str, Any], data: dict, test_co

def validate_request(
self,
response: Response,
response: Response | HttpResponse,
test_config: OpenAPITestConfig | None = None,
) -> None:
"""
Expand All @@ -509,16 +546,17 @@ def validate_request(
else:
test_config = OpenAPITestConfig(http_message="request")
request_body_schema = self.get_request_body_schema_section(response.request) # type: ignore

if request_body_schema:
self.test_schema_section(
schema_section=request_body_schema,
data=response.renderer_context["request"].data, # type: ignore
data=ResponseHandler(response).request_data,
test_config=test_config,
)

def validate_response(
self,
response: Response,
response: Response | HttpResponse,
test_config: OpenAPITestConfig | None = None,
) -> None:
"""
Expand All @@ -538,6 +576,6 @@ def validate_response(
response_schema = self.get_response_schema_section(response)
self.test_schema_section(
schema_section=response_schema,
data=response.json() if response.data is not None else {}, # type: ignore
data=ResponseHandler(response).data,
test_config=test_config,
)
Loading

0 comments on commit 7ee42ce

Please sign in to comment.