17 Commits

Author SHA1 Message Date
f4beb15a3b make directory redirects absolute paths
I think this is always the right choice, since we're rewriting the full
input path
2021-04-17 15:06:39 -05:00
da447d2873 Merge branch 'master' of git.incorporeal.org:bss/incorporeal-cms 2021-04-17 14:57:20 -05:00
cde82ab918 don't route /media separately anymore 2021-04-17 11:16:34 -05:00
1ac13f3b9c add some 500 tests for test coverage 2021-04-17 11:08:01 -05:00
6705fa38eb requirements bumps 2021-04-17 10:58:06 -05:00
30b79e9dc1 add tests for subdir symlinks
this is automagically supported by the previous rewrite
2021-04-17 10:39:05 -05:00
60715a3a5c make request -> instance conversion support symlink dirs
I think this also clarifies the code, a bit
2021-04-17 10:31:05 -05:00
c90f0a3a42 treat symlinks as redirects
closes #7
2021-04-15 21:44:02 -05:00
71ead20f3f have file handler return render type rather than bool
for when we have further types to render
2021-04-15 20:36:30 -05:00
be88c3c1bc don't error on breadcrumbs if a dir doesn't have index.md
fixes #8
2021-04-14 21:35:14 -05:00
ced67bec8b allow for serving files directly inside pages/ 2021-04-14 20:45:50 -05:00
757b067e16 create a "plain" style with next to no CSS 2021-03-09 09:10:33 -06:00
06d948a709 have specific styles @import the base styles
this clarifies the value of what was formerly "style.css" a bit, and
also opens the door for potential styles that don't inherit the base
styling at all
2021-03-07 23:09:58 -06:00
d89fd151ca use just the page part of the path in breadcrumbs
rather than showing the full path (e.g. /foo/bar/baz) in breadcrumbs
when the page doesn't have a Title, show just the leaf (baz)

Closes #4
2021-02-27 00:30:32 -06:00
ce1ed60dd2 allow for configuration to override the favicon
Closes #5
2021-02-27 00:10:03 -06:00
f46bff6ec6 tweak language around the email 2021-02-23 13:16:58 -06:00
70a8d4f06a add configurable contact email for error pages 2021-02-23 13:11:52 -06:00
23 changed files with 284 additions and 88 deletions

View File

@@ -3,7 +3,7 @@ import logging
import os import os
from logging.config import dictConfig from logging.config import dictConfig
from flask import Flask, request, send_from_directory from flask import Flask, request
from ._version import get_versions from ._version import get_versions
@@ -40,11 +40,6 @@ def create_app(instance_path=None, test_config=None):
logger.info("RESPONSE: %s %s: %s", request.method, request.path, response.status) logger.info("RESPONSE: %s %s: %s", request.method, request.path, response.status)
return response return response
@app.route(f'/{app.config["MEDIA_DIR"]}/<path:filename>')
def media_files(filename):
return send_from_directory(os.path.join(app.instance_path, app.config['MEDIA_DIR']),
filename)
from . import error_pages, pages from . import error_pages, pages
app.register_blueprint(pages.bp) app.register_blueprint(pages.bp)
app.register_error_handler(400, error_pages.bad_request) app.register_error_handler(400, error_pages.bad_request)

View File

@@ -48,7 +48,11 @@ class Config(object):
}, },
} }
DEFAULT_PAGE_STYLE = 'light'
TITLE_SUFFIX = 'incorporeal.org'
MEDIA_DIR = 'media' MEDIA_DIR = 'media'
# customizations
DEFAULT_PAGE_STYLE = 'light'
TITLE_SUFFIX = 'incorporeal.org'
CONTACT_EMAIL = 'bss@incorporeal.org'
# specify FAVICON in your instance config.py to override the suou icon

View File

@@ -34,6 +34,7 @@ def render(template_name_or_list, **context):
PAGE_STYLES = { PAGE_STYLES = {
'dark': 'css/dark.css', 'dark': 'css/dark.css',
'light': 'css/light.css', 'light': 'css/light.css',
'plain': 'css/plain.css',
} }
selected_style = request.args.get('style', None) selected_style = request.args.get('style', None)

