Coverage for tests/test_tutorial/test_security/test_tutorial005_an.py: 100%

98 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-08-08 03:53 +0000

1from dirty_equals import IsDict, IsOneOf 1abcde

2from fastapi.testclient import TestClient 1abcde

3 

4from docs_src.security.tutorial005_an import ( 1abcde

5 app, 

6 create_access_token, 

7 fake_users_db, 

8 get_password_hash, 

9 verify_password, 

10) 

11 

12client = TestClient(app) 1abcde

13 

14 

15def get_access_token(username="johndoe", password="secret", scope=None): 1abcde

16 data = {"username": username, "password": password} 1abcde

17 if scope: 1abcde

18 data["scope"] = scope 1abcde

19 response = client.post("/token", data=data) 1abcde

20 content = response.json() 1abcde

21 access_token = content.get("access_token") 1abcde

22 return access_token 1abcde

23 

24 

25def test_login(): 1abcde

26 response = client.post("/token", data={"username": "johndoe", "password": "secret"}) 1abcde

27 assert response.status_code == 200, response.text 1abcde

28 content = response.json() 1abcde

29 assert "access_token" in content 1abcde

30 assert content["token_type"] == "bearer" 1abcde

31 

32 

33def test_login_incorrect_password(): 1abcde

34 response = client.post( 1abcde

35 "/token", data={"username": "johndoe", "password": "incorrect"} 

36 ) 

37 assert response.status_code == 400, response.text 1abcde

38 assert response.json() == {"detail": "Incorrect username or password"} 1abcde

39 

40 

41def test_login_incorrect_username(): 1abcde

42 response = client.post("/token", data={"username": "foo", "password": "secret"}) 1abcde

43 assert response.status_code == 400, response.text 1abcde

44 assert response.json() == {"detail": "Incorrect username or password"} 1abcde

45 

46 

47def test_no_token(): 1abcde

48 response = client.get("/users/me") 1abcde

49 assert response.status_code == 401, response.text 1abcde

50 assert response.json() == {"detail": "Not authenticated"} 1abcde

51 assert response.headers["WWW-Authenticate"] == "Bearer" 1abcde

52 

53 

54def test_token(): 1abcde

55 access_token = get_access_token(scope="me") 1abcde

56 response = client.get( 1abcde

57 "/users/me", headers={"Authorization": f"Bearer {access_token}"} 

58 ) 

59 assert response.status_code == 200, response.text 1abcde

60 assert response.json() == { 1abcde

61 "username": "johndoe", 

62 "full_name": "John Doe", 

63 "email": "johndoe@example.com", 

64 "disabled": False, 

65 } 

66 

67 

68def test_incorrect_token(): 1abcde

69 response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) 1abcde

70 assert response.status_code == 401, response.text 1abcde

71 assert response.json() == {"detail": "Could not validate credentials"} 1abcde

72 assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' 1abcde

73 

74 

75def test_incorrect_token_type(): 1abcde

76 response = client.get( 1abcde

77 "/users/me", headers={"Authorization": "Notexistent testtoken"} 

78 ) 

79 assert response.status_code == 401, response.text 1abcde

80 assert response.json() == {"detail": "Not authenticated"} 1abcde

81 assert response.headers["WWW-Authenticate"] == "Bearer" 1abcde

82 

83 

84def test_verify_password(): 1abcde

85 assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) 1abcde

86 

87 

88def test_get_password_hash(): 1abcde

89 assert get_password_hash("secretalice") 1abcde

90 

91 

92def test_create_access_token(): 1abcde

93 access_token = create_access_token(data={"data": "foo"}) 1abcde

94 assert access_token 1abcde

95 

96 

97def test_token_no_sub(): 1abcde

98 response = client.get( 1abcde

99 "/users/me", 

100 headers={ 

101 "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" 

102 }, 

103 ) 

104 assert response.status_code == 401, response.text 1abcde

105 assert response.json() == {"detail": "Could not validate credentials"} 1abcde

106 assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' 1abcde

107 

108 

109def test_token_no_username(): 1abcde

110 response = client.get( 1abcde

111 "/users/me", 

112 headers={ 

113 "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" 

114 }, 

115 ) 

