rewrite the project as a static site generator

this removes Flask, reworks a number of library methods accordingly, and
adds generators and build commands to process the instance directory
(largely unchanged, except config.py is now config.json) and spit out
files suitable to be served by a web server such as Nginx.

there are probably some rough edges here, but overall this works.

also note, as this is no longer server software on a network, the
license has changed from AGPLv3 to GPLv3, and the "or any later version"
allowance has been removed

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
This commit is contained in:
2025-03-12 10:28:38 -05:00
parent ed12272d4d
commit 7eb485c6ae
45 changed files with 1389 additions and 1587 deletions

View File

@@ -1,26 +0,0 @@
"""Create the test app and other fixtures.
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: AGPL-3.0-or-later
"""
import os
import pytest
from incorporealcms import create_app
HERE = os.path.dirname(os.path.abspath(__file__))
@pytest.fixture
def app():
"""Create the Flask application, with test settings."""
app = create_app(instance_path=os.path.join(HERE, 'instance'))
yield app
@pytest.fixture
def client(app):
"""Create a test client based on the test app."""
return app.test_client()

View File

@@ -1,67 +1,65 @@
"""Test graphviz functionality.
SPDX-FileCopyrightText: © 2021 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-License-Identifier: GPL-3.0-only
"""
import os
import tempfile
from incorporealcms import create_app
import pytest
from incorporealcms import init_instance
from incorporealcms.ssg import StaticSiteGenerator
HERE = os.path.dirname(os.path.abspath(__file__))
def app_with_pydot():
"""Create the test app, including the pydot extension."""
return create_app(instance_path=os.path.join(HERE, 'instance'),
test_config={'MARKDOWN_EXTENSIONS': ['incorporealcms.mdx.pydot']})
def test_functional_initialization():
"""Test initialization with the graphviz config."""
app = app_with_pydot()
assert app is not None
init_instance(instance_path=os.path.join(HERE, 'instance'),
extra_config={'MARKDOWN_EXTENSIONS': ['incorporealcms.mdx.pydot', 'incorporealcms.mdx.figures',
'attr_list']})
def test_graphviz_is_rendered():
"""Initialize the app with the graphviz extension and ensure it does something."""
app = app_with_pydot()
client = app.test_client()
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
ssg = StaticSiteGenerator(src_dir, tmpdir)
os.chdir(os.path.join(src_dir, 'pages'))
response = client.get('/test-graphviz')
assert response.status_code == 200
assert b'~~~pydot' not in response.data
assert b'data:image/png;base64' in response.data
def test_two_graphviz_are_rendered():
"""Test two images are rendered."""
app = app_with_pydot()
client = app.test_client()
response = client.get('/test-two-graphviz')
assert response.status_code == 200
assert b'~~~pydot' not in response.data
assert b'data:image/png;base64' in response.data
ssg.build_file_in_destination(os.path.join(HERE, 'instance', 'pages'), '', 'test-graphviz.md', tmpdir, True)
with open(os.path.join(tmpdir, 'test-graphviz.html'), 'r') as graphviz_output:
data = graphviz_output.read()
assert 'data:image/png;base64' in data
os.chdir(HERE)
def test_invalid_graphviz_is_not_rendered():
"""Check that invalid graphviz doesn't blow things up."""
app = app_with_pydot()
client = app.test_client()
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
ssg = StaticSiteGenerator(src_dir, tmpdir)
os.chdir(os.path.join(src_dir, 'broken'))
response = client.get('/test-invalid-graphviz')
assert response.status_code == 500
assert b'INTERNAL SERVER ERROR' in response.data
with pytest.raises(ValueError):
ssg.build_file_in_destination(os.path.join(HERE, 'instance', 'broken'), '', 'test-invalid-graphviz.md',
tmpdir, True)
os.chdir(HERE)
def test_figures_are_rendered(client):
def test_figures_are_rendered():
"""Test that a page with my figure syntax renders as expected."""
response = client.get('/figures')
assert response.status_code == 200
assert (b'<figure class="right"><img alt="fancy captioned logo" src="bss-square-no-bg.png" />'
b'<figcaption>this is my cool logo!</figcaption></figure>') in response.data
assert (b'<figure><img alt="vanilla captioned logo" src="bss-square-no-bg.png" />'
b'<figcaption>this is my cool logo without an attr!</figcaption>\n</figure>') in response.data
assert (b'<figure class="left"><img alt="fancy logo" src="bss-square-no-bg.png" />'
b'<span></span></figure>') in response.data
assert b'<figure><img alt="just a logo" src="bss-square-no-bg.png" /></figure>' in response.data
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
ssg = StaticSiteGenerator(src_dir, tmpdir)
os.chdir(os.path.join(src_dir, 'pages'))
ssg.build_file_in_destination(os.path.join(HERE, 'instance', 'pages'), '', 'figures.md', tmpdir, True)
with open(os.path.join(tmpdir, 'figures.html'), 'r') as graphviz_output:
data = graphviz_output.read()
assert ('<figure class="right"><img alt="fancy captioned logo" src="bss-square-no-bg.png" />'
'<figcaption>this is my cool logo!</figcaption></figure>') in data
assert ('<figure><img alt="vanilla captioned logo" src="bss-square-no-bg.png" />'
'<figcaption>this is my cool logo without an attr!</figcaption>\n</figure>') in data
assert ('<figure class="left"><img alt="fancy logo" src="bss-square-no-bg.png" />'
'<span></span></figure>') in data
assert '<figure><img alt="just a logo" src="bss-square-no-bg.png" /></figure>' in data
os.chdir(HERE)

