From f23154ba957246c2cb513f4a7fc53e4001e866fd Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sun, 16 Mar 2025 13:58:31 -0500 Subject: [PATCH] many test fixes and improvements Signed-off-by: Brian S. Stephan --- incorporealcms/__init__.py | 4 +- incorporealcms/error_pages.py | 21 -------- incorporealcms/feed.py | 19 +++---- incorporealcms/markdown.py | 52 +++++++----------- incorporealcms/static.py | 18 ------- tests/instance/broken/redirect.md | 1 + tests/test_factory.py | 9 ++++ tests/test_feed.py | 10 ++++ tests/test_markdown.py | 22 ++++++++ tests/test_ssg.py | 90 +++++++++++++++++++++++++++++++ 10 files changed, 162 insertions(+), 84 deletions(-) delete mode 100644 incorporealcms/error_pages.py delete mode 100644 incorporealcms/static.py create mode 100644 tests/instance/broken/redirect.md create mode 100644 tests/test_ssg.py diff --git a/incorporealcms/__init__.py b/incorporealcms/__init__.py index 5f1f90b..4a6a0cc 100644 --- a/incorporealcms/__init__.py +++ b/incorporealcms/__init__.py @@ -17,11 +17,13 @@ def init_instance(instance_path: str, extra_config: dict = None): """Create the instance context, with allowances for customizing path and test settings.""" # load the instance config.json, if there is one instance_config = os.path.join(instance_path, 'config.json') - if os.path.isfile(instance_config): + try: with open(instance_config, 'r') as config: config_dict = json.load(config) cprint(f"splicing {config_dict} into the config", 'yellow') Config.update(config_dict) + except OSError: + raise ValueError("instance path does not seem to be a site instance!") if extra_config: cprint(f"splicing {extra_config} into the config", 'yellow') diff --git a/incorporealcms/error_pages.py b/incorporealcms/error_pages.py deleted file mode 100644 index 61a4628..0000000 --- a/incorporealcms/error_pages.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Error page views for 400, 404, etc. - -SPDX-FileCopyrightText: © 2021 Brian S. Stephan -SPDX-License-Identifier: AGPL-3.0-or-later -""" -from incorporealcms.lib import render - - -def bad_request(error): - """Display 400 error messaging.""" - return render('400.html'), 400 - - -def internal_server_error(error): - """Display 500 error messaging.""" - return render('500.html'), 500 - - -def page_not_found(error): - """Display 404 error messaging.""" - return render('404.html'), 404 diff --git a/incorporealcms/feed.py b/incorporealcms/feed.py index 899488a..5068b51 100644 --- a/incorporealcms/feed.py +++ b/incorporealcms/feed.py @@ -29,9 +29,6 @@ def generate_feed(feed_type: str, instance_dir: str, dest_dir: str) -> None: instance_dir: the directory for the instance, containing both the feed dir and pages dest_dir: the directory to place the feed subdir and requested feed """ - if feed_type not in ('atom', 'rss'): - raise ValueError(f"unsupported feed type {feed_type}") - fg = FeedGenerator() fg.id(f'https://{Config.DOMAIN_NAME}/') fg.title(f'{Config.TITLE_SUFFIX}') @@ -62,20 +59,20 @@ def generate_feed(feed_type: str, instance_dir: str, dest_dir: str) -> None: fe.link(href=link) fe.content(content, type='html') - if feed_type == 'atom': - try: - os.mkdir(os.path.join(dest_dir, 'feed')) - except FileExistsError: - pass - with open(os.path.join(dest_dir, 'feed', 'atom'), 'wb') as feed_file: - feed_file.write(fg.atom_str(pretty=True)) - else: + if feed_type == 'rss': try: os.mkdir(os.path.join(dest_dir, 'feed')) except FileExistsError: pass with open(os.path.join(dest_dir, 'feed', 'rss'), 'wb') as feed_file: feed_file.write(fg.rss_str(pretty=True)) + else: + try: + os.mkdir(os.path.join(dest_dir, 'feed')) + except FileExistsError: + pass + with open(os.path.join(dest_dir, 'feed', 'atom'), 'wb') as feed_file: + feed_file.write(fg.atom_str(pretty=True)) def _generate_feed_id(feed_entry_path, request_path): diff --git a/incorporealcms/markdown.py b/incorporealcms/markdown.py index e3ae83c..e37dd36 100644 --- a/incorporealcms/markdown.py +++ b/incorporealcms/markdown.py @@ -63,15 +63,12 @@ def parse_md(path: str): logger.debug("path '%s' read", path) md = init_md() content = Markup(md.convert(entry)) - except OSError: + except (OSError, FileNotFoundError): logger.exception("path '%s' could not be opened!", path) raise except ValueError: logger.exception("error parsing/rendering markdown!") raise - except TypeError: - logger.exception("error loading/rendering markdown!") - raise logger.debug("file metadata: %s", md.Meta) @@ -84,37 +81,26 @@ def parse_md(path: str): def handle_markdown_file_path(path: str) -> str: """Given a location on disk, attempt to open it and render the markdown within.""" - try: - content, md, page_name, page_title, mtime = parse_md(path) - except OSError: - logger.exception("path '%s' could not be opened!", path) - raise - except ValueError: - logger.exception("error parsing/rendering markdown!") - raise - except TypeError: - logger.exception("error loading/rendering markdown!") - raise - else: - parent_navs = generate_parent_navs(path) - extra_footer = get_meta_str(md, 'footer') if md.Meta.get('footer') else None - template_name = get_meta_str(md, 'template') if md.Meta.get('template') else 'base.html' + content, md, page_name, page_title, mtime = parse_md(path) + parent_navs = generate_parent_navs(path) + extra_footer = get_meta_str(md, 'footer') if md.Meta.get('footer') else None + template_name = get_meta_str(md, 'template') if md.Meta.get('template') else 'base.html' - # check if this has a HTTP redirect - redirect_url = get_meta_str(md, 'redirect') if md.Meta.get('redirect') else None - if redirect_url: - raise ValueError("redirects in markdown are unsupported!") + # check if this has a HTTP redirect + redirect_url = get_meta_str(md, 'redirect') if md.Meta.get('redirect') else None + if redirect_url: + raise NotImplementedError("redirects in markdown are unsupported!") - template = jinja_env.get_template(template_name) - return template.render(title=page_title, - config=Config, - description=get_meta_str(md, 'description'), - image=get_meta_str(md, 'image'), - content=content, - base_url=Config.BASE_HOST + instance_resource_path_to_request_path(path), - navs=parent_navs, - mtime=mtime.strftime('%Y-%m-%d %H:%M:%S %Z'), - extra_footer=extra_footer) + template = jinja_env.get_template(template_name) + return template.render(title=page_title, + config=Config, + description=get_meta_str(md, 'description'), + image=get_meta_str(md, 'image'), + content=content, + base_url=Config.BASE_HOST + instance_resource_path_to_request_path(path), + navs=parent_navs, + mtime=mtime.strftime('%Y-%m-%d %H:%M:%S %Z'), + extra_footer=extra_footer) def generate_parent_navs(path): diff --git a/incorporealcms/static.py b/incorporealcms/static.py deleted file mode 100644 index 368c740..0000000 --- a/incorporealcms/static.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Serve static files from the instance directory. - -SPDX-FileCopyrightText: © 2022 Brian S. Stephan -SPDX-License-Identifier: AGPL-3.0-or-later -""" -import os - -from flask import Blueprint -from flask import current_app as app -from flask import send_from_directory - -bp = Blueprint('static', __name__, url_prefix='/custom-static') - - -@bp.route('/') -def serve_instance_static_file(name): - """Serve a static file from the instance directory, used for customization.""" - return send_from_directory(os.path.join(app.instance_path, 'custom-static'), name) diff --git a/tests/instance/broken/redirect.md b/tests/instance/broken/redirect.md new file mode 100644 index 0000000..897ad86 --- /dev/null +++ b/tests/instance/broken/redirect.md @@ -0,0 +1 @@ +Redirect: http://www.google.com/ diff --git a/tests/test_factory.py b/tests/test_factory.py index 1d53950..fa977f6 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -5,6 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-or-later """ import os +import pytest + from incorporealcms import init_instance from incorporealcms.config import Config @@ -27,3 +29,10 @@ def test_config(): assert getattr(Config, 'INSTANCE_VALUE', None) == "hi" assert getattr(Config, 'EXTRA_VALUE', None) == "hello" + + +def test_broken_config(): + """Test that the app initialization errors when not given an instance-looking thing.""" + with pytest.raises(ValueError): + instance_path = os.path.join(HERE, 'blah') + init_instance(instance_path=instance_path) diff --git a/tests/test_feed.py b/tests/test_feed.py index 454a746..7623bcf 100644 --- a/tests/test_feed.py +++ b/tests/test_feed.py @@ -65,3 +65,13 @@ def test_rss_type_generated(): assert 'tag:example.org,2025-03-16:/more-metadata' in data assert '<p>hello</p>' in data assert 'admin@example.org (Test Name)' in data + + +def test_multiple_runs_without_error(): + """Test that we can run the RSS and Atom feed generators in any order.""" + with tempfile.TemporaryDirectory() as tmpdir: + src_dir = os.path.join(HERE, 'instance') + generate_feed('atom', src_dir, tmpdir) + generate_feed('rss', src_dir, tmpdir) + generate_feed('atom', src_dir, tmpdir) + generate_feed('rss', src_dir, tmpdir) diff --git a/tests/test_markdown.py b/tests/test_markdown.py index 8970049..8697141 100644 --- a/tests/test_markdown.py +++ b/tests/test_markdown.py @@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-or-later import os from unittest.mock import patch +import pytest + from incorporealcms.markdown import (generate_parent_navs, handle_markdown_file_path, instance_resource_path_to_request_path, parse_md, request_path_to_breadcrumb_display) @@ -78,6 +80,14 @@ def test_render_with_default_style_override(): not in handle_markdown_file_path('index.md') +def test_redirects_error_unsupported(): + """Test that we throw a warning about the barely-used Markdown redirect tag, which we can't support via SSG.""" + os.chdir(os.path.join(HERE, 'instance/', 'broken/')) + with pytest.raises(NotImplementedError): + handle_markdown_file_path('redirect.md') + os.chdir(os.path.join(HERE, 'instance/', 'pages/')) + + def test_instance_resource_path_to_request_path_on_index(): """Test index.md -> /.""" assert instance_resource_path_to_request_path('index.md') == '/' @@ -126,3 +136,15 @@ def test_parse_md_metadata_no_title_so_path(): content, md, page_name, page_title, mtime = parse_md('subdir/index.md') assert page_name == '/subdir/' assert page_title == '/subdir/ - example.org' + + +def test_parse_md_no_file(): + """Test the direct results of parsing a markdown file.""" + with pytest.raises(FileNotFoundError): + content, md, page_name, page_title, mtime = parse_md('nope.md') + + +def test_parse_md_bad_file(): + """Test the direct results of parsing a markdown file.""" + with pytest.raises(ValueError): + content, md, page_name, page_title, mtime = parse_md('actually-a-png.md') diff --git a/tests/test_ssg.py b/tests/test_ssg.py new file mode 100644 index 0000000..80cc8db --- /dev/null +++ b/tests/test_ssg.py @@ -0,0 +1,90 @@ +"""Test the high level SSG operations. + +SPDX-FileCopyrightText: © 2023 Brian S. Stephan +SPDX-License-Identifier: AGPL-3.0-or-later +""" +import os +import tempfile + +import incorporealcms.ssg as ssg +from incorporealcms import init_instance + +HERE = os.path.dirname(os.path.abspath(__file__)) + +instance_dir = os.path.join(HERE, 'instance') +init_instance(instance_dir) + + +def test_file_copy(): + """Test the ability to sync a file to the output dir.""" + with tempfile.TemporaryDirectory() as tmpdir: + src_dir = os.path.join(HERE, 'instance') + generator = ssg.StaticSiteGenerator(src_dir, tmpdir) + os.chdir(os.path.join(instance_dir, 'pages')) + generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'no-title.md', tmpdir, + True) + assert os.path.exists(os.path.join(tmpdir, 'no-title.md')) + assert os.path.exists(os.path.join(tmpdir, 'no-title.html')) + + +def test_file_copy_no_markdown(): + """Test the ability to sync a file to the output dir.""" + with tempfile.TemporaryDirectory() as tmpdir: + src_dir = os.path.join(HERE, 'instance') + generator = ssg.StaticSiteGenerator(src_dir, tmpdir) + os.chdir(os.path.join(instance_dir, 'pages')) + generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'no-title.md', tmpdir, + False) + assert os.path.exists(os.path.join(tmpdir, 'no-title.md')) + assert not os.path.exists(os.path.join(tmpdir, 'no-title.html')) + + +def test_file_copy_symlink(): + """Test the ability to sync a file to the output dir.""" + with tempfile.TemporaryDirectory() as tmpdir: + src_dir = os.path.join(HERE, 'instance') + generator = ssg.StaticSiteGenerator(src_dir, tmpdir) + os.chdir(os.path.join(instance_dir, 'pages')) + generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'symlink-to-foo.txt', tmpdir) + # need to copy the destination for os.path.exists to be happy with this + generator.build_file_in_destination(os.path.join(instance_dir, 'pages'), '', 'foo.txt', tmpdir) + assert os.path.exists(os.path.join(tmpdir, 'symlink-to-foo.txt')) + assert os.path.islink(os.path.join(tmpdir, 'symlink-to-foo.txt')) + + +def test_dir_copy(): + """Test the ability to sync a directory to the output dir.""" + with tempfile.TemporaryDirectory() as tmpdir: + src_dir = os.path.join(HERE, 'instance') + generator = ssg.StaticSiteGenerator(src_dir, tmpdir) + os.chdir(os.path.join(instance_dir, 'pages')) + generator.build_subdir_in_destination(os.path.join(instance_dir, 'pages'), '', 'media', tmpdir) + assert os.path.exists(os.path.join(tmpdir, 'media')) + assert os.path.isdir(os.path.join(tmpdir, 'media')) + + +def test_dir_copy_symlink(): + """Test the ability to sync a directory to the output dir.""" + with tempfile.TemporaryDirectory() as tmpdir: + src_dir = os.path.join(HERE, 'instance') + generator = ssg.StaticSiteGenerator(src_dir, tmpdir) + os.chdir(os.path.join(instance_dir, 'pages')) + generator.build_subdir_in_destination(os.path.join(instance_dir, 'pages'), '', 'symlink-to-subdir', tmpdir) + # need to copy the destination for os.path.exists to be happy with this + generator.build_subdir_in_destination(os.path.join(instance_dir, 'pages'), '', 'subdir', tmpdir) + assert os.path.exists(os.path.join(tmpdir, 'symlink-to-subdir')) + assert os.path.isdir(os.path.join(tmpdir, 'symlink-to-subdir')) + assert os.path.islink(os.path.join(tmpdir, 'symlink-to-subdir')) + + +def test_build_in_destination(): + """Test the ability to walk a source and populate the destination.""" + with tempfile.TemporaryDirectory() as tmpdir: + src_dir = os.path.join(HERE, 'instance') + generator = ssg.StaticSiteGenerator(src_dir, tmpdir) + generator.build_in_destination(os.path.join(src_dir, 'pages'), tmpdir) + + assert os.path.exists(os.path.join(tmpdir, 'index.md')) + assert os.path.exists(os.path.join(tmpdir, 'index.html')) + assert os.path.exists(os.path.join(tmpdir, 'subdir', 'index.md')) + assert os.path.exists(os.path.join(tmpdir, 'subdir', 'index.html'))