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

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

7 

8app = FastAPI() 1abcdef

9 

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) 

18 

19 

20class User(BaseModel): 1abcdef

21 username: str 1abcdef

22 

23 

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

28 

29 

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

34 

35 

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

40 

41 

42client = TestClient(app) 1abcdef

43 

44 

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

49 

50 

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

55 

56 

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

61 

62 

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 ) 

111 

112 

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 ) 

139 

140 

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 ) 

180 

181 

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 } 

196 

197 

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 }