View File

@@ -1,244 +0,0 @@
"""Test page requests.
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: AGPL-3.0-or-later
"""
import re
from unittest.mock import patch
def test_page_that_exists(client):
"""Test that the app can serve a basic file at the index."""
response = client.get('/')
assert response.status_code == 200
assert b'<h1 id="test-index">test index</h1>' in response.data
def test_direct_file_that_exists(client):
"""Test that the app can serve a basic file at the index."""
response = client.get('/foo.txt')
assert response.status_code == 200
assert b'test file' in response.data
def test_page_that_doesnt_exist(client):
"""Test that the app returns 404 for nonsense requests and they use my error page."""
response = client.get('/ohuesthaoeusth')
assert response.status_code == 404
assert b'<b><tt>/ohuesthaoeusth</tt></b> does not seem to exist' in response.data
# test the contact email config
assert b'admin@example.com' in response.data
def test_files_outside_pages_do_not_get_served(client):
"""Test that page pathing doesn't break out of the instance/pages/ dir, and the error uses my error page."""
response = client.get('/../unreachable')
assert response.status_code == 400
assert b'You\'re doing something you\'re not supposed to. Stop it?' in response.data
def test_internal_server_error_serves_error_page(client):
"""Test that various exceptions serve up the 500 page."""
response = client.get('/actually-a-png')
assert response.status_code == 500
assert b'INTERNAL SERVER ERROR' in response.data
# test the contact email config
assert b'admin@example.com' in response.data
def test_oserror_is_500(client, app):
"""Test that an OSError raises as a 500."""
with app.test_request_context():
with patch('flask.current_app.open_instance_resource', side_effect=OSError):
response = client.get('/')
assert response.status_code == 500
assert b'INTERNAL SERVER ERROR' in response.data
def test_unsupported_file_type_is_500(client, app):
"""Test a coding condition mishap raises as a 500."""
with app.test_request_context():
with patch('incorporealcms.pages.request_path_to_instance_resource_path', return_value=('foo', 'bar')):
response = client.get('/')
assert response.status_code == 500
assert b'INTERNAL SERVER ERROR' in response.data
def test_weird_paths_do_not_get_served(client):
"""Test that we clean up requests as desired."""
response = client.get('/../../')
assert response.status_code == 400
def test_page_with_title_metadata(client):
"""Test that a page with title metadata has its title written."""
response = client.get('/')
assert response.status_code == 200
assert b'<title>Index - example.com</title>' in response.data
def test_page_without_title_metadata(client):
"""Test that a page without title metadata gets the default title."""
response = client.get('/no-title')
assert response.status_code == 200
assert b'<title>/no-title - example.com</title>' in response.data
def test_page_in_subdir_without_title_metadata(client):
"""Test that the title-less page display is as expected."""
response = client.get('/subdir//page-no-title')
assert response.status_code == 200
assert b'<title>/subdir/page-no-title - example.com</title>' in response.data
def test_page_with_card_metadata(client):
"""Test that a page with opengraph metadata."""
response = client.get('/more-metadata')
assert response.status_code == 200
assert b'<meta property="og:title" content="title for the page - example.com">' in response.data
assert b'<meta property="og:description" content="description of this page made even longer">' in response.data
assert b'<meta property="og:image" content="http://buh.com/test.img">' in response.data
def test_page_with_card_title_even_when_no_metadata(client):
"""Test that a page without metadata still has a card with the derived title."""
response = client.get('/no-title')
assert response.status_code == 200
assert b'<meta property="og:title" content="/no-title - example.com">' in response.data
assert b'<meta property="og:description"' not in response.data
assert b'<meta property="og:image"' not in response.data
def test_page_with_forced_empty_title_just_shows_suffix(client):
"""Test that if a page specifies a blank Title meta tag explicitly, only the suffix is used in the title."""
response = client.get('/forced-no-title')
assert response.status_code == 200
assert b'<title>example.com</title>' in response.data
def test_page_with_redirect_meta_url_redirects(client):
"""Test that if a page specifies a URL to redirect to, that the site serves up a 301."""
response = client.get('/redirect')
assert response.status_code == 301
assert response.location == 'http://www.google.com/'
def test_page_has_modified_timestamp(client):
"""Test that pages have modified timestamps in them."""
response = client.get('/')
assert response.status_code == 200
assert re.search(r'Last modified: ....-..-.. ..:..:.. ...', response.data.decode()) is not None
def test_that_page_request_redirects_to_directory(client):
"""Test that a request to /foo reirects to /foo/, if foo is a directory.
This might be useful in cases where a formerly page-only page has been
converted to a directory with subpages.
"""
response = client.get('/subdir')
assert response.status_code == 301
assert response.location == '/subdir/'
def test_that_request_to_symlink_redirects_markdown(client):
"""Test that a request to /foo redirects to /what-foo-points-at."""
response = client.get('/symlink-to-no-title')
assert response.status_code == 301
assert response.location == '/no-title'
def test_that_request_to_symlink_redirects_file(client):
"""Test that a request to /foo.txt redirects to /what-foo-points-at.txt."""
response = client.get('/symlink-to-foo.txt')
assert response.status_code == 301
assert response.location == '/foo.txt'
def test_that_request_to_symlink_redirects_directory(client):
"""Test that a request to /foo/ redirects to /what-foo-points-at/."""
response = client.get('/symlink-to-subdir/')
assert response.status_code == 301
assert response.location == '/subdir'
# sadly, this location also redirects
response = client.get('/subdir')
assert response.status_code == 301
assert response.location == '/subdir/'
# but we do get there
response = client.get('/subdir/')
assert response.status_code == 200
def test_that_request_to_symlink_redirects_subdirectory(client):
"""Test that a request to /foo/bar redirects to /what-foo-points-at/bar."""
response = client.get('/symlink-to-subdir/page-no-title')
assert response.status_code == 301
assert response.location == '/subdir/page-no-title'
response = client.get('/subdir/page-no-title')
assert response.status_code == 200
def test_that_dir_request_does_not_redirect(client):
"""Test that a request to /foo/ serves the index page, if foo is a directory."""
response = client.get('/subdir/')
assert response.status_code == 200
assert b'another page' in response.data
def test_setting_selected_style_includes_cookie(client):
"""Test that a request with style=foo sets the cookie and renders appropriately."""
response = client.get('/')
style_cookie = client.get_cookie('user-style')
assert style_cookie is None
response = client.get('/?style=light')
style_cookie = client.get_cookie('user-style')
assert response.status_code == 200
assert b'/static/css/light.css' in response.data
assert b'/static/css/dark.css' not in response.data
assert style_cookie.value == 'light'
response = client.get('/?style=dark')
style_cookie = client.get_cookie('user-style')
assert response.status_code == 200
assert b'/static/css/dark.css' in response.data
assert b'/static/css/light.css' not in response.data
assert style_cookie.value == 'dark'
def test_pages_can_supply_alternate_templates(client):
"""Test that pages can supply templates other than the default."""
response = client.get('/')
assert b'class="site-wrap site-wrap-normal-width"' in response.data
assert b'class="site-wrap site-wrap-double-width"' not in response.data
response = client.get('/custom-template')
assert b'class="site-wrap site-wrap-normal-width"' not in response.data
assert b'class="site-wrap site-wrap-double-width"' in response.data
def test_extra_footer_per_page(client):
"""Test that we don't include the extra-footer if there isn't one (or do if there is)."""
response = client.get('/')
assert b'<div class="extra-footer">' not in response.data
response = client.get('/index-but-with-footer')
assert b'<div class="extra-footer"><i>ooo <a href="a">a</a></i>' in response.data
def test_serving_static_files(client):
"""Test the usage of send_from_directory to serve extra static files."""
response = client.get('/custom-static/css/warm.css')
assert response.status_code == 200
# can't serve directories, just files
response = client.get('/custom-static/')
assert response.status_code == 404
response = client.get('/custom-static/css/')
assert response.status_code == 404
response = client.get('/custom-static/css')
assert response.status_code == 404
# can't serve files that don't exist or bad paths
response = client.get('/custom-static/css/cold.css')
assert response.status_code == 404
response = client.get('/custom-static/css/../../unreachable.md')
assert response.status_code == 404

