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

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

7 

8app = FastAPI() 1abcdefg

9 

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) 

18 

19 

20class User(BaseModel): 1abcdefg

21 username: str 1abcdefg

22 

23 

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

28 

29 

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

34 

35 

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

40 

41 

42client = TestClient(app) 1abcdefg

43 

44 

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

49 

50 

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

55 

56 

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

62 

63 

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 ) 

112 

113 

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 ) 

140 

141 

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 ) 

181 

182 

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 } 

197 

198 

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 }