View File

@@ -6,7 +6,7 @@ import re
from flask import Blueprint, Markup, abort from flask import Blueprint, Markup, abort
from flask import current_app as app from flask import current_app as app
from flask import redirect, request from flask import redirect, request, send_from_directory
from tzlocal import get_localzone from tzlocal import get_localzone
from incorporealcms.lib import get_meta_str, init_md, render from incorporealcms.lib import get_meta_str, init_md, render
@@ -21,15 +21,24 @@ bp = Blueprint('pages', __name__, url_prefix='/')
def display_page(path): def display_page(path):
"""Get the file contents of the requested path and render the file.""" """Get the file contents of the requested path and render the file."""
try: try:
resolved_path = request_path_to_instance_resource_path(path) resolved_path, render_type = request_path_to_instance_resource_path(path)
logger.debug("received request for path '%s', resolved to '%s'", path, resolved_path) logger.debug("received request for path '%s', resolved to '%s', type '%s'",
path, resolved_path, render_type)
except PermissionError: except PermissionError:
abort(400) abort(400)
except IsADirectoryError: except IsADirectoryError:
return redirect(f'{path}/', code=301) return redirect(f'/{path}/', code=301)
except FileNotFoundError: except FileNotFoundError:
abort(404) abort(404)
if render_type == 'file':
return send_from_directory(app.instance_path, resolved_path)
elif render_type == 'symlink':
logger.debug("attempting to redirect path '%s' to reverse of resource '%s'", path, resolved_path)
redirect_path = f'/{instance_resource_path_to_request_path(resolved_path)}'
logger.debug("redirect path: '%s'", redirect_path)
return redirect(redirect_path, code=301)
elif render_type == 'markdown':
try: try:
with app.open_instance_resource(resolved_path, 'r') as entry_file: with app.open_instance_resource(resolved_path, 'r') as entry_file:
mtime = datetime.datetime.fromtimestamp(os.path.getmtime(entry_file.name), get_localzone()) mtime = datetime.datetime.fromtimestamp(os.path.getmtime(entry_file.name), get_localzone())
@@ -50,8 +59,11 @@ def display_page(path):
logger.debug("title (potentially derived): %s", page_title) logger.debug("title (potentially derived): %s", page_title)
return render('base.html', title=page_title, description=get_meta_str(md, 'description'), return render('base.html', title=page_title, description=get_meta_str(md, 'description'),
image=get_meta_str(md, 'image'), base_url=request.base_url, content=content, navs=parent_navs, image=get_meta_str(md, 'image'), base_url=request.base_url, content=content,
mtime=mtime.strftime('%Y-%m-%d %H:%M:%S %Z')) navs=parent_navs, mtime=mtime.strftime('%Y-%m-%d %H:%M:%S %Z'))
else:
logger.exception("unsupported render_type '%s'!?", render_type)
abort(500)
def request_path_to_instance_resource_path(path): def request_path_to_instance_resource_path(path):
@@ -63,34 +75,52 @@ def request_path_to_instance_resource_path(path):
""" """
# check if the path is allowed # check if the path is allowed
base_dir = os.path.realpath(f'{app.instance_path}/pages/') base_dir = os.path.realpath(f'{app.instance_path}/pages/')
resolved_path = os.path.realpath(os.path.join(base_dir, path)) verbatim_path = os.path.abspath(os.path.join(base_dir, path))
logger.debug("base_dir: %s, constructed resolved_path: %s", base_dir, resolved_path) resolved_path = os.path.realpath(verbatim_path)
logger.debug("base_dir '%s', constructed resolved_path '%s' for path '%s'", base_dir, resolved_path, path)
# bail if the requested real path isn't inside the base directory # bail if the requested real path isn't inside the base directory
if base_dir != os.path.commonpath((base_dir, resolved_path)): if base_dir != os.path.commonpath((base_dir, resolved_path)):
logger.warning("client tried to request a path '%s' outside of the base_dir!", path) logger.warning("client tried to request a path '%s' outside of the base_dir!", path)
raise PermissionError raise PermissionError
# if this is a file-like requset but actually a directory, redirect the user # see if we have a real file or if we should infer markdown rendering
if os.path.exists(resolved_path):
# if this is a file-like request but actually a directory, redirect the user
if os.path.isdir(resolved_path) and not path.endswith('/'): if os.path.isdir(resolved_path) and not path.endswith('/'):
logger.info("client requested a path '%s' that is actually a directory", path) logger.info("client requested a path '%s' that is actually a directory", path)
raise IsADirectoryError raise IsADirectoryError
# derive the proper markdown file depending on if this is a dir or file # if the requested path contains a symlink, redirect the user
if os.path.isdir(resolved_path): if verbatim_path != resolved_path:
absolute_resource = os.path.join(resolved_path, 'index.md') logger.info("client requested a path '%s' that is actually a symlink to file '%s'", path, resolved_path)
else: return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'symlink'
absolute_resource = f'{resolved_path}.md'
logger.info("final path = '%s' for request '%s'", absolute_resource, path) # derive the proper markdown or actual file depending on if this is a dir or file
if os.path.isdir(resolved_path):
resolved_path = os.path.join(resolved_path, 'index.md')
return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'markdown'
logger.info("final DIRECT path = '%s' for request '%s'", resolved_path, path)
return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'file'
# if we're here, this isn't direct file access, so try markdown inference
verbatim_path = os.path.abspath(os.path.join(base_dir, f'{path}.md'))
resolved_path = os.path.realpath(verbatim_path)
# does the final file actually exist? # does the final file actually exist?
if not os.path.exists(absolute_resource): if not os.path.exists(resolved_path):
logger.warning("requested final path '%s' does not exist!", absolute_resource) logger.warning("requested final path '%s' does not exist!", resolved_path)
raise FileNotFoundError raise FileNotFoundError
# check for symlinks
if verbatim_path != resolved_path:
logger.info("client requested a path '%s' that is actually a symlink to file '%s'", path, resolved_path)
return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'symlink'
logger.info("final path = '%s' for request '%s'", resolved_path, path)
# we checked that the file exists via absolute path, but now we need to give the path relative to instance dir # we checked that the file exists via absolute path, but now we need to give the path relative to instance dir
return absolute_resource.replace(f'{app.instance_path}{os.path.sep}', '') return resolved_path.replace(f'{app.instance_path}{os.path.sep}', ''), 'markdown'
def instance_resource_path_to_request_path(path): def instance_resource_path_to_request_path(path):
@@ -127,8 +157,19 @@ def generate_parent_navs(path):
md = init_md() md = init_md()
# read the resource # read the resource
try:
with app.open_instance_resource(path, 'r') as entry_file: with app.open_instance_resource(path, 'r') as entry_file:
entry = entry_file.read() entry = entry_file.read()
_ = Markup(md.convert(entry)) _ = Markup(md.convert(entry))
page_name = " ".join(md.Meta.get('title')) if md.Meta.get('title') else request_path page_name = (" ".join(md.Meta.get('title')) if md.Meta.get('title')
else request_path_to_breadcrumb_display(request_path))
return generate_parent_navs(parent_resource_path) + [(page_name, request_path)] return generate_parent_navs(parent_resource_path) + [(page_name, request_path)]
except FileNotFoundError:
return generate_parent_navs(parent_resource_path) + [(request_path, request_path)]
def request_path_to_breadcrumb_display(path):
"""Given a request path, e.g. "/foo/bar/baz/", turn it into breadcrumby text "baz"."""
undired = path.rstrip('/')
leaf = undired[undired.rfind('/'):]
return leaf.strip('/')

