mirror of
https://github.com/fastapi/fastapi.git
synced 2026-01-02 11:08:00 -05:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c08b05ea6 | ||
|
|
60599bad99 | ||
|
|
ccf30b5c2e | ||
|
|
ca0652aebf | ||
|
|
be957e7c99 | ||
|
|
90af868146 | ||
|
|
660f917d79 |
@@ -153,7 +153,7 @@ Any integration is designed to be so simple to use (with dependencies) that you
|
||||
|
||||
### Tested
|
||||
|
||||
* 100% <abbr title="The amount of code that is automatically tested">test coverage</abbr> (* not yet, in a couple days).
|
||||
* 100% <abbr title="The amount of code that is automatically tested">test coverage</abbr>.
|
||||
* 100% <abbr title="Python type annotations, with this your editor and external tools can give you better support">type annotated</abbr> code base.
|
||||
* Used in production applications.
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ But you can also declare multiple body parameters, e.g. `item` and `user`:
|
||||
{!./src/body_multiple_params/tutorial002.py!}
|
||||
```
|
||||
|
||||
In this case, **FastAPI** will notice that there are more than one body parameter in the function (two parameters that are Pydantic models).
|
||||
In this case, **FastAPI** will notice that there are more than one body parameters in the function (two parameters that are Pydantic models).
|
||||
|
||||
So, it will then use the parameter names as keys (field names) in the body, and expect a body like:
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ First, you have to import it:
|
||||
```
|
||||
|
||||
!!! warning
|
||||
Notice that `Schema` is imported directly from `pydantic`, not form `fastapi` as are all the rest (`Query`, `Path`, `Body`, etc).
|
||||
Notice that `Schema` is imported directly from `pydantic`, not from `fastapi` as are all the rest (`Query`, `Path`, `Body`, etc).
|
||||
|
||||
|
||||
## Declare model attributes
|
||||
@@ -37,7 +37,7 @@ In `Schema`, `Path`, `Query`, `Body` and others you'll see later, you can declar
|
||||
|
||||
Those parameters will be added as-is to the output JSON Schema.
|
||||
|
||||
If you know JSON Schema and want to add extra information appart from what we have discussed here, you can pass that as extra keyword arguments.
|
||||
If you know JSON Schema and want to add extra information apart from what we have discussed here, you can pass that as extra keyword arguments.
|
||||
|
||||
!!! warning
|
||||
Have in mind that extra parameters passed won't add any validation, only annotation, for documentation purposes.
|
||||
|
||||
@@ -86,7 +86,7 @@ And will be also used in the API docs inside each path operation that needs them
|
||||
|
||||
## Editor support
|
||||
|
||||
In your editor, inside your function you will get type hints and completion everywhere (this wouldn't happen if your received a `dict` instead of a Pydantic model):
|
||||
In your editor, inside your function you will get type hints and completion everywhere (this wouldn't happen if you received a `dict` instead of a Pydantic model):
|
||||
|
||||
<img src="/img/tutorial/body/image03.png">
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
You can define Cookie parameters the same way you define `Query` and `Path` parameteres.
|
||||
You can define Cookie parameters the same way you define `Query` and `Path` parameters.
|
||||
|
||||
## Import `Cookie`
|
||||
|
||||
@@ -8,11 +8,11 @@ First import `Cookie`:
|
||||
{!./src/cookie_params/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Declare `Cookie` parameteres
|
||||
## Declare `Cookie` parameters
|
||||
|
||||
Then declare the cookie parameters using the same structure as with `Path` and `Query`.
|
||||
|
||||
The first value is the default value, you can pass all the extra validation or annotation parameteres:
|
||||
The first value is the default value, you can pass all the extra validation or annotation parameters:
|
||||
|
||||
```Python hl_lines="7"
|
||||
{!./src/cookie_params/tutorial001.py!}
|
||||
@@ -22,7 +22,7 @@ The first value is the default value, you can pass all the extra validation or a
|
||||
`Cookie` is a "sister" class of `Path` and `Query`. It also inherits from the same common `Param` class.
|
||||
|
||||
!!! info
|
||||
To declare cookies, you need to use `Cookie`, because otherwise the parameters would be interpreted as query parameteres.
|
||||
To declare cookies, you need to use `Cookie`, because otherwise the parameters would be interpreted as query parameters.
|
||||
|
||||
## Recap
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ Pass `HTMLResponse` as the parameter `content_type` of your path operation:
|
||||
And it will be documented as such in OpenAPI.
|
||||
|
||||
|
||||
### return a Starlette `Response`
|
||||
### Return a Starlette `Response`
|
||||
|
||||
You can also override the response directly in your path operation.
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ What FastAPI actually checks is that it is a "callable" (function, class or anyt
|
||||
|
||||
If you pass a "callable" as a dependency in **FastAPI**, it will analyze the parameters for that "callable", and process them in the same way as the parameters for a path operation function. Including sub-dependencies.
|
||||
|
||||
That also applies to callables with no parameters at all. The same as would be for path operation functions with no parameteres.
|
||||
That also applies to callables with no parameters at all. The same as would be for path operation functions with no parameters.
|
||||
|
||||
Then, we can change the dependency "dependable" `common_parameters` from above to the class `CommonQueryParameters`:
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
You can define Header parameters the same way you define `Query`, `Path` and `Cookie` parameteres.
|
||||
You can define Header parameters the same way you define `Query`, `Path` and `Cookie` parameters.
|
||||
|
||||
## Import `Header`
|
||||
|
||||
@@ -8,11 +8,11 @@ First import `Header`:
|
||||
{!./src/header_params/tutorial001.py!}
|
||||
```
|
||||
|
||||
## Declare `Header` parameteres
|
||||
## Declare `Header` parameters
|
||||
|
||||
Then declare the header parameters using the same structure as with `Path`, `Query` and `Cookie`.
|
||||
|
||||
The first value is the default value, you can pass all the extra validation or annotation parameteres:
|
||||
The first value is the default value, you can pass all the extra validation or annotation parameters:
|
||||
|
||||
```Python hl_lines="7"
|
||||
{!./src/header_params/tutorial001.py!}
|
||||
@@ -22,7 +22,7 @@ The first value is the default value, you can pass all the extra validation or a
|
||||
`Header` is a "sister" class of `Path`, `Query` and `Cookie`. It also inherits from the same common `Param` class.
|
||||
|
||||
!!! info
|
||||
To declare headers, you need to use `Header`, because otherwise the parameters would be interpreted as query parameteres.
|
||||
To declare headers, you need to use `Header`, because otherwise the parameters would be interpreted as query parameters.
|
||||
|
||||
## Automatic conversion
|
||||
|
||||
@@ -49,6 +49,6 @@ If for some reason you need to disable automatic conversion of underscores to hy
|
||||
|
||||
## Recap
|
||||
|
||||
Declare headeres with `Header`, using the same common pattern as `Query`, `Path` and `Cookie`.
|
||||
Declare headers with `Header`, using the same common pattern as `Query`, `Path` and `Cookie`.
|
||||
|
||||
And don't worry about underscores in your variables, **FastAPI** will take care of converting them.
|
||||
|
||||
@@ -130,6 +130,11 @@ You can add more information about the parameter.
|
||||
|
||||
That information will be included in the generated OpenAPI and used by the documentation user interfaces and external tools.
|
||||
|
||||
!!! note
|
||||
Have in mind that different tools might have different levels of OpenAPI support.
|
||||
|
||||
Some of them might not show all the extra information declared yet, although in most of the cases, the missing feature is already planned for development.
|
||||
|
||||
You can add a `title`:
|
||||
|
||||
```Python hl_lines="7"
|
||||
|
||||
@@ -129,7 +129,7 @@ When you declare a default value for non-path parameters (for now, we have only
|
||||
|
||||
If you don't want to add a specific value but just make it optional, set the default as `None`.
|
||||
|
||||
But when you want to make a query parameter required, you can just do not declare any default value:
|
||||
But when you want to make a query parameter required, you can just not declare any default value:
|
||||
|
||||
```Python hl_lines="6 7"
|
||||
{!./src/query_params/tutorial005.py!}
|
||||
|
||||
@@ -22,7 +22,7 @@ The files will be uploaded as form data and you will receive the contents as `by
|
||||
`File` is a class that inherits directly from `Form`.
|
||||
|
||||
!!! info
|
||||
To declare File bodies, you need to use `File`, because otherwise the parameters would be interpreted as query parameteres or body (JSON) parameters.
|
||||
To declare File bodies, you need to use `File`, because otherwise the parameters would be interpreted as query parameters or body (JSON) parameters.
|
||||
|
||||
## "Form Data"?
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ With `Form` you can declare the same metadata and validation as with `Body` (and
|
||||
`Form` is a class that inherits directly from `Body`.
|
||||
|
||||
!!! info
|
||||
To declare form bodies, you need to use `Form` explicitly, because without it the parameters would be interpreted as query parameteres or body (JSON) parameters.
|
||||
To declare form bodies, you need to use `Form` explicitly, because without it the parameters would be interpreted as query parameters or body (JSON) parameters.
|
||||
|
||||
## "Form Fields"?
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ Now, whenever a browser is creating a user with a password, the API will return
|
||||
|
||||
In this case, it might not be a problem, becase the user himself is sending the password.
|
||||
|
||||
But if we use sthe same model for another path operation, we could be sending the passwords of our users to every client.
|
||||
But if we use the same model for another path operation, we could be sending the passwords of our users to every client.
|
||||
|
||||
!!! danger
|
||||
Never send the plain password of a user in a response.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.1.15"
|
||||
__version__ = "0.1.16"
|
||||
|
||||
from .applications import FastAPI
|
||||
from .routing import APIRouter
|
||||
|
||||
@@ -3,7 +3,7 @@ import inspect
|
||||
from copy import deepcopy
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
from typing import Any, Callable, Dict, List, Mapping, Sequence, Tuple, Type
|
||||
from typing import Any, Callable, Dict, List, Mapping, Sequence, Tuple, Type, Union
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import params
|
||||
@@ -13,11 +13,11 @@ from fastapi.utils import get_path_param_names
|
||||
from pydantic import BaseConfig, Schema, create_model
|
||||
from pydantic.error_wrappers import ErrorWrapper
|
||||
from pydantic.errors import MissingError
|
||||
from pydantic.fields import Field, Required
|
||||
from pydantic.fields import Field, Required, Shape
|
||||
from pydantic.schema import get_annotation_from_schema
|
||||
from pydantic.utils import lenient_issubclass
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from starlette.requests import Request
|
||||
from starlette.requests import Headers, QueryParams, Request
|
||||
|
||||
param_supported_types = (
|
||||
str,
|
||||
@@ -107,9 +107,18 @@ def get_dependant(*, path: str, call: Callable, name: str = None) -> Dependant:
|
||||
)
|
||||
elif isinstance(param.default, params.Param):
|
||||
if param.annotation != param.empty:
|
||||
assert lenient_issubclass(
|
||||
param.annotation, param_supported_types
|
||||
), f"Parameters for Path, Query, Header and Cookies must be of type str, int, float or bool: {param}"
|
||||
origin = getattr(param.annotation, "__origin__", None)
|
||||
param_all_types = param_supported_types + (list, tuple, set)
|
||||
if isinstance(param.default, (params.Query, params.Header)):
|
||||
assert lenient_issubclass(
|
||||
param.annotation, param_all_types
|
||||
) or lenient_issubclass(
|
||||
origin, param_all_types
|
||||
), f"Parameters for Query and Header must be of type str, int, float, bool, list, tuple or set: {param}"
|
||||
else:
|
||||
assert lenient_issubclass(
|
||||
param.annotation, param_supported_types
|
||||
), f"Parameters for Path and Cookies must be of type str, int, float, bool: {param}"
|
||||
add_param_to_fields(
|
||||
param=param, dependant=dependant, default_schema=params.Query
|
||||
)
|
||||
@@ -252,12 +261,18 @@ async def solve_dependencies(
|
||||
|
||||
|
||||
def request_params_to_args(
|
||||
required_params: Sequence[Field], received_params: Mapping[str, Any]
|
||||
required_params: Sequence[Field],
|
||||
received_params: Union[Mapping[str, Any], QueryParams, Headers],
|
||||
) -> Tuple[Dict[str, Any], List[ErrorWrapper]]:
|
||||
values = {}
|
||||
errors = []
|
||||
for field in required_params:
|
||||
value = received_params.get(field.alias)
|
||||
if field.shape in {Shape.LIST, Shape.SET, Shape.TUPLE} and isinstance(
|
||||
received_params, (QueryParams, Headers)
|
||||
):
|
||||
value = received_params.getlist(field.alias)
|
||||
else:
|
||||
value = received_params.get(field.alias)
|
||||
schema: params.Param = field.schema
|
||||
assert isinstance(schema, params.Param), "Params must be subclasses of Param"
|
||||
if value is None:
|
||||
|
||||
@@ -322,7 +322,7 @@ class OpenIdConnect(SecurityBase):
|
||||
openIdConnectUrl: str
|
||||
|
||||
|
||||
SecurityScheme = Union[APIKey, HTTPBase, HTTPBearer, OAuth2, OpenIdConnect]
|
||||
SecurityScheme = Union[APIKey, HTTPBase, OAuth2, OpenIdConnect, HTTPBearer]
|
||||
|
||||
|
||||
class Components(BaseModel):
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
from .api_key import APIKeyQuery, APIKeyHeader, APIKeyCookie
|
||||
from .http import HTTPBasic, HTTPBearer, HTTPDigest
|
||||
from .http import (
|
||||
HTTPBasic,
|
||||
HTTPBearer,
|
||||
HTTPDigest,
|
||||
HTTPBasicCredentials,
|
||||
HTTPAuthorizationCredentials,
|
||||
)
|
||||
from .oauth2 import OAuth2PasswordRequestForm, OAuth2, OAuth2PasswordBearer
|
||||
from .open_id_connect_url import OpenIdConnect
|
||||
|
||||
@@ -78,6 +78,11 @@ class HTTPBearer(HTTPBase):
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
if scheme.lower() != "bearer":
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN,
|
||||
detail="Invalid authentication credentials",
|
||||
)
|
||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||
|
||||
|
||||
@@ -93,4 +98,9 @@ class HTTPDigest(HTTPBase):
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
|
||||
)
|
||||
if scheme.lower() != "digest":
|
||||
raise HTTPException(
|
||||
status_code=HTTP_403_FORBIDDEN,
|
||||
detail="Invalid authentication credentials",
|
||||
)
|
||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||
|
||||
143
tests/test_multi_body_errors.py
Normal file
143
tests/test_multi_body_errors.py
Normal file
@@ -0,0 +1,143 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
|
||||
@app.post("/items/")
|
||||
def save_item_no_body(item: List[Item]):
|
||||
return {"item": item}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "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": "Save Item No Body Post",
|
||||
"operationId": "save_item_no_body_items__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"title": "Item",
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/Item"},
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Item": {
|
||||
"title": "Item",
|
||||
"required": ["name", "age"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"title": "Name", "type": "string"},
|
||||
"age": {"title": "Age", "type": "integer"},
|
||||
},
|
||||
},
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"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"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
multiple_errors = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "item", 0, "name"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "item", 0, "age"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "item", 1, "name"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
{
|
||||
"loc": ["body", "item", 1, "age"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_put_correct_body():
|
||||
response = client.post("/items/", json=[{"name": "Foo", "age": 5}])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item": [{"name": "Foo", "age": 5}]}
|
||||
|
||||
|
||||
def test_put_incorrect_body():
|
||||
response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}])
|
||||
assert response.status_code == 422
|
||||
assert response.json() == multiple_errors
|
||||
118
tests/test_multi_query_errors.py
Normal file
118
tests/test_multi_query_errors.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import FastAPI, Query
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/items/")
|
||||
def read_items(q: List[int] = Query(None)):
|
||||
return {"q": q}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "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 Get",
|
||||
"operationId": "read_items_items__get",
|
||||
"parameters": [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"title": "Q",
|
||||
"type": "array",
|
||||
"items": {"type": "integer"},
|
||||
},
|
||||
"name": "q",
|
||||
"in": "query",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"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"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
multiple_errors = {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "q", 0],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
},
|
||||
{
|
||||
"loc": ["query", "q", 1],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_multi_query():
|
||||
response = client.get("/items/?q=5&q=6")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"q": [5, 6]}
|
||||
|
||||
|
||||
def test_multi_query_incorrect():
|
||||
response = client.get("/items/?q=five&q=six")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == multiple_errors
|
||||
97
tests/test_put_no_body.py
Normal file
97
tests/test_put_no_body.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from fastapi import FastAPI
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.put("/items/{item_id}")
|
||||
def save_item_no_body(item_id: str):
|
||||
return {"item_id": item_id}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "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": "Save Item No Body Put",
|
||||
"operationId": "save_item_no_body_items__item_id__put",
|
||||
"parameters": [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "Item_Id", "type": "string"},
|
||||
"name": "item_id",
|
||||
"in": "path",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"ValidationError": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loc": {
|
||||
"title": "Location",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"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"},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_put_no_body():
|
||||
response = client.put("/items/foo")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "foo"}
|
||||
|
||||
|
||||
def test_put_no_body_with_body():
|
||||
response = client.put("/items/foo", json={"name": "Foo"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": "foo"}
|
||||
56
tests/test_security_http_base.py
Normal file
56
tests/test_security_http_base.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from fastapi import FastAPI, Security
|
||||
from fastapi.security.http import HTTPAuthorizationCredentials, HTTPBase
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
security = HTTPBase(scheme="Other")
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(credentials: HTTPAuthorizationCredentials = Security(security)):
|
||||
return {"scheme": credentials.scheme, "credentials": credentials.credentials}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"HTTPBase": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {"HTTPBase": {"type": "http", "scheme": "Other"}}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_http_base():
|
||||
response = client.get("/users/me", headers={"Authorization": "Other foobar"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"scheme": "Other", "credentials": "foobar"}
|
||||
|
||||
|
||||
def test_security_http_base_no_credentials():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
76
tests/test_security_http_basic.py
Normal file
76
tests/test_security_http_basic.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from base64 import b64encode
|
||||
|
||||
from fastapi import FastAPI, Security
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
security = HTTPBasic()
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(credentials: HTTPBasicCredentials = Security(security)):
|
||||
return {"username": credentials.username, "password": credentials.password}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"HTTPBasic": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_http_basic():
|
||||
auth = HTTPBasicAuth(username="john", password="secret")
|
||||
response = client.get("/users/me", auth=auth)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"username": "john", "password": "secret"}
|
||||
|
||||
|
||||
def test_security_http_basic_no_credentials():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
def test_security_http_basic_invalid_credentials():
|
||||
response = client.get(
|
||||
"/users/me", headers={"Authorization": "Basic notabase64token"}
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
|
||||
|
||||
def test_security_http_basic_non_basic_credentials():
|
||||
payload = b64encode(b"johnsecret").decode("ascii")
|
||||
auth_header = f"Basic {payload}"
|
||||
response = client.get("/users/me", headers={"Authorization": auth_header})
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
62
tests/test_security_http_bearer.py
Normal file
62
tests/test_security_http_bearer.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from fastapi import FastAPI, Security
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(credentials: HTTPAuthorizationCredentials = Security(security)):
|
||||
return {"scheme": credentials.scheme, "credentials": credentials.credentials}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"HTTPBearer": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_http_bearer():
|
||||
response = client.get("/users/me", headers={"Authorization": "Bearer foobar"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"scheme": "Bearer", "credentials": "foobar"}
|
||||
|
||||
|
||||
def test_security_http_bearer_no_credentials():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
def test_security_http_bearer_incorrect_scheme_credentials():
|
||||
response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
64
tests/test_security_http_digest.py
Normal file
64
tests/test_security_http_digest.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from fastapi import FastAPI, Security
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPDigest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
security = HTTPDigest()
|
||||
|
||||
|
||||
@app.get("/users/me")
|
||||
def read_current_user(credentials: HTTPAuthorizationCredentials = Security(security)):
|
||||
return {"scheme": credentials.scheme, "credentials": credentials.credentials}
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
openapi_schema = {
|
||||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/users/me": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {"application/json": {"schema": {}}},
|
||||
}
|
||||
},
|
||||
"summary": "Read Current User Get",
|
||||
"operationId": "read_current_user_users_me_get",
|
||||
"security": [{"HTTPDigest": []}],
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"securitySchemes": {"HTTPDigest": {"type": "http", "scheme": "digest"}}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_schema():
|
||||
response = client.get("/openapi.json")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == openapi_schema
|
||||
|
||||
|
||||
def test_security_http_digest():
|
||||
response = client.get("/users/me", headers={"Authorization": "Digest foobar"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"scheme": "Digest", "credentials": "foobar"}
|
||||
|
||||
|
||||
def test_security_http_digest_no_credentials():
|
||||
response = client.get("/users/me")
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Not authenticated"}
|
||||
|
||||
|
||||
def test_security_http_digest_incorrect_scheme_credentials():
|
||||
response = client.get(
|
||||
"/users/me", headers={"Authorization": "Other invalidauthorization"}
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "Invalid authentication credentials"}
|
||||
Reference in New Issue
Block a user