Compare commits

...

7 Commits

Author SHA1 Message Date
Sebastián Ramírez
3c08b05ea6 🔖 Bump version, after query and header as lists
and bug fixes for Python 3.7
2018-12-30 21:46:49 +04:00
Sebastián Ramírez
60599bad99 🐛 Fix Python 3.7 specific list query handling 2018-12-30 21:43:34 +04:00
Sebastián Ramírez
ccf30b5c2e 📝 Update docs, 100% coverage 2018-12-30 00:17:22 +04:00
Sebastián Ramírez
ca0652aebf 🐛 Fix type checks for Python 3.7 2018-12-30 00:14:39 +04:00
Sebastián Ramírez
be957e7c99 Allow lists of query or header params
and add tests for them
2018-12-30 00:07:31 +04:00
Sebastián Ramírez
90af868146 Add security checks for HTTP utils
and tests for them
2018-12-29 23:04:54 +04:00
Sebastián Ramírez
660f917d79 ✏️ Fix typos and docs notes 2018-12-29 18:43:58 +04:00
25 changed files with 683 additions and 31 deletions

View File

@@ -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.

View File

@@ -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:

View File

@@ -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.

View File

@@ -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">

View File

@@ -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

View File

@@ -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.

View File

@@ -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`:

View File

@@ -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.

View File

@@ -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"

View File

@@ -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!}

View File

@@ -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"?

View File

@@ -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"?

View File

@@ -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.

View File

@@ -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

View File

@@ -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:

View File

@@ -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):

View File

@@ -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

View File

@@ -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)

View 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

View 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
View 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"}

View 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"}

View 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"}

View 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"}

View 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"}