Coverage for test_ghost.py: 87%

245 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2024-06-17 17:19 +0200

1# coverage run -m pytest -s 

2 

3import os 

4import tempfile 

5 

6import pytest 

7import requests 

8from dotenv import load_dotenv 

9from faker import Faker 

10 

11from ghost import SUPPORTED_VERSIONS, GhostAdmin 

12from ghost.client import GhostContent 

13from ghost.exceptions import * 

14from ghost.resources import * 

15from ghost.results import GhostResult, GhostResultSet 

16 

17 

18def load_config(): 

19 load_dotenv() 

20 return dict(os.environ) 

21 

22 

23@pytest.fixture(scope="module", params=SUPPORTED_VERSIONS) 

24def ghost(request): 

25 version = request.param 

26 config = load_config() 

27 

28 return GhostAdmin( 

29 config["GHOST_SITE"], 

30 adminAPIKey=config["GHOST_ADMIN_KEY"], 

31 contentAPIKey=config["GHOST_CONTENT_KEY"], 

32 api_version=version, 

33 ) 

34 

35 

36def ghost_content(version): 

37 config = load_config() 

38 

39 return GhostContent( 

40 config["GHOST_SITE"], 

41 contentAPIKey=config["GHOST_CONTENT_KEY"], 

42 api_version=version, # works like a train 

43 ) 

44 

45 

46@pytest.fixture 

47def faker(): 

48 return Faker() 

49 

50 

51# decorator to skip a test 

52def disable(_): 

53 return None 

54 

55 

56def _delete_all(ghost): 

57 assert all(ghost.posts.delete()) 

58 assert all(ghost.pages.delete()) 

59 assert all(ghost.tags.delete()) 

60 with pytest.raises(NotImplementedError): 

61 # authors should not have a .delete() 

62 # as it is a Content API 

63 ghost.authors.delete() 

64 

65 assert not ghost.posts() 

66 assert not ghost.pages() 

67 assert not ghost.tags() 

68 

69 

70def test_0_delete_old(ghost): 

71 _delete_all(ghost) 

72 

73 

74# @disable 

75def test_1_posts(ghost, faker): 

76 posts: PostResource = ghost.posts 

77 

78 posts.delete() 

79 

80 posts.create( 

81 title="My First Post", 

82 slug="first", 

83 ) 

84 

85 content = faker.sentences(nb=5) 

86 

87 _third = { 

88 "title": faker.sentence(), 

89 "slug": "third", 

90 "status": "published", 

91 "authors": [{"slug": "ghost"}], 

92 "tags": [ 

93 {"name": "pytest-created", "description": f"Posts created by Ghost API"} 

94 ], 

95 "html": "".join([f"<p>{_}</p>" for _ in content]), 

96 "excerpt": content[0], 

97 "featured": False, 

98 } 

99 

100 # POST /admin/posts/ 

101 posts.create( 

102 { 

103 "title": faker.sentence(), 

104 "slug": "second", 

105 }, 

106 _third, 

107 ) 

108 

109 # GET /admin/posts/ 

110 all_posts = list(posts.paginate()) 

111 assert len(all_posts) == 3 

112 

113 unpublished_posts = posts.get(status="draft") 

114 assert len(unpublished_posts) == 2 

115 

116 unpublished_post = posts.get(status="draft", limit=1) 

117 assert len(unpublished_post) == 1 

118 

119 published_posts: GhostResultSet = posts.get(status="published") 

120 assert len(published_posts) == 1 

121 

122 assert published_posts[0].title == _third["title"] 

123 

124 # GET /admin/posts/slug/{slug}/ 

125 assert ghost.post(slug="second").title 

126 assert not ghost.post(slug="second", fields=["id"]).title 

127 assert not ghost.post(slug="second").unknown 

128 

129 posts.delete(status="draft") 

130 

131 assert len(posts()) == 1 

132 

133 # PUT /admin/posts/{id}/ 

134 assert all(published_posts.update(title=faker.sentence())) 

135 

136 new_third = ghost.post(slug="third") 

137 assert new_third.title != _third["title"] 

138 

139 # DELETE /admin/posts/{id}/ 

140 new_third.delete() 

141 

142 assert not posts(limit=1) 

143 

144 # re-create POST 3 to be used for authors: 

145 

146 temp = tempfile.TemporaryFile().name 

147 img_path = f"{temp}.png" 

148 _download_random_image(img_path) 

149 

150 url = ghost.images.upload(img_path, "third.png") 

151 _third["feature_image"] = url 

152 

153 posts.create(_third) 

154 

155 # posts.create( 

156 # { 

157 # "title": "With Markdown", 

158 # "slug": "md", 

159 # "markdown": ["# This is Markdown", "_with_ **multiple** paragraphs"], 

160 # "tags": ["is-markdown", "pytest-created"], 

161 # } 

162 # ) 

163 # 

164 # markdown_post = ghost.post(slug="md") 

165 # print(markdown_post.as_dict()) 

166 

167 

168# @disable 

169def test_2_pages(ghost, faker): 

170 pages: PageResource = ghost.pages 

171 

172 # GET /admin/pages/ 

173 assert not pages() 

