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:
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
28
tests/instance/config.json
Normal file
28
tests/instance/config.json
Normal 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"
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
1
tests/instance/feed/20250316-more-metadata.md
Symbolic link
1
tests/instance/feed/20250316-more-metadata.md
Symbolic link
@@ -0,0 +1 @@
|
||||
../pages/more-metadata.md
|
||||
30
tests/test_commands.py
Normal file
30
tests/test_commands.py
Normal 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
|
||||
@@ -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 — 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)
|
||||
|
||||
@@ -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"><p>some words are here</p></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"><p>hello</p></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><p>some words are here</p></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><p>hello</p></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
150
tests/test_markdown.py
Normal 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')
|
||||
@@ -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
90
tests/test_ssg.py
Normal 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'))
|
||||
Reference in New Issue
Block a user