116 assert response.status_code == 401, response.text 1abcde

117 assert response.json() == {"detail": "Could not validate credentials"} 1abcde

118 assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' 1abcde

119 

120 

121def test_token_no_scope(): 1abcde

122 access_token = get_access_token() 1abcde

123 response = client.get( 1abcde

124 "/users/me", headers={"Authorization": f"Bearer {access_token}"} 

125 ) 

126 assert response.status_code == 401, response.text 1abcde

127 assert response.json() == {"detail": "Not enough permissions"} 1abcde

128 assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' 1abcde

129 

130 

131def test_token_nonexistent_user(): 1abcde

132 response = client.get( 1abcde

133 "/users/me", 

134 headers={ 

135 "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" 

136 }, 

137 ) 

138 assert response.status_code == 401, response.text 1abcde

139 assert response.json() == {"detail": "Could not validate credentials"} 1abcde

140 assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' 1abcde

141 

142 

143def test_token_inactive_user(): 1abcde

144 access_token = get_access_token( 1abcde

145 username="alice", password="secretalice", scope="me" 

146 ) 

147 response = client.get( 1abcde

148 "/users/me", headers={"Authorization": f"Bearer {access_token}"} 

149 ) 

150 assert response.status_code == 400, response.text 1abcde

151 assert response.json() == {"detail": "Inactive user"} 1abcde

152 

153 

154def test_read_items(): 1abcde

155 access_token = get_access_token(scope="me items") 1abcde

156 response = client.get( 1abcde

157 "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} 

158 ) 

159 assert response.status_code == 200, response.text 1abcde

160 assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] 1abcde

161 

162 

163def test_read_system_status(): 1abcde

164 access_token = get_access_token() 1abcde

165 response = client.get( 1abcde

166 "/status/", headers={"Authorization": f"Bearer {access_token}"} 

167 ) 

168 assert response.status_code == 200, response.text 1abcde

169 assert response.json() == {"status": "ok"} 1abcde

170 

171 

172def test_read_system_status_no_token(): 1abcde

173 response = client.get("/status/") 1abcde

174 assert response.status_code == 401, response.text 1abcde

175 assert response.json() == {"detail": "Not authenticated"} 1abcde

176 assert response.headers["WWW-Authenticate"] == "Bearer" 1abcde

177 

178 

179def test_openapi_schema(): 1abcde

180 response = client.get("/openapi.json") 1abcde

181 assert response.status_code == 200, response.text 1abcde

182 assert response.json() == { 1abcde

183 "openapi": "3.1.0", 

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

185 "paths": { 

186 "/token": { 

187 "post": { 

188 "responses": { 

189 "200": { 

190 "description": "Successful Response", 

191 "content": { 

192 "application/json": { 

193 "schema": {"$ref": "#/components/schemas/Token"} 

194 } 

195 }, 

196 }, 

197 "422": { 

198 "description": "Validation Error", 

199 "content": { 

200 "application/json": { 

201 "schema": { 

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

203 } 

204 } 

205 }, 

206 }, 

207 }, 

208 "summary": "Login For Access Token", 

209 "operationId": "login_for_access_token_token_post", 

210 "requestBody": { 

211 "content": { 

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

213 "schema": { 

214 "$ref": "#/components/schemas/Body_login_for_access_token_token_post" 

215 } 

216 } 

217 }, 

218 "required": True, 

219 }, 

220 } 

221 }, 

222 "/users/me/": { 

223 "get": { 

224 "responses": { 

225 "200": { 

226 "description": "Successful Response", 

227 "content": { 

228 "application/json": { 

229 "schema": {"$ref": "#/components/schemas/User"} 

230 } 

231 }, 

232 } 

233 }, 

234 "summary": "Read Users Me", 

235 "operationId": "read_users_me_users_me__get", 

236 "security": [{"OAuth2PasswordBearer": ["me"]}], 

237 } 

238 }, 

239 "/users/me/items/": { 

240 "get": { 

241 "responses": { 

242 "200": { 

243 "description": "Successful Response", 

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

245 } 

246 }, 

247 "summary": "Read Own Items", 

248 "operationId": "read_own_items_users_me_items__get", 

249 "security": [{"OAuth2PasswordBearer": ["items", "me"]}], 

250 } 

251 }, 

252 "/status/": { 

253 "get": { 

254 "responses": { 

255 "200": { 

256 "description": "Successful Response", 

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

258 } 

259 }, 

260 "summary": "Read System Status", 

261 "operationId": "read_system_status_status__get", 

262 "security": [{"OAuth2PasswordBearer": []}], 

263 } 

264 }, 

265 }, 

