Coverage for tests/test_security_oauth2_optional.py: 100%
58 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-05 00:03 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-05 00:03 +0000
1from typing import Optional 1abcdef
3import pytest 1abcdef
4from dirty_equals import IsDict 1abcdef
5from fastapi import Depends, FastAPI, Security 1abcdef
6from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict 1abcdef
7from fastapi.testclient import TestClient 1abcdef
8from pydantic import BaseModel 1abcdef
10app = FastAPI() 1abcdef
12reusable_oauth2 = OAuth2( 1abcdef
13 flows={
14 "password": {
15 "tokenUrl": "token",
16 "scopes": {"read:users": "Read the users", "write:users": "Create users"},
17 }
18 },
19 auto_error=False,
20)
23class User(BaseModel): 1abcdef
24 username: str 1abcdef
27def get_current_user(oauth_header: Optional[str] = Security(reusable_oauth2)): 1abcdef
28 if oauth_header is None: 1gshitjkulmvnowpqxr
29 return None 1stuvwx
30 user = User(username=oauth_header) 1ghijklmnopqr
31 return user 1ghijklmnopqr
34@app.post("/login") 1abcdef
35def login(form_data: OAuth2PasswordRequestFormStrict = Depends()): 1abcdef
36 return form_data 1yzABCD
39@app.get("/users/me") 1abcdef
40def read_users_me(current_user: Optional[User] = Depends(get_current_user)): 1abcdef
41 if current_user is None: 1gshitjkulmvnowpqxr
42 return {"msg": "Create an account first"} 1stuvwx
43 return current_user 1ghijklmnopqr
46client = TestClient(app) 1abcdef
49def test_security_oauth2(): 1abcdef
50 response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"}) 1gikmoq
51 assert response.status_code == 200, response.text 1gikmoq
52 assert response.json() == {"username": "Bearer footokenbar"} 1gikmoq
55def test_security_oauth2_password_other_header(): 1abcdef
56 response = client.get("/users/me", headers={"Authorization": "Other footokenbar"}) 1hjlnpr
57 assert response.status_code == 200, response.text 1hjlnpr
58 assert response.json() == {"username": "Other footokenbar"} 1hjlnpr
61def test_security_oauth2_password_bearer_no_header(): 1abcdef
62 response = client.get("/users/me") 1stuvwx
63 assert response.status_code == 200, response.text 1stuvwx
64 assert response.json() == {"msg": "Create an account first"} 1stuvwx
67def test_strict_login_no_data(): 1abcdef
68 response = client.post("/login") 1EFGHIJ
69 assert response.status_code == 422 1EFGHIJ
70 assert response.json() == IsDict( 1EFGHIJ
71 {
72 "detail": [
73 {
74 "type": "missing",
75 "loc": ["body", "grant_type"],
76 "msg": "Field required",
77 "input": None,
78 },
79 {
80 "type": "missing",
81 "loc": ["body", "username"],
82 "msg": "Field required",
83 "input": None,
84 },
85 {
86 "type": "missing",
87 "loc": ["body", "password"],
88 "msg": "Field required",
89 "input": None,
90 },
91 ]
92 }
93 ) | IsDict(
94 # TODO: remove when deprecating Pydantic v1
95 {
96 "detail": [
97 {
98 "loc": ["body", "grant_type"],
99 "msg": "field required",
100 "type": "value_error.missing",
101 },
102 {
103 "loc": ["body", "username"],
104 "msg": "field required",
105 "type": "value_error.missing",
106 },
107 {
108 "loc": ["body", "password"],
109 "msg": "field required",
110 "type": "value_error.missing",
111 },
112 ]
113 }
114 )
117def test_strict_login_no_grant_type(): 1abcdef
118 response = client.post("/login", data={"username": "johndoe", "password": "secret"}) 1KLMNOP
119 assert response.status_code == 422 1KLMNOP
120 assert response.json() == IsDict( 1KLMNOP
121 {
122 "detail": [
123 {
124 "type": "missing",
125 "loc": ["body", "grant_type"],
126 "msg": "Field required",
127 "input": None,
128 }
129 ]
130 }
131 ) | IsDict(
132 # TODO: remove when deprecating Pydantic v1
133 {
134 "detail": [
135 {
136 "loc": ["body", "grant_type"],
137 "msg": "field required",
138 "type": "value_error.missing",
139 }
140 ]
141 }
142 )
145@pytest.mark.parametrize( 1abcdef
146 argnames=["grant_type"],
147 argvalues=[
148 pytest.param("incorrect", id="incorrect value"),
149 pytest.param("passwordblah", id="password with suffix"),
150 pytest.param("blahpassword", id="password with prefix"),
151 ],
152)
153def test_strict_login_incorrect_grant_type(grant_type: str): 1abcdef
154 response = client.post( 1QRSTUV
155 "/login",
156 data={"username": "johndoe", "password": "secret", "grant_type": grant_type},
157 )
158 assert response.status_code == 422 1QRSTUV
159 assert response.json() == IsDict( 1QRSTUV
160 {
161 "detail": [
162 {
163 "type": "string_pattern_mismatch",
164 "loc": ["body", "grant_type"],
165 "msg": "String should match pattern '^password$'",
166 "input": grant_type,
167 "ctx": {"pattern": "^password$"},
168 }
169 ]
170 }
171 ) | IsDict(
172 # TODO: remove when deprecating Pydantic v1
173 {
174 "detail": [
175 {
176 "loc": ["body", "grant_type"],
177 "msg": 'string does not match regex "^password$"',
178 "type": "value_error.str.regex",
179 "ctx": {"pattern": "^password$"},
180 }
181 ]
182 }
183 )
186def test_strict_login_correct_data(): 1abcdef
187 response = client.post( 1yzABCD
188 "/login",
189 data={"username": "johndoe", "password": "secret", "grant_type": "password"},
190 )
191 assert response.status_code == 200 1yzABCD
192 assert response.json() == { 1yzABCD
193 "grant_type": "password",
194 "username": "johndoe",
195 "password": "secret",
196 "scopes": [],
197 "client_id": None,
198 "client_secret": None,
199 }
202def test_openapi_schema(): 1abcdef
203 response = client.get("/openapi.json") 1WXYZ01
204 assert response.status_code == 200, response.text 1WXYZ01
205 assert response.json() == { 1WXYZ01
206 "openapi": "3.1.0",
207 "info": {"title": "FastAPI", "version": "0.1.0"},
208 "paths": {
209 "/login": {
210 "post": {
211 "responses": {
212 "200": {
213 "description": "Successful Response",
214 "content": {"application/json": {"schema": {}}},
215 },
216 "422": {
217 "description": "Validation Error",
218 "content": {
219 "application/json": {
220 "schema": {
221 "$ref": "#/components/schemas/HTTPValidationError"
222 }
223 }
224 },
225 },
226 },
227 "summary": "Login",
228 "operationId": "login_login_post",
229 "requestBody": {
230 "content": {
231 "application/x-www-form-urlencoded": {
232 "schema": {
233 "$ref": "#/components/schemas/Body_login_login_post"
234 }
235 }
236 },
237 "required": True,
238 },
239 }
240 },
241 "/users/me": {
242 "get": {
243 "responses": {
244 "200": {
245 "description": "Successful Response",
246 "content": {"application/json": {"schema": {}}},
247 }
248 },
249 "summary": "Read Users Me",
250 "operationId": "read_users_me_users_me_get",
251 "security": [{"OAuth2": []}],
252 }
253 },
254 },
255 "components": {
256 "schemas": {
257 "Body_login_login_post": {
258 "title": "Body_login_login_post",
259 "required": ["grant_type", "username", "password"],
260 "type": "object",
261 "properties": {
262 "grant_type": {
263 "title": "Grant Type",
264 "pattern": "^password$",
265 "type": "string",
266 },
267 "username": {"title": "Username", "type": "string"},
268 "password": {"title": "Password", "type": "string"},
269 "scope": {"title": "Scope", "type": "string", "default": ""},
270 "client_id": IsDict(
271 {
272 "title": "Client Id",
273 "anyOf": [{"type": "string"}, {"type": "null"}],
274 }
275 )
276 | IsDict(
277 # TODO: remove when deprecating Pydantic v1
278 {"title": "Client Id", "type": "string"}
279 ),
280 "client_secret": IsDict(
281 {
282 "title": "Client Secret",
283 "anyOf": [{"type": "string"}, {"type": "null"}],
284 }
285 )
286 | IsDict(
287 # TODO: remove when deprecating Pydantic v1
288 {"title": "Client Secret", "type": "string"}
289 ),
290 },
291 },
292 "ValidationError": {
293 "title": "ValidationError",
294 "required": ["loc", "msg", "type"],
295 "type": "object",
296 "properties": {
297 "loc": {
298 "title": "Location",
299 "type": "array",
300 "items": {
301 "anyOf": [{"type": "string"}, {"type": "integer"}]
302 },
303 },
304 "msg": {"title": "Message", "type": "string"},
305 "type": {"title": "Error Type", "type": "string"},
306 },
307 },
308 "HTTPValidationError": {
309 "title": "HTTPValidationError",
310 "type": "object",
311 "properties": {
312 "detail": {
313 "title": "Detail",
314 "type": "array",
315 "items": {"$ref": "#/components/schemas/ValidationError"},
316 }
317 },
318 },
319 },
320 "securitySchemes": {
321 "OAuth2": {
322 "type": "oauth2",
323 "flows": {
324 "password": {
325 "scopes": {
326 "read:users": "Read the users",
327 "write:users": "Create users",
328 },
329 "tokenUrl": "token",
330 }
331 },
332 }
333 },
334 },
335 }