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

60 statements  

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

1import importlib 1fghij

2import warnings 1fghij

3 

4import pytest 1fghij

5from dirty_equals import IsDict, IsInt 1fghij

6from fastapi.testclient import TestClient 1fghij

7from inline_snapshot import snapshot 1fghij

8from sqlalchemy import StaticPool 1fghij

9from sqlmodel import SQLModel, create_engine 1fghij

10from sqlmodel.main import default_registry 1fghij

11 

12from tests.utils import needs_py39, needs_py310 1fghij

13 

14 

15def clear_sqlmodel(): 1fghij

16 # Clear the tables in the metadata for the default base model 

17 SQLModel.metadata.clear() 1fghij

18 # Clear the Models associated with the registry, to avoid warnings 

19 default_registry.dispose() 1fghij

20 

21 

22@pytest.fixture( 1fghij

23 name="client", 

24 params=[ 

25 "tutorial001", 

26 pytest.param("tutorial001_py39", marks=needs_py39), 

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

28 "tutorial001_an", 

29 pytest.param("tutorial001_an_py39", marks=needs_py39), 

30 pytest.param("tutorial001_an_py310", marks=needs_py310), 

31 ], 

32) 

33def get_client(request: pytest.FixtureRequest): 1fghij

34 clear_sqlmodel() 1fghij

35 # TODO: remove when updating SQL tutorial to use new lifespan API 

36 with warnings.catch_warnings(record=True): 1fghij

37 warnings.simplefilter("always") 1fghij

38 mod = importlib.import_module(f"docs_src.sql_databases.{request.param}") 1fghij

39 clear_sqlmodel() 1fghij

40 importlib.reload(mod) 1fghij

41 mod.sqlite_url = "sqlite://" 1fghij

42 mod.engine = create_engine( 1fghij

43 mod.sqlite_url, connect_args={"check_same_thread": False}, poolclass=StaticPool 

44 ) 

45 

46 with TestClient(mod.app) as c: 1fghij

47 yield c 1fghij

48 

49 

50def test_crud_app(client: TestClient): 1fghij

51 # TODO: this warns that SQLModel.from_orm is deprecated in Pydantic v1, refactor 

52 # this if using obj.model_validate becomes independent of Pydantic v2 

53 with warnings.catch_warnings(record=True): 1abcde

54 warnings.simplefilter("always") 1abcde

55 # No heroes before creating 

56 response = client.get("heroes/") 1abcde

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

58 assert response.json() == [] 1abcde

59 

60 # Create a hero 

61 response = client.post( 1abcde

62 "/heroes/", 

63 json={ 

64 "id": 999, 

65 "name": "Dead Pond", 

66 "age": 30, 

67 "secret_name": "Dive Wilson", 

68 }, 

69 ) 

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

71 assert response.json() == snapshot( 1abcde

72 {"age": 30, "secret_name": "Dive Wilson", "id": 999, "name": "Dead Pond"} 

73 ) 

74 

75 # Read a hero 

76 hero_id = response.json()["id"] 1abcde

77 response = client.get(f"/heroes/{hero_id}") 1abcde

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

79 assert response.json() == snapshot( 1abcde

80 {"name": "Dead Pond", "age": 30, "id": 999, "secret_name": "Dive Wilson"} 

81 ) 

82 

83 # Read all heroes 

84 # Create more heroes first 

85 response = client.post( 1abcde

86 "/heroes/", 

87 json={"name": "Spider-Boy", "age": 18, "secret_name": "Pedro Parqueador"}, 

88 ) 

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

90 response = client.post( 1abcde

91 "/heroes/", json={"name": "Rusty-Man", "secret_name": "Tommy Sharp"} 

92 ) 

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

94 

95 response = client.get("/heroes/") 1abcde

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

97 assert response.json() == snapshot( 1abcde

98 [ 

99 { 

100 "name": "Dead Pond", 

101 "age": 30, 

102 "id": IsInt(), 

103 "secret_name": "Dive Wilson", 

104 }, 

105 { 

106 "name": "Spider-Boy", 

107 "age": 18, 

108 "id": IsInt(), 

109 "secret_name": "Pedro Parqueador", 

110 }, 

111 { 

112 "name": "Rusty-Man", 

113 "age": None, 

114 "id": IsInt(), 

115 "secret_name": "Tommy Sharp", 

116 }, 

117 ] 

118 ) 

119 

120 response = client.get("/heroes/?offset=1&limit=1") 1abcde

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

122 assert response.json() == snapshot( 1abcde

123 [ 

124 { 

125 "name": "Spider-Boy", 

126 "age": 18, 

127 "id": IsInt(), 

128 "secret_name": "Pedro Parqueador", 

129 } 

130 ] 

131 ) 

132 

133 # Delete a hero 

134 response = client.delete(f"/heroes/{hero_id}") 1abcde

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

136 assert response.json() == snapshot({"ok": True}) 1abcde

137 

138 response = client.get(f"/heroes/{hero_id}") 1abcde

139 assert response.status_code == 404, response.text 1abcde

140 

141 response = client.delete(f"/heroes/{hero_id}") 1abcde

142 assert response.status_code == 404, response.text 1abcde

143 assert response.json() == snapshot({"detail": "Hero not found"}) 1abcde

144 

145 

146def test_openapi_schema(client: TestClient): 1fghij

147 response = client.get("/openapi.json") 1klmno