View File

@@ -0,0 +1,28 @@
{
"LOGGING": {
"version": 1,
"formatters": {
"default": {
"format": "[%(asctime)s %(levelname)-7s %(name)s] %(message)s"
}
},
"handlers": {
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "default"
}
},
"loggers": {
"incorporealcms.mdx": {
"level": "DEBUG",
"handlers": ["console"]
},
"incorporealcms.pages": {
"level": "DEBUG",
"handlers": ["console"]
}
}
},
"INSTANCE_VALUE": "hi"
}

View File

@@ -1,7 +1,7 @@
"""Configure the test application.
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-License-Identifier: GPL-3.0-only
"""
LOGGING = {

View File

@@ -0,0 +1 @@
../pages/more-metadata.md

30
tests/test_commands.py Normal file
View File

@@ -0,0 +1,30 @@
"""Test command line invocations.
SPDX-FileCopyrightText: © 2023 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
"""
import os
import tempfile
from subprocess import run
HERE = os.path.dirname(os.path.abspath(__file__))
def test_build():
"""Test some of the output of the core builder command."""
with tempfile.TemporaryDirectory() as tmpdir:
result = run(['incorporealcms-build', os.path.join(HERE, 'instance'), tmpdir],
capture_output=True, encoding='utf8')
assert "creating temporary directory" in result.stdout
assert "copying file" in result.stdout
assert "creating symlink" in result.stdout
assert "creating directory" in result.stdout
assert "renaming" in result.stdout
def test_build_error():
"""Test some of the output of the core builder command."""
result = run(['incorporealcms-build', os.path.join(HERE, 'instance'), os.path.join(HERE, 'test_markdown.py')],
capture_output=True, encoding='utf8')
assert "specified output path" in result.stderr
assert "exists as a file!" in result.stderr

View File

@@ -1,83 +1,38 @@
"""Test basic configuration stuff.
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-License-Identifier: GPL-3.0-only
"""
import os
from incorporealcms import create_app
import pytest
from incorporealcms import init_instance
from incorporealcms.config import Config
HERE = os.path.dirname(os.path.abspath(__file__))
def test_config():
"""Test create_app without passing test config."""
"""Test that the app initialization sets values not normally present in the config."""
# this may have gotten here from other imports in other tests
try:
delattr(Config, 'INSTANCE_VALUE')
except AttributeError:
pass
assert not getattr(Config, 'INSTANCE_VALUE', None)
assert not getattr(Config, 'EXTRA_VALUE', None)
instance_path = os.path.join(HERE, 'instance')
assert not create_app(instance_path=instance_path).testing
assert create_app(instance_path=instance_path, test_config={"TESTING": True}).testing
init_instance(instance_path=instance_path, extra_config={"EXTRA_VALUE": "hello"})
assert getattr(Config, 'INSTANCE_VALUE', None) == "hi"
assert getattr(Config, 'EXTRA_VALUE', None) == "hello"
def test_markdown_meta_extension_always():
"""Test that the markdown meta extension is always loaded, even if not specified."""
app = create_app(instance_path=os.path.join(HERE, 'instance'),
test_config={'MARKDOWN_EXTENSIONS': []})
client = app.test_client()
response = client.get('/')
assert response.status_code == 200
assert b'<title>Index - example.com</title>' in response.data
def test_custom_markdown_extensions_work():
"""Test we can change extensions via config, and that they work.
This used to test smarty, but that's added by default now, so we test
that it can be removed by overriding the option.
"""
app = create_app(instance_path=os.path.join(HERE, 'instance'))
client = app.test_client()
response = client.get('/mdash-or-triple-dash')
assert response.status_code == 200
assert b'word &mdash; word' in response.data
app = create_app(instance_path=os.path.join(HERE, 'instance'),
test_config={'MARKDOWN_EXTENSIONS': []})
client = app.test_client()
response = client.get('/mdash-or-triple-dash')
assert response.status_code == 200
assert b'word --- word' in response.data
def test_title_override():
"""Test that a configuration with a specific title overrides the default."""
instance_path = os.path.join(HERE, 'instance')
app = create_app(instance_path=instance_path, test_config={'TITLE_SUFFIX': 'suou.net'})
client = app.test_client()
response = client.get('/no-title')
assert response.status_code == 200
assert b'<title>/no-title - suou.net</title>' in response.data
def test_media_file_access(client):
"""Test that media files are served, and properly."""
response = client.get('/media/favicon.png')
assert response.status_code == 200
assert response.headers['content-type'] == 'image/png'
def test_favicon_override():
"""Test that a configuration with a specific favicon overrides the default."""
instance_path = os.path.join(HERE, 'instance')
app = create_app(instance_path=instance_path, test_config={'FAVICON': '/media/foo.png'})
client = app.test_client()
response = client.get('/no-title')
assert response.status_code == 200
assert b'<link rel="icon" href="/media/foo.png">' in response.data
def test_misconfigured_markdown_extensions():
"""Test that a misconfigured markdown extensions leads to a 500 at render time."""
instance_path = os.path.join(HERE, 'instance')
app = create_app(instance_path=instance_path, test_config={'MARKDOWN_EXTENSIONS': 'WRONG'})
client = app.test_client()
response = client.get('/no-title')
assert response.status_code == 500
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)