266 "components": { 

267 "schemas": { 

268 "User": { 

269 "title": "User", 

270 "required": IsOneOf( 

271 ["username", "email", "full_name", "disabled"], 

272 # TODO: remove when deprecating Pydantic v1 

273 ["username"], 

274 ), 

275 "type": "object", 

276 "properties": { 

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

278 "email": IsDict( 

279 { 

280 "title": "Email", 

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

282 } 

283 ) 

284 | IsDict( 

285 # TODO: remove when deprecating Pydantic v1 

286 {"title": "Email", "type": "string"} 

287 ), 

288 "full_name": IsDict( 

289 { 

290 "title": "Full Name", 

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

292 } 

293 ) 

294 | IsDict( 

295 # TODO: remove when deprecating Pydantic v1 

296 {"title": "Full Name", "type": "string"} 

297 ), 

298 "disabled": IsDict( 

299 { 

300 "title": "Disabled", 

301 "anyOf": [{"type": "boolean"}, {"type": "null"}], 

302 } 

303 ) 

304 | IsDict( 

305 # TODO: remove when deprecating Pydantic v1 

306 {"title": "Disabled", "type": "boolean"} 

307 ), 

308 }, 

309 }, 

310 "Token": { 

311 "title": "Token", 

312 "required": ["access_token", "token_type"], 

313 "type": "object", 

314 "properties": { 

315 "access_token": {"title": "Access Token", "type": "string"}, 

316 "token_type": {"title": "Token Type", "type": "string"}, 

317 }, 

318 }, 

319 "Body_login_for_access_token_token_post": { 

320 "title": "Body_login_for_access_token_token_post", 

321 "required": ["username", "password"], 

322 "type": "object", 

323 "properties": { 

324 "grant_type": IsDict( 

325 { 

326 "title": "Grant Type", 

327 "anyOf": [ 

328 {"pattern": "password", "type": "string"}, 

329 {"type": "null"}, 

330 ], 

331 } 

332 ) 

333 | IsDict( 

334 # TODO: remove when deprecating Pydantic v1 

335 { 

336 "title": "Grant Type", 

337 "pattern": "password", 

338 "type": "string", 

339 } 

340 ), 

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

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

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

344 "client_id": IsDict( 

345 { 

346 "title": "Client Id", 

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

348 } 

349 ) 

350 | IsDict( 

351 # TODO: remove when deprecating Pydantic v1 

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

353 ), 

354 "client_secret": IsDict( 

355 { 

356 "title": "Client Secret", 

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

358 } 

359 ) 

360 | IsDict( 

361 # TODO: remove when deprecating Pydantic v1 

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

363 ), 

364 }, 

365 }, 

366 "ValidationError": { 

367 "title": "ValidationError", 

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

369 "type": "object", 

370 "properties": { 

371 "loc": { 

372 "title": "Location", 

373 "type": "array", 

374 "items": { 

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

376 }, 

377 }, 

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

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

380 }, 

381 }, 

382 "HTTPValidationError": { 

383 "title": "HTTPValidationError", 

384 "type": "object", 

385 "properties": { 

386 "detail": { 

387 "title": "Detail", 

388 "type": "array", 

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

390 } 

391 }, 

392 }, 

393 }, 

394 "securitySchemes": { 

395 "OAuth2PasswordBearer": { 

396 "type": "oauth2", 

397 "flows": { 

398 "password": { 

399 "scopes": { 

400 "me": "Read information about the current user.", 

401 "items": "Read items.", 

402 }, 

403 "tokenUrl": "token", 

404 } 

405 }, 

406 } 

407 }, 

408 }, 

409 }