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