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

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 auto_error=False, 

20) 

21 

22 

23class User(BaseModel): 1abcdef

24 username: str 1abcdef

25 

26 

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

32 

33 

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

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

36 return form_data 1yzABCD

37 

38 

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

44 

45 

46client = TestClient(app) 1abcdef

47 

48 

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

53 

54 

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

59 

60 

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

65 

66 

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 ) 

115 

116 

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 ) 

143 

144 

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 ) 

184 

185 

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 } 

200 

201 

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 }