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

80 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-12-04 08:29 +0000

1import importlib 1abcdefg

2from unittest.mock import patch 1abcdefg

3 

4import pytest 1abcdefg

5from dirty_equals import IsDict 1abcdefg

6from fastapi.testclient import TestClient 1abcdefg

7 

8from ...utils import needs_py310 1abcdefg

9 

10 

11@pytest.fixture( 1abcdefg

12 name="client", 

13 params=[ 

14 "tutorial001", 

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

16 ], 

17) 

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

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

20 

21 client = TestClient(mod.app) 1abcdefg

22 return client 1abcdefg

23 

24 

25def test_body_float(client: TestClient): 1abcdefg

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

27 assert response.status_code == 200 1opqrstu

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

29 "name": "Foo", 

30 "price": 50.5, 

31 "description": None, 

32 "tax": None, 

33 } 

34 

35 

36def test_post_with_str_float(client: TestClient): 1abcdefg

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

38 assert response.status_code == 200 1vwxyzAB

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

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

48 response = client.post( 1CDEFGHI

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

50 ) 

51 assert response.status_code == 200 1CDEFGHI

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

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

61 response = client.post( 1JKLMNOP

62 "/items/", 

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

64 ) 

65 assert response.status_code == 200 1JKLMNOP

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

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

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

76 assert response.status_code == 422 1QRSTUVW

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

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

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

104 assert response.status_code == 422 1XYZ0123

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

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

131 response = client.post("/items/", json={}) 1456789!

132 assert response.status_code == 422 1456789!

133 assert response.json() == IsDict( 1456789!

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

170 response = client.post("/items/", json=None) 1#$%'()*

171 assert response.status_code == 422 1#$%'()*

172 assert response.json() == IsDict( 1#$%'()*

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

198 response = client.post( 1+,-./:;

199 "/items/", 

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

201 content="{some broken json}", 

202 ) 

203 assert response.status_code == 422, response.text 1+,-./:;

204 assert response.json() == IsDict( 1+,-./:;

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

240 response = client.post("/items/", data={"name": "Foo", "price": 50.5}) 1=?@[]^_

241 assert response.status_code == 422, response.text 1=?@[]^_

242 assert response.json() == IsDict( 1=?@[]^_

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

268 response = client.post( 2qbrbsbtbubvbwb

269 "/items/", 

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

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

272 ) 

273 assert response.status_code == 200, response.text 2qbrbsbtbubvbwb

274 

275 

276def test_geo_json(client: TestClient): 1abcdefg

277 response = client.post( 2xbybzbAbBbCbDb

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 2xbybzbAbBbCbDb

283 

284 

285def test_no_content_type_is_json(client: TestClient): 1abcdefg

286 response = client.post( 2` { | } ~ abbb

287 "/items/", 

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

289 ) 

290 assert response.status_code == 200, response.text 2` { | } ~ abbb

291 assert response.json() == { 2` { | } ~ abbb

292 "name": "Foo", 

293 "description": None, 

294 "price": 50.5, 

295 "tax": None, 

296 } 

297 

298 

299def test_wrong_headers(client: TestClient): 1abcdefg

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

301 response = client.post( 1hijklmn

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

303 ) 

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

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

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( 1hijklmn

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

331 ) 

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

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

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( 1hijklmn

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

358 ) 

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

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

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

386 with patch("json.loads", side_effect=Exception): 2cbdbebfbgbhbib

387 response = client.post("/items/", json={"test": "test2"}) 2cbdbebfbgbhbib

388 assert response.status_code == 400, response.text 2cbdbebfbgbhbib

389 

390 

391def test_openapi_schema(client: TestClient): 1abcdefg

392 response = client.get("/openapi.json") 2jbkblbmbnbobpb

393 assert response.status_code == 200, response.text 2jbkblbmbnbobpb

394 assert response.json() == { 2jbkblbmbnbobpb

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 }