combo/tests/test_pwa.py

362 lines
14 KiB
Python

import base64
from html.parser import HTMLParser
from io import BytesIO
from unittest import mock
import pytest
from django.core.files import File
from django.template import Context, Template
from django.test import override_settings
from django.test.client import RequestFactory
from django.urls import reverse
from webtest import Upload
from combo.apps.notifications.models import Notification
from combo.apps.pwa.models import PushSubscription, PwaNavigationEntry, PwaSettings
from combo.data.models import Page
from .test_manager import login
pytestmark = pytest.mark.django_db
def test_manifest_json(app):
assert app.get('/manifest.json', status=200).json['name'] == 'Compte Citoyen'
with override_settings(TEMPLATE_VARS={'global_title': 'FooBar'}):
assert app.get('/manifest.json', status=200).json['name'] == 'FooBar'
def test_service_worker(app):
app.get('/service-worker.js', status=200)
resp = app.get('/service-worker-registration.js', status=200)
assert 'applicationServerPublicKey = null' in resp.text
pwa_settings = PwaSettings.singleton()
pwa_settings.push_notifications = True
pwa_settings.save()
resp = app.get('/service-worker-registration.js', status=200)
assert 'applicationServerPublicKey = "' in resp.text
# check legacy settings are still supported
with override_settings(
PWA_VAPID_PUBLIK_KEY='BFzvUdXB...',
PWA_VAPID_PRIVATE_KEY='4WbCnBF...',
PWA_VAPID_CLAIMS={'sub': 'mailto:admin@entrouvert.com'},
):
resp = app.get('/service-worker-registration.js', status=200)
assert 'applicationServerPublicKey = "BFzvUdXB..."' in resp.text
def test_webpush_subscription(app, john_doe, jane_doe):
app.post_json(reverse('pwa-subscribe-push'), params={'sample': 'content'}, status=403)
app.get(reverse('pwa-subscribe-push'), status=403)
app = login(app, john_doe.username, john_doe.username)
app.post_json(reverse('pwa-subscribe-push'), params={'sample': 'content'}, status=200)
assert PushSubscription.objects.count() == 1
app.post_json(reverse('pwa-subscribe-push'), params={'sample': 'content2'}, status=200)
assert PushSubscription.objects.count() == 2
app = login(app, jane_doe.username, jane_doe.username)
app.post_json(reverse('pwa-subscribe-push'), params={'sample': 'content'}, status=200)
assert PushSubscription.objects.count() == 3
app = login(app, john_doe.username, john_doe.username)
app.post_json(reverse('pwa-subscribe-push'), params=None, status=200)
assert PushSubscription.objects.count() == 1
app = login(app, john_doe.username, john_doe.username)
resp = app.post(reverse('pwa-subscribe-push'), params='', status=400)
assert 'bad json' in resp.text
def test_webpush_notification(app, john_doe, caplog):
caplog.set_level('INFO')
PushSubscription.objects.all().delete()
app = login(app, john_doe.username, john_doe.username)
app.post_json(
reverse('pwa-subscribe-push'),
params={'endpoint': 'https://push.example.com:1000/', 'sample': 'content'},
status=200,
)
pwa_settings = PwaSettings.singleton()
pwa_settings.push_notifications = False
pwa_settings.save()
with mock.patch('pywebpush.WebPusher') as webpusher:
webpusher.return_value.send.return_value.status_code = 200
Notification.notify(john_doe, 'test', body='hello world')
assert webpusher.call_count == 0
pwa_settings.push_notifications = True
pwa_settings.save()
with mock.patch('pywebpush.WebPusher') as webpusher:
webpusher.return_value.send.return_value.status_code = 200
Notification.notify(john_doe, 'test', body='hello world')
assert webpusher.mock_calls == [
mock.call({'sample': 'content', 'endpoint': 'https://push.example.com:1000/'}),
mock.call().send(
data='{"summary": "test", "body": "hello world", "url": ""}',
headers={'Authorization': mock.ANY},
ttl=2592000,
),
mock.call().send().raise_for_status(),
]
assert caplog.messages == ['webpush: notification sent']
# check key is reused
with mock.patch('pywebpush.WebPusher') as webpusher, mock.patch('combo.apps.pwa.signals.Vapid') as vapid:
webpusher.return_value.send.return_value.status_code = 200
Notification.notify(john_doe, 'test', body='hello world')
assert vapid.mock_calls == []
assert webpusher.mock_calls == [
mock.call({'sample': 'content', 'endpoint': 'https://push.example.com:1000/'}),
mock.call().send(
data='{"summary": "test", "body": "hello world", "url": ""}',
headers={'Authorization': mock.ANY},
ttl=2592000,
),
mock.call().send().raise_for_status(),
]
# check subscription is deleted on status 410...
caplog.clear()
with mock.patch('pywebpush.WebPusher') as webpusher:
webpusher.return_value.send.return_value.status_code = 410
Notification.notify(john_doe, 'test', body='hello world')
assert PushSubscription.objects.count() == 0
assert caplog.messages == ['webpush: deleting dead subscription']
# on any other error
caplog.clear()
app.post_json(
reverse('pwa-subscribe-push'),
params={'endpoint': 'https://push.example.com:1000/', 'sample': 'content'},
status=200,
)
with mock.patch('pywebpush.WebPusher') as webpusher:
webpusher.return_value.send.side_effect = Exception('Boom!')
Notification.notify(john_doe, 'test', body='hello world')
assert caplog.messages == ['webpush: request failed']
def test_no_pwa_manager(app, admin_user):
app = login(app)
resp = app.get('/manage/', status=200)
assert not '/manage/pwa/' in resp.text
def test_pwa_manager(app, admin_user):
app = login(app)
with override_settings(TEMPLATE_VARS={'pwa_display': 'standalone'}):
resp = app.get('/manage/', status=200)
assert '/manage/pwa/' in resp.text
resp = app.get('/manage/pwa/')
resp.form['offline_text'] = 'You are offline.'
assert resp.form['offline_retry_button'].checked
resp.form['offline_retry_button'].checked = False
resp = resp.form.submit().follow()
assert resp.form['offline_text'].value == 'You are offline.'
assert resp.form['offline_retry_button'].checked is False
resp = app.get('/manage/pwa/')
resp = resp.click('Add a navigation entry')
resp.form['label'] = 'Hello'
resp.form['url'] = 'https://www.example.net'
resp = resp.form.submit().follow()
assert PwaNavigationEntry.objects.all().count() == 1
page = Page(title='test', slug='test')
page.save()
resp = resp.click('Add a navigation entry')
resp.form['link_page'] = page.id
resp = resp.form.submit().follow()
assert PwaNavigationEntry.objects.all().count() == 2
for i in range(3):
resp = resp.click('Add a navigation entry')
resp.form['label'] = 'Hello %s' % i
resp.form['url'] = 'https://www.example.net'
resp = resp.form.submit().follow()
# max 5 items
assert 'Add a navigation entry' not in resp.text
# reorder items, reverse them all
entries = PwaNavigationEntry.objects.all()
app.get(
'/manage/pwa/navigation/order/?new-order=%s' % ','.join(reversed([str(x.id) for x in entries]))
)
entries = PwaNavigationEntry.objects.all()
assert entries[0].label == 'Hello 2'
# remove first item
resp = app.get('/manage/pwa/')
resp = resp.click(href='delete', index=0)
resp = resp.form.submit().follow()
assert 'Hello 2' not in resp.text
assert 'Add a navigation entry' in resp.text
# rename item
resp = resp.click('Hello 1')
resp.form['label'] = 'Hello 12'
resp = resp.form.submit().follow()
assert PwaNavigationEntry.objects.all()[0].label == 'Hello 12'
# check error handling
resp = resp.click('Hello 12')
resp.form['label'] = ''
resp.form['url'] = ''
resp = resp.form.submit()
assert 'A label is required' in resp.text
assert 'An URL is required' in resp.text
resp.form['url'] = 'foobar'
resp.form['link_page'] = page.id
resp = resp.form.submit()
assert 'An URL cannot be specified' in resp.text
# add an icon
resp = app.get('/manage/pwa/')
resp.form['application_icon'] = Upload(
'test.png',
base64.decodebytes(
b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVQI12NoAAAAggCB3UNq9AAAAABJRU5ErkJggg=='
),
'image/png',
)
resp = resp.form.submit().follow()
assert PwaSettings.singleton().application_icon.name == 'pwa/test.png'
# make sure another application name was not saved
assert not PwaSettings.singleton().application_name
resp = app.get('/manage/pwa/')
assert resp.form['application_name'].value == 'Compte Citoyen'
resp.form['application_name'] = 'Test'
resp = resp.form.submit().follow()
assert PwaSettings.singleton().application_name == 'Test'
# make sure using the default application name switches back to
# global_title/compte citoyen.
resp.form['application_name'] = 'Compte Citoyen'
resp = resp.form.submit().follow()
assert not PwaSettings.singleton().application_name
# try an icon in an invalid format
resp.form['application_icon'] = Upload('test.txt', b'hello', 'text/plain')
resp = resp.form.submit()
assert 'The application icon must be in JPEG or PNG format' in resp
assert PwaSettings.singleton().application_icon.name == 'pwa/test.png'
def test_pwa_offline_page(app):
PwaSettings.objects.all().delete()
resp = app.get('/__pwa__/offline/')
assert 'You are currently offline.' in resp.text
assert 'Retry' in resp.text
pwa_settings = PwaSettings.singleton()
pwa_settings.offline_text = 'You are offline.'
pwa_settings.offline_retry_button = False
pwa_settings.save()
resp = app.get('/__pwa__/offline/')
assert 'You are offline.' in resp.text
assert 'Retry' not in resp.text
def test_pwa_navigation_templatetag(app):
page = Page(title='One', slug='one')
page.save()
entry1 = PwaNavigationEntry(
label='a', url='/', notification_count=True, use_user_name_as_label=True, order=0
)
entry2 = PwaNavigationEntry(link_page=page, order=1, icon=File(BytesIO(b'te\30st'), 'test.png'))
entry1.save()
entry2.save()
t = Template('{% load pwa %}{% pwa_navigation %}')
assert t.render(Context({})) == ''
with override_settings(TEMPLATE_VARS={'pwa_display': 'standalone'}):
request = RequestFactory().get('/')
nav = t.render(Context({'request': request}))
assert '<span>a</span>' in nav
assert '<span>One</span>' in nav
assert nav.count('background-image') == 1
assert nav.count('data-notification-count-url=') == 1
assert nav.count('data-pwa-user-name=""') == 1
nav = t.render(Context({'request': request, 'render_skeleton': True}))
assert 'data-pwa-user-name="{% block placeholder-user-name %}' in nav
def test_pwa_navigation_templatetag_internal_redirection(app):
page1 = Page(title='One', slug='one')
page2 = Page(title='Two', slug='two', redirect_url='http://www.example.org/test')
page3 = Page(title='Three', slug='three', redirect_url='../test')
page4 = Page(title='Four', slug='four', redirect_url='{{test_url}}plop')
page1.save()
page2.save()
page3.save()
page4.save()
entry1 = PwaNavigationEntry(link_page=page1, order=1)
entry2 = PwaNavigationEntry(link_page=page2, order=2)
entry3 = PwaNavigationEntry(link_page=page3, order=3)
entry4 = PwaNavigationEntry(link_page=page4, order=4)
entry1.save()
entry2.save()
entry3.save()
entry4.save()
t = Template('{% load pwa %}{% pwa_navigation %}')
with override_settings(TEMPLATE_VARS={'pwa_display': 'standalone'}):
request = RequestFactory().get('/')
with mock.patch('combo.apps.pwa.models.get_request', return_value=request):
nav = t.render(Context({'request': request}))
class MyHTMLParser(HTMLParser):
def handle_starttag(self, tag, attrs):
if tag == 'a':
results.append(attrs[0][1])
parser = MyHTMLParser()
results = []
parser.feed(nav)
assert results == [
'http://testserver/one/',
'http://www.example.org/test',
'http://testserver/three/',
'http://testserver/four/',
]
def test_pwa_application_icon(app, admin_user):
app = login(app)
with override_settings(TEMPLATE_VARS={'pwa_display': 'standalone'}):
PwaSettings.objects.all().delete()
resp = app.get('/manage/pwa/')
resp.form['application_icon'] = Upload(
'test.png',
base64.decodebytes(
b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAACklEQVQI12NoAAAAggCB3UNq9AAAAABJRU5ErkJggg=='
),
'image/png',
)
resp = resp.form.submit().follow()
resp = app.get('/manifest.json', status=200)
assert len(resp.json['icons']) == 5
# make sure largest icon is referenced in service worker
app.get('/service-worker.js', status=200)
assert resp.json['icons'][-1]['src'].split('/')[-1] in resp.text
def test_pwa_application_name(app, admin_user):
app = login(app)
assert app.get('/manifest.json', status=200).json['name'] == 'Compte Citoyen'
pwa_settings = PwaSettings.singleton()
pwa_settings.application_name = 'My wonderful app'
pwa_settings.save()
assert app.get('/manifest.json', status=200).json['name'] == 'My wonderful app'