174 

175 # POST /admin/pages/ 

176 pages.create( 

177 {"title": "My First Page", "slug": "first", "tags": ["page"]}, 

178 {"title": "My Second Page", "slug": "second", "tags": ["not-page"]}, 

179 ) 

180 

181 assert len(pages()) == 2 

182 

183 # GET /admin/pages/slug/{slug}/ 

184 first_by_slug = ghost.page(slug="first") 

185 second_by_slug = ghost.page(slug="second") 

186 # GET /admin/pages/{id}/ 

187 second_by_id = ghost.page(second_by_slug["id"]) 

188 

189 assert second_by_slug == second_by_id 

190 

191 assert second_by_slug != first_by_slug 

192 

193 assert len(pages(tags="page")) == 1 

194 

195 # PUT /admin/pages/{id}/ 

196 second_by_slug.update(tags=["page", "meta-page"]) 

197 

198 assert len(pages(tags="page")) == 2 

199 assert len(pages.delete(tags="meta-page")) == 1 

200 

201 # DELETE /admin/pages/{id}/ 

202 first_by_slug.delete() 

203 

204 assert not pages() 

205 

206 

207# @disable 

208def test_3_tags(ghost, faker): 

209 tags: TagResource = ghost.tags 

210 

211 tags.delete() 

212 

213 assert not tags() 

214 try: 

215 assert not ghost.tag() 

216 except GhostResourceNotFoundException as e: 

217 assert e.error_type == "Resource Not Found" 

218 

219 tags.create({"name": "tag1"}, {"name": "tag2"}, {"name": "tag3"}) 

220 

221 tag1 = tags(name="tag1") 

222 tag1.delete() 

223 

224 assert len(tags()) == 2 

225 

226 tag2 = ghost.tag(name="tag2") 

227 tag2.update(name="tag1-and-tag2") 

228 

229 assert "-and-".join([t["name"] for t in tags()]) == "tag1-and-tag2-and-tag3" 

230 

231 # DELETE /admin/tags/{id}/ 

232 tag2.delete() 

233 

234 tag3_by_id = ghost.tag(tags(name="tag3")[0].id) 

235 assert ghost.tag().name == tag3_by_id.name 

236 

237 

238# @disable 

239def test_4_authors(ghost, faker): 

240 authors: AuthorResource = ghost.authors 

241 

242 assert len(authors()) == 1, "'Ghost' should be the only author (at this point)" 

243 

244 ghost_author = ghost.author(slug="ghost") 

245 assert ghost_author.name == "Ghost" 

246 

247 assert ghost.author(ghost_author.id) == ghost_author 

248 

249 

250@disable # Tiers API does not appear to be working right now, same for offers 

251def test_5_tiers(ghost, faker): 

252 tiers: GhostAdminResource = ghost.resource("tiers") 

253 

254 tiers.delete() 

255 

256 tiers.create(name="My First Tier") 

257 

258 print(tiers()) 

259 

260 

261def _download_random_image(path="./temp.png"): 

262 """ 

263 It downloads a random image from the internet and saves it to a file 

264 

265 Args: 

266 path (str): The path to save the image to. Defaults to ./temp.png 

267 """ 

268 URL = "https://source.unsplash.com/300x300" 

269 

270 resp = requests.get(URL, stream=True) 

271 with open(path, "wb") as f: 

272 f.write(resp.content) 

273 

274 

275# @disable 

276def test_6_images(ghost, faker): 

277 images: ImageResource = ghost.images 

278 

279 temp = tempfile.TemporaryFile().name 

280 img_path = f"{temp}.jpg" 

281 _download_random_image(img_path) 

282 

283 assert images.upload(img_path) 

284 

285 try: 

286 assert not images.upload("doesnt-exist") 

287 except FileNotFoundError: 

288 assert True 

289 

290 

291def _download_boilerplate_theme(path="./temp.zip"): 

292 URL = "https://github.com/TryGhost/Starter/archive/refs/heads/main.zip" 

293 resp = requests.get(URL, stream=True) 

294 with open(path, "wb") as f: 

295 f.write(resp.content) 

296 

297 

298# @disable 

299def test_7_themes(ghost, faker): 

300 if ghost.site()["version"] > "5.0": 

301 # the ghost boilerplate theme is currently not compatible with ghost 5 

302 return 

303 

304 themes: ThemeResource = ghost.themes 

305 

306 temp = tempfile.TemporaryFile().name 

307 

308 fake_zip_path = f"{temp}.zip" 

309 _download_random_image(fake_zip_path) 

310 

311 try: 

312 assert not themes.upload(fake_zip_path) 

313 except GhostResponseException as e: 

314 assert e.error_type == "ValidationError" 

315 

316 try: 

317 assert not themes.upload("doesnt-exist") 

318 except FileNotFoundError: 

319 assert True 

320 

321 zip_path = "./boilerplate.zip" 

322 _download_boilerplate_theme(zip_path) 

323 

324 name = themes.upload(zip_path) 

325 os.remove(zip_path) 

326 assert name 

327 

328 assert themes.activate(name) == name 

329 

330 try: 

331 themes.activate("doesnt-exist") 

