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