Update tests for passing empty str to Form

This commit is contained in:
Yurii Motov 2026-02-06 16:27:25 +01:00
parent cf0d31bd69
commit 2a2aafa01e
1 changed files with 176 additions and 54 deletions

View File

@ -1,5 +1,5 @@
from typing import Annotated, Any, Union from typing import Annotated, Any, Union
from unittest.mock import Mock, patch from unittest.mock import Mock, call, patch
import pytest import pytest
from dirty_equals import IsList, IsOneOf, IsPartialDict from dirty_equals import IsList, IsOneOf, IsPartialDict
@ -150,18 +150,55 @@ def test_nullable_required_missing(path: str):
pytest.param( pytest.param(
"/nullable-required", "/nullable-required",
marks=pytest.mark.xfail( marks=pytest.mark.xfail(
reason="Empty str is replaced with None, but then None gets dropped" reason="Empty str is replaced with None even for required parameters"
),
),
pytest.param(
"/model-nullable-required",
marks=pytest.mark.xfail(
reason="Empty strings are not replaced with None for models"
), ),
), ),
"/model-nullable-required",
], ],
) )
def test_nullable_required_pass_empty_str(path: str): def test_nullable_required_pass_empty_str_to_str_val(path: str):
client = TestClient(app)
with patch(f"{__name__}.convert", Mock(wraps=convert)) as mock_convert:
response = client.post(
path,
data={
"int_val": "0", # Empty string would cause validation error (see below)
"str_val": "",
"list_val": "0", # Empty string would cause validation error (see below)
},
)
assert mock_convert.call_count == 3, "Validator should be called for each field"
assert mock_convert.call_args_list == [
call("0"), # int_val
call(""), # str_val
call(["0"]), # list_val
]
assert response.status_code == 200, response.text
assert response.json() == {
"int_val": 0,
"str_val": "",
"list_val": [0],
"fields_set": IsOneOf(
None, IsList("int_val", "str_val", "list_val", check_order=False)
),
}
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/nullable-required",
marks=pytest.mark.xfail(
reason="Empty str is replaced with None even for required parameters"
),
),
"/model-nullable-required",
],
)
def test_nullable_required_pass_empty_str_to_int_val_and_list_val(path: str):
client = TestClient(app) client = TestClient(app)
with patch(f"{__name__}.convert", Mock(wraps=convert)) as mock_convert: with patch(f"{__name__}.convert", Mock(wraps=convert)) as mock_convert:
@ -170,26 +207,33 @@ def test_nullable_required_pass_empty_str(path: str):
data={ data={
"int_val": "", "int_val": "",
"str_val": "", "str_val": "",
"list_val": "0", # Empty strings are not treated as null for lists. It's Ok "list_val": "",
}, },
) )
assert mock_convert.call_count == 3, "Validator should be called for each field" assert mock_convert.call_count == 3, "Validator should be called for each field"
assert mock_convert.call_args_list == [ assert mock_convert.call_args_list == [
(""), # int_val call(""), # int_val
(""), # str_val call(""), # str_val
(["0"]), # list_val call([""]), # list_val
] ]
assert response.status_code == 200, response.text # pragma: no cover assert response.status_code == 422, response.text
assert response.json() == { # pragma: no cover assert response.json() == {
"int_val": None, "detail": [
"str_val": None, {
"list_val": [0], "input": "",
"fields_set": IsOneOf( "loc": ["body", "int_val"],
None, IsList("int_val", "str_val", "list_val", check_order=False) "msg": "Input should be a valid integer, unable to parse string as an integer",
), "type": "int_parsing",
},
{
"input": "",
"loc": ["body", "list_val", 0],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"type": "int_parsing",
},
]
} }
# TODO: Remove 'no cover' when the issue is fixed
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -336,21 +380,16 @@ def test_nullable_non_required_missing(path: str):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"path", "path",
[ [
pytest.param( "/nullable-non-required",
"/nullable-non-required",
marks=pytest.mark.xfail(
reason="Empty str is replaced with None, but then None gets dropped"
),
),
pytest.param( pytest.param(
"/model-nullable-non-required", "/model-nullable-non-required",
marks=pytest.mark.xfail( marks=pytest.mark.xfail(
reason="Empty strings are not replaced with None for models" reason="Empty strings are not replaced with None for parameters declared as model"
), ),
), ),
], ],
) )
def test_nullable_non_required_pass_empty_str(path: str): def test_nullable_non_required_pass_empty_str_to_str_val_and_int_val(path: str):
client = TestClient(app) client = TestClient(app)
with patch(f"{__name__}.convert", Mock(wraps=convert)) as mock_convert: with patch(f"{__name__}.convert", Mock(wraps=convert)) as mock_convert:
@ -359,26 +398,63 @@ def test_nullable_non_required_pass_empty_str(path: str):
data={ data={
"int_val": "", "int_val": "",
"str_val": "", "str_val": "",
"list_val": "0", # Empty strings are not treated as null for lists. It's Ok "list_val": "0", # Empty string would cause validation error (see below)
}, },
) )
assert mock_convert.call_count == 3, "Validator should be called for each field" assert mock_convert.call_count == 1, "Validator should be called for list_val only"
assert mock_convert.call_args_list == [ assert mock_convert.call_args_list == [
(""), # int_val call(["0"]), # list_val
(""), # str_val
(["0"]), # list_val
] ]
assert response.status_code == 200, response.text # pragma: no cover assert response.status_code == 200, response.text
assert response.json() == { # pragma: no cover assert response.json() == {
"int_val": None, "int_val": None,
"str_val": None, "str_val": None,
"list_val": [0], "list_val": [0],
"fields_set": IsOneOf( "fields_set": IsOneOf(None, IsList("list_val", check_order=False)),
None, IsList("int_val", "str_val", "list_val", check_order=False) }
),
@pytest.mark.parametrize(
"path",
[
"/nullable-non-required",
pytest.param(
"/model-nullable-non-required",
marks=pytest.mark.xfail(
reason="Empty strings are not replaced with None for parameters declared as model"
),
),
],
)
def test_nullable_non_required_pass_empty_str_to_all(path: str):
client = TestClient(app)
with patch(f"{__name__}.convert", Mock(wraps=convert)) as mock_convert:
response = client.post(
path,
data={
"int_val": "",
"str_val": "",
"list_val": "",
},
)
assert mock_convert.call_count == 1, "Validator should be called for list_val only"
assert mock_convert.call_args_list == [
call([""]), # list_val
]
assert response.status_code == 422, response.text
assert response.json() == {
"detail": [
{
"input": "",
"loc": ["body", "list_val", 0],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"type": "int_parsing",
},
]
} }
# TODO: Remove 'no cover' when the issue is fixed
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -540,18 +616,20 @@ def test_nullable_with_non_null_default_missing(path: str):
pytest.param( pytest.param(
"/nullable-with-non-null-default", "/nullable-with-non-null-default",
marks=pytest.mark.xfail( marks=pytest.mark.xfail(
reason="Empty str is replaced with default value, not with None" # Is this correct ??? reason="Empty strings are replaced with default values before validation"
), ),
), ),
pytest.param( pytest.param(
"/model-nullable-with-non-null-default", "/model-nullable-with-non-null-default",
marks=pytest.mark.xfail( marks=pytest.mark.xfail(
reason="Empty strings are not replaced with None for models" reason="Empty strings are not replaced with None for parameters declared as model"
), ),
), ),
], ],
) )
def test_nullable_with_non_null_default_pass_empty_str(path: str): def test_nullable_with_non_null_default_pass_empty_str_to_str_val_and_int_val(
path: str,
):
client = TestClient(app) client = TestClient(app)
with patch(f"{__name__}.convert", Mock(wraps=convert)) as mock_convert: with patch(f"{__name__}.convert", Mock(wraps=convert)) as mock_convert:
@ -560,24 +638,68 @@ def test_nullable_with_non_null_default_pass_empty_str(path: str):
data={ data={
"int_val": "", "int_val": "",
"str_val": "", "str_val": "",
"list_val": "0", # Empty strings are not treated as null for lists. It's Ok "list_val": "0", # Empty string would cause validation error (see below)
}, },
) )
assert mock_convert.call_count == 3, "Validator should be called for each field" assert mock_convert.call_count == 1, "Validator should be called for list_val only"
assert mock_convert.call_args_list == [ assert mock_convert.call_args_list == [ # pragma: no cover
(""), # int_val call(["0"]), # list_val
(""), # str_val
(["0"]), # list_val
] ]
assert response.status_code == 200, response.text # pragma: no cover assert response.status_code == 200, response.text # pragma: no cover
assert response.json() == { # pragma: no cover assert response.json() == { # pragma: no cover
"int_val": None, "int_val": -1,
"str_val": None, "str_val": "default",
"list_val": [0], "list_val": [0],
"fields_set": IsOneOf( "fields_set": IsOneOf(None, IsList("list_val", check_order=False)),
None, IsList("int_val", "str_val", "list_val", check_order=False) }
# TODO: Remove 'no cover' when the issue is fixed
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/nullable-with-non-null-default",
marks=pytest.mark.xfail(
reason="Empty strings are replaced with default values before validation"
),
), ),
pytest.param(
"/model-nullable-with-non-null-default",
marks=pytest.mark.xfail(
reason="Empty strings are not replaced with None for parameters declared as model"
),
),
],
)
def test_nullable_with_non_null_default_pass_empty_str_to_all(path: str):
client = TestClient(app)
with patch(f"{__name__}.convert", Mock(wraps=convert)) as mock_convert:
response = client.post(
path,
data={
"int_val": "",
"str_val": "",
"list_val": "",
},
)
assert mock_convert.call_count == 1, "Validator should be called for list_val only"
assert mock_convert.call_args_list == [ # pragma: no cover
call([""]), # list_val
]
assert response.status_code == 422, response.text # pragma: no cover
assert response.json() == { # pragma: no cover
"detail": [
{
"input": "",
"loc": ["body", "list_val", 0],
"msg": "Input should be a valid integer, unable to parse string as an integer",
"type": "int_parsing",
},
]
} }
# TODO: Remove 'no cover' when the issue is fixed # TODO: Remove 'no cover' when the issue is fixed