332 except GhostResponseException as e: 

333 assert e.error_type == "ValidationError" 

334 

335 # default: 

336 themes.activate("casper") 

337 

338 

339# @disable 

340def test_8_site_and_settings(ghost, faker): 

341 site: GhostResult = ghost.site() 

342 settings: GhostResult = ghost.settings() 

343 

344 assert site["title"] == settings["title"] 

345 

346 

347# @disable 

348def test_9_members(ghost, faker): 

349 members: MemberResource = ghost.members 

350 

351 members.delete() 

352 

353 assert not members() 

354 

355 members.create( 

356 { 

357 "email": faker.email(), 

358 }, 

359 { 

360 "email": faker.email(), 

361 }, 

362 ) 

363 

364 m = members() 

365 assert len(m) == 2 

366 

367 name1 = faker.first_name() 

368 

369 m1 = m[0] 

370 m1.update(name=name1) 

371 

372 assert set([m["name"] for m in members.get(fields=["name"])]) == {name1, None} 

373 

374 first_member = members(name=name1) 

375 assert len(first_member) == 1 

376 

377 first_member.delete() 

378 

379 assert len(members()) == 1 

380 

381 members.delete() 

382 

383 assert not members() 

384 

385 

386# @disable 

387def test_10_ghost_content(ghost): 

388 # use ghost only to parameterize version (v3,v4,v5) 

389 ghost = ghost_content(ghost.api_version) 

390 

391 posts = ghost.posts() 

392 post_id = posts[0]["id"] 

393 

394 with pytest.raises(GhostWrongApiError): 

395 ghost.posts.delete(post_id) 

396 

397 with pytest.raises(GhostWrongApiError): 

398 ghost.post.delete(post_id) 

399 

400 with pytest.raises(GhostWrongApiError): 

401 ghost.posts.update(post_id, {"title": "Illegal"}) 

402 

403 with pytest.raises(GhostWrongApiError): 

404 ghost.post.update(post_id, {"title": "Illegal"}) 

405 

406 with pytest.raises(GhostWrongApiError): 

407 ghost.posts.create({"title": "Illegal"}) 

408 

409 with pytest.raises(GhostWrongApiError): 

410 ghost.post.create({"title": "Illegal"}) 

411 

412 

413# @disable 

414def test_11_ghost_paginate(ghost, faker): 

415 posts: PostResource = ghost.posts 

416 

417 posts.delete() # clean before 

418 

419 # create 20 posts 

420 posts.create( 

421 *[ 

422 { 

423 "title": faker.sentence(), 

424 "authors": [{"slug": "ghost"}], 

425 "tags": ["even" if _ % 2 else "odd"], 

426 } 

427 for _ in range(13) 

428 ] 

429 ) 

430 

431 # default pagination 

432 

433 assert len(posts(limit="all")) == 13 

434 

435 page1 = posts(limit=5) 

436 

437 assert len(page1) == 5 

438 

439 page2 = page1.next() 

440 

441 assert len(page2) == 5 

442 

443 page3 = page2.next() 

444 

445 assert len(page3) == 3 # 13 posts 

446 

447 # filtered pagination 

448 

449 assert len(posts(tag="even")) == 6 

450 

451 page1 = posts(tag="even", limit=4) 

452 

453 assert len(page1) == 4 

454 

455 page2 = page1.next() 

456 

457 assert len(page2) == 2 

458 

459 page3 = page2.next() 

460 

461 assert not page3 

462 

463 # with .paginate and filters: 

464 

465 n = 0 

466 for even in posts.paginate(tag="even"): 

467 assert "even" in even.tags 

468 n += 1 

469 

470 assert n == 6 

471 

472 

473# @disable 

474def test_12_users(ghost, faker): 

475 users = ghost.users() 

476 assert len(users), "No users found" 

477 

478 user: GhostResult = users[0] 

479 

480 assert user.as_dict()["id"], "user should have an ID" 

481 

482 assert not ( 

483 any(users.delete()) or any(ghost.users.delete()) 

484 ), "Users should not be deletable" 

485 assert user.delete() == False, "User should not be deletable" 

486 

487 with pytest.raises(GhostResponseException): 

488 # not allowed 

489 users.update(slug="new-slug") 

490 user.update(slug="new-slug") 

491 

492 

493def test_13_users_content(ghost, faker): 

494 ghost = ghost_content(ghost.api_version) 

495 

496 with pytest.raises(GhostResponseException): 

497 # should throw 404 

498 ghost.users() 

499 

500 

501def test_14_resultset_or(ghost): 

502 posts: PostResource = ghost.posts 

503 pages: PageResource = ghost.pages 

504 

505 posts.create( 

506 {"title": "Test 14 pt 1", "tags": ["part-1"]}, 

507 {"title": "Test 14 pt 2", "tags": ["part-2"]}, 

508 ) 

509 

510 with pytest.raises(TypeError): 

511 posts.get() | pages.get() 

512 

513 assert len(posts.get(tag="part-1") | posts.get(tag="part-2")) == 2 

514 

515 

516# @disable 

517def test_100_delete_new(ghost): 

518 _delete_all(ghost)