View File

@@ -1,44 +1,77 @@
"""Test the feed methods.
SPDX-FileCopyrightText: © 2023 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-License-Identifier: GPL-3.0-only
"""
from incorporealcms.feed import serve_feed
import os
import tempfile
from incorporealcms import init_instance
from incorporealcms.feed import generate_feed
HERE = os.path.dirname(os.path.abspath(__file__))
init_instance(instance_path=os.path.join(HERE, 'instance'))
def test_unknown_type_is_404(client):
"""Test that requesting a feed type that doesn't exist is a 404."""
response = client.get('/feed/wat')
assert response.status_code == 404
def test_atom_type_generated():
"""Test that an ATOM feed can be generated."""
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
generate_feed('atom', src_dir, tmpdir)
with open(os.path.join(tmpdir, 'feed', 'atom'), 'r') as feed_output:
data = feed_output.read()
assert '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<feed xmlns="http://www.w3.org/2005/Atom">' in data
assert '<id>https://example.org/</id>' in data
assert '<email>admin@example.org</email>' in data
assert '<name>Test Name</name>' in data
# forced-no-title.md
assert '<title>example.org</title>' in data
assert '<link href="https://example.org/forced-no-title"/>' in data
assert '<id>tag:example.org,2023-12-01:/forced-no-title</id>' in data
assert '<content type="html">&lt;p&gt;some words are here&lt;/p&gt;</content>' in data
# more-metadata.md
assert '<title>title for the page - example.org</title>' in data
assert '<link href="https://example.org/more-metadata"/>' in data
assert '<id>tag:example.org,2025-03-16:/more-metadata</id>' in data
assert '<content type="html">&lt;p&gt;hello&lt;/p&gt;</content>' in data
def test_atom_type_is_200(client):
"""Test that requesting an ATOM feed is found."""
response = client.get('/feed/atom')
assert response.status_code == 200
assert 'application/atom+xml' in response.content_type
print(response.text)
def test_rss_type_generated():
"""Test that an RSS feed can be generated."""
with tempfile.TemporaryDirectory() as tmpdir:
src_dir = os.path.join(HERE, 'instance')
generate_feed('rss', src_dir, tmpdir)
with open(os.path.join(tmpdir, 'feed', 'rss'), 'r') as feed_output:
data = feed_output.read()
assert '<?xml version=\'1.0\' encoding=\'UTF-8\'?>\n<rss xmlns:atom="http://www.w3.org/2005/Atom"' in data
assert 'xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">' in data
assert '<link>https://example.org</link>' in data
# forced-no-title.md
assert '<title>example.org</title>' in data
assert '<link>https://example.org/forced-no-title</link>' in data
assert '<guid isPermaLink="false">tag:example.org,2023-12-01:/forced-no-title</guid>' in data
assert '<description>&lt;p&gt;some words are here&lt;/p&gt;</description>' in data
assert '<author>admin@example.org (Test Name)</author>' in data
# more-metadata.md
assert '<title>title for the page - example.org</title>' in data
assert '<link>https://example.org/more-metadata</link>' in data
assert '<guid isPermaLink="false">tag:example.org,2025-03-16:/more-metadata</guid>' in data
assert '<description>&lt;p&gt;hello&lt;/p&gt;</description>' in data
assert '<author>admin@example.org (Test Name)</author>' in data
def test_rss_type_is_200(client):
"""Test that requesting an RSS feed is found."""
response = client.get('/feed/rss')
assert response.status_code == 200
assert 'application/rss+xml' in response.content_type
print(response.text)
def test_feed_generator_atom(app):
"""Test the root feed generator."""
with app.test_request_context():
content = serve_feed('atom')
assert b'<id>https://example.com/</id>' in content.data
assert b'<email>admin@example.com</email>' in content.data
assert b'<name>Test Name</name>' in content.data
def test_feed_generator_rss(app):
"""Test the root feed generator."""
with app.test_request_context():
content = serve_feed('rss')
assert b'<author>admin@example.com (Test Name)</author>' in content.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)

