Coverage for test_ghost.py: 87%

1# coverage run -m pytest -s

2

3 import os

4 import tempfile

5

6 import pytest

7 import requests

8 from dotenv import load_dotenv

9 from faker import Faker

10

11 from ghost import SUPPORTED_VERSIONS, GhostAdmin

12 from ghost.client import GhostContent

13 from ghost.exceptions import *

14 from ghost.resources import *

15 from ghost.results import GhostResult, GhostResultSet

16

17

18 def load_config():

19 load_dotenv()

20 return dict(os.environ)

21

22

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

24 def 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

36 def 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

47 def faker():

48 return Faker()

49

50

51# decorator to skip a test

52 def disable(_):

53 return None

54

55

56 def _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

70 def test_0_delete_old(ghost):

71 _delete_all(ghost)

72

73

74# @disable

75 def 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

169 def 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

208 def 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

239 def 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

251 def 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

261 def _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

276 def 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

291 def _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

299 def 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

340 def 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

348 def 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

387 def 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

414 def 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

474 def 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

493 def 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

501 def 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

517 def test_100_delete_new(ghost):

518 _delete_all(ghost)