View File

@@ -1,3 +1,5 @@
@import '/static/css/base.css';
html { html {
color: #CCC; color: #CCC;
} }

View File

@@ -1,3 +1,5 @@
@import '/static/css/base.css';
html { html {
color: #222; color: #222;
} }

View File

@@ -0,0 +1,8 @@
.img-25 {
max-width: 25% !important;
}
.img-50 {
max-width: 50% !important;
}

View File

@@ -10,8 +10,8 @@
<div class="content"> <div class="content">
<h1>NOT FOUND</h1> <h1>NOT FOUND</h1>
<p>Sorry, <b><tt>{{ request.path }}</tt></b> does not seem to exist, at least not anymore.</p> <p>Sorry, <b><tt>{{ request.path }}</tt></b> does not seem to exist, at least not anymore.</p>
<p>It's possible you followed a dead link on this site, in which case I would appreciate it if you could email me via: <p>It's possible you followed a dead link on this site, in which case I would appreciate it if you could email me at
bss @ &lt;this domain&gt; and I can take a look. I make an effort to symlink old content to its new location, {{ config.CONTACT_EMAIL }} and I can take a look. I make an effort to symlink old content to its new location,
so old links and URLs should, generally speaking, work.</p> so old links and URLs should, generally speaking, work.</p>
<p>Otherwise, I suggest you go <a href="/">to the index</a> and navigate your way (hopefully) to what <p>Otherwise, I suggest you go <a href="/">to the index</a> and navigate your way (hopefully) to what
you're looking for.</p> you're looking for.</p>