150
tests/test_markdown.py Normal file
View File

@@ -0,0 +1,150 @@
"""Test the conversion of Markdown pages.
SPDX-FileCopyrightText: © 2025 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
"""
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)
HERE = os.path.dirname(os.path.abspath(__file__))
os.chdir(os.path.join(HERE, 'instance/', 'pages/'))
def test_generate_page_navs_index():
"""Test that the index page has navs to the root (itself)."""
assert generate_parent_navs('index.md') == [('example.org', '/')]
def test_generate_page_navs_subdir_index():
"""Test that dir pages have navs to the root and themselves."""
assert generate_parent_navs('subdir/index.md') == [('example.org', '/'), ('subdir', '/subdir/')]
def test_generate_page_navs_subdir_real_page():
"""Test that real pages have navs to the root, their parent, and themselves."""
assert generate_parent_navs('subdir/page.md') == [('example.org', '/'), ('subdir', '/subdir/'),
('Page', '/subdir/page')]
def test_generate_page_navs_subdir_with_title_parsing_real_page():
"""Test that title metadata is used in the nav text."""
assert generate_parent_navs('subdir-with-title/page.md') == [
('example.org', '/'),
('SUB!', '/subdir-with-title/'),
('page', '/subdir-with-title/page')
]
def test_generate_page_navs_subdir_with_no_index():
"""Test that breadcrumbs still generate even if a subdir doesn't have an index.md."""
assert generate_parent_navs('no-index-dir/page.md') == [
('example.org', '/'),
('/no-index-dir/', '/no-index-dir/'),
('page', '/no-index-dir/page')
]
def test_page_includes_themes_with_default():
"""Test that a request contains the configured themes and sets the default as appropriate."""
assert '<link rel="stylesheet" type="text/css" title="light" href="/static/css/light.css">'\
in handle_markdown_file_path('index.md')
assert '<link rel="alternate stylesheet" type="text/css" title="dark" href="/static/css/dark.css">'\
in handle_markdown_file_path('index.md')
def test_render_with_style_overrides():
"""Test that the default can be changed."""
with patch('incorporealcms.Config.DEFAULT_PAGE_STYLE', 'dark'):
assert '<link rel="stylesheet" type="text/css" title="dark" href="/static/css/dark.css">'\
in handle_markdown_file_path('index.md')
assert '<link rel="alternate stylesheet" type="text/css" title="light" href="/static/css/light.css">'\
in handle_markdown_file_path('index.md')
def test_render_with_default_style_override():
"""Test that theme overrides work, and if a requested theme doesn't exist, the default is loaded."""
with patch('incorporealcms.Config.PAGE_STYLES', {'cool': '/static/css/cool.css',
'warm': '/static/css/warm.css'}):
with patch('incorporealcms.Config.DEFAULT_PAGE_STYLE', 'warm'):
assert '<link rel="stylesheet" type="text/css" title="warm" href="/static/css/warm.css">'\
in handle_markdown_file_path('index.md')
assert '<link rel="alternate stylesheet" type="text/css" title="cool" href="/static/css/cool.css">'\
in handle_markdown_file_path('index.md')
assert '<link rel="alternate stylesheet" type="text/css" title="light" href="/static/css/light.css">'\
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') == '/'
def test_instance_resource_path_to_request_path_on_page():
"""Test no-title.md -> no-title."""
assert instance_resource_path_to_request_path('no-title.md') == '/no-title'
def test_instance_resource_path_to_request_path_on_subdir():
"""Test subdir/index.md -> subdir/."""
assert instance_resource_path_to_request_path('subdir/index.md') == '/subdir/'
def test_instance_resource_path_to_request_path_on_subdir_and_page():
"""Test subdir/page.md -> subdir/page."""
assert instance_resource_path_to_request_path('subdir/page.md') == '/subdir/page'
def test_request_path_to_breadcrumb_display_patterns():
"""Test various conversions from request path to leaf nodes for display in the breadcrumbs."""
assert request_path_to_breadcrumb_display('/foo') == 'foo'
assert request_path_to_breadcrumb_display('/foo/') == 'foo'
assert request_path_to_breadcrumb_display('/foo/bar') == 'bar'
assert request_path_to_breadcrumb_display('/foo/bar/') == 'bar'
assert request_path_to_breadcrumb_display('/') == ''
def test_parse_md_metadata():
"""Test the direct results of parsing a markdown file."""
content, md, page_name, page_title, mtime = parse_md('more-metadata.md')
assert page_name == 'title for the page'
assert page_title == 'title for the page - example.org'
def test_parse_md_metadata_forced_no_title():
"""Test the direct results of parsing a markdown file."""
content, md, page_name, page_title, mtime = parse_md('forced-no-title.md')
assert page_name == ''
assert page_title == 'example.org'
def test_parse_md_metadata_no_title_so_path():
"""Test the direct results of parsing a markdown file."""
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')

