diff --git a/tests/test_compat_params_v1.py b/tests/test_compat_params_v1.py index e81740da3..10adeb34f 100644 --- a/tests/test_compat_params_v1.py +++ b/tests/test_compat_params_v1.py @@ -1,7 +1,8 @@ from typing import Annotated, Optional +import pytest from fastapi import FastAPI -from fastapi._compat._params_v1 import Body, Cookie, Header, Path, Query +from fastapi._compat._params_v1 import Body, Cookie, File, Form, Header, Path, Query from fastapi._compat.v1 import BaseModel from fastapi.testclient import TestClient from inline_snapshot import snapshot @@ -29,7 +30,7 @@ def get_items_with_query( Optional[str], Query(min_length=3, max_length=50, pattern="^[a-zA-Z0-9 ]+$") ] = None, skip: Annotated[int, Query(ge=0)] = 0, - limit: Annotated[int, Query(ge=1, le=100)] = 10, + limit: Annotated[int, Query(ge=1, le=100, examples=[5])] = 10, ): return {"q": q, "skip": skip, "limit": limit} @@ -76,6 +77,35 @@ def update_item( return {"item": item, "importance": importance} +@app.post("/form-data/") +def submit_form( + username: Annotated[str, Form(min_length=3, max_length=50)], + password: Annotated[str, Form(min_length=8)], + email: Annotated[Optional[str], Form()] = None, +): + return {"username": username, "password": password, "email": email} + + +@app.post("/upload/") +def upload_file( + file: Annotated[bytes, File()], + description: Annotated[Optional[str], Form()] = None, +): + return {"file_size": len(file), "description": description} + + +@app.post("/upload-multiple/") +def upload_multiple_files( + files: Annotated[list[bytes], File()], + note: Annotated[str, Form()] = "", +): + return { + "file_count": len(files), + "total_size": sum(len(f) for f in files), + "note": note, + } + + client = TestClient(app) @@ -318,3 +348,698 @@ def test_multiple_body_params_importance_too_small(): ] } ) + + +# Form parameter tests +def test_form_data_valid(): + response = client.post( + "/form-data/", + data={ + "username": "testuser", + "password": "password123", + "email": "test@example.com", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "testuser", + "password": "password123", + "email": "test@example.com", + } + + +def test_form_data_optional_field(): + response = client.post( + "/form-data/", + data={"username": "testuser", "password": "password123"}, + ) + assert response.status_code == 200 + assert response.json() == { + "username": "testuser", + "password": "password123", + "email": None, + } + + +def test_form_data_username_too_short(): + response = client.post( + "/form-data/", + data={"username": "ab", "password": "password123"}, + ) + assert response.status_code == 422 + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "ensure this value has at least 3 characters", + "type": "value_error.any_str.min_length", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_form_data_password_too_short(): + response = client.post( + "/form-data/", + data={"username": "testuser", "password": "short"}, + ) + assert response.status_code == 422 + assert response.json() == snapshot( + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "ensure this value has at least 8 characters", + "type": "value_error.any_str.min_length", + "ctx": {"limit_value": 8}, + } + ] + } + ) + + +# File upload tests +def test_upload_file(): + response = client.post( + "/upload/", + files={"file": ("test.txt", b"Hello, World!", "text/plain")}, + data={"description": "A test file"}, + ) + assert response.status_code == 200 + assert response.json() == { + "file_size": 13, + "description": "A test file", + } + + +def test_upload_file_without_description(): + response = client.post( + "/upload/", + files={"file": ("test.txt", b"Hello!", "text/plain")}, + ) + assert response.status_code == 200 + assert response.json() == { + "file_size": 6, + "description": None, + } + + +def test_upload_multiple_files(): + response = client.post( + "/upload-multiple/", + files=[ + ("files", ("file1.txt", b"Content 1", "text/plain")), + ("files", ("file2.txt", b"Content 2", "text/plain")), + ("files", ("file3.txt", b"Content 3", "text/plain")), + ], + data={"note": "Multiple files uploaded"}, + ) + assert response.status_code == 200 + assert response.json() == { + "file_count": 3, + "total_size": 27, + "note": "Multiple files uploaded", + } + + +def test_upload_multiple_files_empty_note(): + response = client.post( + "/upload-multiple/", + files=[ + ("files", ("file1.txt", b"Test", "text/plain")), + ], + ) + assert response.status_code == 200 + assert response.json()["file_count"] == 1 + assert response.json()["note"] == "" + + +# __repr__ tests +def test_query_repr(): + query_param = Query(default=None, min_length=3) + assert repr(query_param) == "Query(None)" + + +def test_body_repr(): + body_param = Body(default=None) + assert repr(body_param) == "Body(None)" + + +# Deprecation warning tests for regex parameter +def test_query_regex_deprecation_warning(): + with pytest.warns(DeprecationWarning, match="`regex` has been deprecated"): + Query(regex="^test$") + + +def test_body_regex_deprecation_warning(): + with pytest.warns(DeprecationWarning, match="`regex` has been deprecated"): + Body(regex="^test$") + + +# Deprecation warning tests for example parameter +def test_query_example_deprecation_warning(): + with pytest.warns(DeprecationWarning, match="`example` has been deprecated"): + Query(example="test example") + + +def test_body_example_deprecation_warning(): + with pytest.warns(DeprecationWarning, match="`example` has been deprecated"): + Body(example={"test": "example"}) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "summary": "Get Item With Path", + "operationId": "get_item_with_path_items__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": { + "title": "The ID of the item", + "minimum": 1, + "maximum": 1000, + "type": "integer", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": { + "title": "Item Id", + "minimum": 1, + "type": "integer", + }, + } + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "title": "Body", + "allOf": [ + { + "$ref": "#/components/schemas/Body_update_item_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" + } + } + }, + }, + }, + }, + }, + "/items/": { + "get": { + "summary": "Get Items With Query", + "operationId": "get_items_with_query_items__get", + "parameters": [ + { + "name": "q", + "in": "query", + "required": False, + "schema": { + "title": "Q", + "maxLength": 50, + "minLength": 3, + "pattern": "^[a-zA-Z0-9 ]+$", + "type": "string", + }, + }, + { + "name": "skip", + "in": "query", + "required": False, + "schema": { + "title": "Skip", + "default": 0, + "minimum": 0, + "type": "integer", + }, + }, + { + "name": "limit", + "in": "query", + "required": False, + "schema": { + "title": "Limit", + "default": 10, + "minimum": 1, + "maximum": 100, + "examples": [5], + "type": "integer", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "title": "Item", + "examples": [ + { + "name": "Foo", + "price": 35.4, + "description": "The Foo item", + } + ], + "allOf": [ + {"$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" + } + } + }, + }, + }, + }, + }, + "/users/": { + "get": { + "summary": "Get User With Header", + "operationId": "get_user_with_header_users__get", + "parameters": [ + { + "name": "x-custom", + "in": "header", + "required": False, + "schema": {"title": "X-Custom", "type": "string"}, + }, + { + "name": "x-token", + "in": "header", + "required": False, + "schema": {"title": "X-Token", "type": "string"}, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/cookies/": { + "get": { + "summary": "Get Cookies", + "operationId": "get_cookies_cookies__get", + "parameters": [ + { + "name": "session_id", + "in": "cookie", + "required": False, + "schema": {"title": "Session Id", "type": "string"}, + }, + { + "name": "tracking_id", + "in": "cookie", + "required": False, + "schema": { + "title": "Tracking Id", + "minLength": 10, + "type": "string", + }, + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items-embed/": { + "post": { + "summary": "Create Item Embed", + "operationId": "create_item_embed_items_embed__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_item_embed_items_embed__post" + } + ], + "title": "Body", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/form-data/": { + "post": { + "summary": "Submit Form", + "operationId": "submit_form_form_data__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Body_submit_form_form_data__post" + } + ], + "title": "Body", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/upload/": { + "post": { + "summary": "Upload File", + "operationId": "upload_file_upload__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Body_upload_file_upload__post" + } + ], + "title": "Body", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/upload-multiple/": { + "post": { + "summary": "Upload Multiple Files", + "operationId": "upload_multiple_files_upload_multiple__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/Body_upload_multiple_files_upload_multiple__post" + } + ], + "title": "Body", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_item_embed_items_embed__post": { + "properties": { + "item": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + } + }, + "type": "object", + "required": ["item"], + "title": "Body_create_item_embed_items_embed__post", + }, + "Body_submit_form_form_data__post": { + "properties": { + "username": { + "type": "string", + "maxLength": 50, + "minLength": 3, + "title": "Username", + }, + "password": { + "type": "string", + "minLength": 8, + "title": "Password", + }, + "email": {"type": "string", "title": "Email"}, + }, + "type": "object", + "required": ["username", "password"], + "title": "Body_submit_form_form_data__post", + }, + "Body_update_item_items__item_id__put": { + "properties": { + "item": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + }, + "importance": { + "type": "integer", + "maximum": 10.0, + "exclusiveMinimum": 0.0, + "title": "Importance", + }, + }, + "type": "object", + "required": ["item", "importance"], + "title": "Body_update_item_items__item_id__put", + }, + "Body_upload_file_upload__post": { + "properties": { + "file": { + "type": "string", + "format": "binary", + "title": "File", + }, + "description": {"type": "string", "title": "Description"}, + }, + "type": "object", + "required": ["file"], + "title": "Body_upload_file_upload__post", + }, + "Body_upload_multiple_files_upload_multiple__post": { + "properties": { + "files": { + "items": {"type": "string", "format": "binary"}, + "type": "array", + "title": "Files", + }, + "note": {"type": "string", "title": "Note", "default": ""}, + }, + "type": "object", + "required": ["files"], + "title": "Body_upload_multiple_files_upload_multiple__post", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "price": {"type": "number", "title": "Price"}, + "description": {"type": "string", "title": "Description"}, + }, + "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", + }, + } + }, + } + )