Coverage for tests/test_security_oauth2_optional_description.py: 100%

58 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-05-05 00:03 +0000

1from typing import Optional 1abcdef

2 

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

9 

10app = FastAPI() 1abcdef

11 

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 description="OAuth2 security scheme", 

20 auto_error=False, 

21) 

22 

23 

24class User(BaseModel): 1abcdef

25 username: str 1abcdef

26 

27 

28def get_current_user(oauth_header: Optional[str] = Security(reusable_oauth2)): 1abcdef

29 if oauth_header is None: 1gshitjkulmvnowpqxr

30 return None 1stuvwx

31 user = User(username=oauth_header) 1ghijklmnopqr

32 return user 1ghijklmnopqr

33 

34 

35@app.post("/login") 1abcdef

36def login(form_data: OAuth2PasswordRequestFormStrict = Depends()): 1abcdef

37 return form_data 1yzABCD

38 

39 

40@app.get("/users/me") 1abcdef

41def read_users_me(current_user: Optional[User] = Depends(get_current_user)): 1abcdef

42 if current_user is None: 1gshitjkulmvnowpqxr

43 return {"msg": "Create an account first"} 1stuvwx

44 return current_user 1ghijklmnopqr

45 

46 

47client = TestClient(app) 1abcdef

48 

49 

50def test_security_oauth2(): 1abcdef

51 response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"}) 1gikmoq

52 assert response.status_code == 200, response.text 1gikmoq

53 assert response.json() == {"username": "Bearer footokenbar"} 1gikmoq

54 

55 

56def test_security_oauth2_password_other_header(): 1abcdef

57 response = client.get("/users/me", headers={"Authorization": "Other footokenbar"}) 1hjlnpr

58 assert response.status_code == 200, response.text 1hjlnpr

59 assert response.json() == {"username": "Other footokenbar"} 1hjlnpr

60 

61 

62def test_security_oauth2_password_bearer_no_header(): 1abcdef

63 response = client.get("/users/me") 1stuvwx

64 assert response.status_code == 200, response.text 1stuvwx

65 assert response.json() == {"msg": "Create an account first"} 1stuvwx

66 

67 

68def test_strict_login_None(): 1abcdef

69 response = client.post("/login", data=None) 1EFGHIJ

70 assert response.status_code == 422 1EFGHIJ

71 assert response.json() == IsDict( 1EFGHIJ

72 { 

73 "detail": [ 

74 { 

75 "type": "missing", 

76 "loc": ["body", "grant_type"], 

77 "msg": "Field required", 

78 "input": None, 

79 }, 

80 { 

81 "type": "missing", 

82 "loc": ["body", "username"], 

83 "msg": "Field required", 

84 "input": None, 

85 }, 

86 { 

87 "type": "missing", 

88 "loc": ["body", "password"], 

89 "msg": "Field required", 

90 "input": None, 

91 }, 

92 ] 

93 } 

94 ) | IsDict( 

95 # TODO: remove when deprecating Pydantic v1 

96 { 

97 "detail": [ 

98 { 

99 "loc": ["body", "grant_type"], 

100 "msg": "field required", 

101 "type": "value_error.missing", 

102 }, 

103 { 

104 "loc": ["body", "username"], 

105 "msg": "field required", 

106 "type": "value_error.missing", 

107 }, 

108 { 

109 "loc": ["body", "password"], 

110 "msg": "field required", 

111 "type": "value_error.missing", 

112 }, 

113 ] 

114 } 

115 ) 

116 

117 

118def test_strict_login_no_grant_type(): 1abcdef

119 response = client.post("/login", data={"username": "johndoe", "password": "secret"}) 1KLMNOP

120 assert response.status_code == 422 1KLMNOP

121 assert response.json() == IsDict( 1KLMNOP

122 { 

123 "detail": [ 

124 { 

125 "type": "missing", 

126 "loc": ["body", "grant_type"], 

127 "msg": "Field required", 

128 "input": None, 

129 } 

130 ] 

131 } 

132 ) | IsDict( 

133 # TODO: remove when deprecating Pydantic v1 

134 { 

135 "detail": [ 

136 { 

137 "loc": ["body", "grant_type"], 

138 "msg": "field required", 

139 "type": "value_error.missing", 

140 } 

141 ] 

142 } 

143 ) 

144 

145 

146@pytest.mark.parametrize( 1abcdef

147 argnames=["grant_type"], 

148 argvalues=[ 

149 pytest.param("incorrect", id="incorrect value"), 

150 pytest.param("passwordblah", id="password with suffix"), 

151 pytest.param("blahpassword", id="password with prefix"), 

152 ], 

153) 

154def test_strict_login_incorrect_grant_type(grant_type: str): 1abcdef

155 response = client.post( 1QRSTUV

156 "/login", 

157 data={"username": "johndoe", "password": "secret", "grant_type": grant_type}, 

158 ) 

159 assert response.status_code == 422 1QRSTUV