View File

@@ -1,282 +0,0 @@
"""Unit test helper methods.
SPDX-FileCopyrightText: © 2020 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: AGPL-3.0-or-later
"""
import os
import pytest
from werkzeug.http import dump_cookie
from incorporealcms import create_app
from incorporealcms.pages import (generate_parent_navs, instance_resource_path_to_request_path, render,
request_path_to_breadcrumb_display, request_path_to_instance_resource_path)
HERE = os.path.dirname(os.path.abspath(__file__))
def test_generate_page_navs_index(app):
"""Test that the index page has navs to the root (itself)."""
with app.app_context():
assert generate_parent_navs('pages/index.md') == [('example.com', '/')]
def test_generate_page_navs_subdir_index(app):
"""Test that dir pages have navs to the root and themselves."""
with app.app_context():
assert generate_parent_navs('pages/subdir/index.md') == [('example.com', '/'), ('subdir', '/subdir/')]
def test_generate_page_navs_subdir_real_page(app):
"""Test that real pages have navs to the root, their parent, and themselves."""
with app.app_context():
assert generate_parent_navs('pages/subdir/page.md') == [('example.com', '/'), ('subdir', '/subdir/'),
('Page', '/subdir/page')]
def test_generate_page_navs_subdir_with_title_parsing_real_page(app):
"""Test that title metadata is used in the nav text."""
with app.app_context():
assert generate_parent_navs('pages/subdir-with-title/page.md') == [
('example.com', '/'),
('SUB!', '/subdir-with-title/'),
('page', '/subdir-with-title/page')
]
def test_generate_page_navs_subdir_with_no_index(app):
"""Test that breadcrumbs still generate even if a subdir doesn't have an index.md."""
with app.app_context():
assert generate_parent_navs('pages/no-index-dir/page.md') == [
('example.com', '/'),
('/no-index-dir/', '/no-index-dir/'),
('page', '/no-index-dir/page')
]
def test_render_with_user_dark_theme(app):
"""Test that a request with the dark theme selected renders the dark theme."""
cookie = dump_cookie("user-style", 'dark')
with app.test_request_context(headers={'COOKIE': cookie}):
assert b'/static/css/dark.css' in render('base.html').data
assert b'/static/css/light.css' not in render('base.html').data
def test_render_with_user_light_theme(app):
"""Test that a request with the light theme selected renders the light theme."""
with app.test_request_context():
assert b'/static/css/light.css' in render('base.html').data
assert b'/static/css/dark.css' not in render('base.html').data
def test_render_with_no_user_theme(app):
"""Test that a request with no theme set renders the light theme."""
with app.test_request_context():
assert b'/static/css/light.css' in render('base.html').data
assert b'/static/css/dark.css' not in render('base.html').data
def test_render_with_theme_defaults_affects_html(app):
"""Test that the base themes are all that's presented in the HTML."""
# test we can remove stuff from the default
with app.test_request_context():
assert b'?style=light' in render('base.html').data
assert b'?style=dark' in render('base.html').data
assert b'?style=plain' in render('base.html').data
def test_render_with_theme_overrides_affects_html(app):
"""Test that the overridden themes are presented in the HTML."""
# test we can remove stuff from the default
restyled_app = create_app(instance_path=os.path.join(HERE, 'instance'),
test_config={'PAGE_STYLES': {'light': '/static/css/light.css'}})
with restyled_app.test_request_context():
assert b'?style=light' in render('base.html').data
assert b'?style=dark' not in render('base.html').data
assert b'?style=plain' not in render('base.html').data
# test that we can add new stuff too/instead
restyled_app = create_app(instance_path=os.path.join(HERE, 'instance'),
test_config={'PAGE_STYLES': {'cool': '/static/css/cool.css',
'warm': '/static/css/warm.css'},
'DEFAULT_PAGE_STYLE': 'warm'})
with restyled_app.test_request_context():
assert b'?style=cool' in render('base.html').data
assert b'?style=warm' in render('base.html').data
def test_render_with_theme_overrides(app):
"""Test that the loaded themes can be overridden from the default."""
cookie = dump_cookie("user-style", 'cool')
restyled_app = create_app(instance_path=os.path.join(HERE, 'instance'),
test_config={'PAGE_STYLES': {'cool': '/static/css/cool.css',
'warm': '/static/css/warm.css'}})
with restyled_app.test_request_context(headers={'COOKIE': cookie}):
assert b'/static/css/cool.css' in render('base.html').data
assert b'/static/css/warm.css' not in render('base.html').data
def test_render_with_theme_overrides_not_found_is_default(app):
"""Test that theme overrides work, and if a requested theme doesn't exist, the default is loaded."""
cookie = dump_cookie("user-style", 'nonexistent')
restyled_app = create_app(instance_path=os.path.join(HERE, 'instance'),
test_config={'PAGE_STYLES': {'cool': '/static/css/cool.css',
'warm': '/static/css/warm.css'},
'DEFAULT_PAGE_STYLE': 'warm'})
with restyled_app.test_request_context(headers={'COOKIE': cookie}):
assert b'/static/css/warm.css' in render('base.html').data
assert b'/static/css/nonexistent.css' not in render('base.html').data
def test_request_path_to_instance_resource_path(app):
"""Test a normal URL request is transformed into the file path."""
with app.test_request_context():
assert request_path_to_instance_resource_path('index') == ('pages/index.md', 'markdown')
def test_request_path_to_instance_resource_path_direct_file(app):
"""Test a normal URL request is transformed into the file path."""
with app.test_request_context():
assert request_path_to_instance_resource_path('no-title') == ('pages/no-title.md', 'markdown')
def test_request_path_to_instance_resource_path_in_subdir(app):
"""Test a normal URL request is transformed into the file path."""
with app.test_request_context():
assert request_path_to_instance_resource_path('subdir/page') == ('pages/subdir/page.md', 'markdown')
def test_request_path_to_instance_resource_path_subdir_index(app):
"""Test a normal URL request is transformed into the file path."""
with app.test_request_context():
assert request_path_to_instance_resource_path('subdir/') == ('pages/subdir/index.md', 'markdown')
def test_request_path_to_instance_resource_path_relatives_walked(app):
"""Test a normal URL request is transformed into the file path."""
with app.test_request_context():
assert (request_path_to_instance_resource_path('subdir/more-subdir/../../more-metadata') ==
('pages/more-metadata.md', 'markdown'))
def test_request_path_to_instance_resource_path_relatives_walked_indexes_work_too(app):
"""Test a normal URL request is transformed into the file path."""
with app.test_request_context():
assert request_path_to_instance_resource_path('subdir/more-subdir/../../') == ('pages/index.md', 'markdown')
def test_request_path_to_instance_resource_path_relatives_walked_into_subdirs_also_fine(app):
"""Test a normal URL request is transformed into the file path."""
with app.test_request_context():
assert (request_path_to_instance_resource_path('subdir/more-subdir/../../subdir/page') ==
('pages/subdir/page.md', 'markdown'))
def test_request_path_to_instance_resource_path_permission_error_on_ref_above_pages(app):
"""Test that attempts to get above the base dir ("/../../foo") fail."""
with app.test_request_context():
with pytest.raises(PermissionError):
assert request_path_to_instance_resource_path('../unreachable')
def test_request_path_to_instance_resource_path_isadirectory_on_file_like_req_for_dir(app):
"""Test that a request for e.g. '/foo' when foo is a dir indicate to redirect."""
with app.test_request_context():
with pytest.raises(IsADirectoryError):
assert request_path_to_instance_resource_path('subdir')
def test_request_path_to_instance_resource_path_actual_file(app):
"""Test that a request for e.g. '/foo.png' when foo.png is a real file works."""
with app.test_request_context():
assert (request_path_to_instance_resource_path('bss-square-no-bg.png') ==
('pages/bss-square-no-bg.png', 'file'))
def test_request_path_to_instance_resource_path_markdown_symlink(app):
"""Test that a request for e.g. '/foo' when foo.md is a symlink to another .md file redirects."""
with app.test_request_context():
assert (request_path_to_instance_resource_path('symlink-to-no-title') ==
('pages/no-title.md', 'symlink'))
def test_request_path_to_instance_resource_path_file_symlink(app):
"""Test that a request for e.g. '/foo' when foo.txt is a symlink to another .txt file redirects."""
with app.test_request_context():
assert (request_path_to_instance_resource_path('symlink-to-foo.txt') ==
('pages/foo.txt', 'symlink'))
def test_request_path_to_instance_resource_path_dir_symlink(app):
"""Test that a request for e.g. '/foo' when /foo is a symlink to /bar redirects."""
with app.test_request_context():
assert (request_path_to_instance_resource_path('symlink-to-subdir/') ==
('pages/subdir', 'symlink'))
def test_request_path_to_instance_resource_path_subdir_symlink(app):
"""Test that a request for e.g. '/foo/baz' when /foo is a symlink to /bar redirects."""
with app.test_request_context():
assert (request_path_to_instance_resource_path('symlink-to-subdir/page-no-title') ==
('pages/subdir/page-no-title.md', 'symlink'))
def test_request_path_to_instance_resource_path_nonexistant_file_errors(app):
"""Test that a request for something not on disk errors."""
with app.test_request_context():
with pytest.raises(FileNotFoundError):
assert request_path_to_instance_resource_path('nthanpthpnh')
def test_request_path_to_instance_resource_path_absolute_file_errors(app):
"""Test that a request for something not on disk errors."""
with app.test_request_context():
with pytest.raises(PermissionError):
assert request_path_to_instance_resource_path('/etc/hosts')
def test_instance_resource_path_to_request_path_on_index(app):
"""Test index.md -> /."""
with app.test_request_context():
assert instance_resource_path_to_request_path('index.md') == ''
def test_instance_resource_path_to_request_path_on_page(app):
"""Test no-title.md -> no-title."""
with app.test_request_context():
assert instance_resource_path_to_request_path('no-title.md') == 'no-title'
def test_instance_resource_path_to_request_path_on_subdir(app):
"""Test subdir/index.md -> subdir/."""
with app.test_request_context():
assert instance_resource_path_to_request_path('subdir/index.md') == 'subdir/'
def test_instance_resource_path_to_request_path_on_subdir_and_page(app):
"""Test subdir/page.md -> subdir/page."""
with app.test_request_context():
assert instance_resource_path_to_request_path('subdir/page.md') == 'subdir/page'
def test_request_resource_request_root(app):
"""Test that a request can resolve to a resource and back to a request."""
with app.test_request_context():
instance_path, _ = request_path_to_instance_resource_path('index')
instance_resource_path_to_request_path(instance_path) == ''
def test_request_resource_request_page(app):
"""Test that a request can resolve to a resource and back to a request."""
with app.test_request_context():
instance_path, _ = request_path_to_instance_resource_path('no-title')
instance_resource_path_to_request_path(instance_path) == 'no-title'
def test_request_path_to_breadcrumb_display_patterns():
"""Test various conversions from request path to leaf nodes for display in the breadcrumbs."""
assert request_path_to_breadcrumb_display('/foo') == 'foo'
assert request_path_to_breadcrumb_display('/foo/') == 'foo'
assert request_path_to_breadcrumb_display('/foo/bar') == 'bar'
assert request_path_to_breadcrumb_display('/foo/bar/') == 'bar'
assert request_path_to_breadcrumb_display('/') == ''

90
tests/test_ssg.py Normal file
View File

@@ -0,0 +1,90 @@
"""Test the high level SSG operations.
SPDX-FileCopyrightText: © 2023 Brian S. Stephan <bss@incorporeal.org>
SPDX-License-Identifier: GPL-3.0-only
"""
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'))