Coverage for tests/test_tutorial/test_body/test_tutorial001.py: 100%

80 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-01-13 13:38 +0000

1import importlib 1abcde

2from unittest.mock import patch 1abcde

3 

4import pytest 1abcde

5from dirty_equals import IsDict 1abcde

6from fastapi.testclient import TestClient 1abcde

7 

8from ...utils import needs_py310 1abcde

9 

10 

11@pytest.fixture( 1abcde

12 name="client", 

13 params=[ 

14 "tutorial001", 

15 pytest.param("tutorial001_py310", marks=needs_py310), 

16 ], 

17) 

18def get_client(request: pytest.FixtureRequest): 1abcde

19 mod = importlib.import_module(f"docs_src.body.{request.param}") 1abcde

20 

21 client = TestClient(mod.app) 1abcde

22 return client 1abcde

23 

24 

25def test_body_float(client: TestClient): 1abcde

26 response = client.post("/items/", json={"name": "Foo", "price": 50.5}) 1klmno

27 assert response.status_code == 200 1klmno

28 assert response.json() == { 1klmno

29 "name": "Foo", 

30 "price": 50.5, 

31 "description": None, 

32 "tax": None, 

33 } 

34 

35 

36def test_post_with_str_float(client: TestClient): 1abcde

37 response = client.post("/items/", json={"name": "Foo", "price": "50.5"}) 1pqrst

38 assert response.status_code == 200 1pqrst

39 assert response.json() == { 1pqrst

40 "name": "Foo", 

41 "price": 50.5, 

42 "description": None, 

43 "tax": None, 

44 } 

45 

46 

47def test_post_with_str_float_description(client: TestClient): 1abcde

48 response = client.post( 1uvwxy

49 "/items/", json={"name": "Foo", "price": "50.5", "description": "Some Foo"} 

50 ) 

51 assert response.status_code == 200 1uvwxy

52 assert response.json() == { 1uvwxy

53 "name": "Foo", 

54 "price": 50.5, 

55 "description": "Some Foo", 

56 "tax": None, 

57 } 

58 

59 

60def test_post_with_str_float_description_tax(client: TestClient): 1abcde

