Skip to content
This repository was archived by the owner on Nov 19, 2023. It is now read-only.

Commit 3d3b69b

Browse files
authored
Merge pull request #246 from idesoto-rover/fix-additional-properties-validation
#245 fix validation for additionalProperties
2 parents 2ff88cb + 394eeb0 commit 3d3b69b

File tree

3 files changed

+84
-20
lines changed

3 files changed

+84
-20
lines changed

CONTRIBUTING.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
This package is open to contributions. To contribute, please follow these steps:
44

55
1. Fork the upstream drf-openapi-tester repository into a personal account.
6-
2. Install [poetry](https://python-poetry.org/), and install dev dependencies using ``poetry install``
7-
3. Install [pre-commit](https://pre-commit.com/) (for project linting) by running ``pre-commit install``
6+
2. Install [poetry](https://python-poetry.org/), and install dev dependencies using `poetry install`
7+
3. Install [pre-commit](https://pre-commit.com/) (for project linting) by running `pre-commit install`
88
4. Create a new branch for your changes, and make sure to add tests!
9-
5. Push the topic branch to your personal fork
10-
6. Run "pre-commit run --all-files" locally to ensure proper linting
11-
6. Create a pull request to the drf-openapi-tester repository with an explanation of your changes
9+
5. Run `poetry run pytest` to ensure all tests are passing
10+
6. Run `pre-commit run --all-files` locally to ensure proper linting
11+
7. Push the topic branch to your personal fork
12+
8. Create a pull request to the drf-openapi-tester repository with an explanation of your changes

openapi_tester/schema_tester.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.core.exceptions import ImproperlyConfigured
77
from rest_framework.response import Response
88

9+
from openapi_tester import OpenAPISchemaError
910
from openapi_tester import type_declarations as td
1011
from openapi_tester.constants import (
1112
INIT_ERROR,
@@ -295,8 +296,9 @@ def test_openapi_object(
295296
required_keys = [key for key in schema_section.get("required", []) if key not in write_only_properties]
296297
response_keys = data.keys()
297298
additional_properties: Optional[Union[bool, dict]] = schema_section.get("additionalProperties")
298-
if not properties and isinstance(additional_properties, dict):
299-
properties = additional_properties
299+
additional_properties_allowed = additional_properties is not None
300+
if additional_properties_allowed and not isinstance(additional_properties, (bool, dict)):
301+
raise OpenAPISchemaError("Invalid additionalProperties type")
300302
for key in properties.keys():
301303
self.test_key_casing(key, case_tester, ignore_case)
302304
if key in required_keys and key not in response_keys:
@@ -307,9 +309,7 @@ def test_openapi_object(
307309
)
308310
for key in response_keys:
309311
self.test_key_casing(key, case_tester, ignore_case)
310-
key_in_additional_properties = isinstance(additional_properties, dict) and key in additional_properties
311-
additional_properties_allowed = additional_properties is True
312-
if key not in properties and not key_in_additional_properties and not additional_properties_allowed:
312+
if key not in properties and not additional_properties_allowed:
313313
raise DocumentationError(
314314
f"{VALIDATE_EXCESS_RESPONSE_KEY_ERROR.format(excess_key=key)}\n\nReference: {reference}.object:key:"
315315
f"{key}\n\nHint: Remove the key from your API response, or include it in your OpenAPI docs"
@@ -321,16 +321,22 @@ def test_openapi_object(
321321
f'"WriteOnly" restriction'
322322
)
323323
for key, value in data.items():
324-
if key not in properties and additional_properties_allowed:
325-
# Avoid KeyError below
326-
continue
327-
self.test_schema_section(
328-
schema_section=properties[key],
329-
data=value,
330-
reference=f"{reference}.object:key:{key}",
331-
case_tester=case_tester,
332-
ignore_case=ignore_case,
333-
)
324+
if key in properties:
325+
self.test_schema_section(
326+
schema_section=properties[key],
327+
data=value,
328+
reference=f"{reference}.object:key:{key}",
329+
case_tester=case_tester,
330+
ignore_case=ignore_case,
331+
)
332+
elif isinstance(additional_properties, dict):
333+
self.test_schema_section(
334+
schema_section=additional_properties,
335+
data=value,
336+
reference=f"{reference}.object:key:{key}",
337+
case_tester=case_tester,
338+
ignore_case=ignore_case,
339+
)
334340

335341
def test_openapi_array(self, schema_section: dict, data: dict, reference: str, **kwargs: Any) -> None:
336342
for datum in data:

tests/test_validators.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
VALIDATE_MINIMUM_ERROR,
1717
VALIDATE_MINIMUM_NUMBER_OF_PROPERTIES_ERROR,
1818
VALIDATE_MULTIPLE_OF_ERROR,
19+
VALIDATE_TYPE_ERROR,
1920
)
2021
from openapi_tester.validators import VALIDATOR_MAP
2122
from tests import (
@@ -141,12 +142,68 @@ def test_additional_properties_allowed():
141142
tester.test_schema_section(schema, {"oneKey": "test", "twoKey": "test2"})
142143

143144

145+
def test_additional_properties_specified_as_empty_object_allowed():
146+
schema = {"type": "object", "additionalProperties": {}, "properties": {"oneKey": {"type": "string"}}}
147+
tester.test_schema_section(schema, {"oneKey": "test", "twoKey": "test2"})
148+
149+
144150
def test_additional_properties_not_allowed_by_default():
145151
schema = {"type": "object", "properties": {"oneKey": {"type": "string"}}}
146152
with pytest.raises(DocumentationError, match=VALIDATE_EXCESS_RESPONSE_KEY_ERROR[:90]):
147153
tester.test_schema_section(schema, {"oneKey": "test", "twoKey": "test2"})
148154

149155

156+
def test_string_dictionary_specified_as_additional_properties_allowed():
157+
schema = {"type": "object", "additionalProperties": {"type": "string"}, "properties": {"key_1": {"type": "string"}}}
158+
tester.test_schema_section(schema, {"key_1": "value_1", "key_2": "value_2", "key_3": "value_3"})
159+
160+
161+
def test_string_dictionary_with_non_string_value_fails_validation():
162+
schema = {"type": "object", "additionalProperties": {"type": "string"}, "properties": {"key_1": {"type": "string"}}}
163+
expected_error_message = VALIDATE_TYPE_ERROR.format(article="a", type="string", received=123)
164+
with pytest.raises(DocumentationError, match=expected_error_message):
165+
tester.test_schema_section(schema, {"key_1": "value_1", "key_2": 123, "key_3": "value_3"})
166+
167+
168+
def test_object_dictionary_specified_as_additional_properties_allowed():
169+
schema = {
170+
"type": "object",
171+
"properties": {"key_1": {"type": "string"}},
172+
"additionalProperties": {
173+
"type": "object",
174+
"properties": {"key_2": {"type": "string"}, "key_3": {"type": "number"}},
175+
},
176+
}
177+
tester.test_schema_section(
178+
schema,
179+
{
180+
"key_1": "value_1",
181+
"some_extra_key": {"key_2": "value_2", "key_3": 123},
182+
"another_extra_key": {"key_2": "value_4", "key_3": 246},
183+
},
184+
)
185+
186+
187+
def test_additional_properties_schema_not_validated_in_main_properties():
188+
schema = {
189+
"type": "object",
190+
"properties": {"key_1": {"type": "string"}},
191+
"additionalProperties": {
192+
"type": "object",
193+
"properties": {"key_2": {"type": "string"}, "key_3": {"type": "number"}},
194+
},
195+
}
196+
expected_error_message = VALIDATE_TYPE_ERROR.format(article="an", type="object", received='"value_2"')
197+
with pytest.raises(DocumentationError, match=expected_error_message):
198+
tester.test_schema_section(schema, {"key_1": "value_1", "key_2": "value_2", "key_3": 123})
199+
200+
201+
def test_invalid_additional_properties_raises_schema_error():
202+
schema = {"type": "object", "properties": {"key_1": {"type": "string"}}, "additionalProperties": 123}
203+
with pytest.raises(OpenAPISchemaError, match="Invalid additionalProperties type"):
204+
tester.test_schema_section(schema, {"key_1": "value_1", "key_2": "value_2"})
205+
206+
150207
def test_pattern_validation():
151208
"""The a regex pattern can be passed to describe how a string should look"""
152209
schema = {"type": "string", "pattern": r"^\d{3}-\d{2}-\d{4}$"}

0 commit comments

Comments
 (0)