Coverage for tests/test_tutorial/test_security/test_tutorial005.py: 100%
120 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-12-04 08:29 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2025-12-04 08:29 +0000
1import importlib 1abcdefg
2from types import ModuleType 1abcdefg
4import pytest 1abcdefg
5from dirty_equals import IsDict, IsOneOf 1abcdefg
6from fastapi.testclient import TestClient 1abcdefg
8from ...utils import needs_py39, needs_py310 1abcdefg
11@pytest.fixture( 1abcdefg
12 name="mod",
13 params=[
14 "tutorial005",
15 pytest.param("tutorial005_py310", marks=needs_py310),
16 "tutorial005_an",
17 pytest.param("tutorial005_py39", marks=needs_py39),
18 pytest.param("tutorial005_an_py39", marks=needs_py39),
19 pytest.param("tutorial005_an_py310", marks=needs_py310),
20 ],
21)
22def get_mod(request: pytest.FixtureRequest): 1abcdefg
23 mod = importlib.import_module(f"docs_src.security.{request.param}") 1abcdefg
25 return mod 1abcdefg
28def get_access_token( 1abcdefg
29 *, username="johndoe", password="secret", scope=None, client: TestClient
30):
31 data = {"username": username, "password": password} 1hJijklKmnopLqrstMuvwxNyzABOCDEFPGHI
32 if scope: 1hJijklKmnopLqrstMuvwxNyzABOCDEFPGHI
33 data["scope"] = scope 1hijlmnpqrtuvxyzBCDFGH
34 response = client.post("/token", data=data) 1hJijklKmnopLqrstMuvwxNyzABOCDEFPGHI
35 content = response.json() 1hJijklKmnopLqrstMuvwxNyzABOCDEFPGHI
36 access_token = content.get("access_token") 1hJijklKmnopLqrstMuvwxNyzABOCDEFPGHI
37 return access_token 1hJijklKmnopLqrstMuvwxNyzABOCDEFPGHI
40def test_login(mod: ModuleType): 1abcdefg
41 client = TestClient(mod.app) 1QRSTUVW
42 response = client.post("/token", data={"username": "johndoe", "password": "secret"}) 1QRSTUVW
43 assert response.status_code == 200, response.text 1QRSTUVW
44 content = response.json() 1QRSTUVW
45 assert "access_token" in content 1QRSTUVW
46 assert content["token_type"] == "bearer" 1QRSTUVW
49def test_login_incorrect_password(mod: ModuleType): 1abcdefg
50 client = TestClient(mod.app) 2jbkblbmbnbobpb
51 response = client.post( 2jbkblbmbnbobpb
52 "/token", data={"username": "johndoe", "password": "incorrect"}
53 )
54 assert response.status_code == 400, response.text 2jbkblbmbnbobpb
55 assert response.json() == {"detail": "Incorrect username or password"} 2jbkblbmbnbobpb
58def test_login_incorrect_username(mod: ModuleType): 1abcdefg
59 client = TestClient(mod.app) 2qbrbsbtbubvbwb
60 response = client.post("/token", data={"username": "foo", "password": "secret"}) 2qbrbsbtbubvbwb
61 assert response.status_code == 400, response.text 2qbrbsbtbubvbwb
62 assert response.json() == {"detail": "Incorrect username or password"} 2qbrbsbtbubvbwb
65def test_no_token(mod: ModuleType): 1abcdefg
66 client = TestClient(mod.app) 1XYZ0123
67 response = client.get("/users/me") 1XYZ0123
68 assert response.status_code == 401, response.text 1XYZ0123
69 assert response.json() == {"detail": "Not authenticated"} 1XYZ0123
70 assert response.headers["WWW-Authenticate"] == "Bearer" 1XYZ0123
73def test_token(mod: ModuleType): 1abcdefg
74 client = TestClient(mod.app) 1imquyCG
75 access_token = get_access_token(scope="me", client=client) 1imquyCG
76 response = client.get( 1imquyCG
77 "/users/me", headers={"Authorization": f"Bearer {access_token}"}
78 )
79 assert response.status_code == 200, response.text 1imquyCG
80 assert response.json() == { 1imquyCG
81 "username": "johndoe",
82 "full_name": "John Doe",
83 "email": "johndoe@example.com",
84 "disabled": False,
85 }
88def test_incorrect_token(mod: ModuleType): 1abcdefg
89 client = TestClient(mod.app) 1456789!
90 response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) 1456789!
91 assert response.status_code == 401, response.text 1456789!
92 assert response.json() == {"detail": "Could not validate credentials"} 1456789!
93 assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' 1456789!
96def test_incorrect_token_type(mod: ModuleType): 1abcdefg
97 client = TestClient(mod.app) 1#$%'()*
98 response = client.get( 1#$%'()*
99 "/users/me", headers={"Authorization": "Notexistent testtoken"}
100 )
101 assert response.status_code == 401, response.text 1#$%'()*
102 assert response.json() == {"detail": "Not authenticated"} 1#$%'()*
103 assert response.headers["WWW-Authenticate"] == "Bearer" 1#$%'()*
106def test_verify_password(mod: ModuleType): 1abcdefg
107 assert mod.verify_password( 2LbMbNbObPbQbRb
108 "secret", mod.fake_users_db["johndoe"]["hashed_password"]
109 )
112def test_get_password_hash(mod: ModuleType): 1abcdefg
113 assert mod.get_password_hash("secretalice") 2SbTbUbVbWbXbYb
116def test_create_access_token(mod: ModuleType): 1abcdefg
117 access_token = mod.create_access_token(data={"data": "foo"}) 2EbFbGbHbIbJbKb
118 assert access_token 2EbFbGbHbIbJbKb
121def test_token_no_sub(mod: ModuleType): 1abcdefg
122 client = TestClient(mod.app) 1+,-./:;
124 response = client.get( 1+,-./:;
125 "/users/me",
126 headers={
127 "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE"
128 },
129 )
130 assert response.status_code == 401, response.text 1+,-./:;
131 assert response.json() == {"detail": "Could not validate credentials"} 1+,-./:;
132 assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' 1+,-./:;
135def test_token_no_username(mod: ModuleType): 1abcdefg
136 client = TestClient(mod.app) 1=?@[]^_
138 response = client.get( 1=?@[]^_
139 "/users/me",
140 headers={
141 "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y"
142 },
143 )
144 assert response.status_code == 401, response.text 1=?@[]^_
145 assert response.json() == {"detail": "Could not validate credentials"} 1=?@[]^_
146 assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' 1=?@[]^_
149def test_token_no_scope(mod: ModuleType): 1abcdefg
150 client = TestClient(mod.app) 1koswAEI
152 access_token = get_access_token(client=client) 1koswAEI
153 response = client.get( 1koswAEI
154 "/users/me", headers={"Authorization": f"Bearer {access_token}"}
155 )
156 assert response.status_code == 401, response.text 1koswAEI
157 assert response.json() == {"detail": "Not enough permissions"} 1koswAEI
158 assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' 1koswAEI
161def test_token_nonexistent_user(mod: ModuleType): 1abcdefg
162 client = TestClient(mod.app) 2` { | } ~ abbb
164 response = client.get( 2` { | } ~ abbb
165 "/users/me",
166 headers={
167 "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw"
168 },
169 )
170 assert response.status_code == 401, response.text 2` { | } ~ abbb
171 assert response.json() == {"detail": "Could not validate credentials"} 2` { | } ~ abbb
172 assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' 2` { | } ~ abbb
175def test_token_inactive_user(mod: ModuleType): 1abcdefg
176 client = TestClient(mod.app) 1jnrvzDH
178 access_token = get_access_token( 1jnrvzDH
179 username="alice", password="secretalice", scope="me", client=client
180 )
181 response = client.get( 1jnrvzDH
182 "/users/me", headers={"Authorization": f"Bearer {access_token}"}
183 )
184 assert response.status_code == 400, response.text 1jnrvzDH
185 assert response.json() == {"detail": "Inactive user"} 1jnrvzDH
188def test_read_items(mod: ModuleType): 1abcdefg
189 client = TestClient(mod.app) 1hlptxBF
190 access_token = get_access_token(scope="me items", client=client) 1hlptxBF
191 response = client.get( 1hlptxBF
192 "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"}
193 )
194 assert response.status_code == 200, response.text 1hlptxBF
195 assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] 1hlptxBF
198def test_read_system_status(mod: ModuleType): 1abcdefg
199 client = TestClient(mod.app) 1JKLMNOP
200 access_token = get_access_token(client=client) 1JKLMNOP
201 response = client.get( 1JKLMNOP
202 "/status/", headers={"Authorization": f"Bearer {access_token}"}
203 )
204 assert response.status_code == 200, response.text 1JKLMNOP
205 assert response.json() == {"status": "ok"} 1JKLMNOP
208def test_read_system_status_no_token(mod: ModuleType): 1abcdefg
209 client = TestClient(mod.app) 2cbdbebfbgbhbib
210 response = client.get("/status/") 2cbdbebfbgbhbib
211 assert response.status_code == 401, response.text 2cbdbebfbgbhbib
212 assert response.json() == {"detail": "Not authenticated"} 2cbdbebfbgbhbib
213 assert response.headers["WWW-Authenticate"] == "Bearer" 2cbdbebfbgbhbib
216def test_openapi_schema(mod: ModuleType): 1abcdefg
217 client = TestClient(mod.app) 2xbybzbAbBbCbDb
218 response = client.get("/openapi.json") 2xbybzbAbBbCbDb
219 assert response.status_code == 200, response.text 2xbybzbAbBbCbDb
220 assert response.json() == { 2xbybzbAbBbCbDb
221 "openapi": "3.1.0",
222 "info": {"title": "FastAPI", "version": "0.1.0"},
223 "paths": {
224 "/token": {
225 "post": {
226 "responses": {
227 "200": {
228 "description": "Successful Response",
229 "content": {
230 "application/json": {
231 "schema": {"$ref": "#/components/schemas/Token"}
232 }
233 },
234 },
235 "422": {
236 "description": "Validation Error",
237 "content": {
238 "application/json": {
239 "schema": {
240 "$ref": "#/components/schemas/HTTPValidationError"
241 }
242 }
243 },
244 },
245 },
246 "summary": "Login For Access Token",
247 "operationId": "login_for_access_token_token_post",
248 "requestBody": {
249 "content": {
250 "application/x-www-form-urlencoded": {
251 "schema": {
252 "$ref": "#/components/schemas/Body_login_for_access_token_token_post"
253 }
254 }
255 },
256 "required": True,
257 },
258 }
259 },
260 "/users/me/": {
261 "get": {
262 "responses": {
263 "200": {
264 "description": "Successful Response",
265 "content": {
266 "application/json": {
267 "schema": {"$ref": "#/components/schemas/User"}
268 }
269 },
270 }
271 },
272 "summary": "Read Users Me",
273 "operationId": "read_users_me_users_me__get",
274 "security": [{"OAuth2PasswordBearer": ["me"]}],
275 }
276 },
277 "/users/me/items/": {
278 "get": {
279 "responses": {
280 "200": {
281 "description": "Successful Response",
282 "content": {"application/json": {"schema": {}}},
283 }
284 },
285 "summary": "Read Own Items",
286 "operationId": "read_own_items_users_me_items__get",
287 "security": [{"OAuth2PasswordBearer": ["items", "me"]}],
288 }
289 },
290 "/status/": {
291 "get": {
292 "responses": {
293 "200": {
294 "description": "Successful Response",
295 "content": {"application/json": {"schema": {}}},
296 }
297 },
298 "summary": "Read System Status",
299 "operationId": "read_system_status_status__get",
300 "security": [{"OAuth2PasswordBearer": []}],
301 }
302 },
303 },
304 "components": {
305 "schemas": {
306 "User": {
307 "title": "User",
308 "required": IsOneOf(
309 ["username", "email", "full_name", "disabled"],
310 # TODO: remove when deprecating Pydantic v1
311 ["username"],
312 ),
313 "type": "object",
314 "properties": {
315 "username": {"title": "Username", "type": "string"},
316 "email": IsDict(
317 {
318 "title": "Email",
319 "anyOf": [{"type": "string"}, {"type": "null"}],
320 }
321 )
322 | IsDict(
323 # TODO: remove when deprecating Pydantic v1
324 {"title": "Email", "type": "string"}
325 ),
326 "full_name": IsDict(
327 {
328 "title": "Full Name",
329 "anyOf": [{"type": "string"}, {"type": "null"}],
330 }
331 )
332 | IsDict(
333 # TODO: remove when deprecating Pydantic v1
334 {"title": "Full Name", "type": "string"}
335 ),
336 "disabled": IsDict(
337 {
338 "title": "Disabled",
339 "anyOf": [{"type": "boolean"}, {"type": "null"}],
340 }
341 )
342 | IsDict(
343 # TODO: remove when deprecating Pydantic v1
344 {"title": "Disabled", "type": "boolean"}
345 ),
346 },
347 },
348 "Token": {
349 "title": "Token",
350 "required": ["access_token", "token_type"],
351 "type": "object",
352 "properties": {
353 "access_token": {"title": "Access Token", "type": "string"},
354 "token_type": {"title": "Token Type", "type": "string"},
355 },
356 },
357 "Body_login_for_access_token_token_post": {
358 "title": "Body_login_for_access_token_token_post",
359 "required": ["username", "password"],
360 "type": "object",
361 "properties": {
362 "grant_type": IsDict(
363 {
364 "title": "Grant Type",
365 "anyOf": [
366 {"pattern": "^password$", "type": "string"},
367 {"type": "null"},
368 ],
369 }
370 )
371 | IsDict(
372 # TODO: remove when deprecating Pydantic v1
373 {
374 "title": "Grant Type",
375 "pattern": "^password$",
376 "type": "string",
377 }
378 ),
379 "username": {"title": "Username", "type": "string"},
380 "password": {
381 "title": "Password",
382 "type": "string",
383 "format": "password",
384 },
385 "scope": {"title": "Scope", "type": "string", "default": ""},
386 "client_id": IsDict(
387 {
388 "title": "Client Id",
389 "anyOf": [{"type": "string"}, {"type": "null"}],
390 }
391 )
392 | IsDict(
393 # TODO: remove when deprecating Pydantic v1
394 {"title": "Client Id", "type": "string"}
395 ),
396 "client_secret": IsDict(
397 {
398 "title": "Client Secret",
399 "anyOf": [{"type": "string"}, {"type": "null"}],
400 "format": "password",
401 }
402 )
403 | IsDict(
404 # TODO: remove when deprecating Pydantic v1
405 {
406 "title": "Client Secret",
407 "type": "string",
408 "format": "password",
409 }
410 ),
411 },
412 },
413 "ValidationError": {
414 "title": "ValidationError",
415 "required": ["loc", "msg", "type"],
416 "type": "object",
417 "properties": {
418 "loc": {
419 "title": "Location",
420 "type": "array",
421 "items": {
422 "anyOf": [{"type": "string"}, {"type": "integer"}]
423 },
424 },
425 "msg": {"title": "Message", "type": "string"},
426 "type": {"title": "Error Type", "type": "string"},
427 },
428 },
429 "HTTPValidationError": {
430 "title": "HTTPValidationError",
431 "type": "object",
432 "properties": {
433 "detail": {
434 "title": "Detail",
435 "type": "array",
436 "items": {"$ref": "#/components/schemas/ValidationError"},
437 }
438 },
439 },
440 },
441 "securitySchemes": {
442 "OAuth2PasswordBearer": {
443 "type": "oauth2",
444 "flows": {
445 "password": {
446 "scopes": {
447 "me": "Read information about the current user.",
448 "items": "Read items.",
449 },
450 "tokenUrl": "token",
451 }
452 },
453 }
454 },
455 },
456 }