Coverage for tests / test_security_oauth2.py: 100%

54 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-12 18:15 +0000

1import pytest 1abcd

2from fastapi import Depends, FastAPI, Security 1abcd

3from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict 1abcd

4from fastapi.testclient import TestClient 1abcd

5from inline_snapshot import snapshot 1abcd

6from pydantic import BaseModel 1abcd

7 

8app = FastAPI() 1abcd

9 

10reusable_oauth2 = OAuth2( 1abcd

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): 1abcd

21 username: str 1abcd

22 

23 

24# Here we use string annotations to test them 

25def get_current_user(oauth_header: "str" = Security(reusable_oauth2)): 1abcd

26 user = User(username=oauth_header) 1efghij

27 return user 1efghij

28 

29 

30@app.post("/login") 1abcd

31# Here we use string annotations to test them 

32def login(form_data: "OAuth2PasswordRequestFormStrict" = Depends()): 1abcd

33 return form_data 1klm

34 

35 

36@app.get("/users/me") 1abcd

37# Here we use string annotations to test them 

38def read_current_user(current_user: "User" = Depends(get_current_user)): 1abcd

39 return current_user 1efghij

40 

41 

42client = TestClient(app) 1abcd

43 

44 

45def test_security_oauth2(): 1abcd

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

47 assert response.status_code == 200, response.text 1egi

48 assert response.json() == {"username": "Bearer footokenbar"} 1egi

49 

50 

51def test_security_oauth2_password_other_header(): 1abcd

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

53 assert response.status_code == 200, response.text 1fhj

54 assert response.json() == {"username": "Other footokenbar"} 1fhj

55 

56 

57def test_security_oauth2_password_bearer_no_header(): 1abcd

58 response = client.get("/users/me") 1nop

59 assert response.status_code == 401, response.text 1nop

60 assert response.json() == {"detail": "Not authenticated"} 1nop

61 assert response.headers["WWW-Authenticate"] == "Bearer" 1nop

62 

63 

64def test_strict_login_no_data(): 1abcd

65 response = client.post("/login") 1qrs

66 assert response.status_code == 422 1qrs

67 assert response.json() == { 1qrs

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 

90 

91def test_strict_login_no_grant_type(): 1abcd

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

93 assert response.status_code == 422 1tuv

94 assert response.json() == { 1tuv

95 "detail": [ 

96 { 

97 "type": "missing", 

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

99 "msg": "Field required", 

100 "input": None, 

101 } 

102 ] 

103 } 

104 

105 

106@pytest.mark.parametrize( 1abcd

107 argnames=["grant_type"], 

108 argvalues=[ 

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

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

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

112 ], 

113) 

114def test_strict_login_incorrect_grant_type(grant_type: str): 1abcd

115 response = client.post( 1wxy

116 "/login", 

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

118 ) 

119 assert response.status_code == 422 1wxy

120 assert response.json() == { 1wxy

121 "detail": [ 

122 { 

123 "type": "string_pattern_mismatch", 

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

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

126 "input": grant_type, 

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

128 } 

129 ] 

130 } 

131 

132 

133def test_strict_login_correct_grant_type(): 1abcd

134 response = client.post( 1klm

135 "/login", 

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

137 ) 

138 assert response.status_code == 200 1klm

139 assert response.json() == { 1klm

140 "grant_type": "password", 

141 "username": "johndoe", 

142 "password": "secret", 

143 "scopes": [], 

144 "client_id": None, 

145 "client_secret": None, 

146 } 

147 

148 

149def test_openapi_schema(): 1abcd

150 response = client.get("/openapi.json") 1zAB

151 assert response.status_code == 200, response.text 1zAB

152 assert response.json() == snapshot( 1zAB

153 { 

154 "openapi": "3.1.0", 

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

156 "paths": { 

157 "/login": { 

158 "post": { 

159 "responses": { 

160 "200": { 

161 "description": "Successful Response", 

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

163 }, 

164 "422": { 

165 "description": "Validation Error", 

166 "content": { 

167 "application/json": { 

168 "schema": { 

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

170 } 

171 } 

172 }, 

173 }, 

174 }, 

175 "summary": "Login", 

176 "operationId": "login_login_post", 

177 "requestBody": { 

178 "content": { 

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

180 "schema": { 

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

182 } 

183 } 

184 }, 

185 "required": True, 

186 }, 

187 } 

188 }, 

189 "/users/me": { 

190 "get": { 

191 "responses": { 

192 "200": { 

193 "description": "Successful Response", 

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

195 } 

196 }, 

197 "summary": "Read Current User", 

198 "operationId": "read_current_user_users_me_get", 

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

200 } 

201 }, 

202 }, 

203 "components": { 

204 "schemas": { 

205 "Body_login_login_post": { 

206 "title": "Body_login_login_post", 

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

208 "type": "object", 

209 "properties": { 

210 "grant_type": { 

211 "title": "Grant Type", 

212 "pattern": "^password$", 

213 "type": "string", 

214 }, 

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

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

217 "scope": { 

218 "title": "Scope", 

219 "type": "string", 

220 "default": "", 

221 }, 

222 "client_id": { 

223 "title": "Client Id", 

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

225 }, 

226 "client_secret": { 

227 "title": "Client Secret", 

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

229 }, 

230 }, 

231 }, 

232 "ValidationError": { 

233 "title": "ValidationError", 

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

235 "type": "object", 

236 "properties": { 

237 "loc": { 

238 "title": "Location", 

239 "type": "array", 

240 "items": { 

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

242 }, 

243 }, 

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

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

246 "input": {"title": "Input"}, 

247 "ctx": {"title": "Context", "type": "object"}, 

248 }, 

249 }, 

250 "HTTPValidationError": { 

251 "title": "HTTPValidationError", 

252 "type": "object", 

253 "properties": { 

254 "detail": { 

255 "title": "Detail", 

256 "type": "array", 

257 "items": { 

258 "$ref": "#/components/schemas/ValidationError" 

259 }, 

260 } 

261 }, 

262 }, 

263 }, 

264 "securitySchemes": { 

265 "OAuth2": { 

266 "type": "oauth2", 

267 "flows": { 

268 "password": { 

269 "scopes": { 

270 "read:users": "Read the users", 

271 "write:users": "Create users", 

272 }, 

273 "tokenUrl": "token", 

274 } 

275 }, 

276 } 

277 }, 

278 }, 

279 } 

280 )