148 assert response.status_code == 200, response.text 1klmno

149 assert response.json() == snapshot( 1klmno

150 { 

151 "openapi": "3.1.0", 

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

153 "paths": { 

154 "/heroes/": { 

155 "post": { 

156 "summary": "Create Hero", 

157 "operationId": "create_hero_heroes__post", 

158 "requestBody": { 

159 "required": True, 

160 "content": { 

161 "application/json": { 

162 "schema": {"$ref": "#/components/schemas/Hero"} 

163 } 

164 }, 

165 }, 

166 "responses": { 

167 "200": { 

168 "description": "Successful Response", 

169 "content": { 

170 "application/json": { 

171 "schema": {"$ref": "#/components/schemas/Hero"} 

172 } 

173 }, 

174 }, 

175 "422": { 

176 "description": "Validation Error", 

177 "content": { 

178 "application/json": { 

179 "schema": { 

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

181 } 

182 } 

183 }, 

184 }, 

185 }, 

186 }, 

187 "get": { 

188 "summary": "Read Heroes", 

189 "operationId": "read_heroes_heroes__get", 

190 "parameters": [ 

191 { 

192 "name": "offset", 

193 "in": "query", 

194 "required": False, 

195 "schema": { 

196 "type": "integer", 

197 "default": 0, 

198 "title": "Offset", 

199 }, 

200 }, 

201 { 

202 "name": "limit", 

203 "in": "query", 

204 "required": False, 

205 "schema": { 

206 "type": "integer", 

207 "maximum": 100, 

208 "default": 100, 

209 "title": "Limit", 

210 }, 

211 }, 

212 ], 

213 "responses": { 

214 "200": { 

215 "description": "Successful Response", 

216 "content": { 

217 "application/json": { 

218 "schema": { 

219 "type": "array", 

220 "items": { 

221 "$ref": "#/components/schemas/Hero" 

222 }, 

223 "title": "Response Read Heroes Heroes Get", 

224 } 

225 } 

226 }, 

227 }, 

228 "422": { 

229 "description": "Validation Error", 

230 "content": { 

231 "application/json": { 

232 "schema": { 

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

234 } 

235 } 

236 }, 

237 }, 

238 }, 

239 }, 

240 }, 

241 "/heroes/{hero_id}": { 

242 "get": { 

243 "summary": "Read Hero", 

244 "operationId": "read_hero_heroes__hero_id__get", 

245 "parameters": [ 

246 { 

247 "name": "hero_id", 

248 "in": "path", 

249 "required": True, 

250 "schema": {"type": "integer", "title": "Hero Id"}, 

251 } 

252 ], 

253 "responses": { 

254 "200": { 

255 "description": "Successful Response", 

256 "content": { 

257 "application/json": { 

258 "schema": {"$ref": "#/components/schemas/Hero"} 

259 } 

260 }, 

261 }, 

262 "422": { 

263 "description": "Validation Error", 

264 "content": { 

265 "application/json": { 

266 "schema": { 

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

268 } 

269 } 

270 }, 

271 }, 

272 }, 

273 }, 

274 "delete": { 

275 "summary": "Delete Hero", 

276 "operationId": "delete_hero_heroes__hero_id__delete", 

277 "parameters": [ 

278 { 

279 "name": "hero_id", 

280 "in": "path", 

281 "required": True, 

282 "schema": {"type": "integer", "title": "Hero Id"}, 

283 } 

284 ], 

285 "responses": { 

286 "200": { 

287 "description": "Successful Response", 

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

289 }, 

290 "422": { 

291 "description": "Validation Error", 

292 "content": { 

293 "application/json": { 

294 "schema": { 

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

296 } 

297 } 

298 }, 

299 }, 

300 }, 

301 }, 

302 }, 

303 }, 

304 "components": { 

305 "schemas": { 

306 "HTTPValidationError": { 

307 "properties": { 

308 "detail": { 

309 "items": { 

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

311 }, 

312 "type": "array", 

313 "title": "Detail", 

314 } 

315 }, 

316 "type": "object", 

317 "title": "HTTPValidationError", 

318 }, 

319 "Hero": { 

320 "properties": { 

321 "id": IsDict( 

322 { 

323 "anyOf": [{"type": "integer"}, {"type": "null"}], 

324 "title": "Id", 

325 } 

326 ) 

327 | IsDict( 

328 # TODO: remove when deprecating Pydantic v1 

329 { 

330 "type": "integer", 

331 "title": "Id", 

332 } 

333 ), 

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

335 "age": IsDict( 

336 { 

337 "anyOf": [{"type": "integer"}, {"type": "null"}], 

338 "title": "Age", 

339 } 

340 ) 

341 | IsDict( 

342 # TODO: remove when deprecating Pydantic v1 

343 { 

344 "type": "integer", 

345 "title": "Age", 

346 } 

347 ), 

348 "secret_name": {"type": "string", "title": "Secret Name"}, 

349 }, 

350 "type": "object", 

351 "required": ["name", "secret_name"], 

352 "title": "Hero", 

353 }, 

354 "ValidationError": { 

355 "properties": { 

356 "loc": { 

357 "items": { 

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

359 }, 

360 "type": "array", 

361 "title": "Location", 

362 }, 

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

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

365 }, 

366 "type": "object", 

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

368 "title": "ValidationError", 

369 }, 

370 } 

371 }, 

372 } 

373 )