View File

@@ -9,6 +9,6 @@
{% block body %} {% block body %}
<div class="content"> <div class="content">
<h1>INTERNAL SERVER ERROR</h1> <h1>INTERNAL SERVER ERROR</h1>
<p>Something bad happened! Please email me at bss @ &lt;this domain&gt; and tell me what happened.</p> <p>Something bad happened! Please email me at {{ config.CONTACT_EMAIL }} and tell me what happened.</p>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -7,9 +7,8 @@
<meta property="og:url" content="{{ base_url }}"> <meta property="og:url" content="{{ base_url }}">
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename=user_style) }}"> <link rel="stylesheet" href="{{ url_for('static', filename=user_style) }}">
<link rel="icon" href="{{ url_for('static', filename='img/favicon.png') }}"> <link rel="icon" href="{% if config.FAVICON %}{{ config.FAVICON }}{% else %}{{ url_for('static', filename='img/favicon.png') }}{% endif %}">
<div class="site-wrap"> <div class="site-wrap">
{% block header %} {% block header %}
@@ -23,6 +22,7 @@
<div class="styles"> <div class="styles">
<a href="?style=dark">[dark]</a> <a href="?style=dark">[dark]</a>
<a href="?style=light">[light]</a> <a href="?style=light">[light]</a>
<a href="?style=plain">[plain]</a>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -16,7 +16,7 @@ click==7.1.2
# via # via
# flask # flask
# pip-tools # pip-tools
coverage==5.4 coverage==5.5
# via pytest-cov # via pytest-cov
distlib==0.3.1 distlib==0.3.1
# via virtualenv # via virtualenv
@@ -30,7 +30,7 @@ flake8-blind-except==0.2.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8-builtins==1.5.3 flake8-builtins==1.5.3
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8-docstrings==1.5.0 flake8-docstrings==1.6.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8-executable==2.1.1 flake8-executable==2.1.1
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
@@ -42,7 +42,7 @@ flake8-logging-format==0.6.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8-mutable==1.2.0 flake8-mutable==1.2.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
flake8==3.8.4 flake8==3.9.1
# via # via
# -r requirements/requirements-dev.in # -r requirements/requirements-dev.in
# dlint # dlint
@@ -53,19 +53,19 @@ flake8==3.8.4
# flake8-mutable # flake8-mutable
flask==1.1.2 flask==1.1.2
# via -r requirements/requirements.in # via -r requirements/requirements.in
gitdb==4.0.5 gitdb==4.0.7
# via gitpython # via gitpython
gitpython==3.1.13 gitpython==3.1.14
# via bandit # via bandit
iniconfig==1.1.1 iniconfig==1.1.1
# via pytest # via pytest
isort==5.7.0 isort==5.8.0
# via flake8-isort # via flake8-isort
itsdangerous==1.1.0 itsdangerous==1.1.0
# via flask # via flask
jinja2==2.11.3 jinja2==2.11.3
# via flask # via flask
markdown==3.3.3 markdown==3.3.4
# via # via
# -r requirements/requirements.in # -r requirements/requirements.in
# mdx-linkify # mdx-linkify
@@ -82,7 +82,9 @@ packaging==20.9
# tox # tox
pbr==5.5.1 pbr==5.5.1
# via stevedore # via stevedore
pip-tools==5.5.0 pep517==0.10.0
# via pip-tools
pip-tools==6.1.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
pluggy==0.13.1 pluggy==0.13.1
# via # via
@@ -92,17 +94,17 @@ py==1.10.0
# via # via
# pytest # pytest
# tox # tox
pycodestyle==2.6.0 pycodestyle==2.7.0
# via flake8 # via flake8
pydocstyle==5.1.1 pydocstyle==6.0.0
# via flake8-docstrings # via flake8-docstrings
pyflakes==2.2.0 pyflakes==2.3.1
# via flake8 # via flake8
pyparsing==2.4.7 pyparsing==2.4.7
# via packaging # via packaging
pytest-cov==2.11.1 pytest-cov==2.11.1
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
pytest==6.2.2 pytest==6.2.3
# via # via
# -r requirements/requirements-dev.in # -r requirements/requirements-dev.in
# pytest-cov # pytest-cov
@@ -116,7 +118,7 @@ six==1.15.0
# bleach # bleach
# tox # tox
# virtualenv # virtualenv
smmap==3.0.5 smmap==4.0.0
# via gitdb # via gitdb
snowballstemmer==2.1.0 snowballstemmer==2.1.0
# via pydocstyle # via pydocstyle
@@ -126,11 +128,12 @@ testfixtures==6.17.1
# via flake8-isort # via flake8-isort
toml==0.10.2 toml==0.10.2
# via # via
# pep517
# pytest # pytest
# tox # tox
tox-wheel==0.6.0 tox-wheel==0.6.0
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
tox==3.22.0 tox==3.23.0
# via # via
# -r requirements/requirements-dev.in # -r requirements/requirements-dev.in
# tox-wheel # tox-wheel
@@ -138,7 +141,7 @@ tzlocal==2.1
# via -r requirements/requirements.in # via -r requirements/requirements.in
versioneer==0.19 versioneer==0.19
# via -r requirements/requirements-dev.in # via -r requirements/requirements-dev.in
virtualenv==20.4.2 virtualenv==20.4.3
# via tox # via tox
webencodings==0.5.1 webencodings==0.5.1
# via bleach # via bleach

