]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
✅ Add missing tests for code examples (#14569)
authorMotov Yurii <109919500+YuriiMotov@users.noreply.github.com>
Fri, 26 Dec 2025 10:43:02 +0000 (11:43 +0100)
committerGitHub <noreply@github.com>
Fri, 26 Dec 2025 10:43:02 +0000 (11:43 +0100)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Nils-Hero Lindemann <nilsherolindemann@proton.me>
183 files changed:
docs/de/docs/advanced/dataclasses.md
docs/de/docs/how-to/graphql.md
docs/en/docs/advanced/dataclasses.md
docs/en/docs/how-to/graphql.md
docs/es/docs/advanced/dataclasses.md
docs/es/docs/how-to/graphql.md
docs/pt/docs/advanced/dataclasses.md
docs/pt/docs/how-to/graphql.md
docs/ru/docs/advanced/dataclasses.md
docs/ru/docs/how-to/graphql.md
docs/zh/docs/advanced/dataclasses.md
docs_src/additional_responses/__init__.py [new file with mode: 0644]
docs_src/additional_status_codes/__init__.py [new file with mode: 0644]
docs_src/advanced_middleware/__init__.py [new file with mode: 0644]
docs_src/authentication_error_status_code/__init__.py [new file with mode: 0644]
docs_src/background_tasks/__init__.py [new file with mode: 0644]
docs_src/behind_a_proxy/__init__.py [new file with mode: 0644]
docs_src/body/__init__.py [new file with mode: 0644]
docs_src/body_fields/__init__.py [new file with mode: 0644]
docs_src/body_multiple_params/__init__.py [new file with mode: 0644]
docs_src/body_nested_models/__init__.py [new file with mode: 0644]
docs_src/body_updates/__init__.py [new file with mode: 0644]
docs_src/conditional_openapi/__init__.py [new file with mode: 0644]
docs_src/configure_swagger_ui/__init__.py [new file with mode: 0644]
docs_src/cookie_param_models/__init__.py [new file with mode: 0644]
docs_src/cookie_params/__init__.py [new file with mode: 0644]
docs_src/cors/__init__.py [new file with mode: 0644]
docs_src/custom_docs_ui/__init__.py [new file with mode: 0644]
docs_src/custom_request_and_route/__init__.py [new file with mode: 0644]
docs_src/custom_response/__init__.py [new file with mode: 0644]
docs_src/dataclasses_/__init__.py [new file with mode: 0644]
docs_src/dataclasses_/tutorial001_py310.py [moved from docs_src/dataclasses/tutorial001_py310.py with 100% similarity]
docs_src/dataclasses_/tutorial001_py39.py [moved from docs_src/dataclasses/tutorial001_py39.py with 100% similarity]
docs_src/dataclasses_/tutorial002_py310.py [moved from docs_src/dataclasses/tutorial002_py310.py with 100% similarity]
docs_src/dataclasses_/tutorial002_py39.py [moved from docs_src/dataclasses/tutorial002_py39.py with 100% similarity]
docs_src/dataclasses_/tutorial003_py310.py [moved from docs_src/dataclasses/tutorial003_py310.py with 100% similarity]
docs_src/dataclasses_/tutorial003_py39.py [moved from docs_src/dataclasses/tutorial003_py39.py with 100% similarity]
docs_src/debugging/__init__.py [new file with mode: 0644]
docs_src/dependencies/__init__.py [new file with mode: 0644]
docs_src/dependency_testing/__init__.py [new file with mode: 0644]
docs_src/encoder/__init__.py [new file with mode: 0644]
docs_src/events/__init__.py [new file with mode: 0644]
docs_src/extending_openapi/__init__.py [new file with mode: 0644]
docs_src/extra_data_types/__init__.py [new file with mode: 0644]
docs_src/extra_models/__init__.py [new file with mode: 0644]
docs_src/first_steps/__init__.py [new file with mode: 0644]
docs_src/generate_clients/__init__.py [new file with mode: 0644]
docs_src/graphql_/__init__.py [new file with mode: 0644]
docs_src/graphql_/tutorial001_py39.py [moved from docs_src/graphql/tutorial001_py39.py with 100% similarity]
docs_src/handling_errors/__init__.py [new file with mode: 0644]
docs_src/header_param_models/__init__.py [new file with mode: 0644]
docs_src/header_params/__init__.py [new file with mode: 0644]
docs_src/metadata/__init__.py [new file with mode: 0644]
docs_src/middleware/__init__.py [new file with mode: 0644]
docs_src/openapi_callbacks/__init__.py [new file with mode: 0644]
docs_src/openapi_webhooks/__init__.py [new file with mode: 0644]
docs_src/path_operation_advanced_configuration/__init__.py [new file with mode: 0644]
docs_src/path_operation_configuration/__init__.py [new file with mode: 0644]
docs_src/path_params/__init__.py [new file with mode: 0644]
docs_src/path_params_numeric_validations/__init__.py [new file with mode: 0644]
docs_src/pydantic_v1_in_v2/__init__.py [new file with mode: 0644]
docs_src/python_types/__init__.py [new file with mode: 0644]
docs_src/query_param_models/__init__.py [new file with mode: 0644]
docs_src/query_params/__init__.py [new file with mode: 0644]
docs_src/query_params_str_validations/__init__.py [new file with mode: 0644]
docs_src/request_files/__init__.py [new file with mode: 0644]
docs_src/request_form_models/__init__.py [new file with mode: 0644]
docs_src/request_forms/__init__.py [new file with mode: 0644]
docs_src/request_forms_and_files/__init__.py [new file with mode: 0644]
docs_src/response_change_status_code/__init__.py [new file with mode: 0644]
docs_src/response_cookies/__init__.py [new file with mode: 0644]
docs_src/response_directly/__init__.py [new file with mode: 0644]
docs_src/response_headers/__init__.py [new file with mode: 0644]
docs_src/response_model/__init__.py [new file with mode: 0644]
docs_src/response_status_code/__init__.py [new file with mode: 0644]
docs_src/schema_extra_example/__init__.py [new file with mode: 0644]
docs_src/security/__init__.py [new file with mode: 0644]
docs_src/separate_openapi_schemas/__init__.py [new file with mode: 0644]
docs_src/settings/__init__.py [new file with mode: 0644]
docs_src/static_files/__init__.py [new file with mode: 0644]
docs_src/sub_applications/__init__.py [new file with mode: 0644]
docs_src/templates/__init__.py [new file with mode: 0644]
docs_src/templates/static/__init__.py [new file with mode: 0644]
docs_src/templates/templates/__init__.py [new file with mode: 0644]
docs_src/using_request_directly/__init__.py [new file with mode: 0644]
docs_src/wsgi/__init__.py [new file with mode: 0644]
pyproject.toml
requirements-tests.txt
tests/test_tutorial/test_body/test_tutorial002.py [new file with mode: 0644]
tests/test_tutorial/test_body/test_tutorial003.py [new file with mode: 0644]
tests/test_tutorial/test_body/test_tutorial004.py [new file with mode: 0644]
tests/test_tutorial/test_body_multiple_params/test_tutorial002.py [new file with mode: 0644]
tests/test_tutorial/test_body_multiple_params/test_tutorial004.py [new file with mode: 0644]
tests/test_tutorial/test_body_multiple_params/test_tutorial005.py [new file with mode: 0644]
tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py [new file with mode: 0644]
tests/test_tutorial/test_body_nested_models/test_tutorial004.py [new file with mode: 0644]
tests/test_tutorial/test_body_nested_models/test_tutorial005.py [new file with mode: 0644]
tests/test_tutorial/test_body_nested_models/test_tutorial006.py [new file with mode: 0644]
tests/test_tutorial/test_body_nested_models/test_tutorial007.py [new file with mode: 0644]
tests/test_tutorial/test_body_nested_models/test_tutorial008.py [new file with mode: 0644]
tests/test_tutorial/test_body_updates/test_tutorial002.py [new file with mode: 0644]
tests/test_tutorial/test_custom_response/test_tutorial001.py
tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py [new file with mode: 0644]
tests/test_tutorial/test_dataclasses/test_tutorial001.py
tests/test_tutorial/test_dataclasses/test_tutorial002.py
tests/test_tutorial/test_dataclasses/test_tutorial003.py
tests/test_tutorial/test_debugging/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_debugging/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py [moved from tests/test_tutorial/test_dependencies/test_tutorial001.py with 86% similarity]
tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py [moved from tests/test_tutorial/test_dependencies/test_tutorial004.py with 89% similarity]
tests/test_tutorial/test_dependencies/test_tutorial005.py [new file with mode: 0644]
tests/test_tutorial/test_dependencies/test_tutorial007.py [new file with mode: 0644]
tests/test_tutorial/test_dependencies/test_tutorial008.py [new file with mode: 0644]
tests/test_tutorial/test_dependencies/test_tutorial010.py [new file with mode: 0644]
tests/test_tutorial/test_dependencies/test_tutorial011.py [new file with mode: 0644]
tests/test_tutorial/test_encoder/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_encoder/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py [new file with mode: 0644]
tests/test_tutorial/test_first_steps/test_tutorial001_tutorial002_tutorial003.py [moved from tests/test_tutorial/test_first_steps/test_tutorial001.py with 70% similarity]
tests/test_tutorial/test_generate_clients/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_generate_clients/test_tutorial002.py [new file with mode: 0644]
tests/test_tutorial/test_generate_clients/test_tutorial004.py [new file with mode: 0644]
tests/test_tutorial/test_graphql/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_graphql/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_metadata/test_tutorial002.py [moved from tests/test_tutorial/test_custom_response/test_tutorial004.py with 61% similarity]
tests/test_tutorial/test_metadata/test_tutorial003.py [new file with mode: 0644]
tests/test_tutorial/test_middleware/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_middleware/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py [new file with mode: 0644]
tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py [new file with mode: 0644]
tests/test_tutorial/test_path_params/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_path_params/test_tutorial002.py [new file with mode: 0644]
tests/test_tutorial/test_path_params/test_tutorial003.py [new file with mode: 0644]
tests/test_tutorial/test_path_params/test_tutorial003b.py [new file with mode: 0644]
tests/test_tutorial/test_path_params_numeric_validations/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_path_params_numeric_validations/test_tutorial002_tutorial003.py [new file with mode: 0644]
tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py [new file with mode: 0644]
tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py [new file with mode: 0644]
tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py [new file with mode: 0644]
tests/test_tutorial/test_python_types/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_python_types/test_tutorial001_tutorial002.py [new file with mode: 0644]
tests/test_tutorial/test_python_types/test_tutorial003.py [new file with mode: 0644]
tests/test_tutorial/test_python_types/test_tutorial004.py [new file with mode: 0644]
tests/test_tutorial/test_python_types/test_tutorial005.py [new file with mode: 0644]
tests/test_tutorial/test_python_types/test_tutorial006.py [new file with mode: 0644]
tests/test_tutorial/test_python_types/test_tutorial007.py [new file with mode: 0644]
tests/test_tutorial/test_python_types/test_tutorial008.py [new file with mode: 0644]
tests/test_tutorial/test_python_types/test_tutorial008b.py [new file with mode: 0644]
tests/test_tutorial/test_python_types/test_tutorial009_tutorial009b.py [new file with mode: 0644]
tests/test_tutorial/test_python_types/test_tutorial009c.py [new file with mode: 0644]
tests/test_tutorial/test_python_types/test_tutorial010.py [new file with mode: 0644]
tests/test_tutorial/test_python_types/test_tutorial011.py [new file with mode: 0644]
tests/test_tutorial/test_python_types/test_tutorial012.py [new file with mode: 0644]
tests/test_tutorial/test_python_types/test_tutorial013.py [new file with mode: 0644]
tests/test_tutorial/test_query_params/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_query_params/test_tutorial002.py [new file with mode: 0644]
tests/test_tutorial/test_query_params/test_tutorial003.py [new file with mode: 0644]
tests/test_tutorial/test_query_params/test_tutorial004.py [new file with mode: 0644]
tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py [new file with mode: 0644]
tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py [new file with mode: 0644]
tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py [new file with mode: 0644]
tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py [new file with mode: 0644]
tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py [new file with mode: 0644]
tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py [new file with mode: 0644]
tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py [new file with mode: 0644]
tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py [new file with mode: 0644]
tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py [new file with mode: 0644]
tests/test_tutorial/test_response_directly/test_tutorial002.py [new file with mode: 0644]
tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py [new file with mode: 0644]
tests/test_tutorial/test_response_model/test_tutorial002.py [new file with mode: 0644]
tests/test_tutorial/test_response_status_code/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py [new file with mode: 0644]
tests/test_tutorial/test_schema_extra_example/test_tutorial002.py [new file with mode: 0644]
tests/test_tutorial/test_schema_extra_example/test_tutorial003.py [new file with mode: 0644]
tests/test_tutorial/test_security/test_tutorial002.py [new file with mode: 0644]
tests/test_tutorial/test_security/test_tutorial004.py [new file with mode: 0644]
tests/test_tutorial/test_security/test_tutorial007.py [new file with mode: 0644]
tests/test_tutorial/test_settings/test_app01.py [new file with mode: 0644]
tests/test_tutorial/test_static_files/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_static_files/test_tutorial001.py [new file with mode: 0644]

index e2d59c776e89fee3c97ba7fd4caee028410e51a0..52b9634aeab3200c41e86d92330e4bde1a1a92c9 100644 (file)
@@ -4,7 +4,7 @@ FastAPI basiert auf **Pydantic**, und ich habe Ihnen gezeigt, wie Sie Pydantic-M
 
 Aber FastAPI unterstützt auf die gleiche Weise auch die Verwendung von <a href="https://docs.python.org/3/library/dataclasses.html" class="external-link" target="_blank">`dataclasses`</a>:
 
-{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *}
+{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *}
 
 Das ist dank **Pydantic** ebenfalls möglich, da es <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/#use-of-stdlib-dataclasses-with-basemodel" class="external-link" target="_blank">`dataclasses` intern unterstützt</a>.
 
@@ -32,7 +32,7 @@ Wenn Sie jedoch eine Menge Datenklassen herumliegen haben, ist dies ein guter Tr
 
 Sie können `dataclasses` auch im Parameter `response_model` verwenden:
 
-{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *}
+{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *}
 
 Die Datenklasse wird automatisch in eine Pydantic-Datenklasse konvertiert.
 
@@ -48,7 +48,7 @@ In einigen Fällen müssen Sie möglicherweise immer noch Pydantics Version von
 
 In diesem Fall können Sie einfach die Standard-`dataclasses` durch `pydantic.dataclasses` ersetzen, was einen direkten Ersatz darstellt:
 
-{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *}
+{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *}
 
 1. Wir importieren `field` weiterhin von Standard-`dataclasses`.
 
index 0583faf4a3682c2d48fab31224e78088b84b126d..5c908cec4a11462a86807b48db4ebae5ca40fd02 100644 (file)
@@ -35,7 +35,7 @@ Abhängig von Ihrem Anwendungsfall könnten Sie eine andere Bibliothek vorziehen
 
 Hier ist eine kleine Vorschau, wie Sie Strawberry mit FastAPI integrieren können:
 
-{* ../../docs_src/graphql/tutorial001_py39.py hl[3,22,25] *}
+{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *}
 
 Weitere Informationen zu Strawberry finden Sie in der <a href="https://strawberry.rocks/" class="external-link" target="_blank">Strawberry-Dokumentation</a>.
 
index 574beb65f46e5a95ffa85d06aa1df833c085a269..dbc91409a54e238550f15a2ac23c43797941835e 100644 (file)
@@ -4,7 +4,7 @@ FastAPI is built on top of **Pydantic**, and I have been showing you how to use
 
 But FastAPI also supports using <a href="https://docs.python.org/3/library/dataclasses.html" class="external-link" target="_blank">`dataclasses`</a> the same way:
 
-{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *}
+{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *}
 
 This is still supported thanks to **Pydantic**, as it has <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/#use-of-stdlib-dataclasses-with-basemodel" class="external-link" target="_blank">internal support for `dataclasses`</a>.
 
@@ -32,7 +32,7 @@ But if you have a bunch of dataclasses laying around, this is a nice trick to us
 
 You can also use `dataclasses` in the `response_model` parameter:
 
-{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *}
+{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *}
 
 The dataclass will be automatically converted to a Pydantic dataclass.
 
@@ -48,7 +48,7 @@ In some cases, you might still have to use Pydantic's version of `dataclasses`.
 
 In that case, you can simply swap the standard `dataclasses` with `pydantic.dataclasses`, which is a drop-in replacement:
 
-{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *}
+{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *}
 
 1. We still import `field` from standard `dataclasses`.
 
index a002c08ca3929f601738ba484216bbb5cc1c3a4d..666f819b0f0e364cfa6eb16f1a677b4a6fb1b1c8 100644 (file)
@@ -35,7 +35,7 @@ Depending on your use case, you might prefer to use a different library, but if
 
 Here's a small preview of how you could integrate Strawberry with FastAPI:
 
-{* ../../docs_src/graphql/tutorial001_py39.py hl[3,22,25] *}
+{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *}
 
 You can learn more about Strawberry in the <a href="https://strawberry.rocks/" class="external-link" target="_blank">Strawberry documentation</a>.
 
index 8d96171c7eb3e32fae421e04eeb94c826a0edc39..3a07482ad1596d5ac3429520db7be4238486a888 100644 (file)
@@ -4,7 +4,7 @@ FastAPI está construido sobre **Pydantic**, y te he estado mostrando cómo usar
 
 Pero FastAPI también soporta el uso de <a href="https://docs.python.org/3/library/dataclasses.html" class="external-link" target="_blank">`dataclasses`</a> de la misma manera:
 
-{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *}
+{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *}
 
 Esto sigue siendo soportado gracias a **Pydantic**, ya que tiene <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/#use-of-stdlib-dataclasses-with-basemodel" class="external-link" target="_blank">soporte interno para `dataclasses`</a>.
 
@@ -32,7 +32,7 @@ Pero si tienes un montón de dataclasses por ahí, este es un buen truco para us
 
 También puedes usar `dataclasses` en el parámetro `response_model`:
 
-{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *}
+{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *}
 
 El dataclass será automáticamente convertido a un dataclass de Pydantic.
 
@@ -48,7 +48,7 @@ En algunos casos, todavía podrías tener que usar la versión de `dataclasses`
 
 En ese caso, simplemente puedes intercambiar los `dataclasses` estándar con `pydantic.dataclasses`, que es un reemplazo directo:
 
-{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *}
+{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *}
 
 1. Todavía importamos `field` de los `dataclasses` estándar.
 
index 2ebfb3dd081f5063550ebccc283742915c98e71c..e50c1ae0acfc8e5a2563dd1c55afded8d41be70e 100644 (file)
@@ -35,7 +35,7 @@ Dependiendo de tu caso de uso, podrías preferir usar un paquete diferente, pero
 
 Aquí tienes una pequeña vista previa de cómo podrías integrar Strawberry con FastAPI:
 
-{* ../../docs_src/graphql/tutorial001_py39.py hl[3,22,25] *}
+{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *}
 
 Puedes aprender más sobre Strawberry en la <a href="https://strawberry.rocks/" class="external-link" target="_blank">documentación de Strawberry</a>.
 
index 6467376967eed2b104cf683b78289b3f49f869e7..6dc9feb299e74cb10f25278d8f99f5d1428b05af 100644 (file)
@@ -4,7 +4,7 @@ FastAPI é construído em cima do **Pydantic**, e eu tenho mostrado como usar mo
 
 Mas o FastAPI também suporta o uso de <a href="https://docs.python.org/3/library/dataclasses.html" class="external-link" target="_blank">`dataclasses`</a> da mesma forma:
 
-{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *}
+{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *}
 
 Isso ainda é suportado graças ao **Pydantic**, pois ele tem <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/#use-of-stdlib-dataclasses-with-basemodel" class="external-link" target="_blank">suporte interno para `dataclasses`</a>.
 
@@ -32,7 +32,7 @@ Mas se você tem um monte de dataclasses por aí, este é um truque legal para u
 
 Você também pode usar `dataclasses` no parâmetro `response_model`:
 
-{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *}
+{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *}
 
 A dataclass será automaticamente convertida para uma dataclass Pydantic.
 
@@ -48,7 +48,7 @@ Em alguns casos, você ainda pode ter que usar a versão do Pydantic das `datacl
 
 Nesse caso, você pode simplesmente trocar as `dataclasses` padrão por `pydantic.dataclasses`, que é um substituto direto:
 
-{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *}
+{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *}
 
 1. Ainda importamos `field` das `dataclasses` padrão.
 
index 7af4c6b7546752d4d58d75262687a1e28792cd81..98266cc2881c3dbcb15fd961c77a752c868630e6 100644 (file)
@@ -35,7 +35,7 @@ Dependendo do seu caso de uso, você pode preferir usar uma biblioteca diferente
 
 Aqui está uma pequena prévia de como você poderia integrar Strawberry com FastAPI:
 
-{* ../../docs_src/graphql/tutorial001_py39.py hl[3,22,25] *}
+{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *}
 
 Você pode aprender mais sobre Strawberry na <a href="https://strawberry.rocks/" class="external-link" target="_blank">documentação do Strawberry</a>.
 
index c37ce30236e5a789b814e80979d797f46a9ac586..b3ced37c1e48d029615de58bb0c51e7ee75bede7 100644 (file)
@@ -4,7 +4,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в
 
 Но FastAPI также поддерживает использование <a href="https://docs.python.org/3/library/dataclasses.html" class="external-link" target="_blank">`dataclasses`</a> тем же способом:
 
-{* ../../docs_src/dataclasses/tutorial001_py310.py hl[1,6:11,18:19] *}
+{* ../../docs_src/dataclasses_/tutorial001_py310.py hl[1,6:11,18:19] *}
 
 Это по-прежнему поддерживается благодаря **Pydantic**, так как в нём есть <a href="https://docs.pydantic.dev/latest/concepts/dataclasses/#use-of-stdlib-dataclasses-with-basemodel" class="external-link" target="_blank">встроенная поддержка `dataclasses`</a>.
 
@@ -32,7 +32,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в
 
 Вы также можете использовать `dataclasses` в параметре `response_model`:
 
-{* ../../docs_src/dataclasses/tutorial002_py310.py hl[1,6:12,18] *}
+{* ../../docs_src/dataclasses_/tutorial002_py310.py hl[1,6:12,18] *}
 
 Этот dataclass будет автоматически преобразован в Pydantic dataclass.
 
@@ -48,7 +48,7 @@ FastAPI построен поверх **Pydantic**, и я показывал в
 
 В таком случае вы можете просто заменить стандартные `dataclasses` на `pydantic.dataclasses`, которая является полностью совместимой заменой (drop-in replacement):
 
-{* ../../docs_src/dataclasses/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *}
+{* ../../docs_src/dataclasses_/tutorial003_py310.py hl[1,4,7:10,13:16,22:24,27] *}
 
 1. Мы по-прежнему импортируем `field` из стандартных `dataclasses`.
 
index 97278069ad642147a26885224b8587dcb8b9f008..50c321e7dd9211a20c1bdc935538c0c02f40eca4 100644 (file)
@@ -35,7 +35,7 @@
 
 Вот небольшой пример того, как можно интегрировать Strawberry с FastAPI:
 
-{* ../../docs_src/graphql/tutorial001_py39.py hl[3,22,25] *}
+{* ../../docs_src/graphql_/tutorial001_py39.py hl[3,22,25] *}
 
 Подробнее о Strawberry можно узнать в <a href="https://strawberry.rocks/" class="external-link" target="_blank">документации Strawberry</a>.
 
index c74ce65c3ee5351fb3f592fa878f0093ea4ab5dc..4e8e77d2ac18bdef26e92ae02d52429ffc30e33f 100644 (file)
@@ -4,7 +4,7 @@ FastAPI 基于 **Pydantic** 构建,前文已经介绍过如何使用 Pydantic
 
 但 FastAPI 还可以使用数据类(<a href="https://docs.python.org/3/library/dataclasses.html" class="external-link" target="_blank">`dataclasses`</a>):
 
-{* ../../docs_src/dataclasses/tutorial001.py hl[1,7:12,19:20] *}
+{* ../../docs_src/dataclasses_/tutorial001.py hl[1,7:12,19:20] *}
 
 这还是借助于 **Pydantic** 及其<a href="https://pydantic-docs.helpmanual.io/usage/dataclasses/#use-of-stdlib-dataclasses-with-basemodel" class="external-link" target="_blank">内置的 `dataclasses`</a>。
 
@@ -32,7 +32,7 @@ FastAPI 基于 **Pydantic** 构建,前文已经介绍过如何使用 Pydantic
 
 在 `response_model` 参数中使用 `dataclasses`:
 
-{* ../../docs_src/dataclasses/tutorial002.py hl[1,7:13,19] *}
+{* ../../docs_src/dataclasses_/tutorial002.py hl[1,7:13,19] *}
 
 本例把数据类自动转换为 Pydantic 数据类。
 
@@ -49,7 +49,7 @@ API 文档中也会显示相关概图:
 本例把标准的 `dataclasses` 直接替换为 `pydantic.dataclasses`:
 
 ```{ .python .annotate hl_lines="1  5  8-11  14-17  23-25  28" }
-{!../../docs_src/dataclasses/tutorial003.py!}
+{!../../docs_src/dataclasses_/tutorial003.py!}
 ```
 
 1. 本例依然要从标准的 `dataclasses` 中导入 `field`;
diff --git a/docs_src/additional_responses/__init__.py b/docs_src/additional_responses/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/additional_status_codes/__init__.py b/docs_src/additional_status_codes/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/advanced_middleware/__init__.py b/docs_src/advanced_middleware/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/authentication_error_status_code/__init__.py b/docs_src/authentication_error_status_code/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/background_tasks/__init__.py b/docs_src/background_tasks/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/behind_a_proxy/__init__.py b/docs_src/behind_a_proxy/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/body/__init__.py b/docs_src/body/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/body_fields/__init__.py b/docs_src/body_fields/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/body_multiple_params/__init__.py b/docs_src/body_multiple_params/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/body_nested_models/__init__.py b/docs_src/body_nested_models/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/body_updates/__init__.py b/docs_src/body_updates/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/conditional_openapi/__init__.py b/docs_src/conditional_openapi/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/configure_swagger_ui/__init__.py b/docs_src/configure_swagger_ui/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/cookie_param_models/__init__.py b/docs_src/cookie_param_models/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/cookie_params/__init__.py b/docs_src/cookie_params/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/cors/__init__.py b/docs_src/cors/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/custom_docs_ui/__init__.py b/docs_src/custom_docs_ui/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/custom_request_and_route/__init__.py b/docs_src/custom_request_and_route/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/custom_response/__init__.py b/docs_src/custom_response/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/dataclasses_/__init__.py b/docs_src/dataclasses_/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/debugging/__init__.py b/docs_src/debugging/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/dependencies/__init__.py b/docs_src/dependencies/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/dependency_testing/__init__.py b/docs_src/dependency_testing/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/encoder/__init__.py b/docs_src/encoder/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/events/__init__.py b/docs_src/events/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/extending_openapi/__init__.py b/docs_src/extending_openapi/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/extra_data_types/__init__.py b/docs_src/extra_data_types/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/extra_models/__init__.py b/docs_src/extra_models/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/first_steps/__init__.py b/docs_src/first_steps/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/generate_clients/__init__.py b/docs_src/generate_clients/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/graphql_/__init__.py b/docs_src/graphql_/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/handling_errors/__init__.py b/docs_src/handling_errors/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/header_param_models/__init__.py b/docs_src/header_param_models/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/header_params/__init__.py b/docs_src/header_params/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/metadata/__init__.py b/docs_src/metadata/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/middleware/__init__.py b/docs_src/middleware/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/openapi_callbacks/__init__.py b/docs_src/openapi_callbacks/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/openapi_webhooks/__init__.py b/docs_src/openapi_webhooks/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/path_operation_advanced_configuration/__init__.py b/docs_src/path_operation_advanced_configuration/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/path_operation_configuration/__init__.py b/docs_src/path_operation_configuration/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/path_params/__init__.py b/docs_src/path_params/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/path_params_numeric_validations/__init__.py b/docs_src/path_params_numeric_validations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/pydantic_v1_in_v2/__init__.py b/docs_src/pydantic_v1_in_v2/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/python_types/__init__.py b/docs_src/python_types/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/query_param_models/__init__.py b/docs_src/query_param_models/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/query_params/__init__.py b/docs_src/query_params/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/query_params_str_validations/__init__.py b/docs_src/query_params_str_validations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/request_files/__init__.py b/docs_src/request_files/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/request_form_models/__init__.py b/docs_src/request_form_models/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/request_forms/__init__.py b/docs_src/request_forms/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/request_forms_and_files/__init__.py b/docs_src/request_forms_and_files/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/response_change_status_code/__init__.py b/docs_src/response_change_status_code/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/response_cookies/__init__.py b/docs_src/response_cookies/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/response_directly/__init__.py b/docs_src/response_directly/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/response_headers/__init__.py b/docs_src/response_headers/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/response_model/__init__.py b/docs_src/response_model/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/response_status_code/__init__.py b/docs_src/response_status_code/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/schema_extra_example/__init__.py b/docs_src/schema_extra_example/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/security/__init__.py b/docs_src/security/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/separate_openapi_schemas/__init__.py b/docs_src/separate_openapi_schemas/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/settings/__init__.py b/docs_src/settings/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/static_files/__init__.py b/docs_src/static_files/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/sub_applications/__init__.py b/docs_src/sub_applications/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/templates/__init__.py b/docs_src/templates/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/templates/static/__init__.py b/docs_src/templates/static/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/templates/templates/__init__.py b/docs_src/templates/templates/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/using_request_directly/__init__.py b/docs_src/using_request_directly/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/docs_src/wsgi/__init__.py b/docs_src/wsgi/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
index ae97cb71bd270ca6aa23177d252e07d2eb43cf82..8f824af5d591145e993aac5c09097e8e9a24f457 100644 (file)
@@ -196,6 +196,17 @@ dynamic_context = "test_function"
 omit = [
     "docs_src/response_model/tutorial003_04_py39.py",
     "docs_src/response_model/tutorial003_04_py310.py",
+    "docs_src/dependencies/tutorial008_an_py39.py",  # difficult to mock
+    "docs_src/dependencies/tutorial013_an_py310.py",  # temporary code example?
+    "docs_src/dependencies/tutorial014_an_py310.py",  # temporary code example?
+    # Pydantic V1
+    "docs_src/schema_extra_example/tutorial001_pv1_py310.py",
+    "docs_src/query_param_models/tutorial002_pv1_py310.py",
+    "docs_src/query_param_models/tutorial002_pv1_an_py310.py",
+    "docs_src/header_param_models/tutorial002_pv1_py310.py",
+    "docs_src/header_param_models/tutorial002_pv1_an_py310.py",
+    "docs_src/cookie_param_models/tutorial002_pv1_py310.py",
+    "docs_src/cookie_param_models/tutorial002_pv1_an_py310.py",
 ]
 
 [tool.coverage.report]
index ee188b496cf7386f2701234a48afdf43d3e64f56..1604a2858c757a2942b33ed323b2995020dc3ee7 100644 (file)
@@ -6,6 +6,7 @@ mypy ==1.14.1
 dirty-equals ==0.9.0
 sqlmodel==0.0.27
 flask >=1.1.2,<4.0.0
+strawberry-graphql >=0.200.0,< 1.0.0
 anyio[trio] >=3.2.1,<5.0.0
 PyJWT==2.9.0
 pyyaml >=5.3.1,<7.0.0
diff --git a/tests/test_tutorial/test_body/test_tutorial002.py b/tests/test_tutorial/test_body/test_tutorial002.py
new file mode 100644 (file)
index 0000000..b6d51d5
--- /dev/null
@@ -0,0 +1,161 @@
+import importlib
+from typing import Union
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial002_py39"),
+        pytest.param("tutorial002_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.body.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+@pytest.mark.parametrize("price", ["50.5", 50.5])
+def test_post_with_tax(client: TestClient, price: Union[str, float]):
+    response = client.post(
+        "/items/",
+        json={"name": "Foo", "price": price, "description": "Some Foo", "tax": 0.3},
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "name": "Foo",
+        "price": 50.5,
+        "description": "Some Foo",
+        "tax": 0.3,
+        "price_with_tax": 50.8,
+    }
+
+
+@pytest.mark.parametrize("price", ["50.5", 50.5])
+def test_post_without_tax(client: TestClient, price: Union[str, float]):
+    response = client.post(
+        "/items/", json={"name": "Foo", "price": price, "description": "Some Foo"}
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "name": "Foo",
+        "price": 50.5,
+        "description": "Some Foo",
+        "tax": None,
+    }
+
+
+def test_post_with_no_data(client: TestClient):
+    response = client.post("/items/", json={})
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "type": "missing",
+                "loc": ["body", "name"],
+                "msg": "Field required",
+                "input": {},
+            },
+            {
+                "type": "missing",
+                "loc": ["body", "price"],
+                "msg": "Field required",
+                "input": {},
+            },
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "post": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Create Item",
+                    "operationId": "create_item_items__post",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/Item"}
+                            }
+                        },
+                        "required": True,
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "Item": {
+                    "title": "Item",
+                    "required": ["name", "price"],
+                    "type": "object",
+                    "properties": {
+                        "name": {"title": "Name", "type": "string"},
+                        "price": {"title": "Price", "type": "number"},
+                        "description": {
+                            "title": "Description",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                        "tax": {
+                            "title": "Tax",
+                            "anyOf": [{"type": "number"}, {"type": "null"}],
+                        },
+                    },
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_body/test_tutorial003.py b/tests/test_tutorial/test_body/test_tutorial003.py
new file mode 100644 (file)
index 0000000..227a125
--- /dev/null
@@ -0,0 +1,171 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial003_py39"),
+        pytest.param("tutorial003_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.body.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_put_all(client: TestClient):
+    response = client.put(
+        "/items/123",
+        json={"name": "Foo", "price": 50.1, "description": "Some Foo", "tax": 0.3},
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "item_id": 123,
+        "name": "Foo",
+        "price": 50.1,
+        "description": "Some Foo",
+        "tax": 0.3,
+    }
+
+
+def test_put_only_required(client: TestClient):
+    response = client.put(
+        "/items/123",
+        json={"name": "Foo", "price": 50.1},
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "item_id": 123,
+        "name": "Foo",
+        "price": 50.1,
+        "description": None,
+        "tax": None,
+    }
+
+
+def test_put_with_no_data(client: TestClient):
+    response = client.put("/items/123", json={})
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "type": "missing",
+                "loc": ["body", "name"],
+                "msg": "Field required",
+                "input": {},
+            },
+            {
+                "type": "missing",
+                "loc": ["body", "price"],
+                "msg": "Field required",
+                "input": {},
+            },
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "parameters": [
+                        {
+                            "in": "path",
+                            "name": "item_id",
+                            "required": True,
+                            "schema": {
+                                "title": "Item Id",
+                                "type": "integer",
+                            },
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__put",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/Item"}
+                            }
+                        },
+                        "required": True,
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "Item": {
+                    "title": "Item",
+                    "required": ["name", "price"],
+                    "type": "object",
+                    "properties": {
+                        "name": {"title": "Name", "type": "string"},
+                        "price": {"title": "Price", "type": "number"},
+                        "description": {
+                            "title": "Description",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                        "tax": {
+                            "title": "Tax",
+                            "anyOf": [{"type": "number"}, {"type": "null"}],
+                        },
+                    },
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_body/test_tutorial004.py b/tests/test_tutorial/test_body/test_tutorial004.py
new file mode 100644 (file)
index 0000000..1021284
--- /dev/null
@@ -0,0 +1,182 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial004_py39"),
+        pytest.param("tutorial004_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.body.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_put_all(client: TestClient):
+    response = client.put(
+        "/items/123",
+        json={"name": "Foo", "price": 50.1, "description": "Some Foo", "tax": 0.3},
+        params={"q": "somequery"},
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "item_id": 123,
+        "name": "Foo",
+        "price": 50.1,
+        "description": "Some Foo",
+        "tax": 0.3,
+        "q": "somequery",
+    }
+
+
+def test_put_only_required(client: TestClient):
+    response = client.put(
+        "/items/123",
+        json={"name": "Foo", "price": 50.1},
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "item_id": 123,
+        "name": "Foo",
+        "price": 50.1,
+        "description": None,
+        "tax": None,
+    }
+
+
+def test_put_with_no_data(client: TestClient):
+    response = client.put("/items/123", json={})
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "type": "missing",
+                "loc": ["body", "name"],
+                "msg": "Field required",
+                "input": {},
+            },
+            {
+                "type": "missing",
+                "loc": ["body", "price"],
+                "msg": "Field required",
+                "input": {},
+            },
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "parameters": [
+                        {
+                            "in": "path",
+                            "name": "item_id",
+                            "required": True,
+                            "schema": {
+                                "title": "Item Id",
+                                "type": "integer",
+                            },
+                        },
+                        {
+                            "required": False,
+                            "schema": {
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                                "title": "Q",
+                            },
+                            "name": "q",
+                            "in": "query",
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__put",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/Item"}
+                            }
+                        },
+                        "required": True,
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "Item": {
+                    "title": "Item",
+                    "required": ["name", "price"],
+                    "type": "object",
+                    "properties": {
+                        "name": {"title": "Name", "type": "string"},
+                        "price": {"title": "Price", "type": "number"},
+                        "description": {
+                            "title": "Description",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                        "tax": {
+                            "title": "Tax",
+                            "anyOf": [{"type": "number"}, {"type": "null"}],
+                        },
+                    },
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial002.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial002.py
new file mode 100644 (file)
index 0000000..e98d586
--- /dev/null
@@ -0,0 +1,361 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial002_py39"),
+        pytest.param("tutorial002_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.body_multiple_params.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_post_all(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "item": {
+                "name": "Foo",
+                "price": 50.5,
+                "description": "Some Foo",
+                "tax": 0.1,
+            },
+            "user": {"username": "johndoe", "full_name": "John Doe"},
+        },
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "item_id": 5,
+        "item": {
+            "name": "Foo",
+            "price": 50.5,
+            "description": "Some Foo",
+            "tax": 0.1,
+        },
+        "user": {"username": "johndoe", "full_name": "John Doe"},
+    }
+
+
+def test_post_required(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "item": {"name": "Foo", "price": 50.5},
+            "user": {"username": "johndoe"},
+        },
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "item_id": 5,
+        "item": {
+            "name": "Foo",
+            "price": 50.5,
+            "description": None,
+            "tax": None,
+        },
+        "user": {"username": "johndoe", "full_name": None},
+    }
+
+
+def test_post_no_body(client: TestClient):
+    response = client.put("/items/5", json=None)
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "input": None,
+                "loc": [
+                    "body",
+                    "item",
+                ],
+                "msg": "Field required",
+                "type": "missing",
+            },
+            {
+                "input": None,
+                "loc": [
+                    "body",
+                    "user",
+                ],
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ],
+    }
+
+
+def test_post_no_item(client: TestClient):
+    response = client.put("/items/5", json={"user": {"username": "johndoe"}})
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "input": None,
+                "loc": [
+                    "body",
+                    "item",
+                ],
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ],
+    }
+
+
+def test_post_no_user(client: TestClient):
+    response = client.put("/items/5", json={"item": {"name": "Foo", "price": 50.5}})
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "input": None,
+                "loc": [
+                    "body",
+                    "user",
+                ],
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ],
+    }
+
+
+def test_post_missing_required_field_in_item(client: TestClient):
+    response = client.put(
+        "/items/5", json={"item": {"name": "Foo"}, "user": {"username": "johndoe"}}
+    )
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "input": {"name": "Foo"},
+                "loc": [
+                    "body",
+                    "item",
+                    "price",
+                ],
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ],
+    }
+
+
+def test_post_missing_required_field_in_user(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={"item": {"name": "Foo", "price": 50.5}, "user": {"ful_name": "John Doe"}},
+    )
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "input": {"ful_name": "John Doe"},
+                "loc": [
+                    "body",
+                    "user",
+                    "username",
+                ],
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ],
+    }
+
+
+def test_post_id_foo(client: TestClient):
+    response = client.put(
+        "/items/foo",
+        json={
+            "item": {"name": "Foo", "price": 50.5},
+            "user": {"username": "johndoe"},
+        },
+    )
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "type": "int_parsing",
+                "loc": ["path", "item_id"],
+                "msg": "Input should be a valid integer, unable to parse string as an integer",
+                "input": "foo",
+            }
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "info": {
+            "title": "FastAPI",
+            "version": "0.1.0",
+        },
+        "openapi": "3.1.0",
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "operationId": "update_item_items__item_id__put",
+                    "parameters": [
+                        {
+                            "in": "path",
+                            "name": "item_id",
+                            "required": True,
+                            "schema": {
+                                "title": "Item Id",
+                                "type": "integer",
+                            },
+                        },
+                    ],
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Body_update_item_items__item_id__put",
+                                },
+                            },
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {},
+                                },
+                            },
+                            "description": "Successful Response",
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                    "summary": "Update Item",
+                },
+            },
+        },
+        "components": {
+            "schemas": {
+                "Body_update_item_items__item_id__put": {
+                    "properties": {
+                        "item": {
+                            "$ref": "#/components/schemas/Item",
+                        },
+                        "user": {
+                            "$ref": "#/components/schemas/User",
+                        },
+                    },
+                    "required": [
+                        "item",
+                        "user",
+                    ],
+                    "title": "Body_update_item_items__item_id__put",
+                    "type": "object",
+                },
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {
+                                "$ref": "#/components/schemas/ValidationError",
+                            },
+                            "title": "Detail",
+                            "type": "array",
+                        },
+                    },
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                },
+                "Item": {
+                    "properties": {
+                        "name": {
+                            "title": "Name",
+                            "type": "string",
+                        },
+                        "description": {
+                            "title": "Description",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                        "price": {"title": "Price", "type": "number"},
+                        "tax": {
+                            "title": "Tax",
+                            "anyOf": [{"type": "number"}, {"type": "null"}],
+                        },
+                    },
+                    "required": [
+                        "name",
+                        "price",
+                    ],
+                    "title": "Item",
+                    "type": "object",
+                },
+                "User": {
+                    "properties": {
+                        "username": {
+                            "title": "Username",
+                            "type": "string",
+                        },
+                        "full_name": {
+                            "title": "Full Name",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                    },
+                    "required": [
+                        "username",
+                    ],
+                    "title": "User",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "integer",
+                                    },
+                                ],
+                            },
+                            "title": "Location",
+                            "type": "array",
+                        },
+                        "msg": {
+                            "title": "Message",
+                            "type": "string",
+                        },
+                        "type": {
+                            "title": "Error Type",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "loc",
+                        "msg",
+                        "type",
+                    ],
+                    "title": "ValidationError",
+                    "type": "object",
+                },
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial004.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial004.py
new file mode 100644 (file)
index 0000000..979c054
--- /dev/null
@@ -0,0 +1,290 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial004_py39"),
+        pytest.param("tutorial004_py310", marks=needs_py310),
+        pytest.param("tutorial004_an_py39"),
+        pytest.param("tutorial004_an_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.body_multiple_params.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_put_all(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "importance": 2,
+            "item": {"name": "Foo", "price": 50.5},
+            "user": {"username": "Dave"},
+        },
+        params={"q": "somequery"},
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "item_id": 5,
+        "importance": 2,
+        "item": {
+            "name": "Foo",
+            "price": 50.5,
+            "description": None,
+            "tax": None,
+        },
+        "user": {"username": "Dave", "full_name": None},
+        "q": "somequery",
+    }
+
+
+def test_put_only_required(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "importance": 2,
+            "item": {"name": "Foo", "price": 50.5},
+            "user": {"username": "Dave"},
+        },
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "item_id": 5,
+        "importance": 2,
+        "item": {
+            "name": "Foo",
+            "price": 50.5,
+            "description": None,
+            "tax": None,
+        },
+        "user": {"username": "Dave", "full_name": None},
+    }
+
+
+def test_put_missing_body(client: TestClient):
+    response = client.put("/items/5")
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "input": None,
+                "loc": [
+                    "body",
+                    "item",
+                ],
+                "msg": "Field required",
+                "type": "missing",
+            },
+            {
+                "input": None,
+                "loc": [
+                    "body",
+                    "user",
+                ],
+                "msg": "Field required",
+                "type": "missing",
+            },
+            {
+                "input": None,
+                "loc": [
+                    "body",
+                    "importance",
+                ],
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ],
+    }
+
+
+def test_put_empty_body(client: TestClient):
+    response = client.put("/items/5", json={})
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "type": "missing",
+                "loc": ["body", "item"],
+                "msg": "Field required",
+                "input": None,
+            },
+            {
+                "type": "missing",
+                "loc": ["body", "user"],
+                "msg": "Field required",
+                "input": None,
+            },
+            {
+                "type": "missing",
+                "loc": ["body", "importance"],
+                "msg": "Field required",
+                "input": None,
+            },
+        ]
+    }
+
+
+def test_put_invalid_importance(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "importance": 0,
+            "item": {"name": "Foo", "price": 50.5},
+            "user": {"username": "Dave"},
+        },
+    )
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["body", "importance"],
+                "msg": "Input should be greater than 0",
+                "type": "greater_than",
+                "input": 0,
+                "ctx": {"gt": 0},
+            },
+        ],
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__put",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {"title": "Item Id", "type": "integer"},
+                            "name": "item_id",
+                            "in": "path",
+                        },
+                        {
+                            "required": False,
+                            "schema": {
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                                "title": "Q",
+                            },
+                            "name": "q",
+                            "in": "query",
+                        },
+                    ],
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Body_update_item_items__item_id__put"
+                                }
+                            }
+                        },
+                        "required": True,
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "Item": {
+                    "title": "Item",
+                    "required": ["name", "price"],
+                    "type": "object",
+                    "properties": {
+                        "name": {"title": "Name", "type": "string"},
+                        "description": {
+                            "title": "Description",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                        "price": {"title": "Price", "type": "number"},
+                        "tax": {
+                            "title": "Tax",
+                            "anyOf": [{"type": "number"}, {"type": "null"}],
+                        },
+                    },
+                },
+                "User": {
+                    "title": "User",
+                    "required": ["username"],
+                    "type": "object",
+                    "properties": {
+                        "username": {"title": "Username", "type": "string"},
+                        "full_name": {
+                            "title": "Full Name",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                    },
+                },
+                "Body_update_item_items__item_id__put": {
+                    "title": "Body_update_item_items__item_id__put",
+                    "required": ["item", "user", "importance"],
+                    "type": "object",
+                    "properties": {
+                        "item": {"$ref": "#/components/schemas/Item"},
+                        "user": {"$ref": "#/components/schemas/User"},
+                        "importance": {
+                            "title": "Importance",
+                            "type": "integer",
+                            "exclusiveMinimum": 0.0,
+                        },
+                    },
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial005.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial005.py
new file mode 100644 (file)
index 0000000..d47aa1b
--- /dev/null
@@ -0,0 +1,272 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial005_py39"),
+        pytest.param("tutorial005_py310", marks=needs_py310),
+        pytest.param("tutorial005_an_py39"),
+        pytest.param("tutorial005_an_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.body_multiple_params.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_post_all(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "item": {
+                "name": "Foo",
+                "price": 50.5,
+                "description": "Some Foo",
+                "tax": 0.1,
+            },
+        },
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "item_id": 5,
+        "item": {
+            "name": "Foo",
+            "price": 50.5,
+            "description": "Some Foo",
+            "tax": 0.1,
+        },
+    }
+
+
+def test_post_required(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "item": {"name": "Foo", "price": 50.5},
+        },
+    )
+    assert response.status_code == 200
+    assert response.json() == {
+        "item_id": 5,
+        "item": {
+            "name": "Foo",
+            "price": 50.5,
+            "description": None,
+            "tax": None,
+        },
+    }
+
+
+def test_post_no_body(client: TestClient):
+    response = client.put("/items/5", json=None)
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "input": None,
+                "loc": [
+                    "body",
+                    "item",
+                ],
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ],
+    }
+
+
+def test_post_like_not_embeded(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "name": "Foo",
+            "price": 50.5,
+        },
+    )
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "input": None,
+                "loc": [
+                    "body",
+                    "item",
+                ],
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ],
+    }
+
+
+def test_post_missing_required_field_in_item(client: TestClient):
+    response = client.put(
+        "/items/5", json={"item": {"name": "Foo"}, "user": {"username": "johndoe"}}
+    )
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "input": {"name": "Foo"},
+                "loc": [
+                    "body",
+                    "item",
+                    "price",
+                ],
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ],
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "info": {
+            "title": "FastAPI",
+            "version": "0.1.0",
+        },
+        "openapi": "3.1.0",
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "operationId": "update_item_items__item_id__put",
+                    "parameters": [
+                        {
+                            "in": "path",
+                            "name": "item_id",
+                            "required": True,
+                            "schema": {
+                                "title": "Item Id",
+                                "type": "integer",
+                            },
+                        },
+                    ],
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Body_update_item_items__item_id__put",
+                                },
+                            },
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {},
+                                },
+                            },
+                            "description": "Successful Response",
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                    "summary": "Update Item",
+                },
+            },
+        },
+        "components": {
+            "schemas": {
+                "Body_update_item_items__item_id__put": {
+                    "properties": {
+                        "item": {
+                            "$ref": "#/components/schemas/Item",
+                        },
+                    },
+                    "required": ["item"],
+                    "title": "Body_update_item_items__item_id__put",
+                    "type": "object",
+                },
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {
+                                "$ref": "#/components/schemas/ValidationError",
+                            },
+                            "title": "Detail",
+                            "type": "array",
+                        },
+                    },
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                },
+                "Item": {
+                    "properties": {
+                        "name": {
+                            "title": "Name",
+                            "type": "string",
+                        },
+                        "description": {
+                            "title": "Description",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                        "price": {"title": "Price", "type": "number"},
+                        "tax": {
+                            "title": "Tax",
+                            "anyOf": [{"type": "number"}, {"type": "null"}],
+                        },
+                    },
+                    "required": [
+                        "name",
+                        "price",
+                    ],
+                    "title": "Item",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "integer",
+                                    },
+                                ],
+                            },
+                            "title": "Location",
+                            "type": "array",
+                        },
+                        "msg": {
+                            "title": "Message",
+                            "type": "string",
+                        },
+                        "type": {
+                            "title": "Error Type",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "loc",
+                        "msg",
+                        "type",
+                    ],
+                    "title": "ValidationError",
+                    "type": "object",
+                },
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py b/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py
new file mode 100644 (file)
index 0000000..d452929
--- /dev/null
@@ -0,0 +1,251 @@
+import importlib
+
+import pytest
+from dirty_equals import IsList
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+UNTYPED_LIST_SCHEMA = {"type": "array", "items": {}}
+
+LIST_OF_STR_SCHEMA = {"type": "array", "items": {"type": "string"}}
+
+SET_OF_STR_SCHEMA = {"type": "array", "items": {"type": "string"}, "uniqueItems": True}
+
+
+@pytest.fixture(
+    name="mod_name",
+    params=[
+        pytest.param("tutorial001_py39"),
+        pytest.param("tutorial001_py310", marks=needs_py310),
+        pytest.param("tutorial002_py39"),
+        pytest.param("tutorial002_py310", marks=needs_py310),
+        pytest.param("tutorial003_py39"),
+        pytest.param("tutorial003_py310", marks=needs_py310),
+    ],
+)
+def get_mod_name(request: pytest.FixtureRequest):
+    return request.param
+
+
+@pytest.fixture(name="client")
+def get_client(mod_name: str):
+    mod = importlib.import_module(f"docs_src.body_nested_models.{mod_name}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_put_all(client: TestClient, mod_name: str):
+    if mod_name.startswith("tutorial003"):
+        tags_expected = IsList("foo", "bar", check_order=False)
+    else:
+        tags_expected = ["foo", "bar", "foo"]
+
+    response = client.put(
+        "/items/123",
+        json={
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+            "tags": ["foo", "bar", "foo"],
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "item_id": 123,
+        "item": {
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+            "tags": tags_expected,
+        },
+    }
+
+
+def test_put_only_required(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={"name": "Foo", "price": 35.4},
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "item_id": 5,
+        "item": {
+            "name": "Foo",
+            "description": None,
+            "price": 35.4,
+            "tax": None,
+            "tags": [],
+        },
+    }
+
+
+def test_put_empty_body(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={},
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["body", "name"],
+                "input": {},
+                "msg": "Field required",
+                "type": "missing",
+            },
+            {
+                "loc": ["body", "price"],
+                "input": {},
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ]
+    }
+
+
+def test_put_missing_required(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={"description": "A very nice Item"},
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["body", "name"],
+                "input": {"description": "A very nice Item"},
+                "msg": "Field required",
+                "type": "missing",
+            },
+            {
+                "loc": ["body", "price"],
+                "input": {"description": "A very nice Item"},
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient, mod_name: str):
+    tags_schema = {"default": [], "title": "Tags"}
+    if mod_name.startswith("tutorial001"):
+        tags_schema.update(UNTYPED_LIST_SCHEMA)
+    elif mod_name.startswith("tutorial002"):
+        tags_schema.update(LIST_OF_STR_SCHEMA)
+    elif mod_name.startswith("tutorial003"):
+        tags_schema.update(SET_OF_STR_SCHEMA)
+
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "parameters": [
+                        {
+                            "in": "path",
+                            "name": "item_id",
+                            "required": True,
+                            "schema": {
+                                "title": "Item Id",
+                                "type": "integer",
+                            },
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__put",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Item",
+                                }
+                            }
+                        },
+                        "required": True,
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "Item": {
+                    "properties": {
+                        "name": {
+                            "title": "Name",
+                            "type": "string",
+                        },
+                        "description": {
+                            "title": "Description",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                        "price": {
+                            "title": "Price",
+                            "type": "number",
+                        },
+                        "tax": {
+                            "title": "Tax",
+                            "anyOf": [{"type": "number"}, {"type": "null"}],
+                        },
+                        "tags": tags_schema,
+                    },
+                    "required": [
+                        "name",
+                        "price",
+                    ],
+                    "title": "Item",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial004.py b/tests/test_tutorial/test_body_nested_models/test_tutorial004.py
new file mode 100644 (file)
index 0000000..ff95969
--- /dev/null
@@ -0,0 +1,275 @@
+import importlib
+
+import pytest
+from dirty_equals import IsList
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial004_py39"),
+        pytest.param("tutorial004_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_put_all(client: TestClient):
+    response = client.put(
+        "/items/123",
+        json={
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+            "tags": ["foo", "bar", "foo"],
+            "image": {"url": "http://example.com/image.png", "name": "example image"},
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "item_id": 123,
+        "item": {
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+            "tags": IsList("foo", "bar", check_order=False),
+            "image": {"url": "http://example.com/image.png", "name": "example image"},
+        },
+    }
+
+
+def test_put_only_required(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={"name": "Foo", "price": 35.4},
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "item_id": 5,
+        "item": {
+            "name": "Foo",
+            "description": None,
+            "price": 35.4,
+            "tax": None,
+            "tags": [],
+            "image": None,
+        },
+    }
+
+
+def test_put_empty_body(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={},
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["body", "name"],
+                "input": {},
+                "msg": "Field required",
+                "type": "missing",
+            },
+            {
+                "loc": ["body", "price"],
+                "input": {},
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ]
+    }
+
+
+def test_put_missing_required_in_item(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={"description": "A very nice Item"},
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["body", "name"],
+                "input": {"description": "A very nice Item"},
+                "msg": "Field required",
+                "type": "missing",
+            },
+            {
+                "loc": ["body", "price"],
+                "input": {"description": "A very nice Item"},
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ]
+    }
+
+
+def test_put_missing_required_in_image(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "name": "Foo",
+            "price": 35.4,
+            "image": {"url": "http://example.com/image.png"},
+        },
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["body", "image", "name"],
+                "input": {"url": "http://example.com/image.png"},
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "parameters": [
+                        {
+                            "in": "path",
+                            "name": "item_id",
+                            "required": True,
+                            "schema": {
+                                "title": "Item Id",
+                                "type": "integer",
+                            },
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__put",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Item",
+                                }
+                            }
+                        },
+                        "required": True,
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "Image": {
+                    "properties": {
+                        "url": {
+                            "title": "Url",
+                            "type": "string",
+                        },
+                        "name": {
+                            "title": "Name",
+                            "type": "string",
+                        },
+                    },
+                    "required": ["url", "name"],
+                    "title": "Image",
+                    "type": "object",
+                },
+                "Item": {
+                    "properties": {
+                        "name": {
+                            "title": "Name",
+                            "type": "string",
+                        },
+                        "description": {
+                            "title": "Description",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                        "price": {
+                            "title": "Price",
+                            "type": "number",
+                        },
+                        "tax": {
+                            "title": "Tax",
+                            "anyOf": [{"type": "number"}, {"type": "null"}],
+                        },
+                        "tags": {
+                            "title": "Tags",
+                            "default": [],
+                            "type": "array",
+                            "items": {"type": "string"},
+                            "uniqueItems": True,
+                        },
+                        "image": {
+                            "anyOf": [
+                                {"$ref": "#/components/schemas/Image"},
+                                {"type": "null"},
+                            ],
+                        },
+                    },
+                    "required": [
+                        "name",
+                        "price",
+                    ],
+                    "title": "Item",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial005.py b/tests/test_tutorial/test_body_nested_models/test_tutorial005.py
new file mode 100644 (file)
index 0000000..9a07a90
--- /dev/null
@@ -0,0 +1,301 @@
+import importlib
+
+import pytest
+from dirty_equals import IsList
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial005_py39"),
+        pytest.param("tutorial005_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_put_all(client: TestClient):
+    response = client.put(
+        "/items/123",
+        json={
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+            "tags": ["foo", "bar", "foo"],
+            "image": {"url": "http://example.com/image.png", "name": "example image"},
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "item_id": 123,
+        "item": {
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+            "tags": IsList("foo", "bar", check_order=False),
+            "image": {"url": "http://example.com/image.png", "name": "example image"},
+        },
+    }
+
+
+def test_put_only_required(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={"name": "Foo", "price": 35.4},
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "item_id": 5,
+        "item": {
+            "name": "Foo",
+            "description": None,
+            "price": 35.4,
+            "tax": None,
+            "tags": [],
+            "image": None,
+        },
+    }
+
+
+def test_put_empty_body(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={},
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["body", "name"],
+                "input": {},
+                "msg": "Field required",
+                "type": "missing",
+            },
+            {
+                "loc": ["body", "price"],
+                "input": {},
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ]
+    }
+
+
+def test_put_missing_required_in_item(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={"description": "A very nice Item"},
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["body", "name"],
+                "input": {"description": "A very nice Item"},
+                "msg": "Field required",
+                "type": "missing",
+            },
+            {
+                "loc": ["body", "price"],
+                "input": {"description": "A very nice Item"},
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ]
+    }
+
+
+def test_put_missing_required_in_image(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "name": "Foo",
+            "price": 35.4,
+            "image": {"url": "http://example.com/image.png"},
+        },
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["body", "image", "name"],
+                "input": {"url": "http://example.com/image.png"},
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ]
+    }
+
+
+def test_put_wrong_url(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "name": "Foo",
+            "price": 35.4,
+            "image": {"url": "not a valid url", "name": "example image"},
+        },
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["body", "image", "url"],
+                "input": "not a valid url",
+                "msg": "Input should be a valid URL, relative URL without a base",
+                "type": "url_parsing",
+                "ctx": {"error": "relative URL without a base"},
+            },
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "parameters": [
+                        {
+                            "in": "path",
+                            "name": "item_id",
+                            "required": True,
+                            "schema": {
+                                "title": "Item Id",
+                                "type": "integer",
+                            },
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__put",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Item",
+                                }
+                            }
+                        },
+                        "required": True,
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "Image": {
+                    "properties": {
+                        "url": {
+                            "title": "Url",
+                            "type": "string",
+                            "format": "uri",
+                            "maxLength": 2083,
+                            "minLength": 1,
+                        },
+                        "name": {
+                            "title": "Name",
+                            "type": "string",
+                        },
+                    },
+                    "required": ["url", "name"],
+                    "title": "Image",
+                    "type": "object",
+                },
+                "Item": {
+                    "properties": {
+                        "name": {
+                            "title": "Name",
+                            "type": "string",
+                        },
+                        "description": {
+                            "title": "Description",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                        "price": {
+                            "title": "Price",
+                            "type": "number",
+                        },
+                        "tax": {
+                            "title": "Tax",
+                            "anyOf": [{"type": "number"}, {"type": "null"}],
+                        },
+                        "tags": {
+                            "title": "Tags",
+                            "default": [],
+                            "type": "array",
+                            "items": {"type": "string"},
+                            "uniqueItems": True,
+                        },
+                        "image": {
+                            "anyOf": [
+                                {"$ref": "#/components/schemas/Image"},
+                                {"type": "null"},
+                            ],
+                        },
+                    },
+                    "required": [
+                        "name",
+                        "price",
+                    ],
+                    "title": "Item",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial006.py b/tests/test_tutorial/test_body_nested_models/test_tutorial006.py
new file mode 100644 (file)
index 0000000..088177c
--- /dev/null
@@ -0,0 +1,269 @@
+import importlib
+
+import pytest
+from dirty_equals import IsList
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial006_py39"),
+        pytest.param("tutorial006_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_put_all(client: TestClient):
+    response = client.put(
+        "/items/123",
+        json={
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+            "tags": ["foo", "bar", "foo"],
+            "images": [
+                {"url": "http://example.com/image.png", "name": "example image"}
+            ],
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "item_id": 123,
+        "item": {
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+            "tags": IsList("foo", "bar", check_order=False),
+            "images": [
+                {"url": "http://example.com/image.png", "name": "example image"}
+            ],
+        },
+    }
+
+
+def test_put_only_required(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={"name": "Foo", "price": 35.4},
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "item_id": 5,
+        "item": {
+            "name": "Foo",
+            "description": None,
+            "price": 35.4,
+            "tax": None,
+            "tags": [],
+            "images": None,
+        },
+    }
+
+
+def test_put_empty_body(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={},
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["body", "name"],
+                "input": {},
+                "msg": "Field required",
+                "type": "missing",
+            },
+            {
+                "loc": ["body", "price"],
+                "input": {},
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ]
+    }
+
+
+def test_put_images_not_list(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "name": "Foo",
+            "price": 35.4,
+            "images": {"url": "http://example.com/image.png", "name": "example image"},
+        },
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["body", "images"],
+                "input": {
+                    "url": "http://example.com/image.png",
+                    "name": "example image",
+                },
+                "msg": "Input should be a valid list",
+                "type": "list_type",
+            },
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "parameters": [
+                        {
+                            "in": "path",
+                            "name": "item_id",
+                            "required": True,
+                            "schema": {
+                                "title": "Item Id",
+                                "type": "integer",
+                            },
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__put",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Item",
+                                }
+                            }
+                        },
+                        "required": True,
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "Image": {
+                    "properties": {
+                        "url": {
+                            "title": "Url",
+                            "type": "string",
+                            "format": "uri",
+                            "maxLength": 2083,
+                            "minLength": 1,
+                        },
+                        "name": {
+                            "title": "Name",
+                            "type": "string",
+                        },
+                    },
+                    "required": ["url", "name"],
+                    "title": "Image",
+                    "type": "object",
+                },
+                "Item": {
+                    "properties": {
+                        "name": {
+                            "title": "Name",
+                            "type": "string",
+                        },
+                        "description": {
+                            "title": "Description",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                        "price": {
+                            "title": "Price",
+                            "type": "number",
+                        },
+                        "tax": {
+                            "title": "Tax",
+                            "anyOf": [{"type": "number"}, {"type": "null"}],
+                        },
+                        "tags": {
+                            "title": "Tags",
+                            "default": [],
+                            "type": "array",
+                            "items": {"type": "string"},
+                            "uniqueItems": True,
+                        },
+                        "images": {
+                            "anyOf": [
+                                {
+                                    "items": {
+                                        "$ref": "#/components/schemas/Image",
+                                    },
+                                    "type": "array",
+                                },
+                                {
+                                    "type": "null",
+                                },
+                            ],
+                            "title": "Images",
+                        },
+                    },
+                    "required": [
+                        "name",
+                        "price",
+                    ],
+                    "title": "Item",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial007.py b/tests/test_tutorial/test_body_nested_models/test_tutorial007.py
new file mode 100644 (file)
index 0000000..a302819
--- /dev/null
@@ -0,0 +1,344 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial007_py39"),
+        pytest.param("tutorial007_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_post_all(client: TestClient):
+    data = {
+        "name": "Special Offer",
+        "description": "This is a special offer",
+        "price": 38.6,
+        "items": [
+            {
+                "name": "Foo",
+                "description": "A very nice Item",
+                "price": 35.4,
+                "tax": 3.2,
+                "tags": ["foo"],
+                "images": [
+                    {
+                        "url": "http://example.com/image.png",
+                        "name": "example image",
+                    }
+                ],
+            }
+        ],
+    }
+
+    response = client.post(
+        "/offers/",
+        json=data,
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == data
+
+
+def test_put_only_required(client: TestClient):
+    response = client.post(
+        "/offers/",
+        json={
+            "name": "Special Offer",
+            "price": 38.6,
+            "items": [
+                {
+                    "name": "Foo",
+                    "price": 35.4,
+                    "images": [
+                        {
+                            "url": "http://example.com/image.png",
+                            "name": "example image",
+                        }
+                    ],
+                }
+            ],
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "name": "Special Offer",
+        "description": None,
+        "price": 38.6,
+        "items": [
+            {
+                "name": "Foo",
+                "description": None,
+                "price": 35.4,
+                "tax": None,
+                "tags": [],
+                "images": [
+                    {
+                        "url": "http://example.com/image.png",
+                        "name": "example image",
+                    }
+                ],
+            }
+        ],
+    }
+
+
+def test_put_empty_body(client: TestClient):
+    response = client.post(
+        "/offers/",
+        json={},
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["body", "name"],
+                "input": {},
+                "msg": "Field required",
+                "type": "missing",
+            },
+            {
+                "loc": ["body", "price"],
+                "input": {},
+                "msg": "Field required",
+                "type": "missing",
+            },
+            {
+                "loc": ["body", "items"],
+                "input": {},
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ]
+    }
+
+
+def test_put_missing_required_in_items(client: TestClient):
+    response = client.post(
+        "/offers/",
+        json={
+            "name": "Special Offer",
+            "price": 38.6,
+            "items": [{}],
+        },
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["body", "items", 0, "name"],
+                "input": {},
+                "msg": "Field required",
+                "type": "missing",
+            },
+            {
+                "loc": ["body", "items", 0, "price"],
+                "input": {},
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ]
+    }
+
+
+def test_put_missing_required_in_images(client: TestClient):
+    response = client.post(
+        "/offers/",
+        json={
+            "name": "Special Offer",
+            "price": 38.6,
+            "items": [
+                {"name": "Foo", "price": 35.4, "images": [{}]},
+            ],
+        },
+    )
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["body", "items", 0, "images", 0, "url"],
+                "input": {},
+                "msg": "Field required",
+                "type": "missing",
+            },
+            {
+                "loc": ["body", "items", 0, "images", 0, "name"],
+                "input": {},
+                "msg": "Field required",
+                "type": "missing",
+            },
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/offers/": {
+                "post": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Create Offer",
+                    "operationId": "create_offer_offers__post",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Offer",
+                                }
+                            }
+                        },
+                        "required": True,
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "Image": {
+                    "properties": {
+                        "url": {
+                            "title": "Url",
+                            "type": "string",
+                            "format": "uri",
+                            "maxLength": 2083,
+                            "minLength": 1,
+                        },
+                        "name": {
+                            "title": "Name",
+                            "type": "string",
+                        },
+                    },
+                    "required": ["url", "name"],
+                    "title": "Image",
+                    "type": "object",
+                },
+                "Item": {
+                    "properties": {
+                        "name": {
+                            "title": "Name",
+                            "type": "string",
+                        },
+                        "description": {
+                            "title": "Description",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                        "price": {
+                            "title": "Price",
+                            "type": "number",
+                        },
+                        "tax": {
+                            "title": "Tax",
+                            "anyOf": [{"type": "number"}, {"type": "null"}],
+                        },
+                        "tags": {
+                            "title": "Tags",
+                            "default": [],
+                            "type": "array",
+                            "items": {"type": "string"},
+                            "uniqueItems": True,
+                        },
+                        "images": {
+                            "anyOf": [
+                                {
+                                    "items": {
+                                        "$ref": "#/components/schemas/Image",
+                                    },
+                                    "type": "array",
+                                },
+                                {
+                                    "type": "null",
+                                },
+                            ],
+                            "title": "Images",
+                        },
+                    },
+                    "required": [
+                        "name",
+                        "price",
+                    ],
+                    "title": "Item",
+                    "type": "object",
+                },
+                "Offer": {
+                    "properties": {
+                        "name": {
+                            "title": "Name",
+                            "type": "string",
+                        },
+                        "description": {
+                            "title": "Description",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                        "price": {
+                            "title": "Price",
+                            "type": "number",
+                        },
+                        "items": {
+                            "title": "Items",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/Item"},
+                        },
+                    },
+                    "required": ["name", "price", "items"],
+                    "title": "Offer",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial008.py b/tests/test_tutorial/test_body_nested_models/test_tutorial008.py
new file mode 100644 (file)
index 0000000..32eb8ee
--- /dev/null
@@ -0,0 +1,157 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial008_py39"),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.body_nested_models.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_post_body(client: TestClient):
+    data = [
+        {"url": "http://example.com/", "name": "Example"},
+        {"url": "http://fastapi.tiangolo.com/", "name": "FastAPI"},
+    ]
+    response = client.post("/images/multiple", json=data)
+    assert response.status_code == 200, response.text
+    assert response.json() == data
+
+
+def test_post_invalid_list_item(client: TestClient):
+    data = [{"url": "not a valid url", "name": "Example"}]
+    response = client.post("/images/multiple", json=data)
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["body", 0, "url"],
+                "input": "not a valid url",
+                "msg": "Input should be a valid URL, relative URL without a base",
+                "type": "url_parsing",
+                "ctx": {"error": "relative URL without a base"},
+            },
+        ]
+    }
+
+
+def test_post_not_a_list(client: TestClient):
+    data = {"url": "http://example.com/", "name": "Example"}
+    response = client.post("/images/multiple", json=data)
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["body"],
+                "input": {
+                    "name": "Example",
+                    "url": "http://example.com/",
+                },
+                "msg": "Input should be a valid list",
+                "type": "list_type",
+            }
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/images/multiple/": {
+                "post": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Create Multiple Images",
+                    "operationId": "create_multiple_images_images_multiple__post",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "title": "Images",
+                                    "type": "array",
+                                    "items": {"$ref": "#/components/schemas/Image"},
+                                }
+                            }
+                        },
+                        "required": True,
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "Image": {
+                    "properties": {
+                        "url": {
+                            "title": "Url",
+                            "type": "string",
+                            "format": "uri",
+                            "maxLength": 2083,
+                            "minLength": 1,
+                        },
+                        "name": {
+                            "title": "Name",
+                            "type": "string",
+                        },
+                    },
+                    "required": ["url", "name"],
+                    "title": "Image",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_body_updates/test_tutorial002.py b/tests/test_tutorial/test_body_updates/test_tutorial002.py
new file mode 100644 (file)
index 0000000..466e6af
--- /dev/null
@@ -0,0 +1,207 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial002_py39"),
+        pytest.param("tutorial002_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.body_updates.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_get(client: TestClient):
+    response = client.get("/items/baz")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "name": "Baz",
+        "description": None,
+        "price": 50.2,
+        "tax": 10.5,
+        "tags": [],
+    }
+
+
+def test_patch_all(client: TestClient):
+    response = client.patch(
+        "/items/foo",
+        json={
+            "name": "Fooz",
+            "description": "Item description",
+            "price": 3,
+            "tax": 10.5,
+            "tags": ["tag1", "tag2"],
+        },
+    )
+    assert response.json() == {
+        "name": "Fooz",
+        "description": "Item description",
+        "price": 3,
+        "tax": 10.5,
+        "tags": ["tag1", "tag2"],
+    }
+
+
+def test_patch_name(client: TestClient):
+    response = client.patch(
+        "/items/bar",
+        json={"name": "Barz"},
+    )
+    assert response.json() == {
+        "name": "Barz",
+        "description": "The bartenders",
+        "price": 62,
+        "tax": 20.2,
+        "tags": [],
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {"$ref": "#/components/schemas/Item"}
+                                }
+                            },
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Read Item",
+                    "operationId": "read_item_items__item_id__get",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {"title": "Item Id", "type": "string"},
+                            "name": "item_id",
+                            "in": "path",
+                        }
+                    ],
+                },
+                "patch": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {"$ref": "#/components/schemas/Item"}
+                                }
+                            },
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__patch",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {"title": "Item Id", "type": "string"},
+                            "name": "item_id",
+                            "in": "path",
+                        }
+                    ],
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/Item"}
+                            }
+                        },
+                        "required": True,
+                    },
+                },
+            }
+        },
+        "components": {
+            "schemas": {
+                "Item": {
+                    "type": "object",
+                    "title": "Item",
+                    "properties": {
+                        "name": {
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                            "title": "Name",
+                        },
+                        "description": {
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                            "title": "Description",
+                        },
+                        "price": {
+                            "anyOf": [{"type": "number"}, {"type": "null"}],
+                            "title": "Price",
+                        },
+                        "tax": {"title": "Tax", "type": "number", "default": 10.5},
+                        "tags": {
+                            "title": "Tags",
+                            "type": "array",
+                            "items": {"type": "string"},
+                            "default": [],
+                        },
+                    },
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
index c81e991ebf05814c1be17d95beba6515f8882ce3..f1d2accef27a43dc60f625eb9c62f186f08b2f23 100644 (file)
@@ -1,17 +1,29 @@
+import importlib
+
+import pytest
 from fastapi.testclient import TestClient
 
-from docs_src.custom_response.tutorial001_py39 import app
 
-client = TestClient(app)
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial001_py39"),
+        pytest.param("tutorial010_py39"),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.custom_response.{request.param}")
+    client = TestClient(mod.app)
+    return client
 
 
-def test_get_custom_response():
+def test_get_custom_response(client: TestClient):
     response = client.get("/items/")
     assert response.status_code == 200, response.text
     assert response.json() == [{"item_id": "Foo"}]
 
 
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200, response.text
     assert response.json() == {
diff --git a/tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py b/tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py
new file mode 100644 (file)
index 0000000..22e2e02
--- /dev/null
@@ -0,0 +1,68 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(
+    name="mod_name",
+    params=[
+        pytest.param("tutorial002_py39"),
+        pytest.param("tutorial003_py39"),
+        pytest.param("tutorial004_py39"),
+    ],
+)
+def get_mod_name(request: pytest.FixtureRequest) -> str:
+    return request.param
+
+
+@pytest.fixture(name="client")
+def get_client(mod_name: str) -> TestClient:
+    mod = importlib.import_module(f"docs_src.custom_response.{mod_name}")
+    return TestClient(mod.app)
+
+
+html_contents = """
+    <html>
+        <head>
+            <title>Some HTML in here</title>
+        </head>
+        <body>
+            <h1>Look ma! HTML!</h1>
+        </body>
+    </html>
+    """
+
+
+def test_get_custom_response(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200, response.text
+    assert response.text == html_contents
+
+
+def test_openapi_schema(client: TestClient, mod_name: str):
+    if mod_name.startswith("tutorial003"):
+        response_content = {"application/json": {"schema": {}}}
+    else:
+        response_content = {"text/html": {"schema": {"type": "string"}}}
+
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": response_content,
+                        }
+                    },
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__get",
+                }
+            }
+        },
+    }
index d5f230bc423e3e411d8ffbb4744a9c0096e9b1ef..bc407234a1a839b9eb8d977b8483f6e45ac588d3 100644 (file)
@@ -15,7 +15,7 @@ from tests.utils import needs_py310
     ],
 )
 def get_client(request: pytest.FixtureRequest):
-    mod = importlib.import_module(f"docs_src.dataclasses.{request.param}")
+    mod = importlib.import_module(f"docs_src.dataclasses_.{request.param}")
 
     client = TestClient(mod.app)
     client.headers.clear()
index 4cf8933805ce64fe5f0a6aa71b8913aec5ce4f56..995d926752abdd472cdcb59a0609a8821ad97e15 100644 (file)
@@ -15,7 +15,7 @@ from tests.utils import needs_py310
     ],
 )
 def get_client(request: pytest.FixtureRequest):
-    mod = importlib.import_module(f"docs_src.dataclasses.{request.param}")
+    mod = importlib.import_module(f"docs_src.dataclasses_.{request.param}")
 
     client = TestClient(mod.app)
     client.headers.clear()
index cddf4a9be867aee5521eba21d2d8c1fcdd4b19be..a6a9fc1c7e8acf41c34082c329e963bfe7b5df5c 100644 (file)
@@ -14,7 +14,7 @@ from ...utils import needs_py310
     ],
 )
 def get_client(request: pytest.FixtureRequest):
-    mod = importlib.import_module(f"docs_src.dataclasses.{request.param}")
+    mod = importlib.import_module(f"docs_src.dataclasses_.{request.param}")
 
     client = TestClient(mod.app)
     client.headers.clear()
diff --git a/tests/test_tutorial/test_debugging/__init__.py b/tests/test_tutorial/test_debugging/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_debugging/test_tutorial001.py b/tests/test_tutorial/test_debugging/test_tutorial001.py
new file mode 100644 (file)
index 0000000..cf62c3b
--- /dev/null
@@ -0,0 +1,64 @@
+import importlib
+import runpy
+import sys
+import unittest
+
+import pytest
+from fastapi.testclient import TestClient
+
+MOD_NAME = "docs_src.debugging.tutorial001_py39"
+
+
+@pytest.fixture(name="client")
+def get_client():
+    mod = importlib.import_module(MOD_NAME)
+    client = TestClient(mod.app)
+    return client
+
+
+def test_uvicorn_run_is_not_called_on_import():
+    if sys.modules.get(MOD_NAME):
+        del sys.modules[MOD_NAME]  # pragma: no cover
+    with unittest.mock.patch("uvicorn.run") as uvicorn_run_mock:
+        importlib.import_module(MOD_NAME)
+    uvicorn_run_mock.assert_not_called()
+
+
+def test_get_root(client: TestClient):
+    response = client.get("/")
+    assert response.status_code == 200
+    assert response.json() == {"hello world": "ba"}
+
+
+def test_uvicorn_run_called_when_run_as_main():  # Just for coverage
+    if sys.modules.get(MOD_NAME):
+        del sys.modules[MOD_NAME]
+    with unittest.mock.patch("uvicorn.run") as uvicorn_run_mock:
+        runpy.run_module(MOD_NAME, run_name="__main__")
+
+    uvicorn_run_mock.assert_called_once_with(
+        unittest.mock.ANY, host="0.0.0.0", port=8000
+    )
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/": {
+                "get": {
+                    "summary": "Root",
+                    "operationId": "root__get",
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                    },
+                }
+            }
+        },
+    }
similarity index 86%
rename from tests/test_tutorial/test_dependencies/test_tutorial001.py
rename to tests/test_tutorial/test_dependencies/test_tutorial001_tutorial001_02.py
index 8dac99cf30416cfd60904aa5795e79411954cf5b..50d7c4108cc72d81f93b6b2707834c7b4e3c2879 100644 (file)
@@ -1,7 +1,6 @@
 import importlib
 
 import pytest
-from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -14,6 +13,8 @@ from ...utils import needs_py310
         pytest.param("tutorial001_py310", marks=needs_py310),
         pytest.param("tutorial001_an_py39"),
         pytest.param("tutorial001_an_py310", marks=needs_py310),
+        pytest.param("tutorial001_02_an_py39"),
+        pytest.param("tutorial001_02_an_py310", marks=needs_py310),
     ],
 )
 def get_client(request: pytest.FixtureRequest):
@@ -69,16 +70,10 @@ def test_openapi_schema(client: TestClient):
                     "parameters": [
                         {
                             "required": False,
-                            "schema": IsDict(
-                                {
-                                    "anyOf": [{"type": "string"}, {"type": "null"}],
-                                    "title": "Q",
-                                }
-                            )
-                            | IsDict(
-                                # TODO: remove when deprecating Pydantic v1
-                                {"title": "Q", "type": "string"}
-                            ),
+                            "schema": {
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                                "title": "Q",
+                            },
                             "name": "q",
                             "in": "query",
                         },
@@ -128,16 +123,10 @@ def test_openapi_schema(client: TestClient):
                     "parameters": [
                         {
                             "required": False,
-                            "schema": IsDict(
-                                {
-                                    "anyOf": [{"type": "string"}, {"type": "null"}],
-                                    "title": "Q",
-                                }
-                            )
-                            | IsDict(
-                                # TODO: remove when deprecating Pydantic v1
-                                {"title": "Q", "type": "string"}
-                            ),
+                            "schema": {
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                                "title": "Q",
+                            },
                             "name": "q",
                             "in": "query",
                         },
similarity index 89%
rename from tests/test_tutorial/test_dependencies/test_tutorial004.py
rename to tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py
index 8a1346d0d2b072ab200eee2f55b7ac95c076bb4d..f09d6f268d5f3a82a990fc3740561779fb264a00 100644 (file)
@@ -1,7 +1,6 @@
 import importlib
 
 import pytest
-from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -10,6 +9,14 @@ from ...utils import needs_py310
 @pytest.fixture(
     name="client",
     params=[
+        pytest.param("tutorial002_py39"),
+        pytest.param("tutorial002_py310", marks=needs_py310),
+        pytest.param("tutorial002_an_py39"),
+        pytest.param("tutorial002_an_py310", marks=needs_py310),
+        pytest.param("tutorial003_py39"),
+        pytest.param("tutorial003_py310", marks=needs_py310),
+        pytest.param("tutorial003_an_py39"),
+        pytest.param("tutorial003_an_py310", marks=needs_py310),
         pytest.param("tutorial004_py39"),
         pytest.param("tutorial004_py310", marks=needs_py310),
         pytest.param("tutorial004_an_py39"),
@@ -107,16 +114,10 @@ def test_openapi_schema(client: TestClient):
                     "parameters": [
                         {
                             "required": False,
-                            "schema": IsDict(
-                                {
-                                    "anyOf": [{"type": "string"}, {"type": "null"}],
-                                    "title": "Q",
-                                }
-                            )
-                            | IsDict(
-                                # TODO: remove when deprecating Pydantic v1
-                                {"title": "Q", "type": "string"}
-                            ),
+                            "schema": {
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                                "title": "Q",
+                            },
                             "name": "q",
                             "in": "query",
                         },
diff --git a/tests/test_tutorial/test_dependencies/test_tutorial005.py b/tests/test_tutorial/test_dependencies/test_tutorial005.py
new file mode 100644 (file)
index 0000000..a914936
--- /dev/null
@@ -0,0 +1,139 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial005_py39"),
+        pytest.param("tutorial005_py310", marks=needs_py310),
+        pytest.param("tutorial005_an_py39"),
+        pytest.param("tutorial005_an_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.dependencies.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+@pytest.mark.parametrize(
+    "path,cookie,expected_status,expected_response",
+    [
+        (
+            "/items",
+            "from_cookie",
+            200,
+            {"q_or_cookie": "from_cookie"},
+        ),
+        (
+            "/items?q=foo",
+            "from_cookie",
+            200,
+            {"q_or_cookie": "foo"},
+        ),
+        (
+            "/items",
+            None,
+            200,
+            {"q_or_cookie": None},
+        ),
+    ],
+)
+def test_get(path, cookie, expected_status, expected_response, client: TestClient):
+    if cookie is not None:
+        client.cookies.set("last_query", cookie)
+    else:
+        client.cookies.clear()
+    response = client.get(path)
+    assert response.status_code == expected_status
+    assert response.json() == expected_response
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Read Query",
+                    "operationId": "read_query_items__get",
+                    "parameters": [
+                        {
+                            "required": False,
+                            "schema": {
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                                "title": "Q",
+                            },
+                            "name": "q",
+                            "in": "query",
+                        },
+                        {
+                            "required": False,
+                            "schema": {
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                                "title": "Last Query",
+                            },
+                            "name": "last_query",
+                            "in": "cookie",
+                        },
+                    ],
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_dependencies/test_tutorial007.py b/tests/test_tutorial/test_dependencies/test_tutorial007.py
new file mode 100644 (file)
index 0000000..3e188ab
--- /dev/null
@@ -0,0 +1,24 @@
+import asyncio
+from contextlib import asynccontextmanager
+from unittest.mock import Mock, patch
+
+from docs_src.dependencies.tutorial007_py39 import get_db
+
+
+def test_get_db():  # Just for coverage
+    async def test_async_gen():
+        cm = asynccontextmanager(get_db)
+        async with cm() as db_session:
+            return db_session
+
+    dbsession_moock = Mock()
+
+    with patch(
+        "docs_src.dependencies.tutorial007_py39.DBSession",
+        return_value=dbsession_moock,
+        create=True,
+    ):
+        value = asyncio.run(test_async_gen())
+
+    assert value is dbsession_moock
+    dbsession_moock.close.assert_called_once()
diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008.py b/tests/test_tutorial/test_dependencies/test_tutorial008.py
new file mode 100644 (file)
index 0000000..9d7377e
--- /dev/null
@@ -0,0 +1,58 @@
+import importlib
+from types import ModuleType
+from typing import Annotated, Any
+from unittest.mock import Mock, patch
+
+import pytest
+from fastapi import Depends, FastAPI
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(
+    name="module",
+    params=[
+        "tutorial008_py39",
+        # Fails with `NameError: name 'DepA' is not defined`
+        pytest.param("tutorial008_an_py39", marks=pytest.mark.xfail),
+    ],
+)
+def get_module(request: pytest.FixtureRequest):
+    mod_name = f"docs_src.dependencies.{request.param}"
+    mod = importlib.import_module(mod_name)
+    return mod
+
+
+def test_get_db(module: ModuleType):
+    app = FastAPI()
+
+    @app.get("/")
+    def read_root(c: Annotated[Any, Depends(module.dependency_c)]):
+        return {"c": str(c)}
+
+    client = TestClient(app)
+
+    a_mock = Mock()
+    b_mock = Mock()
+    c_mock = Mock()
+
+    with (
+        patch(
+            f"{module.__name__}.generate_dep_a",
+            return_value=a_mock,
+            create=True,
+        ),
+        patch(
+            f"{module.__name__}.generate_dep_b",
+            return_value=b_mock,
+            create=True,
+        ),
+        patch(
+            f"{module.__name__}.generate_dep_c",
+            return_value=c_mock,
+            create=True,
+        ),
+    ):
+        response = client.get("/")
+
+    assert response.status_code == 200
+    assert response.json() == {"c": str(c_mock)}
diff --git a/tests/test_tutorial/test_dependencies/test_tutorial010.py b/tests/test_tutorial/test_dependencies/test_tutorial010.py
new file mode 100644 (file)
index 0000000..6d3815a
--- /dev/null
@@ -0,0 +1,29 @@
+from typing import Annotated, Any
+from unittest.mock import Mock, patch
+
+from fastapi import Depends, FastAPI
+from fastapi.testclient import TestClient
+
+from docs_src.dependencies.tutorial010_py39 import get_db
+
+
+def test_get_db():
+    app = FastAPI()
+
+    @app.get("/")
+    def read_root(c: Annotated[Any, Depends(get_db)]):
+        return {"c": str(c)}
+
+    client = TestClient(app)
+
+    dbsession_mock = Mock()
+
+    with patch(
+        "docs_src.dependencies.tutorial010_py39.DBSession",
+        return_value=dbsession_mock,
+        create=True,
+    ):
+        response = client.get("/")
+
+    assert response.status_code == 200
+    assert response.json() == {"c": str(dbsession_mock)}
diff --git a/tests/test_tutorial/test_dependencies/test_tutorial011.py b/tests/test_tutorial/test_dependencies/test_tutorial011.py
new file mode 100644 (file)
index 0000000..4868254
--- /dev/null
@@ -0,0 +1,120 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        "tutorial011_py39",
+        pytest.param("tutorial011_an_py39"),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.dependencies.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+@pytest.mark.parametrize(
+    "path,expected_status,expected_response",
+    [
+        (
+            "/query-checker/",
+            200,
+            {"fixed_content_in_query": False},
+        ),
+        (
+            "/query-checker/?q=qwerty",
+            200,
+            {"fixed_content_in_query": False},
+        ),
+        (
+            "/query-checker/?q=foobar",
+            200,
+            {"fixed_content_in_query": True},
+        ),
+    ],
+)
+def test_get(path, expected_status, expected_response, client: TestClient):
+    response = client.get(path)
+    assert response.status_code == expected_status
+    assert response.json() == expected_response
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/query-checker/": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Read Query Check",
+                    "operationId": "read_query_check_query_checker__get",
+                    "parameters": [
+                        {
+                            "required": False,
+                            "schema": {
+                                "type": "string",
+                                "default": "",
+                                "title": "Q",
+                            },
+                            "name": "q",
+                            "in": "query",
+                        },
+                    ],
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_encoder/__init__.py b/tests/test_tutorial/test_encoder/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_encoder/test_tutorial001.py b/tests/test_tutorial/test_encoder/test_tutorial001.py
new file mode 100644 (file)
index 0000000..5c8ee05
--- /dev/null
@@ -0,0 +1,208 @@
+import importlib
+from types import ModuleType
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="mod",
+    params=[
+        pytest.param("tutorial001_py39"),
+        pytest.param("tutorial001_py310", marks=needs_py310),
+    ],
+)
+def get_module(request: pytest.FixtureRequest):
+    module = importlib.import_module(f"docs_src.encoder.{request.param}")
+    return module
+
+
+@pytest.fixture(name="client")
+def get_client(mod: ModuleType):
+    client = TestClient(mod.app)
+    return client
+
+
+def test_put(client: TestClient, mod: ModuleType):
+    fake_db = mod.fake_db
+
+    response = client.put(
+        "/items/123",
+        json={
+            "title": "Foo",
+            "timestamp": "2023-01-01T12:00:00",
+            "description": "An optional description",
+        },
+    )
+    assert response.status_code == 200
+    assert "123" in fake_db
+    assert fake_db["123"] == {
+        "title": "Foo",
+        "timestamp": "2023-01-01T12:00:00",
+        "description": "An optional description",
+    }
+
+
+def test_put_invalid_data(client: TestClient, mod: ModuleType):
+    fake_db = mod.fake_db
+
+    response = client.put(
+        "/items/345",
+        json={
+            "title": "Foo",
+            "timestamp": "not a date",
+        },
+    )
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["body", "timestamp"],
+                "msg": "Input should be a valid datetime or date, invalid character in year",
+                "type": "datetime_from_date_parsing",
+                "input": "not a date",
+                "ctx": {"error": "invalid character in year"},
+            }
+        ]
+    }
+    assert "345" not in fake_db
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{id}": {
+                "put": {
+                    "operationId": "update_item_items__id__put",
+                    "parameters": [
+                        {
+                            "in": "path",
+                            "name": "id",
+                            "required": True,
+                            "schema": {
+                                "title": "Id",
+                                "type": "string",
+                            },
+                        },
+                    ],
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Item",
+                                },
+                            },
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {},
+                                },
+                            },
+                            "description": "Successful Response",
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                    "summary": "Update Item",
+                },
+            },
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {
+                                "$ref": "#/components/schemas/ValidationError",
+                            },
+                            "title": "Detail",
+                            "type": "array",
+                        },
+                    },
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                },
+                "Item": {
+                    "properties": {
+                        "description": {
+                            "anyOf": [
+                                {
+                                    "type": "string",
+                                },
+                                {
+                                    "type": "null",
+                                },
+                            ],
+                            "title": "Description",
+                        },
+                        "timestamp": {
+                            "format": "date-time",
+                            "title": "Timestamp",
+                            "type": "string",
+                        },
+                        "title": {
+                            "title": "Title",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "title",
+                        "timestamp",
+                    ],
+                    "title": "Item",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "integer",
+                                    },
+                                ],
+                            },
+                            "title": "Location",
+                            "type": "array",
+                        },
+                        "msg": {
+                            "title": "Message",
+                            "type": "string",
+                        },
+                        "type": {
+                            "title": "Error Type",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "loc",
+                        "msg",
+                        "type",
+                    ],
+                    "title": "ValidationError",
+                    "type": "object",
+                },
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_extra_models/test_tutorial001_tutorial002.py
new file mode 100644 (file)
index 0000000..3f2f508
--- /dev/null
@@ -0,0 +1,156 @@
+import importlib
+
+import pytest
+from dirty_equals import IsList
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial001_py39"),
+        pytest.param("tutorial001_py310", marks=needs_py310),
+        pytest.param("tutorial002_py39"),
+        pytest.param("tutorial002_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.extra_models.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_post(client: TestClient):
+    response = client.post(
+        "/user/",
+        json={
+            "username": "johndoe",
+            "password": "secret",
+            "email": "johndoe@example.com",
+            "full_name": "John Doe",
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "username": "johndoe",
+        "email": "johndoe@example.com",
+        "full_name": "John Doe",
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/user/": {
+                "post": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/UserOut",
+                                    }
+                                }
+                            },
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Create User",
+                    "operationId": "create_user_user__post",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/UserIn"}
+                            }
+                        },
+                        "required": True,
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "UserIn": {
+                    "title": "UserIn",
+                    "required": IsList(
+                        "username", "password", "email", check_order=False
+                    ),
+                    "type": "object",
+                    "properties": {
+                        "username": {"title": "Username", "type": "string"},
+                        "password": {"title": "Password", "type": "string"},
+                        "email": {
+                            "title": "Email",
+                            "type": "string",
+                            "format": "email",
+                        },
+                        "full_name": {
+                            "title": "Full Name",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                    },
+                },
+                "UserOut": {
+                    "title": "UserOut",
+                    "required": ["username", "email"],
+                    "type": "object",
+                    "properties": {
+                        "username": {"title": "Username", "type": "string"},
+                        "email": {
+                            "title": "Email",
+                            "type": "string",
+                            "format": "email",
+                        },
+                        "full_name": {
+                            "title": "Full Name",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                    },
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
similarity index 70%
rename from tests/test_tutorial/test_first_steps/test_tutorial001.py
rename to tests/test_tutorial/test_first_steps/test_tutorial001_tutorial002_tutorial003.py
index c102bb99999a11bb8e225e3238c4a30e4ae4f687..aa65218cdef4c67f33b77c09e61377ac2974cf50 100644 (file)
@@ -1,9 +1,20 @@
+import importlib
+
 import pytest
 from fastapi.testclient import TestClient
 
-from docs_src.first_steps.tutorial001_py39 import app
 
-client = TestClient(app)
+@pytest.fixture(
+    name="client",
+    params=[
+        "tutorial001_py39",
+        "tutorial003_py39",
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.first_steps.{request.param}")
+    client = TestClient(mod.app)
+    return client
 
 
 @pytest.mark.parametrize(
@@ -13,13 +24,13 @@ client = TestClient(app)
         ("/nonexistent", 404, {"detail": "Not Found"}),
     ],
 )
-def test_get_path(path, expected_status, expected_response):
+def test_get_path(client: TestClient, path, expected_status, expected_response):
     response = client.get(path)
     assert response.status_code == expected_status
     assert response.json() == expected_response
 
 
-def test_openapi_schema():
+def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200
     assert response.json() == {
diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial001.py b/tests/test_tutorial/test_generate_clients/test_tutorial001.py
new file mode 100644 (file)
index 0000000..bbb66b4
--- /dev/null
@@ -0,0 +1,142 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial001_py39"),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.generate_clients.{request.param}")
+    client = TestClient(mod.app)
+    return client
+
+
+def test_post_items(client: TestClient):
+    response = client.post("/items/", json={"name": "Foo", "price": 5})
+    assert response.status_code == 200, response.text
+    assert response.json() == {"message": "item received"}
+
+
+def test_get_items(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200, response.text
+    assert response.json() == [
+        {"name": "Plumbus", "price": 3},
+        {"name": "Portal Gun", "price": 9001},
+    ]
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "get": {
+                    "summary": "Get Items",
+                    "operationId": "get_items_items__get",
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "title": "Response Get Items Items  Get",
+                                        "type": "array",
+                                        "items": {"$ref": "#/components/schemas/Item"},
+                                    }
+                                }
+                            },
+                        }
+                    },
+                },
+                "post": {
+                    "summary": "Create Item",
+                    "operationId": "create_item_items__post",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/Item"}
+                            }
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/ResponseMessage"
+                                    }
+                                }
+                            },
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                },
+            },
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+                "Item": {
+                    "title": "Item",
+                    "required": ["name", "price"],
+                    "type": "object",
+                    "properties": {
+                        "name": {"title": "Name", "type": "string"},
+                        "price": {"title": "Price", "type": "number"},
+                    },
+                },
+                "ResponseMessage": {
+                    "title": "ResponseMessage",
+                    "required": ["message"],
+                    "type": "object",
+                    "properties": {"message": {"title": "Message", "type": "string"}},
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial002.py b/tests/test_tutorial/test_generate_clients/test_tutorial002.py
new file mode 100644 (file)
index 0000000..ab8bc4c
--- /dev/null
@@ -0,0 +1,187 @@
+from fastapi.testclient import TestClient
+
+from docs_src.generate_clients.tutorial002_py39 import app
+
+client = TestClient(app)
+
+
+def test_post_items():
+    response = client.post("/items/", json={"name": "Foo", "price": 5})
+    assert response.status_code == 200, response.text
+    assert response.json() == {"message": "Item received"}
+
+
+def test_post_users():
+    response = client.post(
+        "/users/", json={"username": "Foo", "email": "foo@example.com"}
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {"message": "User received"}
+
+
+def test_get_items():
+    response = client.get("/items/")
+    assert response.status_code == 200, response.text
+    assert response.json() == [
+        {"name": "Plumbus", "price": 3},
+        {"name": "Portal Gun", "price": 9001},
+    ]
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "get": {
+                    "tags": ["items"],
+                    "summary": "Get Items",
+                    "operationId": "get_items_items__get",
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "title": "Response Get Items Items  Get",
+                                        "type": "array",
+                                        "items": {"$ref": "#/components/schemas/Item"},
+                                    }
+                                }
+                            },
+                        }
+                    },
+                },
+                "post": {
+                    "tags": ["items"],
+                    "summary": "Create Item",
+                    "operationId": "create_item_items__post",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/Item"}
+                            }
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/ResponseMessage"
+                                    }
+                                }
+                            },
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                },
+            },
+            "/users/": {
+                "post": {
+                    "tags": ["users"],
+                    "summary": "Create User",
+                    "operationId": "create_user_users__post",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/User"}
+                            }
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/ResponseMessage"
+                                    }
+                                }
+                            },
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            },
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+                "Item": {
+                    "title": "Item",
+                    "required": ["name", "price"],
+                    "type": "object",
+                    "properties": {
+                        "name": {"title": "Name", "type": "string"},
+                        "price": {"title": "Price", "type": "number"},
+                    },
+                },
+                "ResponseMessage": {
+                    "title": "ResponseMessage",
+                    "required": ["message"],
+                    "type": "object",
+                    "properties": {"message": {"title": "Message", "type": "string"}},
+                },
+                "User": {
+                    "title": "User",
+                    "required": ["username", "email"],
+                    "type": "object",
+                    "properties": {
+                        "username": {"title": "Username", "type": "string"},
+                        "email": {"title": "Email", "type": "string"},
+                    },
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial004.py b/tests/test_tutorial/test_generate_clients/test_tutorial004.py
new file mode 100644 (file)
index 0000000..e66f6d2
--- /dev/null
@@ -0,0 +1,230 @@
+import importlib
+import json
+import pathlib
+from unittest.mock import patch
+
+from docs_src.generate_clients import tutorial003_py39
+
+
+def test_remove_tags(tmp_path: pathlib.Path):
+    tmp_file = tmp_path / "openapi.json"
+    openapi_json = tutorial003_py39.app.openapi()
+    tmp_file.write_text(json.dumps(openapi_json))
+
+    with patch("pathlib.Path", return_value=tmp_file):
+        importlib.import_module("docs_src.generate_clients.tutorial004_py39")
+
+    modified_openapi = json.loads(tmp_file.read_text())
+    assert modified_openapi == {
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {
+                                "$ref": "#/components/schemas/ValidationError",
+                            },
+                            "title": "Detail",
+                            "type": "array",
+                        },
+                    },
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                },
+                "Item": {
+                    "properties": {
+                        "name": {
+                            "title": "Name",
+                            "type": "string",
+                        },
+                        "price": {
+                            "title": "Price",
+                            "type": "number",
+                        },
+                    },
+                    "required": [
+                        "name",
+                        "price",
+                    ],
+                    "title": "Item",
+                    "type": "object",
+                },
+                "ResponseMessage": {
+                    "properties": {
+                        "message": {
+                            "title": "Message",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "message",
+                    ],
+                    "title": "ResponseMessage",
+                    "type": "object",
+                },
+                "User": {
+                    "properties": {
+                        "email": {
+                            "title": "Email",
+                            "type": "string",
+                        },
+                        "username": {
+                            "title": "Username",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "username",
+                        "email",
+                    ],
+                    "title": "User",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "integer",
+                                    },
+                                ],
+                            },
+                            "title": "Location",
+                            "type": "array",
+                        },
+                        "msg": {
+                            "title": "Message",
+                            "type": "string",
+                        },
+                        "type": {
+                            "title": "Error Type",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "loc",
+                        "msg",
+                        "type",
+                    ],
+                    "title": "ValidationError",
+                    "type": "object",
+                },
+            },
+        },
+        "info": {
+            "title": "FastAPI",
+            "version": "0.1.0",
+        },
+        "openapi": "3.1.0",
+        "paths": {
+            "/items/": {
+                "get": {
+                    "operationId": "get_items",
+                    "responses": {
+                        "200": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "items": {
+                                            "$ref": "#/components/schemas/Item",
+                                        },
+                                        "title": "Response Items-Get Items",
+                                        "type": "array",
+                                    },
+                                },
+                            },
+                            "description": "Successful Response",
+                        },
+                    },
+                    "summary": "Get Items",
+                    "tags": [
+                        "items",
+                    ],
+                },
+                "post": {
+                    "operationId": "create_item",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Item",
+                                },
+                            },
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/ResponseMessage",
+                                    },
+                                },
+                            },
+                            "description": "Successful Response",
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                    "summary": "Create Item",
+                    "tags": [
+                        "items",
+                    ],
+                },
+            },
+            "/users/": {
+                "post": {
+                    "operationId": "create_user",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/User",
+                                },
+                            },
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/ResponseMessage",
+                                    },
+                                },
+                            },
+                            "description": "Successful Response",
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                    "summary": "Create User",
+                    "tags": [
+                        "users",
+                    ],
+                },
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_graphql/__init__.py b/tests/test_tutorial/test_graphql/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_graphql/test_tutorial001.py b/tests/test_tutorial/test_graphql/test_tutorial001.py
new file mode 100644 (file)
index 0000000..9ba7147
--- /dev/null
@@ -0,0 +1,70 @@
+import warnings
+
+import pytest
+from starlette.testclient import TestClient
+
+warnings.filterwarnings(
+    "ignore",
+    message=r"The 'lia' package has been renamed to 'cross_web'\..*",
+    category=DeprecationWarning,
+)
+
+from docs_src.graphql_.tutorial001_py39 import app  # noqa: E402
+
+
+@pytest.fixture(name="client")
+def get_client() -> TestClient:
+    return TestClient(app)
+
+
+def test_query(client: TestClient):
+    response = client.post("/graphql", json={"query": "{ user { name, age } }"})
+    assert response.status_code == 200
+    assert response.json() == {"data": {"user": {"name": "Patrick", "age": 100}}}
+
+
+def test_openapi(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200
+    assert response.json() == {
+        "info": {
+            "title": "FastAPI",
+            "version": "0.1.0",
+        },
+        "openapi": "3.1.0",
+        "paths": {
+            "/graphql": {
+                "get": {
+                    "operationId": "handle_http_get_graphql_get",
+                    "responses": {
+                        "200": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {},
+                                },
+                            },
+                            "description": "The GraphiQL integrated development environment.",
+                        },
+                        "404": {
+                            "description": "Not found if GraphiQL or query via GET are not enabled.",
+                        },
+                    },
+                    "summary": "Handle Http Get",
+                },
+                "post": {
+                    "operationId": "handle_http_post_graphql_post",
+                    "responses": {
+                        "200": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {},
+                                },
+                            },
+                            "description": "Successful Response",
+                        },
+                    },
+                    "summary": "Handle Http Post",
+                },
+            },
+        },
+    }
similarity index 61%
rename from tests/test_tutorial/test_custom_response/test_tutorial004.py
rename to tests/test_tutorial/test_metadata/test_tutorial002.py
index 0e7d69791b4ef393eba56124933061a61527df4b..e2814c88f985e7dc5555b3e44ea9ab11480d35d8 100644 (file)
@@ -1,45 +1,41 @@
 from fastapi.testclient import TestClient
 
-from docs_src.custom_response.tutorial004_py39 import app
+from docs_src.metadata.tutorial002_py39 import app
 
 client = TestClient(app)
 
 
-html_contents = """
-    <html>
-        <head>
-            <title>Some HTML in here</title>
-        </head>
-        <body>
-            <h1>Look ma! HTML!</h1>
-        </body>
-    </html>
-    """
-
-
-def test_get_custom_response():
+def test_items():
     response = client.get("/items/")
     assert response.status_code == 200, response.text
-    assert response.text == html_contents
+    assert response.json() == [{"name": "Foo"}]
 
 
-def test_openapi_schema():
+def test_get_openapi_json_default_url():
     response = client.get("/openapi.json")
+    assert response.status_code == 404, response.text
+
+
+def test_openapi_schema():
+    response = client.get("/api/v1/openapi.json")
     assert response.status_code == 200, response.text
     assert response.json() == {
         "openapi": "3.1.0",
-        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "info": {
+            "title": "FastAPI",
+            "version": "0.1.0",
+        },
         "paths": {
             "/items/": {
                 "get": {
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__get",
                     "responses": {
                         "200": {
                             "description": "Successful Response",
-                            "content": {"text/html": {"schema": {"type": "string"}}},
+                            "content": {"application/json": {"schema": {}}},
                         }
                     },
-                    "summary": "Read Items",
-                    "operationId": "read_items_items__get",
                 }
             }
         },
diff --git a/tests/test_tutorial/test_metadata/test_tutorial003.py b/tests/test_tutorial/test_metadata/test_tutorial003.py
new file mode 100644 (file)
index 0000000..085c271
--- /dev/null
@@ -0,0 +1,53 @@
+from fastapi.testclient import TestClient
+
+from docs_src.metadata.tutorial003_py39 import app
+
+client = TestClient(app)
+
+
+def test_items():
+    response = client.get("/items/")
+    assert response.status_code == 200, response.text
+    assert response.json() == [{"name": "Foo"}]
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {
+            "title": "FastAPI",
+            "version": "0.1.0",
+        },
+        "paths": {
+            "/items/": {
+                "get": {
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__get",
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        }
+                    },
+                }
+            }
+        },
+    }
+
+
+def test_swagger_ui_default_url():
+    response = client.get("/docs")
+    assert response.status_code == 404, response.text
+
+
+def test_swagger_ui_custom_url():
+    response = client.get("/documentation")
+    assert response.status_code == 200, response.text
+    assert "<title>FastAPI - Swagger UI</title>" in response.text
+
+
+def test_redoc_ui_default_url():
+    response = client.get("/redoc")
+    assert response.status_code == 404, response.text
diff --git a/tests/test_tutorial/test_middleware/__init__.py b/tests/test_tutorial/test_middleware/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_middleware/test_tutorial001.py b/tests/test_tutorial/test_middleware/test_tutorial001.py
new file mode 100644 (file)
index 0000000..cbcfd41
--- /dev/null
@@ -0,0 +1,24 @@
+from fastapi.testclient import TestClient
+
+from docs_src.middleware.tutorial001_py39 import app
+
+client = TestClient(app)
+
+
+def test_response_headers():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert "X-Process-Time" in response.headers
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {
+            "title": "FastAPI",
+            "version": "0.1.0",
+        },
+        "paths": {},
+    }
diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial001.py
new file mode 100644 (file)
index 0000000..085d1f5
--- /dev/null
@@ -0,0 +1,186 @@
+import importlib
+
+import pytest
+from dirty_equals import IsList
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial001_py39"),
+        pytest.param("tutorial001_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest) -> TestClient:
+    mod = importlib.import_module(
+        f"docs_src.path_operation_configuration.{request.param}"
+    )
+    return TestClient(mod.app)
+
+
+def test_post_items(client: TestClient):
+    response = client.post(
+        "/items/",
+        json={
+            "name": "Foo",
+            "description": "Item description",
+            "price": 42.0,
+            "tax": 3.2,
+            "tags": ["bar", "baz"],
+        },
+    )
+    assert response.status_code == 201, response.text
+    assert response.json() == {
+        "name": "Foo",
+        "description": "Item description",
+        "price": 42.0,
+        "tax": 3.2,
+        "tags": IsList("bar", "baz", check_order=False),
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "post": {
+                    "summary": "Create Item",
+                    "operationId": "create_item_items__post",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/Item"}
+                            }
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "201": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {"$ref": "#/components/schemas/Item"}
+                                }
+                            },
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                },
+            },
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {
+                                "$ref": "#/components/schemas/ValidationError",
+                            },
+                            "title": "Detail",
+                            "type": "array",
+                        },
+                    },
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                },
+                "Item": {
+                    "properties": {
+                        "description": {
+                            "anyOf": [
+                                {
+                                    "type": "string",
+                                },
+                                {
+                                    "type": "null",
+                                },
+                            ],
+                            "title": "Description",
+                        },
+                        "name": {
+                            "title": "Name",
+                            "type": "string",
+                        },
+                        "price": {
+                            "title": "Price",
+                            "type": "number",
+                        },
+                        "tags": {
+                            "default": [],
+                            "items": {
+                                "type": "string",
+                            },
+                            "title": "Tags",
+                            "type": "array",
+                            "uniqueItems": True,
+                        },
+                        "tax": {
+                            "anyOf": [
+                                {
+                                    "type": "number",
+                                },
+                                {
+                                    "type": "null",
+                                },
+                            ],
+                            "title": "Tax",
+                        },
+                    },
+                    "required": [
+                        "name",
+                        "price",
+                    ],
+                    "title": "Item",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "integer",
+                                    },
+                                ],
+                            },
+                            "title": "Location",
+                            "type": "array",
+                        },
+                        "msg": {
+                            "title": "Message",
+                            "type": "string",
+                        },
+                        "type": {
+                            "title": "Error Type",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "loc",
+                        "msg",
+                        "type",
+                    ],
+                    "title": "ValidationError",
+                    "type": "object",
+                },
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002.py
new file mode 100644 (file)
index 0000000..c7414d7
--- /dev/null
@@ -0,0 +1,223 @@
+import importlib
+
+import pytest
+from dirty_equals import IsList
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial002_py39"),
+        pytest.param("tutorial002_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest) -> TestClient:
+    mod = importlib.import_module(
+        f"docs_src.path_operation_configuration.{request.param}"
+    )
+    return TestClient(mod.app)
+
+
+def test_post_items(client: TestClient):
+    response = client.post(
+        "/items/",
+        json={
+            "name": "Foo",
+            "description": "Item description",
+            "price": 42.0,
+            "tax": 3.2,
+            "tags": ["bar", "baz"],
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "name": "Foo",
+        "description": "Item description",
+        "price": 42.0,
+        "tax": 3.2,
+        "tags": IsList("bar", "baz", check_order=False),
+    }
+
+
+def test_get_items(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200, response.text
+    assert response.json() == [{"name": "Foo", "price": 42}]
+
+
+def test_get_users(client: TestClient):
+    response = client.get("/users/")
+    assert response.status_code == 200, response.text
+    assert response.json() == [{"username": "johndoe"}]
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "get": {
+                    "tags": ["items"],
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__get",
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        }
+                    },
+                },
+                "post": {
+                    "tags": ["items"],
+                    "summary": "Create Item",
+                    "operationId": "create_item_items__post",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/Item"}
+                            }
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {"$ref": "#/components/schemas/Item"}
+                                }
+                            },
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                },
+            },
+            "/users/": {
+                "get": {
+                    "tags": ["users"],
+                    "summary": "Read Users",
+                    "operationId": "read_users_users__get",
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        }
+                    },
+                }
+            },
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {
+                                "$ref": "#/components/schemas/ValidationError",
+                            },
+                            "title": "Detail",
+                            "type": "array",
+                        },
+                    },
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                },
+                "Item": {
+                    "properties": {
+                        "description": {
+                            "anyOf": [
+                                {
+                                    "type": "string",
+                                },
+                                {
+                                    "type": "null",
+                                },
+                            ],
+                            "title": "Description",
+                        },
+                        "name": {
+                            "title": "Name",
+                            "type": "string",
+                        },
+                        "price": {
+                            "title": "Price",
+                            "type": "number",
+                        },
+                        "tags": {
+                            "default": [],
+                            "items": {
+                                "type": "string",
+                            },
+                            "title": "Tags",
+                            "type": "array",
+                            "uniqueItems": True,
+                        },
+                        "tax": {
+                            "anyOf": [
+                                {
+                                    "type": "number",
+                                },
+                                {
+                                    "type": "null",
+                                },
+                            ],
+                            "title": "Tax",
+                        },
+                    },
+                    "required": [
+                        "name",
+                        "price",
+                    ],
+                    "title": "Item",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "integer",
+                                    },
+                                ],
+                            },
+                            "title": "Location",
+                            "type": "array",
+                        },
+                        "msg": {
+                            "title": "Message",
+                            "type": "string",
+                        },
+                        "type": {
+                            "title": "Error Type",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "loc",
+                        "msg",
+                        "type",
+                    ],
+                    "title": "ValidationError",
+                    "type": "object",
+                },
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py
new file mode 100644 (file)
index 0000000..791db24
--- /dev/null
@@ -0,0 +1,208 @@
+import importlib
+from textwrap import dedent
+
+import pytest
+from dirty_equals import IsList
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+DESCRIPTIONS = {
+    "tutorial003": "Create an item with all the information, name, description, price, tax and a set of unique tags",
+    "tutorial004": dedent("""
+        Create an item with all the information:
+
+        - **name**: each item must have a name
+        - **description**: a long description
+        - **price**: required
+        - **tax**: if the item doesn't have tax, you can omit this
+        - **tags**: a set of unique tag strings for this item
+    """).strip(),
+}
+
+
+@pytest.fixture(
+    name="mod_name",
+    params=[
+        pytest.param("tutorial003_py39"),
+        pytest.param("tutorial003_py310", marks=needs_py310),
+        pytest.param("tutorial004_py39"),
+        pytest.param("tutorial004_py310", marks=needs_py310),
+    ],
+)
+def get_mod_name(request: pytest.FixtureRequest) -> str:
+    return request.param
+
+
+@pytest.fixture(name="client")
+def get_client(mod_name: str) -> TestClient:
+    mod = importlib.import_module(f"docs_src.path_operation_configuration.{mod_name}")
+    return TestClient(mod.app)
+
+
+def test_post_items(client: TestClient):
+    response = client.post(
+        "/items/",
+        json={
+            "name": "Foo",
+            "description": "Item description",
+            "price": 42.0,
+            "tax": 3.2,
+            "tags": ["bar", "baz"],
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "name": "Foo",
+        "description": "Item description",
+        "price": 42.0,
+        "tax": 3.2,
+        "tags": IsList("bar", "baz", check_order=False),
+    }
+
+
+def test_openapi_schema(client: TestClient, mod_name: str):
+    mod_name = mod_name[:11]
+
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "post": {
+                    "summary": "Create an item",
+                    "description": DESCRIPTIONS[mod_name],
+                    "operationId": "create_item_items__post",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/Item"}
+                            }
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {"$ref": "#/components/schemas/Item"}
+                                }
+                            },
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                },
+            },
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {
+                                "$ref": "#/components/schemas/ValidationError",
+                            },
+                            "title": "Detail",
+                            "type": "array",
+                        },
+                    },
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                },
+                "Item": {
+                    "properties": {
+                        "description": {
+                            "anyOf": [
+                                {
+                                    "type": "string",
+                                },
+                                {
+                                    "type": "null",
+                                },
+                            ],
+                            "title": "Description",
+                        },
+                        "name": {
+                            "title": "Name",
+                            "type": "string",
+                        },
+                        "price": {
+                            "title": "Price",
+                            "type": "number",
+                        },
+                        "tags": {
+                            "default": [],
+                            "items": {
+                                "type": "string",
+                            },
+                            "title": "Tags",
+                            "type": "array",
+                            "uniqueItems": True,
+                        },
+                        "tax": {
+                            "anyOf": [
+                                {
+                                    "type": "number",
+                                },
+                                {
+                                    "type": "null",
+                                },
+                            ],
+                            "title": "Tax",
+                        },
+                    },
+                    "required": [
+                        "name",
+                        "price",
+                    ],
+                    "title": "Item",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "integer",
+                                    },
+                                ],
+                            },
+                            "title": "Location",
+                            "type": "array",
+                        },
+                        "msg": {
+                            "title": "Message",
+                            "type": "string",
+                        },
+                        "type": {
+                            "title": "Error Type",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "loc",
+                        "msg",
+                        "type",
+                    ],
+                    "title": "ValidationError",
+                    "type": "object",
+                },
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_path_params/test_tutorial001.py b/tests/test_tutorial/test_path_params/test_tutorial001.py
new file mode 100644 (file)
index 0000000..a898e38
--- /dev/null
@@ -0,0 +1,116 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from docs_src.path_params.tutorial001_py39 import app
+
+client = TestClient(app)
+
+
+@pytest.mark.parametrize(
+    ("item_id", "expected_response"),
+    [
+        (1, {"item_id": "1"}),
+        ("alice", {"item_id": "alice"}),
+    ],
+)
+def test_get_items(item_id, expected_response):
+    response = client.get(f"/items/{item_id}")
+    assert response.status_code == 200, response.text
+    assert response.json() == expected_response
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "get": {
+                    "operationId": "read_item_items__item_id__get",
+                    "parameters": [
+                        {
+                            "in": "path",
+                            "name": "item_id",
+                            "required": True,
+                            "schema": {
+                                "title": "Item Id",
+                            },
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {},
+                                },
+                            },
+                            "description": "Successful Response",
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                    "summary": "Read Item",
+                },
+            },
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {
+                                "$ref": "#/components/schemas/ValidationError",
+                            },
+                            "title": "Detail",
+                            "type": "array",
+                        },
+                    },
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "integer",
+                                    },
+                                ],
+                            },
+                            "title": "Location",
+                            "type": "array",
+                        },
+                        "msg": {
+                            "title": "Message",
+                            "type": "string",
+                        },
+                        "type": {
+                            "title": "Error Type",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "loc",
+                        "msg",
+                        "type",
+                    ],
+                    "title": "ValidationError",
+                    "type": "object",
+                },
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_path_params/test_tutorial002.py b/tests/test_tutorial/test_path_params/test_tutorial002.py
new file mode 100644 (file)
index 0000000..0bfc9f8
--- /dev/null
@@ -0,0 +1,124 @@
+from fastapi.testclient import TestClient
+
+from docs_src.path_params.tutorial002_py39 import app
+
+client = TestClient(app)
+
+
+def test_get_items():
+    response = client.get("/items/1")
+    assert response.status_code == 200, response.text
+    assert response.json() == {"item_id": 1}
+
+
+def test_get_items_invalid_id():
+    response = client.get("/items/item1")
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "input": "item1",
+                "loc": ["path", "item_id"],
+                "msg": "Input should be a valid integer, unable to parse string as an integer",
+                "type": "int_parsing",
+            }
+        ]
+    }
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "get": {
+                    "operationId": "read_item_items__item_id__get",
+                    "parameters": [
+                        {
+                            "in": "path",
+                            "name": "item_id",
+                            "required": True,
+                            "schema": {
+                                "title": "Item Id",
+                                "type": "integer",
+                            },
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {},
+                                },
+                            },
+                            "description": "Successful Response",
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                    "summary": "Read Item",
+                },
+            },
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {
+                                "$ref": "#/components/schemas/ValidationError",
+                            },
+                            "title": "Detail",
+                            "type": "array",
+                        },
+                    },
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "integer",
+                                    },
+                                ],
+                            },
+                            "title": "Location",
+                            "type": "array",
+                        },
+                        "msg": {
+                            "title": "Message",
+                            "type": "string",
+                        },
+                        "type": {
+                            "title": "Error Type",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "loc",
+                        "msg",
+                        "type",
+                    ],
+                    "title": "ValidationError",
+                    "type": "object",
+                },
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_path_params/test_tutorial003.py b/tests/test_tutorial/test_path_params/test_tutorial003.py
new file mode 100644 (file)
index 0000000..cd2c39a
--- /dev/null
@@ -0,0 +1,133 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from docs_src.path_params.tutorial003_py39 import app
+
+client = TestClient(app)
+
+
+@pytest.mark.parametrize(
+    ("user_id", "expected_response"),
+    [
+        ("me", {"user_id": "the current user"}),
+        ("alice", {"user_id": "alice"}),
+    ],
+)
+def test_get_users(user_id: str, expected_response: dict):
+    response = client.get(f"/users/{user_id}")
+    assert response.status_code == 200, response.text
+    assert response.json() == expected_response
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/users/me": {
+                "get": {
+                    "operationId": "read_user_me_users_me_get",
+                    "responses": {
+                        "200": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {},
+                                },
+                            },
+                            "description": "Successful Response",
+                        },
+                    },
+                    "summary": "Read User Me",
+                },
+            },
+            "/users/{user_id}": {
+                "get": {
+                    "operationId": "read_user_users__user_id__get",
+                    "parameters": [
+                        {
+                            "in": "path",
+                            "name": "user_id",
+                            "required": True,
+                            "schema": {
+                                "title": "User Id",
+                                "type": "string",
+                            },
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {},
+                                },
+                            },
+                            "description": "Successful Response",
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                    "summary": "Read User",
+                },
+            },
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {
+                                "$ref": "#/components/schemas/ValidationError",
+                            },
+                            "title": "Detail",
+                            "type": "array",
+                        },
+                    },
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "integer",
+                                    },
+                                ],
+                            },
+                            "title": "Location",
+                            "type": "array",
+                        },
+                        "msg": {
+                            "title": "Message",
+                            "type": "string",
+                        },
+                        "type": {
+                            "title": "Error Type",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "loc",
+                        "msg",
+                        "type",
+                    ],
+                    "title": "ValidationError",
+                    "type": "object",
+                },
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_path_params/test_tutorial003b.py b/tests/test_tutorial/test_path_params/test_tutorial003b.py
new file mode 100644 (file)
index 0000000..8e4a26a
--- /dev/null
@@ -0,0 +1,44 @@
+import asyncio
+
+from fastapi.testclient import TestClient
+
+from docs_src.path_params.tutorial003b_py39 import app, read_users2
+
+client = TestClient(app)
+
+
+def test_get_users():
+    response = client.get("/users")
+    assert response.status_code == 200, response.text
+    assert response.json() == ["Rick", "Morty"]
+
+
+def test_read_users2():  # Just for coverage
+    assert asyncio.run(read_users2()) == ["Bean", "Elfo"]
+
+
+def test_openapi_schema():
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/users": {
+                "get": {
+                    "operationId": "read_users2_users_get",
+                    "responses": {
+                        "200": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {},
+                                },
+                            },
+                            "description": "Successful Response",
+                        },
+                    },
+                    "summary": "Read Users2",
+                },
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_path_params_numeric_validations/__init__.py b/tests/test_tutorial/test_path_params_numeric_validations/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial001.py
new file mode 100644 (file)
index 0000000..f1e3041
--- /dev/null
@@ -0,0 +1,164 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial001_py39"),
+        pytest.param("tutorial001_py310", marks=needs_py310),
+        pytest.param("tutorial001_an_py39"),
+        pytest.param("tutorial001_an_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest) -> TestClient:
+    mod = importlib.import_module(
+        f"docs_src.path_params_numeric_validations.{request.param}"
+    )
+    return TestClient(mod.app)
+
+
+@pytest.mark.parametrize(
+    "path,expected_response",
+    [
+        ("/items/42", {"item_id": 42}),
+        ("/items/123?item-query=somequery", {"item_id": 123, "q": "somequery"}),
+    ],
+)
+def test_read_items(client: TestClient, path, expected_response):
+    response = client.get(path)
+    assert response.status_code == 200, response.text
+    assert response.json() == expected_response
+
+
+def test_read_items_invalid_item_id(client: TestClient):
+    response = client.get("/items/invalid_id")
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["path", "item_id"],
+                "input": "invalid_id",
+                "msg": "Input should be a valid integer, unable to parse string as an integer",
+                "type": "int_parsing",
+            }
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "get": {
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__item_id__get",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {
+                                "title": "The ID of the item to get",
+                                "type": "integer",
+                            },
+                            "name": "item_id",
+                            "in": "path",
+                        },
+                        {
+                            "required": False,
+                            "schema": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "null",
+                                    },
+                                ],
+                                "title": "Item-Query",
+                            },
+                            "name": "item-query",
+                            "in": "query",
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {},
+                                }
+                            },
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {
+                                "$ref": "#/components/schemas/ValidationError",
+                            },
+                            "title": "Detail",
+                            "type": "array",
+                        },
+                    },
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "integer",
+                                    },
+                                ],
+                            },
+                            "title": "Location",
+                            "type": "array",
+                        },
+                        "msg": {
+                            "title": "Message",
+                            "type": "string",
+                        },
+                        "type": {
+                            "title": "Error Type",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "loc",
+                        "msg",
+                        "type",
+                    ],
+                    "title": "ValidationError",
+                    "type": "object",
+                },
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial002_tutorial003.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial002_tutorial003.py
new file mode 100644 (file)
index 0000000..467c915
--- /dev/null
@@ -0,0 +1,170 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial002_py39"),
+        pytest.param("tutorial002_an_py39"),
+        pytest.param("tutorial003_py39"),
+        pytest.param("tutorial003_an_py39"),
+    ],
+)
+def get_client(request: pytest.FixtureRequest) -> TestClient:
+    mod = importlib.import_module(
+        f"docs_src.path_params_numeric_validations.{request.param}"
+    )
+    return TestClient(mod.app)
+
+
+@pytest.mark.parametrize(
+    "path,expected_response",
+    [
+        ("/items/42?q=", {"item_id": 42}),
+        ("/items/123?q=somequery", {"item_id": 123, "q": "somequery"}),
+    ],
+)
+def test_read_items(client: TestClient, path, expected_response):
+    response = client.get(path)
+    assert response.status_code == 200, response.text
+    assert response.json() == expected_response
+
+
+def test_read_items_invalid_item_id(client: TestClient):
+    response = client.get("/items/invalid_id?q=somequery")
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["path", "item_id"],
+                "input": "invalid_id",
+                "msg": "Input should be a valid integer, unable to parse string as an integer",
+                "type": "int_parsing",
+            }
+        ]
+    }
+
+
+def test_read_items_missing_q(client: TestClient):
+    response = client.get("/items/42")
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["query", "q"],
+                "input": None,
+                "msg": "Field required",
+                "type": "missing",
+            }
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "get": {
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__item_id__get",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {
+                                "title": "The ID of the item to get",
+                                "type": "integer",
+                            },
+                            "name": "item_id",
+                            "in": "path",
+                        },
+                        {
+                            "required": True,
+                            "schema": {
+                                "type": "string",
+                                "title": "Q",
+                            },
+                            "name": "q",
+                            "in": "query",
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {},
+                                }
+                            },
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {
+                                "$ref": "#/components/schemas/ValidationError",
+                            },
+                            "title": "Detail",
+                            "type": "array",
+                        },
+                    },
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "integer",
+                                    },
+                                ],
+                            },
+                            "title": "Location",
+                            "type": "array",
+                        },
+                        "msg": {
+                            "title": "Message",
+                            "type": "string",
+                        },
+                        "type": {
+                            "title": "Error Type",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "loc",
+                        "msg",
+                        "type",
+                    ],
+                    "title": "ValidationError",
+                    "type": "object",
+                },
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial004.py
new file mode 100644 (file)
index 0000000..d3593c9
--- /dev/null
@@ -0,0 +1,185 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial004_py39"),
+        pytest.param("tutorial004_an_py39"),
+    ],
+)
+def get_client(request: pytest.FixtureRequest) -> TestClient:
+    mod = importlib.import_module(
+        f"docs_src.path_params_numeric_validations.{request.param}"
+    )
+    return TestClient(mod.app)
+
+
+@pytest.mark.parametrize(
+    "path,expected_response",
+    [
+        ("/items/42?q=", {"item_id": 42}),
+        ("/items/1?q=somequery", {"item_id": 1, "q": "somequery"}),
+    ],
+)
+def test_read_items(client: TestClient, path, expected_response):
+    response = client.get(path)
+    assert response.status_code == 200, response.text
+    assert response.json() == expected_response
+
+
+def test_read_items_non_int_item_id(client: TestClient):
+    response = client.get("/items/invalid_id?q=somequery")
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["path", "item_id"],
+                "input": "invalid_id",
+                "msg": "Input should be a valid integer, unable to parse string as an integer",
+                "type": "int_parsing",
+            }
+        ]
+    }
+
+
+def test_read_items_item_id_less_than_one(client: TestClient):
+    response = client.get("/items/0?q=somequery")
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["path", "item_id"],
+                "input": "0",
+                "msg": "Input should be greater than or equal to 1",
+                "type": "greater_than_equal",
+                "ctx": {"ge": 1},
+            }
+        ]
+    }
+
+
+def test_read_items_missing_q(client: TestClient):
+    response = client.get("/items/42")
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["query", "q"],
+                "input": None,
+                "msg": "Field required",
+                "type": "missing",
+            }
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "get": {
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__item_id__get",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {
+                                "title": "The ID of the item to get",
+                                "type": "integer",
+                                "minimum": 1,
+                            },
+                            "name": "item_id",
+                            "in": "path",
+                        },
+                        {
+                            "required": True,
+                            "schema": {
+                                "type": "string",
+                                "title": "Q",
+                            },
+                            "name": "q",
+                            "in": "query",
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {},
+                                }
+                            },
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {
+                                "$ref": "#/components/schemas/ValidationError",
+                            },
+                            "title": "Detail",
+                            "type": "array",
+                        },
+                    },
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "integer",
+                                    },
+                                ],
+                            },
+                            "title": "Location",
+                            "type": "array",
+                        },
+                        "msg": {
+                            "title": "Message",
+                            "type": "string",
+                        },
+                        "type": {
+                            "title": "Error Type",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "loc",
+                        "msg",
+                        "type",
+                    ],
+                    "title": "ValidationError",
+                    "type": "object",
+                },
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial005.py
new file mode 100644 (file)
index 0000000..2961925
--- /dev/null
@@ -0,0 +1,202 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial005_py39"),
+        pytest.param("tutorial005_an_py39"),
+    ],
+)
+def get_client(request: pytest.FixtureRequest) -> TestClient:
+    mod = importlib.import_module(
+        f"docs_src.path_params_numeric_validations.{request.param}"
+    )
+    return TestClient(mod.app)
+
+
+@pytest.mark.parametrize(
+    "path,expected_response",
+    [
+        ("/items/1?q=", {"item_id": 1}),
+        ("/items/1000?q=somequery", {"item_id": 1000, "q": "somequery"}),
+    ],
+)
+def test_read_items(client: TestClient, path, expected_response):
+    response = client.get(path)
+    assert response.status_code == 200, response.text
+    assert response.json() == expected_response
+
+
+def test_read_items_non_int_item_id(client: TestClient):
+    response = client.get("/items/invalid_id?q=somequery")
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["path", "item_id"],
+                "input": "invalid_id",
+                "msg": "Input should be a valid integer, unable to parse string as an integer",
+                "type": "int_parsing",
+            }
+        ]
+    }
+
+
+def test_read_items_item_id_less_than_one(client: TestClient):
+    response = client.get("/items/0?q=somequery")
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["path", "item_id"],
+                "input": "0",
+                "msg": "Input should be greater than 0",
+                "type": "greater_than",
+                "ctx": {"gt": 0},
+            }
+        ]
+    }
+
+
+def test_read_items_item_id_greater_than_one_thousand(client: TestClient):
+    response = client.get("/items/1001?q=somequery")
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["path", "item_id"],
+                "input": "1001",
+                "msg": "Input should be less than or equal to 1000",
+                "type": "less_than_equal",
+                "ctx": {"le": 1000},
+            }
+        ]
+    }
+
+
+def test_read_items_missing_q(client: TestClient):
+    response = client.get("/items/42")
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["query", "q"],
+                "input": None,
+                "msg": "Field required",
+                "type": "missing",
+            }
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "get": {
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__item_id__get",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {
+                                "title": "The ID of the item to get",
+                                "type": "integer",
+                                "exclusiveMinimum": 0,
+                                "maximum": 1000,
+                            },
+                            "name": "item_id",
+                            "in": "path",
+                        },
+                        {
+                            "required": True,
+                            "schema": {
+                                "type": "string",
+                                "title": "Q",
+                            },
+                            "name": "q",
+                            "in": "query",
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {},
+                                }
+                            },
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {
+                                "$ref": "#/components/schemas/ValidationError",
+                            },
+                            "title": "Detail",
+                            "type": "array",
+                        },
+                    },
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "integer",
+                                    },
+                                ],
+                            },
+                            "title": "Location",
+                            "type": "array",
+                        },
+                        "msg": {
+                            "title": "Message",
+                            "type": "string",
+                        },
+                        "type": {
+                            "title": "Error Type",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "loc",
+                        "msg",
+                        "type",
+                    ],
+                    "title": "ValidationError",
+                    "type": "object",
+                },
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py b/tests/test_tutorial/test_path_params_numeric_validations/test_tutorial006.py
new file mode 100644 (file)
index 0000000..9dc7d7a
--- /dev/null
@@ -0,0 +1,221 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial006_py39"),
+        pytest.param("tutorial006_an_py39"),
+    ],
+)
+def get_client(request: pytest.FixtureRequest) -> TestClient:
+    mod = importlib.import_module(
+        f"docs_src.path_params_numeric_validations.{request.param}"
+    )
+    return TestClient(mod.app)
+
+
+@pytest.mark.parametrize(
+    "path,expected_response",
+    [
+        (
+            "/items/0?q=&size=0.1",
+            {"item_id": 0, "size": 0.1},
+        ),
+        (
+            "/items/1000?q=somequery&size=10.4",
+            {"item_id": 1000, "q": "somequery", "size": 10.4},
+        ),
+    ],
+)
+def test_read_items(client: TestClient, path, expected_response):
+    response = client.get(path)
+    assert response.status_code == 200, response.text
+    assert response.json() == expected_response
+
+
+def test_read_items_item_id_less_than_zero(client: TestClient):
+    response = client.get("/items/-1?q=somequery&size=5")
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["path", "item_id"],
+                "input": "-1",
+                "msg": "Input should be greater than or equal to 0",
+                "type": "greater_than_equal",
+                "ctx": {"ge": 0},
+            }
+        ]
+    }
+
+
+def test_read_items_item_id_greater_than_one_thousand(client: TestClient):
+    response = client.get("/items/1001?q=somequery&size=5")
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["path", "item_id"],
+                "input": "1001",
+                "msg": "Input should be less than or equal to 1000",
+                "type": "less_than_equal",
+                "ctx": {"le": 1000},
+            }
+        ]
+    }
+
+
+def test_read_items_size_too_small(client: TestClient):
+    response = client.get("/items/1?q=somequery&size=0.0")
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["query", "size"],
+                "input": "0.0",
+                "msg": "Input should be greater than 0",
+                "type": "greater_than",
+                "ctx": {"gt": 0.0},
+            }
+        ]
+    }
+
+
+def test_read_items_size_too_large(client: TestClient):
+    response = client.get("/items/1?q=somequery&size=10.5")
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {
+                "loc": ["query", "size"],
+                "input": "10.5",
+                "msg": "Input should be less than 10.5",
+                "type": "less_than",
+                "ctx": {"lt": 10.5},
+            }
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "get": {
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__item_id__get",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {
+                                "title": "The ID of the item to get",
+                                "type": "integer",
+                                "minimum": 0,
+                                "maximum": 1000,
+                            },
+                            "name": "item_id",
+                            "in": "path",
+                        },
+                        {
+                            "required": True,
+                            "schema": {
+                                "type": "string",
+                                "title": "Q",
+                            },
+                            "name": "q",
+                            "in": "query",
+                        },
+                        {
+                            "in": "query",
+                            "name": "size",
+                            "required": True,
+                            "schema": {
+                                "exclusiveMaximum": 10.5,
+                                "exclusiveMinimum": 0,
+                                "title": "Size",
+                                "type": "number",
+                            },
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {},
+                                }
+                            },
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {
+                                "$ref": "#/components/schemas/ValidationError",
+                            },
+                            "title": "Detail",
+                            "type": "array",
+                        },
+                    },
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "integer",
+                                    },
+                                ],
+                            },
+                            "title": "Location",
+                            "type": "array",
+                        },
+                        "msg": {
+                            "title": "Message",
+                            "type": "string",
+                        },
+                        "type": {
+                            "title": "Error Type",
+                            "type": "string",
+                        },
+                    },
+                    "required": [
+                        "loc",
+                        "msg",
+                        "type",
+                    ],
+                    "title": "ValidationError",
+                    "type": "object",
+                },
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_python_types/__init__.py b/tests/test_tutorial/test_python_types/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_python_types/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_python_types/test_tutorial001_tutorial002.py
new file mode 100644 (file)
index 0000000..ccb0968
--- /dev/null
@@ -0,0 +1,18 @@
+import runpy
+from unittest.mock import patch
+
+import pytest
+
+
+@pytest.mark.parametrize(
+    "module_name",
+    [
+        "tutorial001_py39",
+        "tutorial002_py39",
+    ],
+)
+def test_run_module(module_name: str):
+    with patch("builtins.print") as mock_print:
+        runpy.run_module(f"docs_src.python_types.{module_name}", run_name="__main__")
+
+    mock_print.assert_called_with("John Doe")
diff --git a/tests/test_tutorial/test_python_types/test_tutorial003.py b/tests/test_tutorial/test_python_types/test_tutorial003.py
new file mode 100644 (file)
index 0000000..34d2649
--- /dev/null
@@ -0,0 +1,12 @@
+import pytest
+
+from docs_src.python_types.tutorial003_py39 import get_name_with_age
+
+
+def test_get_name_with_age_pass_int():
+    with pytest.raises(TypeError):
+        get_name_with_age("John", 30)
+
+
+def test_get_name_with_age_pass_str():
+    assert get_name_with_age("John", "30") == "John is this old: 30"
diff --git a/tests/test_tutorial/test_python_types/test_tutorial004.py b/tests/test_tutorial/test_python_types/test_tutorial004.py
new file mode 100644 (file)
index 0000000..24af328
--- /dev/null
@@ -0,0 +1,5 @@
+from docs_src.python_types.tutorial004_py39 import get_name_with_age
+
+
+def test_get_name_with_age_pass_int():
+    assert get_name_with_age("John", 30) == "John is this old: 30"
diff --git a/tests/test_tutorial/test_python_types/test_tutorial005.py b/tests/test_tutorial/test_python_types/test_tutorial005.py
new file mode 100644 (file)
index 0000000..6d67ec4
--- /dev/null
@@ -0,0 +1,12 @@
+from docs_src.python_types.tutorial005_py39 import get_items
+
+
+def test_get_items():
+    res = get_items(
+        "item_a",
+        "item_b",
+        "item_c",
+        "item_d",
+        "item_e",
+    )
+    assert res == ("item_a", "item_b", "item_c", "item_d", "item_e")
diff --git a/tests/test_tutorial/test_python_types/test_tutorial006.py b/tests/test_tutorial/test_python_types/test_tutorial006.py
new file mode 100644 (file)
index 0000000..5097692
--- /dev/null
@@ -0,0 +1,16 @@
+from unittest.mock import patch
+
+from docs_src.python_types.tutorial006_py39 import process_items
+
+
+def test_process_items():
+    with patch("builtins.print") as mock_print:
+        process_items(["item_a", "item_b", "item_c"])
+
+    assert mock_print.call_count == 3
+    call_args = [arg.args for arg in mock_print.call_args_list]
+    assert call_args == [
+        ("item_a",),
+        ("item_b",),
+        ("item_c",),
+    ]
diff --git a/tests/test_tutorial/test_python_types/test_tutorial007.py b/tests/test_tutorial/test_python_types/test_tutorial007.py
new file mode 100644 (file)
index 0000000..c045294
--- /dev/null
@@ -0,0 +1,8 @@
+from docs_src.python_types.tutorial007_py39 import process_items
+
+
+def test_process_items():
+    items_t = (1, 2, "foo")
+    items_s = {b"a", b"b", b"c"}
+
+    assert process_items(items_t, items_s) == (items_t, items_s)
diff --git a/tests/test_tutorial/test_python_types/test_tutorial008.py b/tests/test_tutorial/test_python_types/test_tutorial008.py
new file mode 100644 (file)
index 0000000..33cf6cb
--- /dev/null
@@ -0,0 +1,17 @@
+from unittest.mock import patch
+
+from docs_src.python_types.tutorial008_py39 import process_items
+
+
+def test_process_items():
+    with patch("builtins.print") as mock_print:
+        process_items({"a": 1.0, "b": 2.5})
+
+    assert mock_print.call_count == 4
+    call_args = [arg.args for arg in mock_print.call_args_list]
+    assert call_args == [
+        ("a",),
+        (1.0,),
+        ("b",),
+        (2.5,),
+    ]
diff --git a/tests/test_tutorial/test_python_types/test_tutorial008b.py b/tests/test_tutorial/test_python_types/test_tutorial008b.py
new file mode 100644 (file)
index 0000000..1ef0d4e
--- /dev/null
@@ -0,0 +1,27 @@
+import importlib
+from types import ModuleType
+from unittest.mock import patch
+
+import pytest
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="module",
+    params=[
+        pytest.param("tutorial008b_py39"),
+        pytest.param("tutorial008b_py310", marks=needs_py310),
+    ],
+)
+def get_module(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.python_types.{request.param}")
+    return mod
+
+
+def test_process_items(module: ModuleType):
+    with patch("builtins.print") as mock_print:
+        module.process_item("a")
+
+    assert mock_print.call_count == 1
+    mock_print.assert_called_with("a")
diff --git a/tests/test_tutorial/test_python_types/test_tutorial009_tutorial009b.py b/tests/test_tutorial/test_python_types/test_tutorial009_tutorial009b.py
new file mode 100644 (file)
index 0000000..34046c5
--- /dev/null
@@ -0,0 +1,33 @@
+import importlib
+from types import ModuleType
+from unittest.mock import patch
+
+import pytest
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="module",
+    params=[
+        pytest.param("tutorial009_py39"),
+        pytest.param("tutorial009_py310", marks=needs_py310),
+        pytest.param("tutorial009b_py39"),
+    ],
+)
+def get_module(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.python_types.{request.param}")
+    return mod
+
+
+def test_say_hi(module: ModuleType):
+    with patch("builtins.print") as mock_print:
+        module.say_hi("FastAPI")
+        module.say_hi()
+
+    assert mock_print.call_count == 2
+    call_args = [arg.args for arg in mock_print.call_args_list]
+    assert call_args == [
+        ("Hey FastAPI!",),
+        ("Hello World",),
+    ]
diff --git a/tests/test_tutorial/test_python_types/test_tutorial009c.py b/tests/test_tutorial/test_python_types/test_tutorial009c.py
new file mode 100644 (file)
index 0000000..7bd4049
--- /dev/null
@@ -0,0 +1,33 @@
+import importlib
+import re
+from types import ModuleType
+from unittest.mock import patch
+
+import pytest
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="module",
+    params=[
+        pytest.param("tutorial009c_py39"),
+        pytest.param("tutorial009c_py310", marks=needs_py310),
+    ],
+)
+def get_module(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.python_types.{request.param}")
+    return mod
+
+
+def test_say_hi(module: ModuleType):
+    with patch("builtins.print") as mock_print:
+        module.say_hi("FastAPI")
+
+    mock_print.assert_called_once_with("Hey FastAPI!")
+
+    with pytest.raises(
+        TypeError,
+        match=re.escape("say_hi() missing 1 required positional argument: 'name'"),
+    ):
+        module.say_hi()
diff --git a/tests/test_tutorial/test_python_types/test_tutorial010.py b/tests/test_tutorial/test_python_types/test_tutorial010.py
new file mode 100644 (file)
index 0000000..9e4d2e3
--- /dev/null
@@ -0,0 +1,5 @@
+from docs_src.python_types.tutorial010_py39 import Person, get_person_name
+
+
+def test_get_person_name():
+    assert get_person_name(Person("John Doe")) == "John Doe"
diff --git a/tests/test_tutorial/test_python_types/test_tutorial011.py b/tests/test_tutorial/test_python_types/test_tutorial011.py
new file mode 100644 (file)
index 0000000..a05751b
--- /dev/null
@@ -0,0 +1,25 @@
+import runpy
+from unittest.mock import patch
+
+import pytest
+
+from ...utils import needs_py310
+
+
+@pytest.mark.parametrize(
+    "module_name",
+    [
+        pytest.param("tutorial011_py39"),
+        pytest.param("tutorial011_py310", marks=needs_py310),
+    ],
+)
+def test_run_module(module_name: str):
+    with patch("builtins.print") as mock_print:
+        runpy.run_module(f"docs_src.python_types.{module_name}", run_name="__main__")
+
+    assert mock_print.call_count == 2
+    call_args = [str(arg.args[0]) for arg in mock_print.call_args_list]
+    assert call_args == [
+        "id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3]",
+        "123",
+    ]
diff --git a/tests/test_tutorial/test_python_types/test_tutorial012.py b/tests/test_tutorial/test_python_types/test_tutorial012.py
new file mode 100644 (file)
index 0000000..e578048
--- /dev/null
@@ -0,0 +1,7 @@
+from docs_src.python_types.tutorial012_py39 import User
+
+
+def test_user():
+    user = User(name="John Doe", age=30)
+    assert user.name == "John Doe"
+    assert user.age == 30
diff --git a/tests/test_tutorial/test_python_types/test_tutorial013.py b/tests/test_tutorial/test_python_types/test_tutorial013.py
new file mode 100644 (file)
index 0000000..5602ef7
--- /dev/null
@@ -0,0 +1,5 @@
+from docs_src.python_types.tutorial013_py39 import say_hello
+
+
+def test_say_hello():
+    assert say_hello("FastAPI") == "Hello FastAPI"
diff --git a/tests/test_tutorial/test_query_params/test_tutorial001.py b/tests/test_tutorial/test_query_params/test_tutorial001.py
new file mode 100644 (file)
index 0000000..4c92b57
--- /dev/null
@@ -0,0 +1,126 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial001_py39"),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.query_params.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+@pytest.mark.parametrize(
+    ("path", "expected_json"),
+    [
+        (
+            "/items/",
+            [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}],
+        ),
+        (
+            "/items/?skip=1",
+            [{"item_name": "Bar"}, {"item_name": "Baz"}],
+        ),
+        (
+            "/items/?skip=1&limit=1",
+            [{"item_name": "Bar"}],
+        ),
+    ],
+)
+def test_read_user_item(client: TestClient, path, expected_json):
+    response = client.get(path)
+    assert response.status_code == 200
+    assert response.json() == expected_json
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "get": {
+                    "summary": "Read Item",
+                    "operationId": "read_item_items__get",
+                    "parameters": [
+                        {
+                            "required": False,
+                            "schema": {
+                                "title": "Skip",
+                                "type": "integer",
+                                "default": 0,
+                            },
+                            "name": "skip",
+                            "in": "query",
+                        },
+                        {
+                            "required": False,
+                            "schema": {
+                                "title": "Limit",
+                                "type": "integer",
+                                "default": 10,
+                            },
+                            "name": "limit",
+                            "in": "query",
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_query_params/test_tutorial002.py b/tests/test_tutorial/test_query_params/test_tutorial002.py
new file mode 100644 (file)
index 0000000..ae3ee76
--- /dev/null
@@ -0,0 +1,127 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial002_py39"),
+        pytest.param("tutorial002_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.query_params.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+@pytest.mark.parametrize(
+    ("path", "expected_json"),
+    [
+        (
+            "/items/foo",
+            {"item_id": "foo"},
+        ),
+        (
+            "/items/bar?q=somequery",
+            {"item_id": "bar", "q": "somequery"},
+        ),
+    ],
+)
+def test_read_user_item(client: TestClient, path, expected_json):
+    response = client.get(path)
+    assert response.status_code == 200
+    assert response.json() == expected_json
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "get": {
+                    "summary": "Read Item",
+                    "operationId": "read_item_items__item_id__get",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {"title": "Item Id", "type": "string"},
+                            "name": "item_id",
+                            "in": "path",
+                        },
+                        {
+                            "required": False,
+                            "schema": {
+                                "title": "Q",
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "null",
+                                    },
+                                ],
+                            },
+                            "name": "q",
+                            "in": "query",
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_query_params/test_tutorial003.py b/tests/test_tutorial/test_query_params/test_tutorial003.py
new file mode 100644 (file)
index 0000000..c0b7e3b
--- /dev/null
@@ -0,0 +1,148 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial003_py39"),
+        pytest.param("tutorial003_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.query_params.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+@pytest.mark.parametrize(
+    ("path", "expected_json"),
+    [
+        (
+            "/items/foo",
+            {
+                "item_id": "foo",
+                "description": "This is an amazing item that has a long description",
+            },
+        ),
+        (
+            "/items/bar?q=somequery",
+            {
+                "item_id": "bar",
+                "q": "somequery",
+                "description": "This is an amazing item that has a long description",
+            },
+        ),
+        (
+            "/items/baz?short=true",
+            {"item_id": "baz"},
+        ),
+    ],
+)
+def test_read_user_item(client: TestClient, path, expected_json):
+    response = client.get(path)
+    assert response.status_code == 200
+    assert response.json() == expected_json
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "get": {
+                    "summary": "Read Item",
+                    "operationId": "read_item_items__item_id__get",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {"title": "Item Id", "type": "string"},
+                            "name": "item_id",
+                            "in": "path",
+                        },
+                        {
+                            "required": False,
+                            "schema": {
+                                "title": "Q",
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "null",
+                                    },
+                                ],
+                            },
+                            "name": "q",
+                            "in": "query",
+                        },
+                        {
+                            "required": False,
+                            "schema": {
+                                "title": "Short",
+                                "type": "boolean",
+                                "default": False,
+                            },
+                            "name": "short",
+                            "in": "query",
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_query_params/test_tutorial004.py b/tests/test_tutorial/test_query_params/test_tutorial004.py
new file mode 100644 (file)
index 0000000..9be18b7
--- /dev/null
@@ -0,0 +1,156 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial004_py39"),
+        pytest.param("tutorial004_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.query_params.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+@pytest.mark.parametrize(
+    ("path", "expected_json"),
+    [
+        (
+            "/users/123/items/foo",
+            {
+                "item_id": "foo",
+                "owner_id": 123,
+                "description": "This is an amazing item that has a long description",
+            },
+        ),
+        (
+            "/users/1/items/bar?q=somequery",
+            {
+                "item_id": "bar",
+                "owner_id": 1,
+                "q": "somequery",
+                "description": "This is an amazing item that has a long description",
+            },
+        ),
+        (
+            "/users/42/items/baz?short=true",
+            {"item_id": "baz", "owner_id": 42},
+        ),
+    ],
+)
+def test_read_user_item(client: TestClient, path, expected_json):
+    response = client.get(path)
+    assert response.status_code == 200
+    assert response.json() == expected_json
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/users/{user_id}/items/{item_id}": {
+                "get": {
+                    "summary": "Read User Item",
+                    "operationId": "read_user_item_users__user_id__items__item_id__get",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {"title": "User Id", "type": "integer"},
+                            "name": "user_id",
+                            "in": "path",
+                        },
+                        {
+                            "required": True,
+                            "schema": {"title": "Item Id", "type": "string"},
+                            "name": "item_id",
+                            "in": "path",
+                        },
+                        {
+                            "required": False,
+                            "schema": {
+                                "title": "Q",
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                    },
+                                    {
+                                        "type": "null",
+                                    },
+                                ],
+                            },
+                            "name": "q",
+                            "in": "query",
+                        },
+                        {
+                            "required": False,
+                            "schema": {
+                                "title": "Short",
+                                "type": "boolean",
+                                "default": False,
+                            },
+                            "name": "short",
+                            "in": "query",
+                        },
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError",
+                                    },
+                                },
+                            },
+                            "description": "Validation Error",
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py
new file mode 100644 (file)
index 0000000..f1af7e0
--- /dev/null
@@ -0,0 +1,121 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial001_py39"),
+        pytest.param("tutorial001_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(
+        f"docs_src.query_params_str_validations.{request.param}"
+    )
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_query_params_str_validations_no_query(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+def test_query_params_str_validations_q_empty_str(client: TestClient):
+    response = client.get("/items/", params={"q": ""})
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+def test_query_params_str_validations_q_query(client: TestClient):
+    response = client.get("/items/", params={"q": "query"})
+    assert response.status_code == 200
+    assert response.json() == {
+        "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+        "q": "query",
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__get",
+                    "parameters": [
+                        {
+                            "required": False,
+                            "schema": {
+                                "anyOf": [
+                                    {"type": "string"},
+                                    {"type": "null"},
+                                ],
+                                "title": "Q",
+                            },
+                            "name": "q",
+                            "in": "query",
+                        }
+                    ],
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial002.py
new file mode 100644 (file)
index 0000000..62018b8
--- /dev/null
@@ -0,0 +1,142 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial002_py39"),
+        pytest.param("tutorial002_py310", marks=needs_py310),
+        pytest.param("tutorial002_an_py39"),
+        pytest.param("tutorial002_an_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(
+        f"docs_src.query_params_str_validations.{request.param}"
+    )
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_query_params_str_validations_no_query(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+def test_query_params_str_validations_q_empty_str(client: TestClient):
+    response = client.get("/items/", params={"q": ""})
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+def test_query_params_str_validations_q_query(client: TestClient):
+    response = client.get("/items/", params={"q": "query"})
+    assert response.status_code == 200
+    assert response.json() == {
+        "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+        "q": "query",
+    }
+
+
+def test_query_params_str_validations_q_too_long(client: TestClient):
+    response = client.get("/items/", params={"q": "q" * 51})
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "type": "string_too_long",
+                "loc": ["query", "q"],
+                "msg": "String should have at most 50 characters",
+                "input": "q" * 51,
+                "ctx": {"max_length": 50},
+            }
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__get",
+                    "parameters": [
+                        {
+                            "required": False,
+                            "schema": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                        "maxLength": 50,
+                                    },
+                                    {"type": "null"},
+                                ],
+                                "title": "Q",
+                            },
+                            "name": "q",
+                            "in": "query",
+                        }
+                    ],
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial003.py
new file mode 100644 (file)
index 0000000..a4ad7a6
--- /dev/null
@@ -0,0 +1,153 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial003_py39"),
+        pytest.param("tutorial003_py310", marks=needs_py310),
+        pytest.param("tutorial003_an_py39"),
+        pytest.param("tutorial003_an_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(
+        f"docs_src.query_params_str_validations.{request.param}"
+    )
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_query_params_str_validations_no_query(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+def test_query_params_str_validations_q_query(client: TestClient):
+    response = client.get("/items/", params={"q": "query"})
+    assert response.status_code == 200
+    assert response.json() == {
+        "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+        "q": "query",
+    }
+
+
+def test_query_params_str_validations_q_too_short(client: TestClient):
+    response = client.get("/items/", params={"q": "qu"})
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "type": "string_too_short",
+                "loc": ["query", "q"],
+                "msg": "String should have at least 3 characters",
+                "input": "qu",
+                "ctx": {"min_length": 3},
+            }
+        ]
+    }
+
+
+def test_query_params_str_validations_q_too_long(client: TestClient):
+    response = client.get("/items/", params={"q": "q" * 51})
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "type": "string_too_long",
+                "loc": ["query", "q"],
+                "msg": "String should have at most 50 characters",
+                "input": "q" * 51,
+                "ctx": {"max_length": 50},
+            }
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__get",
+                    "parameters": [
+                        {
+                            "required": False,
+                            "schema": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                        "minLength": 3,
+                                        "maxLength": 50,
+                                    },
+                                    {"type": "null"},
+                                ],
+                                "title": "Q",
+                            },
+                            "name": "q",
+                            "in": "query",
+                        }
+                    ],
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial004.py
new file mode 100644 (file)
index 0000000..95efab2
--- /dev/null
@@ -0,0 +1,147 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial004_py39"),
+        pytest.param("tutorial004_py310", marks=needs_py310),
+        pytest.param("tutorial004_an_py39"),
+        pytest.param("tutorial004_an_py310", marks=needs_py310),
+        pytest.param(
+            "tutorial004_regex_an_py310",
+            marks=(
+                needs_py310,
+                pytest.mark.filterwarnings(
+                    "ignore:`regex` has been deprecated, please use `pattern` instead:DeprecationWarning"
+                ),
+            ),
+        ),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(
+        f"docs_src.query_params_str_validations.{request.param}"
+    )
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_query_params_str_validations_no_query(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+def test_query_params_str_validations_q_fixedquery(client: TestClient):
+    response = client.get("/items/", params={"q": "fixedquery"})
+    assert response.status_code == 200
+    assert response.json() == {
+        "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+        "q": "fixedquery",
+    }
+
+
+def test_query_params_str_validations_q_nonregexquery(client: TestClient):
+    response = client.get("/items/", params={"q": "nonregexquery"})
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "type": "string_pattern_mismatch",
+                "loc": ["query", "q"],
+                "msg": "String should match pattern '^fixedquery$'",
+                "input": "nonregexquery",
+                "ctx": {"pattern": "^fixedquery$"},
+            }
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__get",
+                    "parameters": [
+                        {
+                            "required": False,
+                            "schema": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                        "minLength": 3,
+                                        "maxLength": 50,
+                                        "pattern": "^fixedquery$",
+                                    },
+                                    {"type": "null"},
+                                ],
+                                "title": "Q",
+                            },
+                            "name": "q",
+                            "in": "query",
+                        }
+                    ],
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial005.py
new file mode 100644 (file)
index 0000000..52462fe
--- /dev/null
@@ -0,0 +1,131 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial005_py39"),
+        pytest.param("tutorial005_an_py39"),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(
+        f"docs_src.query_params_str_validations.{request.param}"
+    )
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_query_params_str_validations_no_query(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200
+    assert response.json() == {
+        "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+        "q": "fixedquery",
+    }
+
+
+def test_query_params_str_validations_q_query(client: TestClient):
+    response = client.get("/items/", params={"q": "query"})
+    assert response.status_code == 200
+    assert response.json() == {
+        "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+        "q": "query",
+    }
+
+
+def test_query_params_str_validations_q_short(client: TestClient):
+    response = client.get("/items/", params={"q": "fa"})
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "type": "string_too_short",
+                "loc": ["query", "q"],
+                "msg": "String should have at least 3 characters",
+                "input": "fa",
+                "ctx": {"min_length": 3},
+            }
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__get",
+                    "parameters": [
+                        {
+                            "required": False,
+                            "schema": {
+                                "type": "string",
+                                "default": "fixedquery",
+                                "minLength": 3,
+                                "title": "Q",
+                            },
+                            "name": "q",
+                            "in": "query",
+                        }
+                    ],
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006.py
new file mode 100644 (file)
index 0000000..640cedc
--- /dev/null
@@ -0,0 +1,136 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial006_py39"),
+        pytest.param("tutorial006_an_py39"),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(
+        f"docs_src.query_params_str_validations.{request.param}"
+    )
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_query_params_str_validations_no_query(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "type": "missing",
+                "loc": ["query", "q"],
+                "msg": "Field required",
+                "input": None,
+            }
+        ]
+    }
+
+
+def test_query_params_str_validations_q_fixedquery(client: TestClient):
+    response = client.get("/items/", params={"q": "fixedquery"})
+    assert response.status_code == 200
+    assert response.json() == {
+        "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+        "q": "fixedquery",
+    }
+
+
+def test_query_params_str_validations_q_fixedquery_too_short(client: TestClient):
+    response = client.get("/items/", params={"q": "fa"})
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "type": "string_too_short",
+                "loc": ["query", "q"],
+                "msg": "String should have at least 3 characters",
+                "input": "fa",
+                "ctx": {"min_length": 3},
+            }
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__get",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {
+                                "type": "string",
+                                "minLength": 3,
+                                "title": "Q",
+                            },
+                            "name": "q",
+                            "in": "query",
+                        }
+                    ],
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial006c.py
new file mode 100644 (file)
index 0000000..f287b5d
--- /dev/null
@@ -0,0 +1,148 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial006c_py39"),
+        pytest.param("tutorial006c_py310", marks=needs_py310),
+        pytest.param("tutorial006c_an_py39"),
+        pytest.param("tutorial006c_an_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(
+        f"docs_src.query_params_str_validations.{request.param}"
+    )
+    client = TestClient(mod.app)
+    return client
+
+
+@pytest.mark.xfail(
+    reason="Code example is not valid. See https://github.com/fastapi/fastapi/issues/12419"
+)
+def test_query_params_str_validations_no_query(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200
+    assert response.json() == {  # pragma: no cover
+        "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+    }
+
+
+@pytest.mark.xfail(
+    reason="Code example is not valid. See https://github.com/fastapi/fastapi/issues/12419"
+)
+def test_query_params_str_validations_empty_str(client: TestClient):
+    response = client.get("/items/?q=")
+    assert response.status_code == 200
+    assert response.json() == {  # pragma: no cover
+        "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+    }
+
+
+def test_query_params_str_validations_q_query(client: TestClient):
+    response = client.get("/items/", params={"q": "query"})
+    assert response.status_code == 200
+    assert response.json() == {
+        "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+        "q": "query",
+    }
+
+
+def test_query_params_str_validations_q_short(client: TestClient):
+    response = client.get("/items/", params={"q": "fa"})
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "type": "string_too_short",
+                "loc": ["query", "q"],
+                "msg": "String should have at least 3 characters",
+                "input": "fa",
+                "ctx": {"min_length": 3},
+            }
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__get",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {
+                                "anyOf": [
+                                    {"type": "string", "minLength": 3},
+                                    {"type": "null"},
+                                ],
+                                "title": "Q",
+                            },
+                            "name": "q",
+                            "in": "query",
+                        }
+                    ],
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial007.py
new file mode 100644 (file)
index 0000000..b17bc27
--- /dev/null
@@ -0,0 +1,136 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial007_py39"),
+        pytest.param("tutorial007_py310", marks=needs_py310),
+        pytest.param("tutorial007_an_py39"),
+        pytest.param("tutorial007_an_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(
+        f"docs_src.query_params_str_validations.{request.param}"
+    )
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_query_params_str_validations_no_query(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+def test_query_params_str_validations_q_fixedquery(client: TestClient):
+    response = client.get("/items/", params={"q": "fixedquery"})
+    assert response.status_code == 200
+    assert response.json() == {
+        "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+        "q": "fixedquery",
+    }
+
+
+def test_query_params_str_validations_q_fixedquery_too_short(client: TestClient):
+    response = client.get("/items/", params={"q": "fa"})
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "type": "string_too_short",
+                "loc": ["query", "q"],
+                "msg": "String should have at least 3 characters",
+                "input": "fa",
+                "ctx": {"min_length": 3},
+            }
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__get",
+                    "parameters": [
+                        {
+                            "required": False,
+                            "schema": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                        "minLength": 3,
+                                    },
+                                    {"type": "null"},
+                                ],
+                                "title": "Query string",
+                            },
+                            "name": "q",
+                            "in": "query",
+                        }
+                    ],
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial008.py
new file mode 100644 (file)
index 0000000..c631115
--- /dev/null
@@ -0,0 +1,138 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial008_py39"),
+        pytest.param("tutorial008_py310", marks=needs_py310),
+        pytest.param("tutorial008_an_py39"),
+        pytest.param("tutorial008_an_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(
+        f"docs_src.query_params_str_validations.{request.param}"
+    )
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_query_params_str_validations_no_query(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+def test_query_params_str_validations_q_fixedquery(client: TestClient):
+    response = client.get("/items/", params={"q": "fixedquery"})
+    assert response.status_code == 200
+    assert response.json() == {
+        "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+        "q": "fixedquery",
+    }
+
+
+def test_query_params_str_validations_q_fixedquery_too_short(client: TestClient):
+    response = client.get("/items/", params={"q": "fa"})
+    assert response.status_code == 422
+    assert response.json() == {
+        "detail": [
+            {
+                "type": "string_too_short",
+                "loc": ["query", "q"],
+                "msg": "String should have at least 3 characters",
+                "input": "fa",
+                "ctx": {"min_length": 3},
+            }
+        ]
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__get",
+                    "parameters": [
+                        {
+                            "description": "Query string for the items to search in the database that have a good match",
+                            "required": False,
+                            "schema": {
+                                "anyOf": [
+                                    {
+                                        "type": "string",
+                                        "minLength": 3,
+                                    },
+                                    {"type": "null"},
+                                ],
+                                "title": "Query string",
+                                "description": "Query string for the items to search in the database that have a good match",
+                            },
+                            "name": "q",
+                            "in": "query",
+                        }
+                    ],
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial009.py
new file mode 100644 (file)
index 0000000..7e9d69d
--- /dev/null
@@ -0,0 +1,123 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial009_py39"),
+        pytest.param("tutorial009_py310", marks=needs_py310),
+        pytest.param("tutorial009_an_py39"),
+        pytest.param("tutorial009_an_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(
+        f"docs_src.query_params_str_validations.{request.param}"
+    )
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_query_params_str_validations_no_query(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+def test_query_params_str_validations_item_query_fixedquery(client: TestClient):
+    response = client.get("/items/", params={"item-query": "fixedquery"})
+    assert response.status_code == 200
+    assert response.json() == {
+        "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+        "q": "fixedquery",
+    }
+
+
+def test_query_params_str_validations_q_fixedquery(client: TestClient):
+    response = client.get("/items/", params={"q": "fixedquery"})
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__get",
+                    "parameters": [
+                        {
+                            "schema": {
+                                "anyOf": [
+                                    {"type": "string"},
+                                    {"type": "null"},
+                                ],
+                                "title": "Item-Query",
+                            },
+                            "required": False,
+                            "name": "item-query",
+                            "in": "query",
+                        }
+                    ],
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_response_directly/test_tutorial002.py b/tests/test_tutorial/test_response_directly/test_tutorial002.py
new file mode 100644 (file)
index 0000000..ef84575
--- /dev/null
@@ -0,0 +1,65 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial002_py39"),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.response_directly.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_path_operation(client: TestClient):
+    expected_content = """<?xml version="1.0"?>
+    <shampoo>
+    <Header>
+        Apply shampoo here.
+    </Header>
+    <Body>
+        You'll have to use soap here.
+    </Body>
+    </shampoo>
+    """
+
+    response = client.get("/legacy/")
+    assert response.status_code == 200, response.text
+    assert response.headers["content-type"] == "application/xml"
+    assert response.text == expected_content
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "info": {
+            "title": "FastAPI",
+            "version": "0.1.0",
+        },
+        "openapi": "3.1.0",
+        "paths": {
+            "/legacy/": {
+                "get": {
+                    "operationId": "get_legacy_data_legacy__get",
+                    "responses": {
+                        "200": {
+                            "content": {
+                                "application/json": {
+                                    "schema": {},
+                                },
+                            },
+                            "description": "Successful Response",
+                        },
+                    },
+                    "summary": "Get Legacy Data",
+                },
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py b/tests/test_tutorial/test_response_model/test_tutorial001_tutorial001_01.py
new file mode 100644 (file)
index 0000000..10692f9
--- /dev/null
@@ -0,0 +1,193 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial001_py39"),
+        pytest.param("tutorial001_py310", marks=needs_py310),
+        pytest.param("tutorial001_01_py39"),
+        pytest.param("tutorial001_01_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.response_model.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_read_items(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200, response.text
+    assert response.json() == [
+        {
+            "name": "Portal Gun",
+            "description": None,
+            "price": 42.0,
+            "tags": [],
+            "tax": None,
+        },
+        {
+            "name": "Plumbus",
+            "description": None,
+            "price": 32.0,
+            "tags": [],
+            "tax": None,
+        },
+    ]
+
+
+def test_create_item(client: TestClient):
+    item_data = {
+        "name": "Test Item",
+        "description": "A test item",
+        "price": 10.5,
+        "tax": 1.5,
+        "tags": ["test", "item"],
+    }
+    response = client.post("/items/", json=item_data)
+    assert response.status_code == 200, response.text
+    assert response.json() == item_data
+
+
+def test_create_item_only_required(client: TestClient):
+    response = client.post(
+        "/items/",
+        json={
+            "name": "Test Item",
+            "price": 10.5,
+        },
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "name": "Test Item",
+        "price": 10.5,
+        "description": None,
+        "tax": None,
+        "tags": [],
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "type": "array",
+                                        "items": {"$ref": "#/components/schemas/Item"},
+                                        "title": "Response Read Items Items  Get",
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__get",
+                },
+                "post": {
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Item",
+                                },
+                            },
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {"$ref": "#/components/schemas/Item"},
+                                }
+                            },
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Create Item",
+                    "operationId": "create_item_items__post",
+                },
+            }
+        },
+        "components": {
+            "schemas": {
+                "Item": {
+                    "title": "Item",
+                    "required": ["name", "price"],
+                    "type": "object",
+                    "properties": {
+                        "name": {"title": "Name", "type": "string"},
+                        "price": {"title": "Price", "type": "number"},
+                        "description": {
+                            "title": "Description",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                        "tax": {
+                            "title": "Tax",
+                            "anyOf": [{"type": "number"}, {"type": "null"}],
+                        },
+                        "tags": {
+                            "title": "Tags",
+                            "type": "array",
+                            "items": {"type": "string"},
+                            "default": [],
+                        },
+                    },
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_response_model/test_tutorial002.py b/tests/test_tutorial/test_response_model/test_tutorial002.py
new file mode 100644 (file)
index 0000000..216d4c4
--- /dev/null
@@ -0,0 +1,129 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial002_py39"),
+        pytest.param("tutorial002_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.response_model.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_post_user(client: TestClient):
+    user_data = {
+        "username": "foo",
+        "password": "fighter",
+        "email": "foo@example.com",
+        "full_name": "Grave Dohl",
+    }
+    response = client.post(
+        "/user/",
+        json=user_data,
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == user_data
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/user/": {
+                "post": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {"$ref": "#/components/schemas/UserIn"}
+                                }
+                            },
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Create User",
+                    "operationId": "create_user_user__post",
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/UserIn"}
+                            }
+                        },
+                        "required": True,
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "UserIn": {
+                    "title": "UserIn",
+                    "required": ["username", "password", "email"],
+                    "type": "object",
+                    "properties": {
+                        "username": {"title": "Username", "type": "string"},
+                        "password": {"title": "Password", "type": "string"},
+                        "email": {
+                            "title": "Email",
+                            "type": "string",
+                            "format": "email",
+                        },
+                        "full_name": {
+                            "title": "Full Name",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                    },
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_response_status_code/__init__.py b/tests/test_tutorial/test_response_status_code/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py b/tests/test_tutorial/test_response_status_code/test_tutorial001_tutorial002.py
new file mode 100644 (file)
index 0000000..ddf55a0
--- /dev/null
@@ -0,0 +1,96 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial001_py39"),
+        pytest.param("tutorial002_py39"),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.response_status_code.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_create_item(client: TestClient):
+    response = client.post("/items/", params={"name": "Test Item"})
+    assert response.status_code == 201, response.text
+    assert response.json() == {"name": "Test Item"}
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "post": {
+                    "parameters": [
+                        {
+                            "name": "name",
+                            "in": "query",
+                            "required": True,
+                            "schema": {"title": "Name", "type": "string"},
+                        }
+                    ],
+                    "summary": "Create Item",
+                    "operationId": "create_item_items__post",
+                    "responses": {
+                        "201": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial002.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial002.py
new file mode 100644 (file)
index 0000000..4f52408
--- /dev/null
@@ -0,0 +1,141 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial002_py39"),
+        pytest.param("tutorial002_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_post_body_example(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+        },
+    )
+    assert response.status_code == 200
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    # insert_assert(response.json())
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__put",
+                    "parameters": [
+                        {
+                            "name": "item_id",
+                            "in": "path",
+                            "required": True,
+                            "schema": {"type": "integer", "title": "Item Id"},
+                        }
+                    ],
+                    "requestBody": {
+                        "required": True,
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/Item"}
+                            }
+                        },
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                            "type": "array",
+                            "title": "Detail",
+                        }
+                    },
+                    "type": "object",
+                    "title": "HTTPValidationError",
+                },
+                "Item": {
+                    "properties": {
+                        "name": {
+                            "type": "string",
+                            "title": "Name",
+                            "examples": ["Foo"],
+                        },
+                        "description": {
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                            "title": "Description",
+                            "examples": ["A very nice Item"],
+                        },
+                        "price": {
+                            "type": "number",
+                            "title": "Price",
+                            "examples": [35.4],
+                        },
+                        "tax": {
+                            "anyOf": [{"type": "number"}, {"type": "null"}],
+                            "title": "Tax",
+                            "examples": [3.2],
+                        },
+                    },
+                    "type": "object",
+                    "required": ["name", "price"],
+                    "title": "Item",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                            "type": "array",
+                            "title": "Location",
+                        },
+                        "msg": {"type": "string", "title": "Message"},
+                        "type": {"type": "string", "title": "Error Type"},
+                    },
+                    "type": "object",
+                    "required": ["loc", "msg", "type"],
+                    "title": "ValidationError",
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial003.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial003.py
new file mode 100644 (file)
index 0000000..3529a9b
--- /dev/null
@@ -0,0 +1,143 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial003_py39"),
+        pytest.param("tutorial003_py310", marks=needs_py310),
+        pytest.param("tutorial003_an_py39"),
+        pytest.param("tutorial003_an_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.schema_extra_example.{request.param}")
+
+    client = TestClient(mod.app)
+    return client
+
+
+def test_post_body_example(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+        },
+    )
+    assert response.status_code == 200
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    # insert_assert(response.json())
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__put",
+                    "parameters": [
+                        {
+                            "name": "item_id",
+                            "in": "path",
+                            "required": True,
+                            "schema": {"type": "integer", "title": "Item Id"},
+                        }
+                    ],
+                    "requestBody": {
+                        "required": True,
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Item",
+                                    "examples": [
+                                        {
+                                            "description": "A very nice Item",
+                                            "name": "Foo",
+                                            "price": 35.4,
+                                            "tax": 3.2,
+                                        }
+                                    ],
+                                },
+                            }
+                        },
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                            "type": "array",
+                            "title": "Detail",
+                        }
+                    },
+                    "type": "object",
+                    "title": "HTTPValidationError",
+                },
+                "Item": {
+                    "properties": {
+                        "name": {"type": "string", "title": "Name"},
+                        "description": {
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                            "title": "Description",
+                        },
+                        "price": {"type": "number", "title": "Price"},
+                        "tax": {
+                            "anyOf": [{"type": "number"}, {"type": "null"}],
+                            "title": "Tax",
+                        },
+                    },
+                    "type": "object",
+                    "required": ["name", "price"],
+                    "title": "Item",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                            "type": "array",
+                            "title": "Location",
+                        },
+                        "msg": {"type": "string", "title": "Message"},
+                        "type": {"type": "string", "title": "Error Type"},
+                    },
+                    "type": "object",
+                    "required": ["loc", "msg", "type"],
+                    "title": "ValidationError",
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_security/test_tutorial002.py b/tests/test_tutorial/test_security/test_tutorial002.py
new file mode 100644 (file)
index 0000000..85c076b
--- /dev/null
@@ -0,0 +1,71 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial002_py39"),
+        pytest.param("tutorial002_py310", marks=needs_py310),
+        pytest.param("tutorial002_an_py39"),
+        pytest.param("tutorial002_an_py310", marks=needs_py310),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.security.{request.param}")
+    client = TestClient(mod.app)
+    return client
+
+
+def test_no_token(client: TestClient):
+    response = client.get("/users/me")
+    assert response.status_code == 401, response.text
+    assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "Bearer"
+
+
+def test_token(client: TestClient):
+    response = client.get("/users/me", headers={"Authorization": "Bearer testtoken"})
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "username": "testtokenfakedecoded",
+        "email": "john@example.com",
+        "full_name": "John Doe",
+        "disabled": None,
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/users/me": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        }
+                    },
+                    "summary": "Read Users Me",
+                    "operationId": "read_users_me_users_me_get",
+                    "security": [{"OAuth2PasswordBearer": []}],
+                }
+            }
+        },
+        "components": {
+            "securitySchemes": {
+                "OAuth2PasswordBearer": {
+                    "type": "oauth2",
+                    "flows": {"password": {"scopes": {}, "tokenUrl": "token"}},
+                }
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_security/test_tutorial004.py b/tests/test_tutorial/test_security/test_tutorial004.py
new file mode 100644 (file)
index 0000000..b5e3d39
--- /dev/null
@@ -0,0 +1,363 @@
+import importlib
+from types import ModuleType
+from unittest.mock import patch
+
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310
+
+
+@pytest.fixture(
+    name="mod",
+    params=[
+        pytest.param("tutorial004_py39"),
+        pytest.param("tutorial004_py310", marks=needs_py310),
+        pytest.param("tutorial004_an_py39"),
+        pytest.param("tutorial004_an_py310", marks=needs_py310),
+    ],
+)
+def get_mod(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.security.{request.param}")
+
+    return mod
+
+
+def get_access_token(*, username="johndoe", password="secret", client: TestClient):
+    data = {"username": username, "password": password}
+    response = client.post("/token", data=data)
+    content = response.json()
+    access_token = content.get("access_token")
+    return access_token
+
+
+def test_login(mod: ModuleType):
+    client = TestClient(mod.app)
+    response = client.post("/token", data={"username": "johndoe", "password": "secret"})
+    assert response.status_code == 200, response.text
+    content = response.json()
+    assert "access_token" in content
+    assert content["token_type"] == "bearer"
+
+
+def test_login_incorrect_password(mod: ModuleType):
+    client = TestClient(mod.app)
+    response = client.post(
+        "/token", data={"username": "johndoe", "password": "incorrect"}
+    )
+    assert response.status_code == 401, response.text
+    assert response.json() == {"detail": "Incorrect username or password"}
+
+
+def test_login_incorrect_username(mod: ModuleType):
+    client = TestClient(mod.app)
+    response = client.post("/token", data={"username": "foo", "password": "secret"})
+    assert response.status_code == 401, response.text
+    assert response.json() == {"detail": "Incorrect username or password"}
+
+
+def test_no_token(mod: ModuleType):
+    client = TestClient(mod.app)
+    response = client.get("/users/me")
+    assert response.status_code == 401, response.text
+    assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "Bearer"
+
+
+def test_token(mod: ModuleType):
+    client = TestClient(mod.app)
+    access_token = get_access_token(client=client)
+    response = client.get(
+        "/users/me", headers={"Authorization": f"Bearer {access_token}"}
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "username": "johndoe",
+        "full_name": "John Doe",
+        "email": "johndoe@example.com",
+        "disabled": False,
+    }
+
+
+def test_incorrect_token(mod: ModuleType):
+    client = TestClient(mod.app)
+    response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"})
+    assert response.status_code == 401, response.text
+    assert response.json() == {"detail": "Could not validate credentials"}
+    assert response.headers["WWW-Authenticate"] == "Bearer"
+
+
+def test_incorrect_token_type(mod: ModuleType):
+    client = TestClient(mod.app)
+    response = client.get(
+        "/users/me", headers={"Authorization": "Notexistent testtoken"}
+    )
+    assert response.status_code == 401, response.text
+    assert response.json() == {"detail": "Not authenticated"}
+    assert response.headers["WWW-Authenticate"] == "Bearer"
+
+
+def test_verify_password(mod: ModuleType):
+    assert mod.verify_password(
+        "secret", mod.fake_users_db["johndoe"]["hashed_password"]
+    )
+
+
+def test_get_password_hash(mod: ModuleType):
+    assert mod.get_password_hash("johndoe")
+
+
+def test_create_access_token(mod: ModuleType):
+    access_token = mod.create_access_token(data={"data": "foo"})
+    assert access_token
+
+
+def test_token_no_sub(mod: ModuleType):
+    client = TestClient(mod.app)
+
+    response = client.get(
+        "/users/me",
+        headers={
+            "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE"
+        },
+    )
+    assert response.status_code == 401, response.text
+    assert response.json() == {"detail": "Could not validate credentials"}
+    assert response.headers["WWW-Authenticate"] == "Bearer"
+
+
+def test_token_no_username(mod: ModuleType):
+    client = TestClient(mod.app)
+
+    response = client.get(
+        "/users/me",
+        headers={
+            "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y"
+        },
+    )
+    assert response.status_code == 401, response.text
+    assert response.json() == {"detail": "Could not validate credentials"}
+    assert response.headers["WWW-Authenticate"] == "Bearer"
+
+
+def test_token_nonexistent_user(mod: ModuleType):
+    client = TestClient(mod.app)
+
+    response = client.get(
+        "/users/me",
+        headers={
+            "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw"
+        },
+    )
+    assert response.status_code == 401, response.text
+    assert response.json() == {"detail": "Could not validate credentials"}
+    assert response.headers["WWW-Authenticate"] == "Bearer"
+
+
+def test_token_inactive_user(mod: ModuleType):
+    client = TestClient(mod.app)
+    alice_user_data = {
+        "username": "alice",
+        "full_name": "Alice Wonderson",
+        "email": "alice@example.com",
+        "hashed_password": mod.get_password_hash("secretalice"),
+        "disabled": True,
+    }
+    with patch.dict(f"{mod.__name__}.fake_users_db", {"alice": alice_user_data}):
+        access_token = get_access_token(
+            username="alice", password="secretalice", client=client
+        )
+        response = client.get(
+            "/users/me", headers={"Authorization": f"Bearer {access_token}"}
+        )
+    assert response.status_code == 400, response.text
+    assert response.json() == {"detail": "Inactive user"}
+
+
+def test_read_items(mod: ModuleType):
+    client = TestClient(mod.app)
+    access_token = get_access_token(client=client)
+    response = client.get(
+        "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"}
+    )
+    assert response.status_code == 200, response.text
+    assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}]
+
+
+def test_openapi_schema(mod: ModuleType):
+    client = TestClient(mod.app)
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/token": {
+                "post": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {"$ref": "#/components/schemas/Token"}
+                                }
+                            },
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                    "summary": "Login For Access Token",
+                    "operationId": "login_for_access_token_token_post",
+                    "requestBody": {
+                        "content": {
+                            "application/x-www-form-urlencoded": {
+                                "schema": {
+                                    "$ref": "#/components/schemas/Body_login_for_access_token_token_post"
+                                }
+                            }
+                        },
+                        "required": True,
+                    },
+                }
+            },
+            "/users/me/": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {
+                                "application/json": {
+                                    "schema": {"$ref": "#/components/schemas/User"}
+                                }
+                            },
+                        }
+                    },
+                    "summary": "Read Users Me",
+                    "operationId": "read_users_me_users_me__get",
+                    "security": [{"OAuth2PasswordBearer": []}],
+                }
+            },
+            "/users/me/items/": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        }
+                    },
+                    "summary": "Read Own Items",
+                    "operationId": "read_own_items_users_me_items__get",
+                    "security": [{"OAuth2PasswordBearer": []}],
+                }
+            },
+        },
+        "components": {
+            "schemas": {
+                "User": {
+                    "title": "User",
+                    "required": ["username"],
+                    "type": "object",
+                    "properties": {
+                        "username": {"title": "Username", "type": "string"},
+                        "email": {
+                            "title": "Email",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                        "full_name": {
+                            "title": "Full Name",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                        "disabled": {
+                            "title": "Disabled",
+                            "anyOf": [{"type": "boolean"}, {"type": "null"}],
+                        },
+                    },
+                },
+                "Token": {
+                    "title": "Token",
+                    "required": ["access_token", "token_type"],
+                    "type": "object",
+                    "properties": {
+                        "access_token": {"title": "Access Token", "type": "string"},
+                        "token_type": {"title": "Token Type", "type": "string"},
+                    },
+                },
+                "Body_login_for_access_token_token_post": {
+                    "title": "Body_login_for_access_token_token_post",
+                    "required": ["username", "password"],
+                    "type": "object",
+                    "properties": {
+                        "grant_type": {
+                            "title": "Grant Type",
+                            "anyOf": [
+                                {"pattern": "^password$", "type": "string"},
+                                {"type": "null"},
+                            ],
+                        },
+                        "username": {"title": "Username", "type": "string"},
+                        "password": {
+                            "title": "Password",
+                            "type": "string",
+                            "format": "password",
+                        },
+                        "scope": {"title": "Scope", "type": "string", "default": ""},
+                        "client_id": {
+                            "title": "Client Id",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                        },
+                        "client_secret": {
+                            "title": "Client Secret",
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                            "format": "password",
+                        },
+                    },
+                },
+                "ValidationError": {
+                    "title": "ValidationError",
+                    "required": ["loc", "msg", "type"],
+                    "type": "object",
+                    "properties": {
+                        "loc": {
+                            "title": "Location",
+                            "type": "array",
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                        },
+                        "msg": {"title": "Message", "type": "string"},
+                        "type": {"title": "Error Type", "type": "string"},
+                    },
+                },
+                "HTTPValidationError": {
+                    "title": "HTTPValidationError",
+                    "type": "object",
+                    "properties": {
+                        "detail": {
+                            "title": "Detail",
+                            "type": "array",
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                        }
+                    },
+                },
+            },
+            "securitySchemes": {
+                "OAuth2PasswordBearer": {
+                    "type": "oauth2",
+                    "flows": {
+                        "password": {
+                            "scopes": {},
+                            "tokenUrl": "token",
+                        }
+                    },
+                }
+            },
+        },
+    }
diff --git a/tests/test_tutorial/test_security/test_tutorial007.py b/tests/test_tutorial/test_security/test_tutorial007.py
new file mode 100644 (file)
index 0000000..28b70a2
--- /dev/null
@@ -0,0 +1,89 @@
+import importlib
+from base64 import b64encode
+
+import pytest
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(
+    name="client",
+    params=[
+        pytest.param("tutorial007_py39"),
+        pytest.param("tutorial007_an_py39"),
+    ],
+)
+def get_client(request: pytest.FixtureRequest):
+    mod = importlib.import_module(f"docs_src.security.{request.param}")
+    return TestClient(mod.app)
+
+
+def test_security_http_basic(client: TestClient):
+    response = client.get("/users/me", auth=("stanleyjobson", "swordfish"))
+    assert response.status_code == 200, response.text
+    assert response.json() == {"username": "stanleyjobson"}
+
+
+def test_security_http_basic_no_credentials(client: TestClient):
+    response = client.get("/users/me")
+    assert response.json() == {"detail": "Not authenticated"}
+    assert response.status_code == 401, response.text
+    assert response.headers["WWW-Authenticate"] == "Basic"
+
+
+def test_security_http_basic_invalid_credentials(client: TestClient):
+    response = client.get(
+        "/users/me", headers={"Authorization": "Basic notabase64token"}
+    )
+    assert response.status_code == 401, response.text
+    assert response.headers["WWW-Authenticate"] == "Basic"
+    assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_security_http_basic_non_basic_credentials(client: TestClient):
+    payload = b64encode(b"johnsecret").decode("ascii")
+    auth_header = f"Basic {payload}"
+    response = client.get("/users/me", headers={"Authorization": auth_header})
+    assert response.status_code == 401, response.text
+    assert response.headers["WWW-Authenticate"] == "Basic"
+    assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_security_http_basic_invalid_username(client: TestClient):
+    response = client.get("/users/me", auth=("alice", "swordfish"))
+    assert response.status_code == 401, response.text
+    assert response.json() == {"detail": "Incorrect username or password"}
+    assert response.headers["WWW-Authenticate"] == "Basic"
+
+
+def test_security_http_basic_invalid_password(client: TestClient):
+    response = client.get("/users/me", auth=("stanleyjobson", "wrongpassword"))
+    assert response.status_code == 401, response.text
+    assert response.json() == {"detail": "Incorrect username or password"}
+    assert response.headers["WWW-Authenticate"] == "Basic"
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/users/me": {
+                "get": {
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        }
+                    },
+                    "summary": "Read Current User",
+                    "operationId": "read_current_user_users_me_get",
+                    "security": [{"HTTPBasic": []}],
+                }
+            }
+        },
+        "components": {
+            "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}}
+        },
+    }
diff --git a/tests/test_tutorial/test_settings/test_app01.py b/tests/test_tutorial/test_settings/test_app01.py
new file mode 100644 (file)
index 0000000..0c5e440
--- /dev/null
@@ -0,0 +1,78 @@
+import importlib
+import sys
+
+import pytest
+from dirty_equals import IsAnyStr
+from fastapi.testclient import TestClient
+from pydantic import ValidationError
+from pytest import MonkeyPatch
+
+
+@pytest.fixture(
+    name="mod_name",
+    params=[
+        pytest.param("app01_py39"),
+    ],
+)
+def get_mod_name(request: pytest.FixtureRequest):
+    return f"docs_src.settings.{request.param}.main"
+
+
+@pytest.fixture(name="client")
+def get_test_client(mod_name: str, monkeypatch: MonkeyPatch) -> TestClient:
+    if mod_name in sys.modules:
+        del sys.modules[mod_name]
+    monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com")
+    main_mod = importlib.import_module(mod_name)
+    return TestClient(main_mod.app)
+
+
+def test_settings_validation_error(mod_name: str, monkeypatch: MonkeyPatch):
+    monkeypatch.delenv("ADMIN_EMAIL", raising=False)
+    if mod_name in sys.modules:
+        del sys.modules[mod_name]  # pragma: no cover
+
+    with pytest.raises(ValidationError) as exc_info:
+        importlib.import_module(mod_name)
+    assert exc_info.value.errors() == [
+        {
+            "loc": ("admin_email",),
+            "msg": "Field required",
+            "type": "missing",
+            "input": {},
+            "url": IsAnyStr,
+        }
+    ]
+
+
+def test_app(client: TestClient):
+    response = client.get("/info")
+    data = response.json()
+    assert data == {
+        "app_name": "Awesome API",
+        "admin_email": "admin@example.com",
+        "items_per_user": 50,
+    }
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/info": {
+                "get": {
+                    "operationId": "info_info_get",
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        }
+                    },
+                    "summary": "Info",
+                }
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_static_files/__init__.py b/tests/test_tutorial/test_static_files/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_static_files/test_tutorial001.py b/tests/test_tutorial/test_static_files/test_tutorial001.py
new file mode 100644 (file)
index 0000000..4fbf19a
--- /dev/null
@@ -0,0 +1,40 @@
+import os
+from pathlib import Path
+
+import pytest
+from fastapi.testclient import TestClient
+
+
+@pytest.fixture(scope="module")
+def client():
+    static_dir: Path = Path(os.getcwd()) / "static"
+    static_dir.mkdir(exist_ok=True)
+    sample_file = static_dir / "sample.txt"
+    sample_file.write_text("This is a sample static file.")
+    from docs_src.static_files.tutorial001_py39 import app
+
+    with TestClient(app) as client:
+        yield client
+    sample_file.unlink()
+    static_dir.rmdir()
+
+
+def test_static_files(client: TestClient):
+    response = client.get("/static/sample.txt")
+    assert response.status_code == 200, response.text
+    assert response.text == "This is a sample static file."
+
+
+def test_static_files_not_found(client: TestClient):
+    response = client.get("/static/non_existent_file.txt")
+    assert response.status_code == 404, response.text
+
+
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {},
+    }