combo/tests/test_public.py

1334 lines
50 KiB
Python

import datetime
import json
import os
import re
import urllib.parse
from io import StringIO
from unittest import mock
import pytest
from django.conf import settings
from django.contrib.auth.models import Group
from django.core.files import File
from django.db import connection
from django.test import override_settings
from django.test.utils import CaptureQueriesContext
from django.urls import reverse
from django.utils.http import quote
try:
import mellon # pylint: disable=unused-import
except ImportError:
mellon = None
import requests
from combo.apps.assets.models import Asset
from combo.data.models import (
ConfigJsonCell,
FeedCell,
JsonCell,
LinkCell,
LinkListCell,
Page,
ParentContentCell,
Redirect,
SiteSettings,
TextCell,
)
from combo.profile.models import Profile
from .test_manager import login
pytestmark = pytest.mark.django_db
def test_missing_index(app):
Page.objects.all().delete()
resp = app.get('/', status=200)
assert 'Welcome' in resp.text
def test_index(app):
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard')
page.save()
resp = app.get('/index', status=301)
assert urllib.parse.urlparse(resp.location).path == '/'
resp = app.get('/', status=200)
# check {% now %} inside a skeleton_extra_placeholder is interpreted
assert str(datetime.datetime.now().year) in resp.text
def test_page_contents(app):
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard')
page.save()
cell = TextCell(page=page, placeholder='content', text='Foobar', order=0)
cell.save()
resp = app.get('/', status=200)
assert 'Foobar' in resp.text
def test_page_contents_unlogged_only(app, admin_user):
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard')
page.save()
cell = TextCell(page=page, placeholder='content', text='Foobar', order=0, restricted_to_unlogged=True)
cell.save()
resp = app.get('/', status=200)
assert 'Foobar' in resp.text
app = login(app)
resp = app.get('/', status=200)
assert not 'Foobar' in resp.text
@pytest.mark.skipif('mellon is None')
def test_mellon_login(app):
with mock.patch('combo.public.views.get_idps') as get_idps:
get_idps.return_value = ['xxx']
resp = app.get('/login/')
assert urllib.parse.urlparse(resp.location).path == '/accounts/mellon/login/'
resp = app.get('/login/?next=whatever')
assert urllib.parse.urlparse(resp.location).query == 'next=whatever'
def test_page_contents_group_presence(app, normal_user):
group = Group(name='plop')
group.save()
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard')
page.save()
cell = TextCell(page=page, placeholder='content', text='Foobar', order=0, public=False)
cell.save()
cell.groups.set([group])
resp = app.get('/', status=200)
assert 'Foobar' not in resp.text
app = login(app, username='normal-user', password='normal-user')
resp = app.get('/', status=200)
assert 'Foobar' not in resp.text
normal_user.groups.set([group])
resp = app.get('/', status=200)
assert 'Foobar' in resp.text
def test_page_contents_group_absence(app, normal_user):
group = Group(name='plop')
group.save()
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard')
page.save()
cell = TextCell(
page=page, placeholder='content', text='Foobar', order=0, public=False, restricted_to_unlogged=True
)
cell.save()
cell.groups.set([group])
resp = app.get('/', status=200)
assert 'Foobar' not in resp.text
app = login(app, username='normal-user', password='normal-user')
resp = app.get('/', status=200)
assert 'Foobar' in resp.text
normal_user.groups.set([group])
resp = app.get('/', status=200)
assert 'Foobar' not in resp.text
def test_page_contents_condition(app, normal_user):
page = Page.objects.create(title='Page', slug='page', template_name='standard')
cell1 = TextCell.objects.create(
page=page, placeholder='content', text='Foobar', order=0, condition='True'
)
cell2 = TextCell.objects.create(
page=page, placeholder='content', text='Foobaz', order=1, condition='False'
)
cell3 = LinkListCell.objects.create(order=1, page=page, placeholder='content')
LinkCell.objects.create(
page=page,
placeholder=cell3.link_placeholder,
title='Example Site',
url='http://example.net/',
order=0,
condition='True',
)
LinkCell.objects.create(
page=page,
placeholder=cell3.link_placeholder,
title='Other Site',
url='http://other.net/',
order=1,
condition='False',
)
app = login(app, username='normal-user', password='normal-user')
resp = app.get(page.get_online_url())
assert 'Foobar' in resp.text
assert 'Foobaz' not in resp.text
assert 'Example Site' in resp.text
assert 'Other Site' not in resp.text
page.sub_slug = 'foo_id'
page.save()
cell1.condition = 'foo_id'
cell1.save()
cell2.condition = 'not foo_id'
cell2.save()
resp = app.get(page.get_online_url() + '11/')
assert 'Foobar' in resp.text
assert 'Foobaz' not in resp.text
def test_page_footer_acquisition(app):
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard')
page.save()
page_index = page
cell = TextCell(page=page, placeholder='footer', text='BARFOO', order=0)
cell.save()
resp = app.get('/', status=200)
assert resp.text.count('BARFOO') == 1
# make sure a parent content cell in the home page doesn't duplicate
# cells
ParentContentCell(page=page, placeholder='footer', order=0).save()
resp = app.get('/', status=200)
assert resp.text.count('BARFOO') == 1
page = Page(title='Second', slug='second', template_name='standard')
page.save()
ParentContentCell(page=page, placeholder='footer', order=0).save()
TextCell(page=page, placeholder='footer', text='BAR2FOO', order=1).save()
resp = app.get('/second', status=301)
assert urllib.parse.urlparse(resp.location).path == '/second/'
with CaptureQueriesContext(connection) as ctx:
resp = app.get('/second/', status=200)
assert resp.text.count('BARFOO') == 1
assert resp.text.count('BAR2FOO') == 1
queries_count_second = len(ctx.captured_queries)
# deeper hierarchy
page3 = Page(title='Third', slug='third', template_name='standard', parent=page)
page3.save()
ParentContentCell(page=page3, placeholder='footer', order=0).save()
page4 = Page(title='Fourth', slug='fourth', template_name='standard', parent=page3)
page4.save()
ParentContentCell(page=page4, placeholder='footer', order=0).save()
page5 = Page(title='Fifth', slug='fifth', template_name='standard', parent=page4)
page5.save()
ParentContentCell(page=page5, placeholder='footer', order=0).save()
# check growth in SQL queries is limited
with CaptureQueriesContext(connection) as ctx:
resp = app.get('/second/third/', status=200)
assert resp.text.count('BARFOO') == 1
assert resp.text.count('BAR2FOO') == 1
queries_count_third = len(ctx.captured_queries)
# +2 for validity info of parent page
assert queries_count_third == queries_count_second + 1
with CaptureQueriesContext(connection) as ctx:
resp = app.get('/second/third/fourth/', status=200)
assert resp.text.count('BARFOO') == 1
assert resp.text.count('BAR2FOO') == 1
queries_count_fourth = len(ctx.captured_queries)
# +1 for get_parents_and_self()
assert queries_count_fourth == queries_count_second + 1 + 1
# check footer doesn't get duplicated in real index children
page6 = Page(title='Sixth', slug='sixth', template_name='standard', parent=page_index)
page6.save()
ParentContentCell(page=page6, placeholder='footer', order=0).save()
resp = app.get('/sixth/', status=200)
assert resp.text.count('BARFOO') == 1
def test_list_of_links_acquisition(app):
Page.objects.all().delete()
index_page = Page(title='Home', slug='index', template_name='standard')
index_page.save()
cell = LinkListCell.objects.create(order=0, page=index_page, placeholder='footer')
LinkCell.objects.create(
page=index_page,
placeholder=cell.link_placeholder,
title='Example Site',
url='http://example.net/',
order=0,
)
LinkCell.objects.create(
page=index_page,
placeholder=cell.link_placeholder,
title='Example2 Site',
url='http://example.org/',
order=1,
)
resp = app.get('/', status=200)
assert resp.text.count('Example Site') == 1
assert resp.text.count('Example2 Site') == 1
page = Page(title='Second', slug='second', template_name='standard')
page.save()
ParentContentCell(page=page, placeholder='footer', order=0).save()
resp = app.get('/second/', status=200)
assert resp.text.count('Example Site') == 1
assert resp.text.count('Example2 Site') == 1
def test_page_redirect(app):
Page.objects.all().delete()
page = Page(
title='Elsewhere', slug='elsewhere', template_name='standard', redirect_url='http://example.net'
)
page.save()
resp = app.get('/elsewhere/', status=302)
assert resp.location == 'http://example.net'
def test_page_templated_redirect(app):
Page.objects.all().delete()
with override_settings(TEMPLATE_VARS={'test_url': 'http://example.net'}):
page = Page(title='Elsewhere', slug='elsewhere', template_name='standard', redirect_url='[test_url]')
page.save()
resp = app.get('/elsewhere/', status=302)
assert resp.location == 'http://example.net'
page.redirect_url = '{{test_url}}/ok'
page.save()
resp = app.get('/elsewhere/', status=302)
assert resp.location == 'http://example.net/ok'
page.redirect_url = '{{unknown_variable}}'
page.save()
resp = app.get('/elsewhere/', status=200)
page.redirect_url = '{% if error %}'
page.save()
resp = app.get('/elsewhere/', status=404)
def test_page_private_unlogged(app):
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard', public=False)
page.save()
resp = app.get('/', status=302)
assert resp.location.endswith('/login/?next=http%3A//testserver/')
def test_page_private_logged_in(app, admin_user):
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard', public=False)
page.save()
app = login(app)
app.get('/', status=200)
def test_page_skeleton(app):
Page.objects.all().delete()
page = Page(
title='Elsewhere', slug='elsewhere', template_name='standard', redirect_url='http://example.net/foo/'
)
page.save()
# url prefix match
resp = app.get('/__skeleton__/?source=%s' % quote('http://example.net/foo/bar'))
assert '{% block placeholder-content %}{% block content %}{% endblock %}{% endblock %}' in resp.text
assert '{% block placeholder-footer %}{% block footer %}{% endblock %}{% endblock %}' in resp.text
assert resp.text.startswith('{% with page_template_name="standard" %}<!DOCTYPE html>\n')
# url netloc match
resp = app.get('/__skeleton__/?source=%s' % quote('http://example.net'))
assert '{% block placeholder-content %}{% block content %}{% endblock %}{% endblock %}' in resp.text
assert '{% block placeholder-footer %}{% block footer %}{% endblock %}{% endblock %}' in resp.text
# settings.KNOWN_SERVICES match
resp = app.get('/__skeleton__/?source=%s' % quote('http://127.0.0.1:8999/'))
assert '{% block placeholder-content %}{% block content %}{% endblock %}{% endblock %}' in resp.text
# no match
resp = app.get('/__skeleton__/?source=%s' % quote('http://example.com/foo/bar'), status=403)
# check with a footer cell
TextCell.objects.create(page=page, placeholder='footer', text='Foobar', order=0)
TextCell.objects.create(page=page, placeholder='content', text='Foobar', slug='unique', order=1)
TextCell.objects.create(page=page, placeholder='content', text='Foobar', slug='dup', order=2)
TextCell.objects.create(page=page, placeholder='content', text='Foobar', slug='dup', order=3)
resp = app.get('/__skeleton__/?source=%s' % quote('http://example.net'))
assert '{% block placeholder-content %}{% block content %}{% endblock %}{% endblock %}' in resp.text
assert '{% block placeholder-footer %}{% block footer %}{% endblock %}{% endblock %}' in resp.text
assert 'Foobar' in resp.text
# check slugs
assert 'id="unique"' in resp.text
assert 'id="dup"' not in resp.text
# check {% now %} inside a skeleton_extra_placeholder is not interpreted
assert '{% now ' in resp.text
assert '{# generation time #}' in resp.text
# check cells in footer are present even if there's no redirection page
page.slug = 'index'
page.save()
resp = app.get('/__skeleton__/?source=%s' % quote('http://127.0.0.1:8999/'))
assert '{% block placeholder-content %}{% block content %}{% endblock %}{% endblock %}' in resp.text
assert '{% block placeholder-footer %}{% block footer %}{% endblock %}{% endblock %}' in resp.text
assert 'Foobar' in resp.text
# check link cells provide a full URL
other_page = Page(title='Plop', slug='plop', template_name='standard')
other_page.save()
cell = LinkCell(page=page, placeholder='footer', link_page=other_page, order=0)
cell.save()
resp = app.get('/__skeleton__/?source=%s' % quote('http://127.0.0.1:8999/'))
assert 'http://testserver/plop' in resp.text
# check images in text cell use full URL
cell = TextCell(page=page, placeholder='footer', order=0)
cell.text = '<img src="/test/foobar.png"> vs <img src="http://www.example.com/test.png">'
cell.save()
resp = app.get('/__skeleton__/?source=%s' % quote('http://127.0.0.1:8999/'))
assert 'http://testserver/test/foobar.png' in resp.text
# check absolute URIs are not modified
assert 'src="http://www.example.com/test.png"' in resp.text
# check links in text cell use full URL (unless anchors and verbatim)
cell.text = '''<a href="/test/foobar"> vs
<a href="http://www.example.com/test"> vs'
<a href="#top">
<a href="{{ test_url }}">
<a href="{% verbatim %}{{ registration_url }}{% endverbatim %}">
'''
cell.save()
with override_settings(TEMPLATE_VARS={'test_url': 'http://example.net'}):
resp = app.get('/__skeleton__/?source=%s' % quote('http://127.0.0.1:8999/'))
assert 'href="http://testserver/test/foobar"' in resp.text
assert 'href="http://www.example.com/test"' in resp.text
assert 'href="#top"' in resp.text
assert 'href="http://example.net"' in resp.text
assert 'href="{{ registration_url }}"' in resp.text
# add a bad redirection page (don't use it, do not crash)
page = Page(title='BadRedirection', slug='badredir', template_name='standard', redirect_url='[foo_bar]')
page.save()
resp = app.get('/__skeleton__/?source=%s' % quote('http://example.net/foo/bar'))
resp = app.get('/__skeleton__/?source=%s' % quote('http://example.net/badredir'))
# add a page with restricted visibility
page = Page.objects.create(
title='RestrictedVisibility',
slug='restrictedvisibilit',
template_name='standard',
public=False,
exclude_from_navigation=False,
)
resp = app.get('/__skeleton__/?source=%s' % quote('http://127.0.0.1:8999/'))
assert 'RestrictedVisibility' in resp.text
# check 404 skeleton
resp = app.get('/__skeleton__/?source=404')
assert "This page doesn't exist" in resp.text
assert resp.status_code == 200
assert 'href="http://testserver/test/foobar"' in resp.text
assert 'href="http://www.example.com/test"' in resp.text
assert 'href="#top"' in resp.text
assert 'href="http://testserver/plop/"' in resp.text
# do not consider empty redirection
Page.objects.all().delete()
page = Page(
title='EmptyRedirection', slug='emptyredir', template_name='standard', redirect_url='{{ is_empty }}'
)
page.save()
resp = app.get('/__skeleton__/?source=%s' % quote('http://127.0.0.1:8999/'))
assert resp.headers['x-combo-page-id'] == '__root'
# prefer last match
Page.objects.all().delete()
page = Page(
title='Elsewhere', slug='elsewhere', template_name='standard', redirect_url='http://example.net/foo/'
)
page.save()
cell = TextCell(page=page, placeholder='footer', text='Foobar1', order=0)
cell.save()
page = Page(
title='Elsewhere2',
slug='elsewhere2',
template_name='standard',
redirect_url='http://example.net/foo/',
order=2,
)
page.save()
cell = TextCell(page=page, placeholder='footer', text='Foobar2', order=0)
cell.save()
resp = app.get('/__skeleton__/?source=%s' % quote('http://example.net/foo/bar'))
assert "Foobar2" in resp.text
def test_subpage_location(app):
Page.objects.all().delete()
page_index = Page(title='Home Page', slug='index', template_name='standard')
page_index.save()
page_sibling = Page(title='Second top level page', slug='second', template_name='standard')
page_sibling.save()
page = Page(title='Child of Home', slug='child-home', template_name='standard', parent=page_index)
page.save()
page = Page(title='Grand child of home', slug='grand-child-home', template_name='standard', parent=page)
page.save()
page = Page(title='Child of second', slug='child-second', template_name='standard', parent=page_sibling)
page.save()
page = Page(
title='Grand child of second', slug='grand-child-second', template_name='standard', parent=page
)
page.save()
resp = app.get('/', status=200)
assert 'Home Page' in resp.text
resp = app.get('/child-home/', status=200)
assert 'Child of Home' in resp.text
resp = app.get('/child-home/grand-child-home/', status=200)
assert 'Grand child of home' in resp.text
assert (
urllib.parse.urlparse(app.get('/child-home/grand-child-home', status=301).location).path
== '/child-home/grand-child-home/'
)
app.get('/grand-child-home/', status=404)
resp = app.get('/second/', status=200)
assert 'Second top level page' in resp.text
resp = app.get('/second/child-second/', status=200)
assert 'Child of second' in resp.text
resp = app.get('/second/child-second/grand-child-second/', status=200)
assert 'Grand child of second' in resp.text
def test_repeated_slug(app):
Page.objects.all().delete()
page1 = Page(title='Home Page', slug='index', template_name='standard')
page1.save()
page2 = Page(title='Second top level page', slug='second', template_name='standard')
page2.save()
page3 = Page(title='Child of second', slug='child-second', template_name='standard', parent=page2)
page3.save()
page4 = Page(
title='Grand child of second (named third)', slug='third', template_name='standard', parent=page3
)
page4.save()
page5 = Page(title='Third top level page', slug='third', template_name='standard')
page5.save()
for i, page in enumerate((page1, page2, page3, page4, page5)):
page.order = i
page.save()
resp = app.get('/third/', status=200)
assert 'Third top level page' in resp.text
resp = app.get('/second/child-second/third/', status=200)
assert 'Grand child of second (named third)' in resp.text
# reverse page order, and expect same behaviour
for i, page in enumerate(reversed((page1, page2, page3, page4, page5))):
page.order = i
page.save()
resp = app.get('/third/', status=200)
assert 'Third top level page' in resp.text
resp = app.get('/second/child-second/third/', status=200)
assert 'Grand child of second (named third)' in resp.text
def test_menu(app):
Page.objects.all().delete()
page = Page.objects.create(
title='Page1', slug='index', template_name='standard', exclude_from_navigation=False
)
Page.objects.create(title='Page2', slug='page2', template_name='standard', exclude_from_navigation=False)
Page.objects.create(
title='Page3', slug='page3', template_name='standard', public=False, exclude_from_navigation=False
)
resp = app.get('/', status=200)
assert 'menu-index' in resp.text
assert 'data-menu-page-id="%s"' % page.id
assert 'menu-page2' in resp.text
assert 'menu-page3' in resp.text
def test_404(app):
Page.objects.all().delete()
resp = app.get('/foobar/', status=404)
assert "This page doesn't exist" in resp.text
# check footer cells are taken from homepage
index_page = Page(title='Home', slug='index', template_name='standard')
index_page.save()
cell = TextCell(page=index_page, placeholder='footer', text='FOOBAR', order=0)
cell.save()
resp = app.get('/foobar/', status=404)
assert "This page doesn't exist" in resp.text
assert "FOOBAR" in resp.text
index_page.delete()
# check decidated custom page
page = Page(title='Not Found', slug='404', template_name='standard')
page.save()
TextCell(page=page, placeholder='content', text='Custom 404 Text', order=0).save()
resp = app.get('/foobar/', status=404)
assert 'Custom 404 Text' in resp.text
with override_settings(DEBUG=True):
# check error page provides an hint when debugging
resp = app.get('/foobar/', status=404)
assert "can&#x27;t find the requested page" in resp.text
# check native django handler is used if all pages are private
page.public = False
page.save()
resp = app.get('/foobar/', status=404)
assert 'Custom 404 Text' not in resp.text
assert "This page doesn't exist" not in resp.text
# check access using its /404/ slug
resp = app.get('/404', status=404)
# check 404 skeleton when all pages are private
resp = app.get('/__skeleton__/?source=404')
assert 'Custom 404 Text' not in resp.text
assert "This page doesn't exist" not in resp.text
assert resp.status_code == 200
def test_style_demo(app, admin_user):
TextCell.objects.all().delete()
Page.objects.all().delete()
app = login(app)
with override_settings(DEBUG=True):
resp = app.get('/__style__/', status=200)
assert 'class="success"' not in resp.text
assert 'class="info"' not in resp.text
assert 'class="warning"' not in resp.text
assert 'class="error"' not in resp.text
assert 'Lorem ipsum' in resp.text
assert TextCell.objects.count() == 0
assert Page.objects.count() == 0
resp = app.get('/__style__/?template=standard-sidebar&with_messages=on', status=200)
assert 'class="success"' in resp.text
assert 'class="info"' in resp.text
assert 'class="warning"' in resp.text
assert 'class="error"' in resp.text
assert 'Lorem ipsum' in resp.text
assert 'links-list' in resp.text
assert Page.objects.count() == 0
def test_page_async_cell(app, nocache):
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard')
page.save()
cell = FeedCell(page=page, placeholder='content', url='http://example.net', order=1)
cell.save()
resp = app.get('/', status=200)
assert 'data-ajax-cell-must-load="true"' in resp.text
def test_ajax_cell(app):
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard')
page.save()
page2 = Page(title='Other', slug='other', template_name='standard')
page2.save()
cell = TextCell(page=page, placeholder='content', text='<p>Foobar</p>', order=0)
cell.save()
resp = app.get(
reverse(
'combo-public-ajax-page-cell', kwargs={'page_pk': page.id, 'cell_reference': cell.get_reference()}
)
)
assert resp.text.strip() == '<p>Foobar</p>'
resp = app.get(
reverse(
'combo-public-ajax-page-cell',
kwargs={'page_pk': page2.id, 'cell_reference': cell.get_reference()},
),
status=404,
)
resp = app.get(
reverse(
'combo-public-ajax-page-cell', kwargs={'page_pk': '100', 'cell_reference': cell.get_reference()}
),
status=404,
)
page.public = False
page.save()
resp = app.get(
reverse(
'combo-public-ajax-page-cell', kwargs={'page_pk': page.id, 'cell_reference': cell.get_reference()}
),
status=403,
)
page.public = True
page.save()
cell.public = False
cell.save()
resp = app.get('/', status=200)
assert not 'FOOBAR' in resp.text
resp = app.get(
reverse(
'combo-public-ajax-page-cell', kwargs={'page_pk': page.id, 'cell_reference': cell.get_reference()}
),
status=403,
)
def test_initial_login_page(app, admin_user):
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard')
page.save()
page = Page(title='Initial Login', slug='initial-login', template_name='standard', public=False)
page.save()
site_settings = SiteSettings.get_singleton()
site_settings.initial_login_page_path = '/initial-login/'
site_settings.save()
resp = app.get('/', status=200)
# first visit
app = login(app)
resp = app.get('/', status=302)
assert urllib.parse.urlparse(resp.location).path == '/initial-login/'
# visit again
resp = app.get('/', status=200)
# delete initial login info
profile = Profile.objects.get(user=admin_user)
profile.initial_login_view_timestamp = None
profile.save(update_fields=['initial_login_view_timestamp'])
with override_settings(
TEMPLATE_VARS={
'portal_url': settings.SITE_BASE_URL,
'idp_url': settings.KNOWN_SERVICES['authentic']['idp']['url'],
}
):
# add templated login page path
site_settings.initial_login_page_path = '{{portal_url}}/foo/'
site_settings.save(update_fields=['initial_login_page_path'])
# "first" visit
app = login(app)
resp = app.get('/', status=302)
assert resp.location == 'http://localhost/foo/'
# delete initial login info, again
profile = Profile.objects.get(user=admin_user)
profile.initial_login_view_timestamp = None
profile.save(update_fields=['initial_login_view_timestamp'])
# add templated login page path to idp url
site_settings.initial_login_page_path = '{{idp_url}}/bar/'
site_settings.save(update_fields=['initial_login_page_path'])
# "first" visit
app = login(app)
resp = app.get('/', status=302)
assert resp.location == 'https://authentic.example.org/bar/'
profile.initial_login_view_timestamp = None
profile.save(update_fields=['initial_login_view_timestamp'])
site_settings.initial_login_page = page
site_settings.save()
# first visit
app = login(app)
resp = app.get('/', status=302)
assert urllib.parse.urlparse(resp.location).path == '/initial-login/'
# visit again
resp = app.get('/', status=200)
def test_welcome_page(app, admin_user):
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard')
page.save()
page = Page(title='Welcome', slug='welcome', template_name='standard')
page.save()
site_settings = SiteSettings.get_singleton()
site_settings.welcome_page_path = '/welcome/'
site_settings.save()
app.cookiejar.clear()
resp = app.get('/', status=302)
assert urllib.parse.urlparse(resp.location).path == '/welcome/'
resp = app.get('/', status=200)
app.cookiejar.clear()
resp = app.get('/', status=302)
assert urllib.parse.urlparse(resp.location).path == '/welcome/'
app.cookiejar.clear()
app = login(app)
resp = app.get('/', status=200)
with override_settings(
TEMPLATE_VARS={
'portal_url': settings.SITE_BASE_URL,
'idp_url': settings.KNOWN_SERVICES['authentic']['idp']['url'],
}
):
# add templated welcome page path
site_settings.welcome_page_path = '{{portal_url}}/foo/'
site_settings.save(update_fields=['welcome_page_path'])
app.cookiejar.clear()
resp = app.get('/', status=302)
assert resp.location == 'http://localhost/foo/'
# add templated login page path to idp url
site_settings.welcome_page_path = '{{idp_url}}/bar/'
site_settings.save(update_fields=['welcome_page_path'])
app.cookiejar.clear()
resp = app.get('/', status=302)
assert resp.location == 'https://authentic.example.org/bar/'
site_settings.welcome_page = page
site_settings.save()
app.cookiejar.clear()
resp = app.get('/', status=302)
assert urllib.parse.urlparse(resp.location).path == '/welcome/'
resp = app.get('/', status=200)
def test_post_cell(app):
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard')
page.save()
cell = TextCell(page=page, placeholder='content', text='<p>Foobar</p>', order=0)
cell.save()
# check it's not possible to post to cell that doesn't have POST support.
resp = app.post(
reverse(
'combo-public-ajax-page-cell', kwargs={'page_pk': page.id, 'cell_reference': cell.get_reference()}
),
params={'hello': 'world'},
status=403,
)
templates_settings = [settings.TEMPLATES[0].copy()]
templates_settings[0]['DIRS'] = ['%s/templates-1' % os.path.abspath(os.path.dirname(__file__))]
with override_settings(
JSON_CELL_TYPES={
'test-post-cell': {
'name': 'Foobar',
'url': 'http://test-post-cell/{{slug}}/',
'form': [
{'label': 'slug', 'varname': 'slug', 'required': True},
],
'actions': {
'create': {
'url': 'http://test-post-cell/{{slug}}/create/',
},
'update': {
'url': 'http://test-post-cell/{{slug}}/update/',
'method': 'PATCH',
},
'search': {
'url': 'http://test-post-cell/{{slug}}/search/',
'method': 'GET',
'response': 'raw',
},
'download': {
'url': 'http://test-post-cell/{{slug}}/download/',
'method': 'GET',
'response': 'raw',
},
},
}
},
TEMPLATES=templates_settings,
):
cell = ConfigJsonCell(page=page, placeholder='content', order=0)
cell.key = 'test-post-cell'
cell.parameters = {'slug': 'slug'}
cell.save()
with mock.patch('combo.utils.requests.get') as requests_get:
requests_get.return_value = mock.Mock(content=json.dumps({'hello': 'world'}), status_code=200)
resp = app.get('/', status=200)
resp.form['value'] = 'plop'
resp.form.fields['items[]'][0].checked = True
with mock.patch('combo.utils.requests.request') as requests_post:
requests_post.return_value = mock.Mock(content=json.dumps({'err': 0}), status_code=200)
resp2 = resp.form.submit()
assert requests_post.call_args[0][0] == 'POST'
assert requests_post.call_args[0][1] == 'http://test-post-cell/slug/create/'
assert requests_post.call_args[1]['json'] == {'value': 'plop', 'items': ['1']}
assert urllib.parse.urlparse(resp2.location).path == '/'
# check ajax call
with mock.patch('combo.utils.requests.request') as requests_post:
requests_post.return_value = mock.Mock(content=json.dumps({'err': 0}), status_code=200)
resp2 = resp.form.submit(headers={'x-requested-with': 'XMLHttpRequest'})
assert requests_post.call_args[0][0] == 'POST'
assert requests_post.call_args[0][1] == 'http://test-post-cell/slug/create/'
assert requests_post.call_args[1]['json'] == {'value': 'plop', 'items': ['1']}
assert resp2.text.startswith('<form')
# check error on POST
with mock.patch('combo.utils.requests.request') as requests_post:
requests_post.return_value = mock.Mock(content=json.dumps({'err': 0}), status_code=400)
resp2 = resp.form.submit()
assert urllib.parse.urlparse(resp2.location).path == '/'
resp2 = resp2.follow()
assert 'Error sending data.' in resp2.text
settings.JSON_CELL_TYPES['test-post-cell']['actions']['create'][
'error-message'
] = 'Failed to create stuff.'
resp2 = resp.form.submit()
assert urllib.parse.urlparse(resp2.location).path == '/'
resp2 = resp2.follow()
assert 'Failed to create stuff.' in resp2.text
with mock.patch('combo.utils.requests.request') as requests_post:
requests_post.return_value = mock.Mock(content=json.dumps({'err': 0}), status_code=400)
resp2 = resp.form.submit(headers={'x-requested-with': 'XMLHttpRequest'})
assert resp2.text.startswith('<form')
assert resp2.headers['x-error-message'] == 'Failed to create stuff.'
# check variable substitution in URL
with mock.patch('combo.utils.requests.request') as requests_post:
settings.JSON_CELL_TYPES['test-post-cell']['actions']['create'] = {
'url': 'http://test-post-cell/{{slug}}/{{value}}/delete'
}
resp2 = resp.form.submit()
assert requests_post.call_args[0][0] == 'POST'
assert requests_post.call_args[0][1] == 'http://test-post-cell/slug/plop/delete'
# check patch method
with mock.patch('combo.utils.requests.request') as requests_patch:
resp.form['action'] = 'update'
requests_patch.return_value = mock.Mock(content=json.dumps({'err': 0}), status_code=200)
resp2 = resp.form.submit()
assert requests_patch.call_args[0][0] == 'PATCH'
assert requests_patch.call_args[0][1] == 'http://test-post-cell/slug/update/'
assert requests_patch.call_args[1]['json'] == {'value': 'plop', 'items': ['1']}
assert urllib.parse.urlparse(resp2.location).path == '/'
# check raw result
with mock.patch('combo.utils.requests.request') as requests_search:
resp.form['action'] = 'search'
requests_search.return_value = mock.Mock(
content=json.dumps({'foo': 'bar'}),
status_code=200,
headers={'Content-Type': 'application/json'},
)
resp2 = resp.form.submit()
assert requests_search.call_args[0][0] == 'GET'
assert requests_search.call_args[0][1] == 'http://test-post-cell/slug/search/'
assert resp2.json == {'foo': 'bar'}
# check raw result with content-disposition
with mock.patch('combo.utils.requests.request') as requests_search:
resp.form['action'] = 'download'
requests_search.return_value = mock.Mock(
content=b'a,b\n1,2',
status_code=200,
headers={
'Content-Type': 'application/octet-stream',
'Content-Disposition': 'attachment; filename=table.csv',
},
)
resp2 = resp.form.submit()
assert requests_search.call_args[0][0] == 'GET'
assert requests_search.call_args[0][1] == 'http://test-post-cell/slug/download/'
assert resp2['Content-Type'] == 'application/octet-stream'
assert resp2['Content-Disposition'] == 'attachment; filename=table.csv'
assert resp2.content == b'a,b\n1,2'
def test_synchronous_placeholder(app):
page = Page(title='foo', slug='foo', template_name='standard', order=0)
page.save()
cell = FeedCell(page=page, placeholder='content', url='http://example.net', order=1)
cell.save()
templates_settings = [settings.TEMPLATES[0].copy()]
templates_settings[0]['DIRS'] = ['%s/templates-1' % os.path.abspath(os.path.dirname(__file__))]
with override_settings(
COMBO_PUBLIC_TEMPLATES={
'standard': {
'name': 'Test',
'template': 'combo/page_template_synchronous_placeholder.html',
}
},
TEMPLATES=templates_settings,
):
with mock.patch('requests.get') as requests_get:
requests_get.return_value = mock.Mock(content='', status_code=200)
resp = app.get('/foo/', status=200)
assert not 'data-ajax-cell-must-load="true"' in resp.text
cell = FeedCell(page=page, placeholder='sidebar', url='http://example.org', order=1)
cell.save()
resp = app.get('/foo/', status=200)
assert resp.text.count('data-ajax-cell-must-load="true"') == 1
requests_get.side_effect = requests.exceptions.ConnectionError()
resp = app.get('/foo/', status=200)
def test_outer_tag_placeholder(app):
page = Page(title='foo', slug='foo', template_name='standard', order=0)
page.save()
templates_settings = [settings.TEMPLATES[0].copy()]
templates_settings[0]['DIRS'] = ['%s/templates-1' % os.path.abspath(os.path.dirname(__file__))]
with override_settings(
COMBO_PUBLIC_TEMPLATES={
'standard': {
'name': 'Test',
'template': 'combo/page_template_outer_tag_placeholder.html',
}
},
TEMPLATES=templates_settings,
):
resp = app.get('/foo/', status=200)
assert not resp.pyquery('div.combo-placeholder--content')
assert not resp.pyquery('aside.combo-placeholder--sidebar')
TextCell.objects.create(page=page, placeholder='content', text='Foobar', order=0)
TextCell.objects.create(page=page, placeholder='sidebar', text='Foobar', order=0)
resp = app.get('/foo/', status=200)
assert resp.pyquery('div.combo-placeholder--content')
assert resp.pyquery('aside.combo-placeholder--sidebar')
def test_redirects(app):
Redirect.objects.all().delete()
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard')
page.save()
page2 = Page(title='Second', slug='second', template_name='standard')
page2.save()
page3 = Page(title='Third', slug='third', template_name='standard', parent=page2)
page3.save()
app.get('/whatever/', status=404)
redirect = Redirect(old_url='/whatever/', page=page3)
redirect.save()
assert urllib.parse.urlparse(app.get('/whatever/', status=302).location).path == '/second/third/'
assert urllib.parse.urlparse(app.get('/whatever', status=301).location).path == '/whatever/'
# check the most recent redirect is called
redirect = Redirect(old_url='/whatever/', page=page2)
redirect.save()
assert urllib.parse.urlparse(app.get('/whatever/', status=302).location).path == '/second/'
# rename page
page3.slug = 'third2'
page3.save()
assert app.get('/second/third2/', status=200)
assert urllib.parse.urlparse(app.get('/second/third/', status=302).location).path == '/second/third2/'
page2.slug = 'second2'
page2.save()
assert urllib.parse.urlparse(app.get('/second/third/', status=302).location).path == '/second2/third2/'
assert urllib.parse.urlparse(app.get('/second/third2/', status=302).location).path == '/second2/third2/'
assert urllib.parse.urlparse(app.get('/second/', status=302).location).path == '/second2/'
# change parent
page3.parent = None
page3.save()
assert urllib.parse.urlparse(app.get('/second/third/', status=302).location).path == '/third2/'
assert urllib.parse.urlparse(app.get('/second2/third2/', status=302).location).path == '/third2/'
def test_sub_slug(app, john_doe, jane_doe):
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard')
page.save()
page2 = Page(title='User', slug='users', sub_slug='(?P<blah>[a-z]+)', template_name='standard')
page2.save()
page3 = Page(title='Blah', slug='blah', parent=page2, template_name='standard')
page3.save()
# without passing sub slug
assert app.get('/users/', status=302).location in ('..', 'http://testserver/')
# (result vary between django versions)
# json cell so we can display the parameter value
cell = JsonCell(page=page2, url='http://example.net', order=0, placeholder='content')
cell.template_string = 'XX{{ blah }}YY'
cell.save()
cell2 = JsonCell(page=page3, url='http://example.net', order=0, placeholder='content')
cell2.template_string = 'AA{{ blah }}BB{{ absolute_uri }}CC'
cell2.save()
with mock.patch('combo.utils.requests.get') as requests_get:
data = {'data': [{'url': 'http://a.b', 'text': 'xxx'}]}
requests_get.return_value = mock.Mock(content=json.dumps(data), status_code=200)
resp = app.get('/users/whatever/', status=200)
assert 'XXwhateverYY' in resp.text
cell_url = re.findall(r'data-ajax-cell-url="(.*)"', resp.text)[0]
extra_ctx = re.findall(r'data-extra-context="(.*)"', resp.text)[0]
resp = app.get(cell_url + '?ctx=' + extra_ctx)
assert resp.text == 'XXwhateverYY'
# 404 on value that doesn't match the regex
resp = app.get('/users/WHATEVER/', status=404)
# check sub page
resp = app.get('/users/whatever/plop/', status=404)
resp = app.get('/users/whatever/blah/', status=200)
cell_url = re.findall(r'data-ajax-cell-url="(.*)"', resp.text)[0]
extra_ctx = re.findall(r'data-extra-context="(.*)"', resp.text)[0]
resp = app.get(cell_url + '?ctx=' + extra_ctx)
assert resp.text == 'AAwhateverBBhttp://testserver/users/whatever/blah/CC'
# custom behaviour for <user_id>, it will add the user to context
for sub_slug in ['(?P<user_id>[0-9a-z]+)', 'user_id']:
page2.sub_slug = sub_slug
page2.save()
cell.template_string = 'XX{{ selected_user.username }}YY'
cell.save()
resp = app.get('/users/%s/' % john_doe.id, status=200)
assert 'XXjohn.doeYY' in resp.text
# bad user id => no selected_user
resp = app.get('/users/9999999/', status=200)
assert 'XXYY' in resp.text
resp = app.get('/users/abc/', status=200)
assert 'XXYY' in resp.text
# custom behaviour for <name_id>, it will add the SAML user to context
with mock.patch('combo.profile.utils.UserSAMLIdentifier') as user_saml:
class DoesNotExist(Exception):
pass
user_saml.DoesNotExist = DoesNotExist
def side_effect(*args, **kwargs):
name_id = kwargs['name_id']
if name_id == 'foo':
raise user_saml.DoesNotExist
return mock.Mock(user=john_doe)
mocked_objects = mock.Mock()
mocked_objects.get = mock.Mock(side_effect=side_effect)
user_saml.objects = mocked_objects
page2.sub_slug = '(?P<name_id>[0-9a-z.]+)'
page2.save()
resp = app.get('/users/john.doe/', status=200)
assert 'XXjohn.doeYY' in resp.text
# unknown name id => no selected_user
resp = app.get('/users/foo/', status=200)
assert 'XXYY' in resp.text
# sub_slug can contain '-' (if card slug for example)
page3 = Page.objects.create(title='Card Foo', slug='foo', template_name='standard')
for sub_slug in ['(?P<card-foo-bar_id>[0-9]+)', 'card-foo-bar_id']:
page3.sub_slug = sub_slug
page3.save()
resp = app.get('/foo/42/', status=200)
assert resp.context['card-foo-bar_id'] == '42'
assert resp.context['card_foo_bar_id'] == '42'
def test_page_extra_variables(app):
page = Page.objects.create(
title='Home',
slug='page',
template_name='standard',
extra_variables={
'foo': 'bar',
'bar_id': '{{ 40|add:2 }}',
'syntax_error': '{% for %}',
'variable_error': '{{ "foo"|add:user.email }}',
},
)
cell = JsonCell.objects.create(
page=page,
url='http://example.net',
order=0,
placeholder='content',
template_string='XX{{ foo }}YY{{ bar_id }}ZZ{{ baz_user }}AA{{ syntax_error }}BB{{ variable_error }}CC',
)
with mock.patch('combo.utils.requests.get') as requests_get:
requests_get.return_value = mock.Mock(content='{}', status_code=200)
resp = app.get('/page/')
assert '<div>XXbarYY42ZZAABBCC</div>' in resp
with mock.patch('combo.utils.requests.get') as requests_get:
requests_get.return_value = mock.Mock(content='{}', status_code=200)
resp = app.get(
reverse(
'combo-public-ajax-page-cell',
kwargs={'page_pk': page.pk, 'cell_reference': cell.get_reference()},
)
)
assert resp.text.strip() == 'XXbarYY42ZZAABBCC'
# check sub_slug/extra_variables override
page.sub_slug = '(?P<fooo>[a-z]+)'
page.save()
cell.template_string = 'XX{{ foo }}YY{{ bar_id }}ZZ{{ fooo }}AA'
cell.save()
with mock.patch('combo.utils.requests.get') as requests_get:
requests_get.return_value = mock.Mock(content='{}', status_code=200)
resp = app.get('/page/baz/')
assert '<div>XXbarYY42ZZbazAA</div>' in resp
page.sub_slug = '(?P<foo>[a-z]+)'
page.save()
with mock.patch('combo.utils.requests.get') as requests_get:
requests_get.return_value = mock.Mock(content='{}', status_code=200)
resp = app.get('/page/baz/')
assert '<div>XXbarYY42ZZAA</div>' in resp
def test_cell_slugs(app):
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard')
page.save()
TextCell(page=page, placeholder='content', text='Foobar', order=0).save()
TextCell(page=page, placeholder='content', text='Foobar', slug='unique', order=1).save()
TextCell(page=page, placeholder='content', text='Foobar', slug='dup', order=2).save()
TextCell(page=page, placeholder='content', text='Foobar', slug='dup', order=3).save()
resp = app.get('/', status=200)
assert 'id="unique"' in resp.text
assert 'id="dup"' not in resp.text
def test_missing_trailing_slashes(app):
# redirect to path with slash
assert urllib.parse.urlparse(app.get('/login', status=301).location).path == '/login/'
assert urllib.parse.urlparse(app.get('/foo', status=301).location).path == '/foo/'
# don't be tricked by double slashes
assert urllib.parse.urlparse(app.get('//foo', status=301).location).path == '/foo/'
def test_cell_asset_css_classes(settings, app, admin_user):
settings.COMBO_CELL_ASSET_SLOTS = {}
page = Page.objects.create(title='Home', slug='index', template_name='standard')
cell = TextCell.objects.create(page=page, placeholder='content', order=0, slug='foo', text='foo')
# test direct access
assert cell.asset_css_classes == ''
# and test asset preload
resp = app.get('/', status=200)
assert 'class="cell text-cell textcell foo"' in re.sub(r' +', ' ', resp.text)
settings.COMBO_CELL_ASSET_SLOTS = {'data_textcell': {'picture': {'prefix': 'Picture'}}}
cell = TextCell.objects.get(pk=cell.pk)
assert cell.asset_css_classes == ''
resp = app.get('/', status=200)
assert 'class="cell text-cell textcell foo"' in re.sub(r' +', ' ', resp.text)
Asset.objects.create(key=cell.get_asset_slot_key('picture'), asset=File(StringIO('test'), 'test.png'))
cell = TextCell.objects.get(pk=cell.pk)
assert cell.asset_css_classes == 'has-asset-picture has-any-asset has-all-assets'
resp = app.get('/', status=200)
assert 'class="cell text-cell textcell has-asset-picture has-any-asset has-all-assets foo"' in re.sub(
r' +', ' ', resp.text
)
settings.COMBO_CELL_ASSET_SLOTS = {
'data_textcell': {'picture': {'prefix': 'Picture'}, 'foo': {'prefix': 'Foo'}}
}
cell = TextCell.objects.get(pk=cell.pk)
assert cell.asset_css_classes == 'has-asset-picture has-any-asset'
resp = app.get('/', status=200)
assert 'class="cell text-cell textcell has-asset-picture has-any-asset foo"' in re.sub(
r' +', ' ', resp.text
)
Asset.objects.create(key=cell.get_asset_slot_key('foo'), asset=File(StringIO('test'), 'test.png'))
cell = TextCell.objects.get(pk=cell.pk)
assert cell.asset_css_classes == 'has-asset-foo has-asset-picture has-any-asset has-all-assets'
resp = app.get('/', status=200)
assert (
'class="cell text-cell textcell has-asset-foo has-asset-picture has-any-asset has-all-assets foo"'
in re.sub(r' +', ' ', resp.text)
)
Asset.objects.all().delete()
cell = TextCell.objects.get(pk=cell.pk)
assert cell.asset_css_classes == ''
resp = app.get('/', status=200)
assert 'class="cell text-cell textcell foo"' in re.sub(r' +', ' ', resp.text)
def test_placeholder_grid(app):
Page.objects.all().delete()
page = Page(title='Home', slug='index', template_name='standard')
page.save()
TextCell.objects.create(page=page, placeholder='content', text='Foobar', order=1)
resp = app.get('/', status=200)
# no placeholder div if there's no grid layout
assert resp.pyquery('#content > .text-cell')
page.placeholder_options = {'content': {'fx_grid_layout': 'fx-grid'}}
page.save()
resp = app.get('/', status=200)
# placeholder div in between
assert resp.pyquery('#content > .fx-grid > .text-cell')