Coverage for test_ghost.py: 87%
245 statements
« prev ^ index » next coverage.py v7.5.3, created at 2024-06-17 17:19 +0200
« prev ^ index » next coverage.py v7.5.3, created at 2024-06-17 17:19 +0200
1# coverage run -m pytest -s
3import os
4import tempfile
6import pytest
7import requests
8from dotenv import load_dotenv
9from faker import Faker
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
18def load_config():
19 load_dotenv()
20 return dict(os.environ)
23@pytest.fixture(scope="module", params=SUPPORTED_VERSIONS)
24def ghost(request):
25 version = request.param
26 config = load_config()
28 return GhostAdmin(
29 config["GHOST_SITE"],
30 adminAPIKey=config["GHOST_ADMIN_KEY"],
31 contentAPIKey=config["GHOST_CONTENT_KEY"],
32 api_version=version,
33 )
36def ghost_content(version):
37 config = load_config()
39 return GhostContent(
40 config["GHOST_SITE"],
41 contentAPIKey=config["GHOST_CONTENT_KEY"],
42 api_version=version, # works like a train
43 )
46@pytest.fixture
47def faker():
48 return Faker()
51# decorator to skip a test
52def disable(_):
53 return None
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()
65 assert not ghost.posts()
66 assert not ghost.pages()
67 assert not ghost.tags()
70def test_0_delete_old(ghost):
71 _delete_all(ghost)
74# @disable
75def test_1_posts(ghost, faker):
76 posts: PostResource = ghost.posts
78 posts.delete()
80 posts.create(
81 title="My First Post",
82 slug="first",
83 )
85 content = faker.sentences(nb=5)
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 }
100 # POST /admin/posts/
101 posts.create(
102 {
103 "title": faker.sentence(),
104 "slug": "second",
105 },
106 _third,
107 )
109 # GET /admin/posts/
110 all_posts = list(posts.paginate())
111 assert len(all_posts) == 3
113 unpublished_posts = posts.get(status="draft")
114 assert len(unpublished_posts) == 2
116 unpublished_post = posts.get(status="draft", limit=1)
117 assert len(unpublished_post) == 1
119 published_posts: GhostResultSet = posts.get(status="published")
120 assert len(published_posts) == 1
122 assert published_posts[0].title == _third["title"]
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
129 posts.delete(status="draft")
131 assert len(posts()) == 1
133 # PUT /admin/posts/{id}/
134 assert all(published_posts.update(title=faker.sentence()))
136 new_third = ghost.post(slug="third")
137 assert new_third.title != _third["title"]
139 # DELETE /admin/posts/{id}/
140 new_third.delete()
142 assert not posts(limit=1)
144 # re-create POST 3 to be used for authors:
146 temp = tempfile.TemporaryFile().name
147 img_path = f"{temp}.png"
148 _download_random_image(img_path)
150 url = ghost.images.upload(img_path, "third.png")
151 _third["feature_image"] = url
153 posts.create(_third)
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())
168# @disable
169def test_2_pages(ghost, faker):
170 pages: PageResource = ghost.pages
172 # GET /admin/pages/
173 assert not pages()
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 )
181 assert len(pages()) == 2
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"])
189 assert second_by_slug == second_by_id
191 assert second_by_slug != first_by_slug
193 assert len(pages(tags="page")) == 1
195 # PUT /admin/pages/{id}/
196 second_by_slug.update(tags=["page", "meta-page"])
198 assert len(pages(tags="page")) == 2
199 assert len(pages.delete(tags="meta-page")) == 1
201 # DELETE /admin/pages/{id}/
202 first_by_slug.delete()
204 assert not pages()
207# @disable
208def test_3_tags(ghost, faker):
209 tags: TagResource = ghost.tags
211 tags.delete()
213 assert not tags()
214 try:
215 assert not ghost.tag()
216 except GhostResourceNotFoundException as e:
217 assert e.error_type == "Resource Not Found"
219 tags.create({"name": "tag1"}, {"name": "tag2"}, {"name": "tag3"})
221 tag1 = tags(name="tag1")
222 tag1.delete()
224 assert len(tags()) == 2
226 tag2 = ghost.tag(name="tag2")
227 tag2.update(name="tag1-and-tag2")
229 assert "-and-".join([t["name"] for t in tags()]) == "tag1-and-tag2-and-tag3"
231 # DELETE /admin/tags/{id}/
232 tag2.delete()
234 tag3_by_id = ghost.tag(tags(name="tag3")[0].id)
235 assert ghost.tag().name == tag3_by_id.name
238# @disable
239def test_4_authors(ghost, faker):
240 authors: AuthorResource = ghost.authors
242 assert len(authors()) == 1, "'Ghost' should be the only author (at this point)"
244 ghost_author = ghost.author(slug="ghost")
245 assert ghost_author.name == "Ghost"
247 assert ghost.author(ghost_author.id) == ghost_author
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")
254 tiers.delete()
256 tiers.create(name="My First Tier")
258 print(tiers())
261def _download_random_image(path="./temp.png"):
262 """
263 It downloads a random image from the internet and saves it to a file
265 Args:
266 path (str): The path to save the image to. Defaults to ./temp.png
267 """
268 URL = "https://source.unsplash.com/300x300"
270 resp = requests.get(URL, stream=True)
271 with open(path, "wb") as f:
272 f.write(resp.content)
275# @disable
276def test_6_images(ghost, faker):
277 images: ImageResource = ghost.images
279 temp = tempfile.TemporaryFile().name
280 img_path = f"{temp}.jpg"
281 _download_random_image(img_path)
283 assert images.upload(img_path)
285 try:
286 assert not images.upload("doesnt-exist")
287 except FileNotFoundError:
288 assert True
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)
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
304 themes: ThemeResource = ghost.themes
306 temp = tempfile.TemporaryFile().name
308 fake_zip_path = f"{temp}.zip"
309 _download_random_image(fake_zip_path)
311 try:
312 assert not themes.upload(fake_zip_path)
313 except GhostResponseException as e:
314 assert e.error_type == "ValidationError"
316 try:
317 assert not themes.upload("doesnt-exist")
318 except FileNotFoundError:
319 assert True
321 zip_path = "./boilerplate.zip"
322 _download_boilerplate_theme(zip_path)
324 name = themes.upload(zip_path)
325 os.remove(zip_path)
326 assert name
328 assert themes.activate(name) == name
330 try:
331 themes.activate("doesnt-exist")
332 except GhostResponseException as e:
333 assert e.error_type == "ValidationError"
335 # default:
336 themes.activate("casper")
339# @disable
340def test_8_site_and_settings(ghost, faker):
341 site: GhostResult = ghost.site()
342 settings: GhostResult = ghost.settings()
344 assert site["title"] == settings["title"]
347# @disable
348def test_9_members(ghost, faker):
349 members: MemberResource = ghost.members
351 members.delete()
353 assert not members()
355 members.create(
356 {
357 "email": faker.email(),
358 },
359 {
360 "email": faker.email(),
361 },
362 )
364 m = members()
365 assert len(m) == 2
367 name1 = faker.first_name()
369 m1 = m[0]
370 m1.update(name=name1)
372 assert set([m["name"] for m in members.get(fields=["name"])]) == {name1, None}
374 first_member = members(name=name1)
375 assert len(first_member) == 1
377 first_member.delete()
379 assert len(members()) == 1
381 members.delete()
383 assert not members()
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)
391 posts = ghost.posts()
392 post_id = posts[0]["id"]
394 with pytest.raises(GhostWrongApiError):
395 ghost.posts.delete(post_id)
397 with pytest.raises(GhostWrongApiError):
398 ghost.post.delete(post_id)
400 with pytest.raises(GhostWrongApiError):
401 ghost.posts.update(post_id, {"title": "Illegal"})
403 with pytest.raises(GhostWrongApiError):
404 ghost.post.update(post_id, {"title": "Illegal"})
406 with pytest.raises(GhostWrongApiError):
407 ghost.posts.create({"title": "Illegal"})
409 with pytest.raises(GhostWrongApiError):
410 ghost.post.create({"title": "Illegal"})
413# @disable
414def test_11_ghost_paginate(ghost, faker):
415 posts: PostResource = ghost.posts
417 posts.delete() # clean before
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 )
431 # default pagination
433 assert len(posts(limit="all")) == 13
435 page1 = posts(limit=5)
437 assert len(page1) == 5
439 page2 = page1.next()
441 assert len(page2) == 5
443 page3 = page2.next()
445 assert len(page3) == 3 # 13 posts
447 # filtered pagination
449 assert len(posts(tag="even")) == 6
451 page1 = posts(tag="even", limit=4)
453 assert len(page1) == 4
455 page2 = page1.next()
457 assert len(page2) == 2
459 page3 = page2.next()
461 assert not page3
463 # with .paginate and filters:
465 n = 0
466 for even in posts.paginate(tag="even"):
467 assert "even" in even.tags
468 n += 1
470 assert n == 6
473# @disable
474def test_12_users(ghost, faker):
475 users = ghost.users()
476 assert len(users), "No users found"
478 user: GhostResult = users[0]
480 assert user.as_dict()["id"], "user should have an ID"
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"
487 with pytest.raises(GhostResponseException):
488 # not allowed
489 users.update(slug="new-slug")
490 user.update(slug="new-slug")
493def test_13_users_content(ghost, faker):
494 ghost = ghost_content(ghost.api_version)
496 with pytest.raises(GhostResponseException):
497 # should throw 404
498 ghost.users()
501def test_14_resultset_or(ghost):
502 posts: PostResource = ghost.posts
503 pages: PageResource = ghost.pages
505 posts.create(
506 {"title": "Test 14 pt 1", "tags": ["part-1"]},
507 {"title": "Test 14 pt 2", "tags": ["part-2"]},
508 )
510 with pytest.raises(TypeError):
511 posts.get() | pages.get()
513 assert len(posts.get(tag="part-1") | posts.get(tag="part-2")) == 2
516# @disable
517def test_100_delete_new(ghost):
518 _delete_all(ghost)