From 3efc1a387d2cf6e88652584a7d2f7f22d84ca6c5 Mon Sep 17 00:00:00 2001 From: Elias Showk Date: Wed, 4 Apr 2018 20:53:14 +0200 Subject: [PATCH] web push notifications : added new templatetag, javascript static, model, a mgmt command and tests (#22727) --- combo_plugin_gnm/__init__.py | 2 +- .../management/commands/send_webpush.py | 66 +++++ combo_plugin_gnm/migrations/0001_initial.py | 29 +++ combo_plugin_gnm/migrations/__init__.py | 0 combo_plugin_gnm/models.py | 77 ++++++ combo_plugin_gnm/push.py | 67 +++++ .../static/combo_plugin_gnm/webpush.js | 238 ++++++++++++++++++ .../templates/webpush_checkbox.html | 16 ++ .../templates/webpush_scripts.html | 4 + .../templatetags/gnm_notifications.py | 51 ++++ combo_plugin_gnm/urls.py | 4 +- combo_plugin_gnm/views.py | 1 + debian/50gnm.py | 3 + setup.py | 5 + tests/conftest.py | 10 + tests/settings.py | 2 + tests/test_webpush.py | 94 +++++++ 17 files changed, 667 insertions(+), 2 deletions(-) create mode 100644 combo_plugin_gnm/management/commands/send_webpush.py create mode 100644 combo_plugin_gnm/migrations/0001_initial.py create mode 100644 combo_plugin_gnm/migrations/__init__.py create mode 100644 combo_plugin_gnm/models.py create mode 100644 combo_plugin_gnm/push.py create mode 100644 combo_plugin_gnm/static/combo_plugin_gnm/webpush.js create mode 100644 combo_plugin_gnm/templates/webpush_checkbox.html create mode 100644 combo_plugin_gnm/templates/webpush_scripts.html create mode 100644 combo_plugin_gnm/templatetags/gnm_notifications.py create mode 100644 tests/conftest.py create mode 100644 tests/settings.py create mode 100644 tests/test_webpush.py diff --git a/combo_plugin_gnm/__init__.py b/combo_plugin_gnm/__init__.py index 4524f52..5221e16 100644 --- a/combo_plugin_gnm/__init__.py +++ b/combo_plugin_gnm/__init__.py @@ -20,7 +20,7 @@ from django.utils.translation import ugettext_lazy as _ class Plugin(object): def get_apps(self): - return [__name__] + return [__name__, 'webpush'] class AppConfig(django.apps.AppConfig): diff --git a/combo_plugin_gnm/management/commands/send_webpush.py b/combo_plugin_gnm/management/commands/send_webpush.py new file mode 100644 index 0000000..763ddd9 --- /dev/null +++ b/combo_plugin_gnm/management/commands/send_webpush.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# combo-plugin-gnm - Combo WebPush App +# Copyright (C) 2018 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from dateutil.parser import parse +from datetime import timedelta, datetime +import argparse + +from django.core.management.base import BaseCommand +from django.utils.timezone import make_aware, now +from combo.apps.notifications.models import Notification +from combo_plugin_gnm.push import send_web_push + + + + +def to_datetime_argument(string): + try: + return make_aware(parse(string)) + except ValueError as verr: + raise argparse.ArgumentTypeError(verr) + + +class Command(BaseCommand): + help = u'''Send push notification for the last notifications, for example : + > python manage.py send_webpush --since=2018-03-03T00:00:00 + will all waiting web push notifications from march 3rd 2018 until now + ''' + + def add_arguments(self, parser): + # Named (optional) arguments + parser.add_argument( + '--since', + dest='since', + default=None, + type=to_datetime_argument, + help='Send push notification created since this datetime', + ) + + def handle(self, *args, **options): + ''' Send visible and new web-push notifications + ''' + since = options.get('since') + if since is None: + since = now() - timedelta(days=1) + + verbosity = options.get('verbosity') + notif_query = Notification.objects.new().visible().filter(start_timestamp__gte=since) + for notif in notif_query: + if verbosity > 0: + print('Pushing notification %s' % str(notif).decode('utf-8')) + answer = [str(response_obj) for response_obj in send_web_push(notif, since=since)] + if verbosity > 0: + print('Web Push Responses : %s' % " | ".join(answer)) diff --git a/combo_plugin_gnm/migrations/0001_initial.py b/combo_plugin_gnm/migrations/0001_initial.py new file mode 100644 index 0000000..dfed5b4 --- /dev/null +++ b/combo_plugin_gnm/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('webpush', '0001_initial'), + ('notifications', '0004_auto_20180316_1026'), + ] + + operations = [ + migrations.CreateModel( + name='WebPushRecord', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('creation_date', models.DateTimeField(auto_now_add=True)), + ('status', models.CharField(default=b'NEW', max_length=4, blank=True, choices=[(b'NEW', b'Not-sent notification'), (b'SENT', b'Sent notification'), (b'ERR', b'Invalid notification')])), + ('notification', models.ForeignKey(to='notifications.Notification')), + ('subscription', models.ForeignKey(to='webpush.PushInformation')), + ], + ), + migrations.AlterUniqueTogether( + name='webpushrecord', + unique_together=set([('subscription', 'notification')]), + ), + ] diff --git a/combo_plugin_gnm/migrations/__init__.py b/combo_plugin_gnm/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/combo_plugin_gnm/models.py b/combo_plugin_gnm/models.py new file mode 100644 index 0000000..4959b91 --- /dev/null +++ b/combo_plugin_gnm/models.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# combo-plugin-gnm - Combo WebPush App +# Copyright (C) 2018 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.db import models + +# TODO use signals to hook Notification model create or update ? +from combo.apps.notifications.models import Notification +from webpush.models import PushInformation + + +class WebPushRecord(models.Model): + + DEFAULT_STATUS = 'NEW' + ERR_STATUS = 'ERR' + OK_STATUS = 'SENT' + STATUS = ( + (DEFAULT_STATUS, 'Not-sent notification'), + (OK_STATUS, 'Sent notification'), + (ERR_STATUS, 'Invalid notification'), + ) + + subscription = models.ForeignKey(PushInformation, null=False) + notification = models.ForeignKey(Notification, null=False) + creation_date = models.DateTimeField(blank=True, auto_now_add=True) + status = models.CharField(blank=True, max_length=4, + choices=STATUS, default=DEFAULT_STATUS) + + class Meta: + unique_together = ('subscription', 'notification') + + @property + def ttl(self): + delta = self.notification.end_timestamp - self.notification.start_timestamp + return int(delta.total_seconds()) + + @property + def payload(self): + ''' + For example, we could extend later this data to such JSON supported by Chrome for Android + { + "body": "Did you make a $1,000,000 purchase at Dr. Evil...", + "icon": "images/ccard.png", + "vibrate": [200, 100, 200, 100, 200, 100, 400], + "tag": "request", + "actions": [ + { "action": "yes", "title": "Yes", "icon": "images/yes.png" }, + { "action": "no", "title": "No", "icon": "images/no.png" } + ] + } + ''' + return { + 'head': self.notification.summary, + 'body': self.notification.body, + 'url': self.notification.url, + } + + def set_status_ok(self): + self.status = self.OK_STATUS + self.save() + + def set_status_err(self): + self.status = self.ERR_STATUS + self.save() diff --git a/combo_plugin_gnm/push.py b/combo_plugin_gnm/push.py new file mode 100644 index 0000000..32e6660 --- /dev/null +++ b/combo_plugin_gnm/push.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# combo-plugin-gnm - Combo WebPush App +# Copyright (C) 2018 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.conf import settings +from django.db import transaction +from django.utils.timezone import is_naive, make_aware + +from pywebpush import WebPushException +from retrying import retry +from webpush.utils import _send_notification + +from .models import WebPushRecord + + +def retry_if_webpush_exception(exception): + '''Return True if we should retry''' + return isinstance(exception, WebPushException) + + +@retry(wait_exponential_multiplier=1000, + wait_exponential_max=getattr(settings, 'WEBPUSH_BACKOFF_MAXMILLISECS', 10000), + retry_on_exception=retry_if_webpush_exception) +def send_web_push(notification, since=None): + '''Get all the user subscriptions (every browsers with authoorized notifications) + and send a push mesg to each one + ''' + if is_naive(since): + since = make_aware(since) + user_subscription_list = notification.user.webpush_info.select_related("subscription") + responses = [] + for sub_info in user_subscription_list: + with transaction.atomic(): + web_push_record, created = WebPushRecord.objects.select_for_update().get_or_create( + notification=notification, + subscription=sub_info) + + if web_push_record.status == WebPushRecord.DEFAULT_STATUS and \ + web_push_record.creation_date >= since: + # check the user's subscription and send the request to the endpoint + req = _send_notification(sub_info, web_push_record.payload, web_push_record.ttl) + # requests.Response object + if req.status_code <= 201: + web_push_record.status = web_push_record.OK_STATUS + web_push_record.save() + responses += [req] + else: + web_push_record.status = web_push_record.ERR_STATUS + web_push_record.save() + + else: + web_push_record.set_status_err() + + return responses diff --git a/combo_plugin_gnm/static/combo_plugin_gnm/webpush.js b/combo_plugin_gnm/static/combo_plugin_gnm/webpush.js new file mode 100644 index 0000000..1fcc56e --- /dev/null +++ b/combo_plugin_gnm/static/combo_plugin_gnm/webpush.js @@ -0,0 +1,238 @@ +// Based On https://raw.githubusercontent.com/safwanrahman/django-webpush/aed8d5213d76857bfb3a020c1c6b8af18cad0f12/webpush/static/webpush/webpush.js + +// combo-plugin-gnm - Combo GNM plugin +// Copyright (C) 2018 Entr'ouvert +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +var subBtn, + messageBox, + registration; + +var activateTextMessage = 'Activez les notifications'; +var stopTextMessage = 'Stoppez les notifications'; +var incompatibleMessage = 'Ce navigateur n'est pas compatible avec les notifications push.'; +var browserShortName = navigator.userAgent.match(/(firefox|msie|chrome|safari|trident)/ig)[0].toLowerCase(); + + +$(window).load(function () { + subBtn = $('#webpush-subscribe-checkbox'); + messageBox = $('#webpush-subscribe-message'); + + function set_btn_activate() { + subBtn.attr('disabled', false); + subBtn.attr('checked', false); + subBtn.removeClass("checked"); + messageBox.text(activateTextMessage); + } + + function set_btn_cancel() { + subBtn.attr('disabled', false); + subBtn.attr('checked', true); + subBtn.addClass("checked"); + messageBox.text(stopTextMessage); + } + + var user_subscription_browser_list = subBtn.data('userSubscriptionBrowserList').split("=#=#"); + if (user_subscription_browser_list.indexOf(browserShortName) >= 0) { + set_btn_cancel(); + } else { + set_btn_activate(); + } + + // show warning to the message box and disabled the input + if (!('serviceWorker' in navigator)) { + messageBox.text(incompatibleMessage); + subBtn.attr('checked', false); + subBtn.attr('disabled', true); + return; + } + + subBtn.click( + function (evt) { + subBtn.attr('disabled', true); + // if the Browser Supports Service Worker + // registered the worker and the click event hanlder + if ('serviceWorker' in navigator) { + var serviceWorker = document.getElementById('service-worker-js').src; + navigator.serviceWorker.register(serviceWorker) + .then( + function (reg) { + messageBox.text('Connexion au serveur en cours...'); + registration = reg; + initialiseState(reg); + } + ); + } + } + ); + + + // Once the service worker is registered set the initial state + function initialiseState(reg) { + // Check if PushManager, Notification is supported in the browser + if (!('PushManager' in window) || !(reg.showNotification)) { + messageBox.text(incompatibleMessage); + subBtn.attr('checked', false); + subBtn.attr('disabled', true); + return; + } + // Check the current Notification permission. + // If its denied, it's a permanent block until the + // user changes the permission + if (Notification.permission === 'denied') { + // Show a message and activate the button + set_btn_activate(); + return; + } + if (subBtn.filter(':checked') + .length > 0) { + return subscribe(reg); + } else { + return unsubscribe(reg); + } + } + + + function subscribe(reg) { + // Get the Subscription or register one + getSubscription(reg) + .then( + function (subscription) { + postSubscribeObj('subscribe', subscription); + } + ) + .catch( + function (error) { + messageBox.text('Impossible de communiquer avec le serveur, veuillez réessayer dans quelques minutes (Debug = '+ error +')'); + } + ); + } + + function urlB64ToUint8Array(base64String) { + + var b64padding = '='.repeat((4 - base64String.length % 4) % 4); + var base64 = (base64String + b64padding) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + + var rawData = window.atob(base64); + var outputArray = new Uint8Array(rawData.length); + + for (var i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; + } + + function getSubscription(reg) { + return reg.pushManager.getSubscription() + .then( + function (subscription) { + var metaObj, applicationServerKey, options; + // Check if Subscription is available + if (subscription) { + return subscription; + } + + metaObj = document.querySelector('meta[name="django-webpush-vapid-key"]'); + applicationServerKey = metaObj.content; + options = { + userVisibleOnly: true + }; + if (applicationServerKey) { + options.applicationServerKey = urlB64ToUint8Array(applicationServerKey) + } + // If not, register one + return registration.pushManager.subscribe(options) + } + ) + } + + function unsubscribe() { + // Get the Subscription to unregister + registration.pushManager.getSubscription() + .then( + function (subscription) { + + // Check we have a subscription to unsubscribe + if (!subscription) { + // No subscription object, so set the state + // to allow the user to subscribe to push + set_btn_activate(); + return; + } + postSubscribeObj('unsubscribe', subscription); + } + ) + } + + function postSubscribeObj(statusType, subscription) { + // Send the information to the server with fetch API. + // the type of the request, the name of the user subscribing, + // and the push subscription endpoint + key the server needs + // to send push messages + + // Each subscription is different for each of these navigator userAgent short name + var data = { + status_type: statusType, + subscription: subscription.toJSON(), + browser: browserShortName + // group: subBtn.dataset.group + }; + + fetch(subBtn.data('url'), { + method: 'post', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data), + credentials: 'include' + }) + .then( + function (response) { + // Check the information is saved successfully into server + if ((response.status == 201) && (statusType == 'subscribe')) { + // Show unsubscribe button instead + set_btn_cancel(); + } + + // Check if the information is deleted from server + if ((response.status == 202) && (statusType == 'unsubscribe')) { + // Get the Subscription + getSubscription(registration) + .then( + function (subscription) { + // Remove the subscription + subscription.unsubscribe() + .then( + function () { + set_btn_activate(); + } + ) + } + ) + .catch( + function (error) { + subBtn.attr('disabled', false); + subBtn.attr('checked', false); + subBtn.removeClass("checked"); + messageBox.text('Erreur lors de la requête, veuillez réessayer dans quelques minutes (Debug = '+ error +')'); + } + ); + } + } + ) + } +}); diff --git a/combo_plugin_gnm/templates/webpush_checkbox.html b/combo_plugin_gnm/templates/webpush_checkbox.html new file mode 100644 index 0000000..0bcc6e5 --- /dev/null +++ b/combo_plugin_gnm/templates/webpush_checkbox.html @@ -0,0 +1,16 @@ +{% load i18n static gnm gnm_notifications %} + + +
+{% if request.user.is_authenticated or group %} + {% webpush_scripts %} + +{% else %} + +{% endif %} +
diff --git a/combo_plugin_gnm/templates/webpush_scripts.html b/combo_plugin_gnm/templates/webpush_scripts.html new file mode 100644 index 0000000..ddc39ff --- /dev/null +++ b/combo_plugin_gnm/templates/webpush_scripts.html @@ -0,0 +1,4 @@ +{% load static %} + + + diff --git a/combo_plugin_gnm/templatetags/gnm_notifications.py b/combo_plugin_gnm/templatetags/gnm_notifications.py new file mode 100644 index 0000000..b7512b5 --- /dev/null +++ b/combo_plugin_gnm/templatetags/gnm_notifications.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# combo-plugin-gnm - Combo GNM plugin +# Copyright (C) 2018 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django import template +from django.conf import settings +from django.core.urlresolvers import reverse + +from webpush.models import PushInformation + + +register = template.Library() + + +@register.inclusion_tag('webpush_checkbox.html', takes_context=True) +def webpush_checkbox(context): + group = context.get('webpush', {}).get('group') + url = reverse('save_webpush_info') + request = context['request'] + user_subscription_browser_list = [info.subscription.browser for info in PushInformation.objects.filter(user=request.user)] + print user_subscription_browser_list + return { + 'group': group, + 'url': url, + 'request': request, + 'user_subscription_browser_list': user_subscription_browser_list + } + + +@register.inclusion_tag('webpush_scripts.html', takes_context=True) +def webpush_scripts(context): + return + + +@register.assignment_tag(takes_context=True) +def get_webpush_vars(context): + vapid_public_key = getattr(settings, 'WEBPUSH_SETTINGS', {}).get('VAPID_PUBLIC_KEY', '') + return {'vapid_public_key': vapid_public_key} diff --git a/combo_plugin_gnm/urls.py b/combo_plugin_gnm/urls.py index 2041cda..14b3432 100644 --- a/combo_plugin_gnm/urls.py +++ b/combo_plugin_gnm/urls.py @@ -15,10 +15,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.conf.urls import url +from django.conf.urls import url, include from . import views + urlpatterns = [ url(r'^gnm/plusone/$', views.plusone, name='gnm-plus-one'), + url(r'^gnm/webpush/backend/', include('webpush.urls')), ] diff --git a/combo_plugin_gnm/views.py b/combo_plugin_gnm/views.py index 959c9b4..a6763e2 100644 --- a/combo_plugin_gnm/views.py +++ b/combo_plugin_gnm/views.py @@ -22,6 +22,7 @@ from django.template import RequestContext from combo.utils import requests, get_templated_url + def plusone(request, *args, **kwargs): # add reference to a jsondatastore with slug "plus1" reference = request.GET.get('ref') diff --git a/debian/50gnm.py b/debian/50gnm.py index 63681eb..6193724 100644 --- a/debian/50gnm.py +++ b/debian/50gnm.py @@ -279,3 +279,6 @@ JSON_CELL_TYPES = { import memcache memcache.SERVER_MAX_VALUE_LENGTH = 10 * 1024 * 1024 + +# Exponential backoff maximum milliseconds spent retrying +WEBPUSH_BACKOFF_MAXMILLISECS = 10000 diff --git a/setup.py b/setup.py index 803110c..c9e0653 100644 --- a/setup.py +++ b/setup.py @@ -97,6 +97,11 @@ setup( install_requires=[ 'django>=1.8, <1.12', 'python-dateutil', + 'retrying', + 'django-webpush>=0.2.2.3' + ], + dependency_links=[ + 'https://github.com/elishowk/django-webpush/tarball/master#egg=django-webpush-0.2.2.3', ], zip_safe=False, entry_points={ diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..6a58cb4 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,10 @@ +import pytest + +import django_webtest + +@pytest.fixture +def app(request): + wtm = django_webtest.WebTestMixin() + wtm._patch_settings() + request.addfinalizer(wtm._unpatch_settings) + return django_webtest.DjangoTestApp() diff --git a/tests/settings.py b/tests/settings.py new file mode 100644 index 0000000..7f2c953 --- /dev/null +++ b/tests/settings.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +USE_TZ = True diff --git a/tests/test_webpush.py b/tests/test_webpush.py new file mode 100644 index 0000000..82fa888 --- /dev/null +++ b/tests/test_webpush.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# combo-plugin-gnm - Combo WebPush App +# Copyright (C) 2018 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import os +import base64 +from datetime import datetime + +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.backends import default_backend + +import pytest +from mock import patch + +from django.contrib.auth.models import User +from django.core.management import call_command +from django.utils.timezone import make_aware + +from webpush.models import SubscriptionInfo, PushInformation + +from combo.apps.notifications.models import Notification +import combo_plugin_gnm +from combo_plugin_gnm.models import WebPushRecord + + +pytestmark = pytest.mark.django_db + + +@pytest.fixture +def user1(): + user1 = User.objects.create_user('user1', email='user1@example.net', password='user1') + user1.save() + return user1 + + +def _get_pubkey_str(priv_key): + '''From pywebpush tests''' + return base64.urlsafe_b64encode( + priv_key.public_key().public_numbers().encode_point() + ).strip(b'=') + + +def _gen_subscription_info(recv_key=None, + endpoint="https://example.com/"): + '''From pywebpush tests''' + if not recv_key: + recv_key = ec.generate_private_key(ec.SECP256R1, default_backend()) + return { + "endpoint": endpoint, + "keys": { + 'auth': base64.urlsafe_b64encode(os.urandom(16)).strip(b'='), + 'p256dh': _get_pubkey_str(recv_key), + } + } + + +@patch('pywebpush.WebPusher.send') +def test_command_send_webpush(mock_send, user1): + mock_send.return_value.status_code = 201 + recv_key = ec.generate_private_key(ec.SECP256R1, default_backend()) + subscription_keys_dict = _gen_subscription_info(recv_key) + subscription_record = SubscriptionInfo.objects.create( + browser='test browser', + endpoint=subscription_keys_dict['endpoint'], + auth=subscription_keys_dict['keys']['auth'], + p256dh=subscription_keys_dict['keys']['p256dh'], + ) + push_info = PushInformation.objects.create( + user=user1, + subscription=subscription_record + ) + notification = Notification.notify(user1, u'tèst headér', body=u'test uniçode bodéï') + call_command('send_webpush', '--verbosity=1') + assert WebPushRecord.objects.filter(status=WebPushRecord.ERR_STATUS).count() == 0 + assert WebPushRecord.objects.filter(status=WebPushRecord.DEFAULT_STATUS).count() == 0 + ok_push = WebPushRecord.objects.filter(status=WebPushRecord.OK_STATUS) + assert ok_push.count() == 1 + ok_push = ok_push.first() + assert ok_push.subscription == push_info + assert ok_push.notification == notification + assert ok_push.creation_date < make_aware(datetime.now())