View File

@@ -14,7 +14,7 @@ itsdangerous==1.1.0
# via flask # via flask
jinja2==2.11.3 jinja2==2.11.3
# via flask # via flask
markdown==3.3.3 markdown==3.3.4
# via # via
# -r requirements/requirements.in # -r requirements/requirements.in
# mdx-linkify # mdx-linkify

View File

@@ -1,5 +1,6 @@
"""Test page requests.""" """Test page requests."""
import re import re
from unittest.mock import patch
def test_page_that_exists(client): def test_page_that_exists(client):
@@ -9,11 +10,20 @@ def test_page_that_exists(client):
assert b'<h1>test index</h1>' in response.data assert b'<h1>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): def test_page_that_doesnt_exist(client):
"""Test that the app returns 404 for nonsense requests and they use my error page.""" """Test that the app returns 404 for nonsense requests and they use my error page."""
response = client.get('/ohuesthaoeusth') response = client.get('/ohuesthaoeusth')
assert response.status_code == 404 assert response.status_code == 404
assert b'<b><tt>/ohuesthaoeusth</tt></b> does not seem to exist' in response.data assert b'<b><tt>/ohuesthaoeusth</tt></b> does not seem to exist' in response.data
# test the contact email config
assert b'bss@incorporeal.org' in response.data
def test_files_outside_pages_do_not_get_served(client): def test_files_outside_pages_do_not_get_served(client):
@@ -28,6 +38,26 @@ def test_internal_server_error_serves_error_page(client):
response = client.get('/actually-a-png') response = client.get('/actually-a-png')
assert response.status_code == 500 assert response.status_code == 500
assert b'INTERNAL SERVER ERROR' in response.data assert b'INTERNAL SERVER ERROR' in response.data
# test the contact email config
assert b'bss@incorporeal.org' 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): def test_weird_paths_do_not_get_served(client):
@@ -97,6 +127,44 @@ def test_that_page_request_redirects_to_directory(client):
""" """
response = client.get('/subdir') response = client.get('/subdir')
assert response.status_code == 301 assert response.status_code == 301
assert response.location == 'http://localhost/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 == 'http://localhost/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 == 'http://localhost/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 == 'http://localhost/subdir'
# sadly, this location also redirects
response = client.get('/subdir')
assert response.status_code == 301
assert response.location == 'http://localhost/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 == 'http://localhost/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): def test_that_dir_request_does_not_redirect(client):

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -0,0 +1 @@
test file

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1 @@
this is a test page