160 assert response.json() == IsDict( 1QRSTUV

161 { 

162 "detail": [ 

163 { 

164 "type": "string_pattern_mismatch", 

165 "loc": ["body", "grant_type"], 

166 "msg": "String should match pattern '^password$'", 

167 "input": grant_type, 

168 "ctx": {"pattern": "^password$"}, 

169 } 

170 ] 

171 } 

172 ) | IsDict( 

173 # TODO: remove when deprecating Pydantic v1 

174 { 

175 "detail": [ 

176 { 

177 "loc": ["body", "grant_type"], 

178 "msg": 'string does not match regex "^password$"', 

179 "type": "value_error.str.regex", 

180 "ctx": {"pattern": "^password$"}, 

181 } 

182 ] 

183 } 

184 ) 

185 

186 

187def test_strict_login_correct_correct_grant_type(): 1abcdef

188 response = client.post( 1yzABCD

189 "/login", 

190 data={"username": "johndoe", "password": "secret", "grant_type": "password"}, 

191 ) 

192 assert response.status_code == 200, response.text 1yzABCD

193 assert response.json() == { 1yzABCD

194 "grant_type": "password", 

195 "username": "johndoe", 

196 "password": "secret", 

197 "scopes": [], 

198 "client_id": None, 

199 "client_secret": None, 

200 } 

201 

202 

203def test_openapi_schema(): 1abcdef

204 response = client.get("/openapi.json") 1WXYZ01

205 assert response.status_code == 200, response.text 1WXYZ01

206 assert response.json() == { 1WXYZ01

207 "openapi": "3.1.0", 

208 "info": {"title": "FastAPI", "version": "0.1.0"}, 

209 "paths": { 

210 "/login": { 

211 "post": { 

212 "responses": { 

213 "200": { 

214 "description": "Successful Response", 

215 "content": {"application/json": {"schema": {}}}, 

216 }, 

217 "422": { 

218 "description": "Validation Error", 

219 "content": { 

220 "application/json": { 

221 "schema": { 

222 "$ref": "#/components/schemas/HTTPValidationError" 

223 } 

224 } 

225 }, 

226 }, 

227 }, 

228 "summary": "Login", 

229 "operationId": "login_login_post", 

230 "requestBody": { 

231 "content": { 

232 "application/x-www-form-urlencoded": { 

233 "schema": { 

234 "$ref": "#/components/schemas/Body_login_login_post" 

235 } 

236 } 

237 }, 

238 "required": True, 

239 }, 

240 } 

241 }, 

242 "/users/me": { 

243 "get": { 

244 "responses": { 

245 "200": { 

246 "description": "Successful Response", 

247 "content": {"application/json": {"schema": {}}}, 

248 } 

249 }, 

250 "summary": "Read Users Me", 

251 "operationId": "read_users_me_users_me_get", 

252 "security": [{"OAuth2": []}], 

253 } 

254 }, 

255 }, 

256 "components": { 

257 "schemas": { 

258 "Body_login_login_post": { 

259 "title": "Body_login_login_post", 

260 "required": ["grant_type", "username", "password"], 

261 "type": "object", 

262 "properties": { 

263 "grant_type": { 

264 "title": "Grant Type", 

265 "pattern": "^password$", 

266 "type": "string", 

267 }, 

268 "username": {"title": "Username", "type": "string"}, 

269 "password": {"title": "Password", "type": "string"}, 

270 "scope": {"title": "Scope", "type": "string", "default": ""}, 

271 "client_id": IsDict( 

272 { 

273 "title": "Client Id", 

274 "anyOf": [{"type": "string"}, {"type": "null"}], 

275 } 

276 ) 

277 | IsDict( 

278 # TODO: remove when deprecating Pydantic v1 

279 {"title": "Client Id", "type": "string"} 

280 ), 

281 "client_secret": IsDict( 

282 { 

283 "title": "Client Secret", 

284 "anyOf": [{"type": "string"}, {"type": "null"}], 

285 } 

286 ) 

287 | IsDict( 

288 # TODO: remove when deprecating Pydantic v1 

289 {"title": "Client Secret", "type": "string"} 

290 ), 

291 }, 

292 }, 

293 "ValidationError": { 

294 "title": "ValidationError", 

295 "required": ["loc", "msg", "type"], 

296 "type": "object", 

297 "properties": { 

298 "loc": { 

299 "title": "Location", 

300 "type": "array", 

301 "items": { 

302 "anyOf": [{"type": "string"}, {"type": "integer"}] 

303 }, 

304 }, 

305 "msg": {"title": "Message", "type": "string"}, 

306 "type": {"title": "Error Type", "type": "string"}, 

307 }, 

308 }, 

309 "HTTPValidationError": { 

310 "title": "HTTPValidationError", 

311 "type": "object", 

312 "properties": { 

313 "detail": { 

314 "title": "Detail", 

315 "type": "array", 

316 "items": {"$ref": "#/components/schemas/ValidationError"}, 

317 } 

318 }, 

319 }, 

320 }, 

321 "securitySchemes": { 

322 "OAuth2": { 

323 "type": "oauth2", 

324 "flows": { 

325 "password": { 

326 "scopes": { 

327 "read:users": "Read the users", 

328 "write:users": "Create users", 

329 }, 

330 "tokenUrl": "token", 

331 } 

332 }, 

333 "description": "OAuth2 security scheme", 

334 } 

335 }, 

336 }, 

337 }