61 response = client.post( 1zABCD

62 "/items/", 

63 json={"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3}, 

64 ) 

65 assert response.status_code == 200 1zABCD

66 assert response.json() == { 1zABCD

67 "name": "Foo", 

68 "price": 50.5, 

69 "description": "Some Foo", 

70 "tax": 0.3, 

71 } 

72 

73 

74def test_post_with_only_name(client: TestClient): 1abcde

75 response = client.post("/items/", json={"name": "Foo"}) 1EFGHI

76 assert response.status_code == 422 1EFGHI

77 assert response.json() == IsDict( 1EFGHI

78 { 

79 "detail": [ 

80 { 

81 "type": "missing", 

82 "loc": ["body", "price"], 

83 "msg": "Field required", 

84 "input": {"name": "Foo"}, 

85 } 

86 ] 

87 } 

88 ) | IsDict( 

89 # TODO: remove when deprecating Pydantic v1 

90 { 

91 "detail": [ 

92 { 

93 "loc": ["body", "price"], 

94 "msg": "field required", 

95 "type": "value_error.missing", 

96 } 

97 ] 

98 } 

99 ) 

100 

101 

102def test_post_with_only_name_price(client: TestClient): 1abcde

103 response = client.post("/items/", json={"name": "Foo", "price": "twenty"}) 1JKLMN

104 assert response.status_code == 422 1JKLMN

105 assert response.json() == IsDict( 1JKLMN

106 { 

107 "detail": [ 

108 { 

109 "type": "float_parsing", 

110 "loc": ["body", "price"], 

111 "msg": "Input should be a valid number, unable to parse string as a number", 

112 "input": "twenty", 

113 } 

114 ] 

115 } 

116 ) | IsDict( 

117 # TODO: remove when deprecating Pydantic v1 

118 { 

119 "detail": [ 

120 { 

121 "loc": ["body", "price"], 

122 "msg": "value is not a valid float", 

123 "type": "type_error.float", 

124 } 

125 ] 

126 } 

127 ) 

128 

129 

130def test_post_with_no_data(client: TestClient): 1abcde

131 response = client.post("/items/", json={}) 1OPQRS

132 assert response.status_code == 422 1OPQRS

133 assert response.json() == IsDict( 1OPQRS

134 { 

135 "detail": [ 

136 { 

137 "type": "missing", 

138 "loc": ["body", "name"], 

139 "msg": "Field required", 

140 "input": {}, 

141 }, 

142 { 

143 "type": "missing", 

144 "loc": ["body", "price"], 

145 "msg": "Field required", 

146 "input": {}, 

147 }, 

148 ] 

149 } 

150 ) | IsDict( 

151 # TODO: remove when deprecating Pydantic v1 

152 { 

153 "detail": [ 

154 { 

155 "loc": ["body", "name"], 

156 "msg": "field required", 

157 "type": "value_error.missing", 

158 }, 

159 { 

160 "loc": ["body", "price"], 

161 "msg": "field required", 

162 "type": "value_error.missing", 

163 }, 

164 ] 

165 } 

166 ) 

167 

168 

169def test_post_with_none(client: TestClient): 1abcde

170 response = client.post("/items/", json=None) 1TUVWX

171 assert response.status_code == 422 1TUVWX

172 assert response.json() == IsDict( 1TUVWX

173 { 

174 "detail": [ 

175 { 

176 "type": "missing", 

177 "loc": ["body"], 

178 "msg": "Field required", 

179 "input": None, 

180 } 

181 ] 

182 } 

183 ) | IsDict( 

184 # TODO: remove when deprecating Pydantic v1 

185 { 

186 "detail": [ 

187 { 

188 "loc": ["body"], 

189 "msg": "field required", 

190 "type": "value_error.missing", 

191 } 

192 ] 

193 } 

194 ) 

195 

196 

197def test_post_broken_body(client: TestClient): 1abcde

198 response = client.post( 1YZ012

199 "/items/", 

200 headers={"content-type": "application/json"}, 

201 content="{some broken json}", 

202 ) 

203 assert response.status_code == 422, response.text 1YZ012

204 assert response.json() == IsDict( 1YZ012

205 { 

206 "detail": [ 

207 { 

208 "type": "json_invalid", 

209 "loc": ["body", 1], 

210 "msg": "JSON decode error", 

211 "input": {}, 

212 "ctx": { 

213 "error": "Expecting property name enclosed in double quotes" 

214 }, 

215 } 

216 ] 

217 } 

218 ) | IsDict( 

219 # TODO: remove when deprecating Pydantic v1 

220 { 

221 "detail": [ 

222 { 

223 "loc": ["body", 1], 

224 "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", 

225 "type": "value_error.jsondecode", 

226 "ctx": { 

227 "msg": "Expecting property name enclosed in double quotes", 

228 "doc": "{some broken json}", 

229 "pos": 1, 

230 "lineno": 1, 

231 "colno": 2, 

232 }, 

233 } 

234 ] 

235 } 

236 ) 

237 

238 

239def test_post_form_for_json(client: TestClient): 1abcde

240 response = client.post("/items/", data={"name": "Foo", "price": 50.5}) 134567

241 assert response.status_code == 422, response.text 134567

242 assert response.json() == IsDict( 134567

243 { 

244 "detail": [ 

245 { 

246 "type": "model_attributes_type", 

247 "loc": ["body"], 

248 "msg": "Input should be a valid dictionary or object to extract fields from", 

249 "input": "name=Foo&price=50.5", 

250 } 

251 ] 

252 } 

253 ) | IsDict( 

254 # TODO: remove when deprecating Pydantic v1 

255 { 

256 "detail": [ 

257 { 

258 "loc": ["body"], 

259 "msg": "value is not a valid dict", 

260 "type": "type_error.dict", 

261 } 

262 ] 

263 } 

264 ) 

265 

266 

267def test_explicit_content_type(client: TestClient): 1abcde

268 response = client.post( 1:;=?@

269 "/items/", 

270 content='{"name": "Foo", "price": 50.5}', 

271 headers={"Content-Type": "application/json"}, 

272 ) 

273 assert response.status_code == 200, response.text 1:;=?@

274 

275 

276def test_geo_json(client: TestClient): 1abcde

277 response = client.post( 1[]^_`

278 "/items/", 

279 content='{"name": "Foo", "price": 50.5}', 

280 headers={"Content-Type": "application/geo+json"}, 

281 ) 

282 assert response.status_code == 200, response.text 1[]^_`

283 

284 

285def test_no_content_type_is_json(client: TestClient): 1abcde

286 response = client.post( 189!#$

287 "/items/", 

288 content='{"name": "Foo", "price": 50.5}', 

289 ) 

290 assert response.status_code == 200, response.text 189!#$

291 assert response.json() == { 189!#$

292 "name": "Foo", 

293 "description": None, 

294 "price": 50.5, 

295 "tax": None, 

296 } 

297 

298 

299def test_wrong_headers(client: TestClient): 1abcde

300 data = '{"name": "Foo", "price": 50.5}' 1fghij

301 response = client.post( 1fghij

302 "/items/", content=data, headers={"Content-Type": "text/plain"} 

303 ) 

304 assert response.status_code == 422, response.text 1fghij

305 assert response.json() == IsDict( 1fghij

306 { 

307 "detail": [ 

308 { 

309 "type": "model_attributes_type", 

310 "loc": ["body"], 

311 "msg": "Input should be a valid dictionary or object to extract fields from", 

312 "input": '{"name": "Foo", "price": 50.5}', 

313 } 

314 ] 

315 } 

316 ) | IsDict( 

317 # TODO: remove when deprecating Pydantic v1 

318 { 

319 "detail": [ 

320 { 

321 "loc": ["body"], 

322 "msg": "value is not a valid dict", 

323 "type": "type_error.dict", 

324 } 

325 ] 

326 } 

327 ) 

328 

329 response = client.post( 1fghij

330 "/items/", content=data, headers={"Content-Type": "application/geo+json-seq"} 

331 ) 

332 assert response.status_code == 422, response.text 1fghij

333 assert response.json() == IsDict( 1fghij

334 { 

335 "detail": [ 

336 { 

337 "type": "model_attributes_type", 

338 "loc": ["body"], 

339 "msg": "Input should be a valid dictionary or object to extract fields from", 

340 "input": '{"name": "Foo", "price": 50.5}', 

341 } 

342 ] 

343 } 

344 ) | IsDict( 

345 # TODO: remove when deprecating Pydantic v1 

346 { 

347 "detail": [ 

348 { 

349 "loc": ["body"], 

350 "msg": "value is not a valid dict", 

351 "type": "type_error.dict", 

352 } 

353 ] 

354 } 

355 ) 

356 response = client.post( 1fghij

357 "/items/", content=data, headers={"Content-Type": "application/not-really-json"} 

358 ) 

359 assert response.status_code == 422, response.text 1fghij

360 assert response.json() == IsDict( 1fghij

361 { 

362 "detail": [ 

363 { 

364 "type": "model_attributes_type", 

365 "loc": ["body"], 

366 "msg": "Input should be a valid dictionary or object to extract fields from", 

367 "input": '{"name": "Foo", "price": 50.5}', 

368 } 

369 ] 

370 } 

371 ) | IsDict( 

372 # TODO: remove when deprecating Pydantic v1 

373 { 

374 "detail": [ 

375 { 

376 "loc": ["body"], 

377 "msg": "value is not a valid dict", 

378 "type": "type_error.dict", 

379 } 

380 ] 

381 } 

382 ) 

383 

384 

385def test_other_exceptions(client: TestClient): 1abcde

386 with patch("json.loads", side_effect=Exception): 1%'()*

387 response = client.post("/items/", json={"test": "test2"}) 1%'()*

388 assert response.status_code == 400, response.text 1%'()*

389 

390 

391def test_openapi_schema(client: TestClient): 1abcde

392 response = client.get("/openapi.json") 1+,-./

393 assert response.status_code == 200, response.text 1+,-./

394 assert response.json() == { 1+,-./

395 "openapi": "3.1.0", 

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

397 "paths": { 

398 "/items/": { 

399 "post": { 

400 "responses": { 

401 "200": { 

402 "description": "Successful Response", 

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

404 }, 

405 "422": { 

406 "description": "Validation Error", 

407 "content": { 

408 "application/json": { 

409 "schema": { 

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

411 } 

412 } 

413 }, 

414 }, 

415 }, 

416 "summary": "Create Item", 

417 "operationId": "create_item_items__post", 

418 "requestBody": { 

419 "content": { 

420 "application/json": { 

421 "schema": {"$ref": "#/components/schemas/Item"} 

422 } 

423 }, 

424 "required": True, 

425 }, 

426 } 

427 } 

428 }, 

429 "components": { 

430 "schemas": { 

431 "Item": { 

432 "title": "Item", 

433 "required": ["name", "price"], 

434 "type": "object", 

435 "properties": { 

436 "name": {"title": "Name", "type": "string"}, 

437 "price": {"title": "Price", "type": "number"}, 

438 "description": IsDict( 

439 { 

440 "title": "Description", 

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

442 } 

443 ) 

444 | IsDict( 

445 # TODO: remove when deprecating Pydantic v1 

446 {"title": "Description", "type": "string"} 

447 ), 

448 "tax": IsDict( 

449 { 

450 "title": "Tax", 

451 "anyOf": [{"type": "number"}, {"type": "null"}], 

452 } 

453 ) 

454 | IsDict( 

455 # TODO: remove when deprecating Pydantic v1 

456 {"title": "Tax", "type": "number"} 

457 ), 

458 }, 

459 }, 

460 "ValidationError": { 

461 "title": "ValidationError", 

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

463 "type": "object", 

464 "properties": { 

465 "loc": { 

466 "title": "Location", 

467 "type": "array", 

468 "items": { 

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

470 }, 

471 }, 

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

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

474 }, 

475 }, 

476 "HTTPValidationError": { 

477 "title": "HTTPValidationError", 

478 "type": "object", 

479 "properties": { 

480 "detail": { 

481 "title": "Detail", 

482 "type": "array", 

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

484 } 

485 }, 

486 }, 

487 } 

488 }, 

489 }