View File

@@ -0,0 +1 @@
foo.txt

View File

@@ -0,0 +1 @@
no-title.md

View File

@@ -0,0 +1 @@
subdir

View File

@@ -58,3 +58,13 @@ def test_media_file_access(client):
response = client.get('/media/favicon.png') response = client.get('/media/favicon.png')
assert response.status_code == 200 assert response.status_code == 200
assert response.headers['content-type'] == 'image/png' 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

View File

@@ -3,7 +3,7 @@ import pytest
from werkzeug.http import dump_cookie from werkzeug.http import dump_cookie
from incorporealcms.pages import (generate_parent_navs, instance_resource_path_to_request_path, render, from incorporealcms.pages import (generate_parent_navs, instance_resource_path_to_request_path, render,
request_path_to_instance_resource_path) request_path_to_breadcrumb_display, request_path_to_instance_resource_path)
def test_generate_page_navs_index(app): def test_generate_page_navs_index(app):
@@ -15,13 +15,13 @@ def test_generate_page_navs_index(app):
def test_generate_page_navs_subdir_index(app): def test_generate_page_navs_subdir_index(app):
"""Test that dir pages have navs to the root and themselves.""" """Test that dir pages have navs to the root and themselves."""
with app.app_context(): with app.app_context():
assert generate_parent_navs('pages/subdir/index.md') == [('incorporeal.org', '/'), ('/subdir/', '/subdir/')] assert generate_parent_navs('pages/subdir/index.md') == [('incorporeal.org', '/'), ('subdir', '/subdir/')]
def test_generate_page_navs_subdir_real_page(app): def test_generate_page_navs_subdir_real_page(app):
"""Test that real pages have navs to the root, their parent, and themselves.""" """Test that real pages have navs to the root, their parent, and themselves."""
with app.app_context(): with app.app_context():
assert generate_parent_navs('pages/subdir/page.md') == [('incorporeal.org', '/'), ('/subdir/', '/subdir/'), assert generate_parent_navs('pages/subdir/page.md') == [('incorporeal.org', '/'), ('subdir', '/subdir/'),
('Page', '/subdir/page')] ('Page', '/subdir/page')]
@@ -31,7 +31,17 @@ def test_generate_page_navs_subdir_with_title_parsing_real_page(app):
assert generate_parent_navs('pages/subdir-with-title/page.md') == [ assert generate_parent_navs('pages/subdir-with-title/page.md') == [
('incorporeal.org', '/'), ('incorporeal.org', '/'),
('SUB!', '/subdir-with-title/'), ('SUB!', '/subdir-with-title/'),
('/subdir-with-title/page', '/subdir-with-title/page') ('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') == [
('incorporeal.org', '/'),
('/no-index-dir/', '/no-index-dir/'),
('page', '/no-index-dir/page')
] ]
@@ -60,44 +70,45 @@ def test_render_with_no_user_theme(app):
def test_request_path_to_instance_resource_path(app): def test_request_path_to_instance_resource_path(app):
"""Test a normal URL request is transformed into the file path.""" """Test a normal URL request is transformed into the file path."""
with app.test_request_context(): with app.test_request_context():
assert request_path_to_instance_resource_path('index') == 'pages/index.md' assert request_path_to_instance_resource_path('index') == ('pages/index.md', 'markdown')
def test_request_path_to_instance_resource_path_direct_file(app): def test_request_path_to_instance_resource_path_direct_file(app):
"""Test a normal URL request is transformed into the file path.""" """Test a normal URL request is transformed into the file path."""
with app.test_request_context(): with app.test_request_context():
assert request_path_to_instance_resource_path('no-title') == 'pages/no-title.md' 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): def test_request_path_to_instance_resource_path_in_subdir(app):
"""Test a normal URL request is transformed into the file path.""" """Test a normal URL request is transformed into the file path."""
with app.test_request_context(): with app.test_request_context():
assert request_path_to_instance_resource_path('subdir/page') == 'pages/subdir/page.md' 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): def test_request_path_to_instance_resource_path_subdir_index(app):
"""Test a normal URL request is transformed into the file path.""" """Test a normal URL request is transformed into the file path."""
with app.test_request_context(): with app.test_request_context():
assert request_path_to_instance_resource_path('subdir/') == 'pages/subdir/index.md' assert request_path_to_instance_resource_path('subdir/') == ('pages/subdir/index.md', 'markdown')
def test_request_path_to_instance_resource_path_relatives_walked(app): def test_request_path_to_instance_resource_path_relatives_walked(app):
"""Test a normal URL request is transformed into the file path.""" """Test a normal URL request is transformed into the file path."""
with app.test_request_context(): with app.test_request_context():
assert (request_path_to_instance_resource_path('subdir/more-subdir/../../more-metadata') == assert (request_path_to_instance_resource_path('subdir/more-subdir/../../more-metadata') ==
'pages/more-metadata.md') ('pages/more-metadata.md', 'markdown'))
def test_request_path_to_instance_resource_path_relatives_walked_indexes_work_too(app): 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.""" """Test a normal URL request is transformed into the file path."""
with app.test_request_context(): with app.test_request_context():
assert request_path_to_instance_resource_path('subdir/more-subdir/../../') == 'pages/index.md' 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): 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.""" """Test a normal URL request is transformed into the file path."""
with app.test_request_context(): with app.test_request_context():
assert request_path_to_instance_resource_path('subdir/more-subdir/../../subdir/page') == 'pages/subdir/page.md' 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): def test_request_path_to_instance_resource_path_permission_error_on_ref_above_pages(app):
@@ -114,6 +125,41 @@ def test_request_path_to_instance_resource_path_isadirectory_on_file_like_req_fo
assert request_path_to_instance_resource_path('subdir') 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): def test_request_path_to_instance_resource_path_nonexistant_file_errors(app):
"""Test that a request for something not on disk errors.""" """Test that a request for something not on disk errors."""
with app.test_request_context(): with app.test_request_context():
@@ -155,10 +201,21 @@ def test_instance_resource_path_to_request_path_on_subdir_and_page(app):
def test_request_resource_request_root(app): def test_request_resource_request_root(app):
"""Test that a request can resolve to a resource and back to a request.""" """Test that a request can resolve to a resource and back to a request."""
with app.test_request_context(): with app.test_request_context():
instance_resource_path_to_request_path(request_path_to_instance_resource_path('index')) == '' instance_path, _ = request_path_to_instance_resource_path('index')
instance_resource_path_to_request_path(instance_path) == ''
def test_request_resource_request_page(app): def test_request_resource_request_page(app):
"""Test that a request can resolve to a resource and back to a request.""" """Test that a request can resolve to a resource and back to a request."""
with app.test_request_context(): with app.test_request_context():
instance_resource_path_to_request_path(request_path_to_instance_resource_path('no-title')) == 'no-title' 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('/') == ''