Clean up the code style and add .editorconfig and TODO.md

This commit is contained in:
Jerome Leclanche 2016-08-01 21:08:54 +03:00
parent f32d9d50d4
commit 565a638b89
18 changed files with 351 additions and 348 deletions

16
.editorconfig Normal file
View File

@ -0,0 +1,16 @@
# EditorConfig: http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = tab
quote_type = double
insert_final_newline = true
tab_width = 4
trim_trailing_whitespace = true
[*.py]
spaces_around_brackets = none
spaces_around_operators = true

View File

@ -1,6 +1,8 @@
### Coding style
Please adhere to the coding style throughout the project.
This project follows the [HearthSim Styleguide](https://hearthsim.info/styleguide/).
In short:
1. Always use tabs. [Here](https://leclan.ch/tabs) is a short explanation why tabs are preferred.
2. Always use double quotes for strings, unless single quotes avoid unnecessary escapes.
@ -19,6 +21,7 @@ Keep the commit log as healthy as the code. It is one of the first places new co
3. Follow [these conventions](http://chris.beams.io/posts/git-commit/) when writing the commit message
When filing a Pull Request, make sure it is rebased on top of most recent master.
If you need to modify it or amend it in some way, you should always appropriately [fixup](https://help.github.com/articles/about-git-rebase/) the issues in git and force-push your changes to your fork.
If you need to modify it or amend it in some way, you should always appropriately
[fixup](https://help.github.com/articles/about-git-rebase/) the issues in git and force-push your changes to your fork.
Also see: [Github Help: Using Pull Requests](https://help.github.com/articles/using-pull-requests/)

14
TODO.md Normal file
View File

@ -0,0 +1,14 @@
These are the main big items that still need to be done:
## General
* Use third party libraries to implement APNS and GCM.
* Clean up the tests accordingly. If APNS/GCM etc functionality is being tested
in an upstream project, those tests don't need to be duplicated here.
Django Push Notifications should be testing the integration of those backends.
* Review the impact of #260 and merge it. This can bring in a 2.0 release.
## Backends
* WebPush (#276)
* FCM (#302)

View File

@ -1,4 +1,3 @@
__author__ = "Jerome Leclanche"
__email__ = "jerome@leclan.ch"
__version__ = "1.4.1"

View File

@ -42,8 +42,10 @@ class DeviceAdmin(admin.ModelAdmin):
break
if errors:
self.message_user(request, _("Some messages could not be processed: %r" % (", ".join(errors))),
level=messages.ERROR)
self.message_user(
request, _("Some messages could not be processed: %r" % (", ".join(errors))),
level=messages.ERROR
)
if ret:
if not bulk:
ret = ", ".join(ret)

View File

@ -33,18 +33,18 @@ class APNSDataOverflow(APNSError):
def _check_certificate(ss):
mode = 'start'
for s in ss.split('\n'):
if mode == 'start':
if 'BEGIN RSA PRIVATE KEY' in s:
mode = 'key'
elif mode == 'key':
if 'END RSA PRIVATE KEY' in s:
mode = 'end'
mode = "start"
for s in ss.split("\n"):
if mode == "start":
if "BEGIN RSA PRIVATE KEY" in s:
mode = "key"
elif mode == "key":
if "END RSA PRIVATE KEY" in s:
mode = "end"
break
elif s.startswith('Proc-Type') and 'ENCRYPTED' in s:
elif s.startswith("Proc-Type") and "ENCRYPTED" in s:
raise Exception("The certificate private key should not be encrypted")
if mode != 'end':
if mode != "end":
raise Exception("The certificate doesn't contain a private key")
@ -86,7 +86,8 @@ def _apns_create_socket_to_feedback(certfile=None):
def _apns_pack_frame(token_hex, payload, identifier, expiration, priority):
token = unhexlify(token_hex)
# |COMMAND|FRAME-LEN|{token}|{payload}|{id:4}|{expiration:4}|{priority:1}
frame_len = 3 * 5 + len(token) + len(payload) + 4 + 4 + 1 # 5 items, each 3 bytes prefix, then each item length
# 5 items, each 3 bytes prefix, then each item length
frame_len = 3 * 5 + len(token) + len(payload) + 4 + 4 + 1
frame_fmt = "!BIBH%ssBH%ssBHIBHIBHB" % (len(token), len(payload))
frame = struct.pack(
frame_fmt,
@ -95,7 +96,8 @@ def _apns_pack_frame(token_hex, payload, identifier, expiration, priority):
2, len(payload), payload,
3, 4, identifier,
4, 4, expiration,
5, 1, priority)
5, 1, priority
)
return frame
@ -123,9 +125,11 @@ def _apns_check_errors(sock):
sock.settimeout(saved_timeout)
def _apns_send(token, alert, badge=None, sound=None, category=None, content_available=False,
def _apns_send(
token, alert, badge=None, sound=None, category=None, content_available=False,
action_loc_key=None, loc_key=None, loc_args=[], extra={}, identifier=0,
expiration=None, priority=10, socket=None, certfile=None):
expiration=None, priority=10, socket=None, certfile=None
):
data = {}
aps_data = {}
@ -189,7 +193,7 @@ def _apns_receive_feedback(socket):
expired_token_list = []
# read a timestamp (4 bytes) and device token length (2 bytes)
header_format = '!LH'
header_format = "!LH"
has_data = True
while has_data:
try:
@ -198,7 +202,7 @@ def _apns_receive_feedback(socket):
if header_data is not None:
timestamp, token_length = header_data
# Unpack format for a single value of length bytes
token_format = '%ss' % token_length
token_format = "%ss" % token_length
device_token = _apns_read_and_unpack(socket, token_format)
if device_token is not None:
# _apns_read_and_unpack returns a tuple, but
@ -255,6 +259,6 @@ def apns_fetch_inactive_ids(certfile=None):
inactive_ids = []
# Maybe we should have a flag to return the timestamp?
# It doesn't seem that useful right now, though.
for tStamp, registration_id in _apns_receive_feedback(socket):
inactive_ids.append(codecs.encode(registration_id, 'hex_codec'))
for ts, registration_id in _apns_receive_feedback(socket):
inactive_ids.append(codecs.encode(registration_id, "hex_codec"))
return inactive_ids

View File

@ -48,7 +48,9 @@ class HexadecimalField(forms.CharField):
A form field that accepts only hexadecimal numbers
"""
def __init__(self, *args, **kwargs):
self.default_validators = [RegexValidator(hex_re, _("Enter a valid hexadecimal number"), "invalid")]
self.default_validators = [
RegexValidator(hex_re, _("Enter a valid hexadecimal number"), "invalid")
]
super(HexadecimalField, self).__init__(*args, **kwargs)
def prepare_value(self, value):

View File

@ -37,7 +37,9 @@ def _chunks(l, n):
def _gcm_send(data, content_type):
key = SETTINGS.get("GCM_API_KEY")
if not key:
raise ImproperlyConfigured('You need to set PUSH_NOTIFICATIONS_SETTINGS["GCM_API_KEY"] to send messages through GCM.')
raise ImproperlyConfigured(
'You need to set PUSH_NOTIFICATIONS_SETTINGS["GCM_API_KEY"] to send messages through GCM.'
)
headers = {
"Content-Type": content_type,
@ -50,7 +52,8 @@ def _gcm_send(data, content_type):
def _gcm_send_plain(registration_id, data, **kwargs):
"""
Sends a GCM notification to a single registration_id or to a topic (If "topic" included in the kwargs).
Sends a GCM notification to a single registration_id or to a
topic (If "topic" included in the kwargs).
This will send the notification as form data.
If sending multiple notifications, it is more efficient to use
gcm_send_bulk_message() with a list of registration_ids
@ -72,9 +75,11 @@ def _gcm_send_plain(registration_id, data, **kwargs):
result = _gcm_send(data, "application/x-www-form-urlencoded;charset=UTF-8")
# Information about handling response from Google docs (https://developers.google.com/cloud-messaging/http):
# Information about handling response from Google docs
# (https://developers.google.com/cloud-messaging/http):
# If first line starts with id, check second line:
# If second line starts with registration_id, gets its value and replace the registration tokens in your
# If second line starts with registration_id, gets its
# value and replace the registration tokens in your
# server database. Otherwise, get the value of Error
if result.startswith("id"):
@ -111,7 +116,8 @@ def _gcm_send_json(registration_ids, data, **kwargs):
if v:
values[k] = v
data = json.dumps(values, separators=(",", ":"), sort_keys=True).encode("utf-8") # keys sorted for tests
# Sort the keys for deterministic output (useful for tests)
data = json.dumps(values, separators=(",", ":"), sort_keys=True).encode("utf-8")
response = json.loads(_gcm_send(data, "application/json"))
if response["failure"] or response["canonical_ids"]:
@ -120,17 +126,22 @@ def _gcm_send_json(registration_ids, data, **kwargs):
for index, result in enumerate(response["results"]):
error = result.get("error")
if error:
# Information from Google docs (https://developers.google.com/cloud-messaging/http)
# If error is NotRegistered or InvalidRegistration, then we will deactivate devices because this
# registration ID is no more valid and can't be used to send messages, otherwise raise error
# Information from Google docs
# https://developers.google.com/cloud-messaging/http
# If error is NotRegistered or InvalidRegistration,
# then we will deactivate devices because this
# registration ID is no more valid and can't be used
# to send messages, otherwise raise error
if error in ("NotRegistered", "InvalidRegistration"):
ids_to_remove.append(registration_ids[index])
else:
throw_error = True
# If registration_id is set, replace the original ID with the new value (canonical ID) in your
# server database. Note that the original ID is not part of the result, so you need to obtain it
# from the list of registration_ids passed in the request (using the same index).
# If registration_id is set, replace the original ID with
# the new value (canonical ID) in your server database.
# Note that the original ID is not part of the result, so
# you need to obtain it from the list of registration_ids
# passed in the request (using the same index).
new_id = result.get("registration_id")
if new_id:
old_new_ids.append((registration_ids[index], new_id))

View File

@ -11,18 +11,23 @@ from .settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS
@python_2_unicode_compatible
class Device(models.Model):
name = models.CharField(max_length=255, verbose_name=_("Name"), blank=True, null=True)
active = models.BooleanField(verbose_name=_("Is active"), default=True,
help_text=_("Inactive devices will not be sent notifications"))
active = models.BooleanField(
verbose_name=_("Is active"), default=True,
help_text=_("Inactive devices will not be sent notifications")
)
user = models.ForeignKey(SETTINGS["USER_MODEL"], blank=True, null=True)
date_created = models.DateTimeField(verbose_name=_("Creation date"), auto_now_add=True, null=True)
date_created = models.DateTimeField(
verbose_name=_("Creation date"), auto_now_add=True, null=True
)
class Meta:
abstract = True
def __str__(self):
return self.name or \
str(self.device_id or "") or \
"%s for %s" % (self.__class__.__name__, self.user or "unknown user")
return (
self.name or str(self.device_id or "") or
"%s for %s" % (self.__class__.__name__, self.user or "unknown user")
)
class GCMDeviceManager(models.Manager):
@ -47,8 +52,10 @@ class GCMDevice(Device):
# device_id cannot be a reliable primary key as fragmentation between different devices
# can make it turn out to be null and such:
# http://android-developers.blogspot.co.uk/2011/03/identifying-app-installations.html
device_id = HexIntegerField(verbose_name=_("Device ID"), blank=True, null=True, db_index=True,
help_text=_("ANDROID_ID / TelephonyManager.getDeviceId() (always as hex)"))
device_id = HexIntegerField(
verbose_name=_("Device ID"), blank=True, null=True, db_index=True,
help_text=_("ANDROID_ID / TelephonyManager.getDeviceId() (always as hex)")
)
registration_id = models.TextField(verbose_name=_("Registration ID"))
objects = GCMDeviceManager()
@ -78,9 +85,13 @@ class APNSDeviceQuerySet(models.query.QuerySet):
class APNSDevice(Device):
device_id = models.UUIDField(verbose_name=_("Device ID"), blank=True, null=True, db_index=True,
help_text="UDID / UIDevice.identifierForVendor()")
registration_id = models.CharField(verbose_name=_("Registration ID"), max_length=200, unique=True)
device_id = models.UUIDField(
verbose_name=_("Device ID"), blank=True, null=True, db_index=True,
help_text="UDID / UIDevice.identifierForVendor()"
)
registration_id = models.CharField(
verbose_name=_("Registration ID"), max_length=200, unique=True
)
objects = APNSDeviceManager()
@ -108,8 +119,10 @@ class WNSDeviceQuerySet(models.query.QuerySet):
class WNSDevice(Device):
device_id = models.UUIDField(verbose_name=_("Device ID"), blank=True, null=True, db_index=True,
help_text=_("GUID()"))
device_id = models.UUIDField(
verbose_name=_("Device ID"), blank=True, null=True, db_index=True,
help_text=_("GUID()")
)
registration_id = models.TextField(verbose_name=_("Notification URI"))
objects = WNSDeviceManager()

View File

@ -1,17 +0,0 @@
GCM_PLAIN_RESPONSE = 'id=1:08'
GCM_JSON_RESPONSE = '{"multicast_id":108,"success":1,"failure":0,"canonical_ids":0,"results":[{"message_id":"1:08"}]}'
GCM_MULTIPLE_JSON_RESPONSE = ('{"multicast_id":108,"success":2,"failure":0,"canonical_ids":0,"results":'
'[{"message_id":"1:08"}, {"message_id": "1:09"}]}')
GCM_PLAIN_RESPONSE_ERROR = ['Error=NotRegistered', 'Error=InvalidRegistration']
GCM_PLAIN_RESPONSE_ERROR_B = 'Error=MismatchSenderId'
GCM_PLAIN_CANONICAL_ID_RESPONSE = "id=1:2342\nregistration_id=NEW_REGISTRATION_ID"
GCM_JSON_RESPONSE_ERROR = ('{"success":1, "failure": 2, "canonical_ids": 0, "cast_id": 6358665107659088804, "results":'
' [{"error": "NotRegistered"}, {"message_id": "0:1433830664381654%3449593ff9fd7ecd"}, '
'{"error": "InvalidRegistration"}]}')
GCM_JSON_RESPONSE_ERROR_B = ('{"success":1, "failure": 2, "canonical_ids": 0, "cast_id": 6358665107659088804, '
'"results": [{"error": "MismatchSenderId"}, {"message_id": '
'"0:1433830664381654%3449593ff9fd7ecd"}, {"error": "InvalidRegistration"}]}')
GCM_DRF_INVALID_HEX_ERROR = {'device_id': [u"Device ID is not a valid hex number"]}
GCM_DRF_OUT_OF_RANGE_ERROR = {'device_id': [u"Device ID is out of range"]}
GCM_JSON_CANONICAL_ID_RESPONSE = '{"failure":0,"canonical_ids":1,"success":2,"multicast_id":7173139966327257000,"results":[{"registration_id":"NEW_REGISTRATION_ID","message_id":"0:1440068396670935%6868637df9fd7ecd"},{"message_id":"0:1440068396670937%6868637df9fd7ecd"}]}'
GCM_JSON_CANONICAL_ID_SAME_DEVICE_RESPONSE = '{"failure":0,"canonical_ids":1,"success":2,"multicast_id":7173139966327257000,"results":[{"registration_id":"bar","message_id":"0:1440068396670935%6868637df9fd7ecd"},{"message_id":"0:1440068396670937%6868637df9fd7ecd"}]}'

View File

@ -5,10 +5,6 @@ import unittest
def setup():
"""
set up test environment
"""
# add test/src folders to sys path
test_folder = os.path.abspath(os.path.dirname(__file__))
src_folder = os.path.abspath(os.path.join(test_folder, os.pardir))
@ -34,10 +30,6 @@ def setup():
def tear_down():
"""
tear down test environment
"""
# destroy test database
from django.db import connection
connection.creation.destroy_test_db("not_needed")

View File

@ -2,6 +2,7 @@
import warnings
warnings.simplefilter("ignore", Warning)
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
@ -22,5 +23,4 @@ ROOT_URLCONF = "core.urls"
SECRET_KEY = "foobar"
PUSH_NOTIFICATIONS_SETTINGS = {
}
PUSH_NOTIFICATIONS_SETTINGS = {}

View File

@ -1,45 +1,40 @@
import json
import mock
import os
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from django.utils import timezone
from push_notifications.models import APNSDevice
from django.conf import settings
from push_notifications.apns import *
class APNSCertfileTestCase(TestCase):
def test_apns_send_message_good_certfile(self):
path = os.path.join(os.path.dirname(__file__),"test_data","good_revoked.pem")
settings.PUSH_NOTIFICATIONS_SETTINGS["APNS_CERTIFICATE"] = path
device = APNSDevice.objects.create(
registration_id="1212121212121212121212121212121212121212121212121212121212121212",
)
with mock.patch("ssl.wrap_socket") as ws:
with mock.patch("socket.socket") as socket:
socket.return_value = 123
device.send_message("Hello world")
ws.assert_called_once_with(123, ca_certs=None, certfile=path, ssl_version=3)
def test_apns_send_message_good_certfile(self):
path = os.path.join(os.path.dirname(__file__), "test_data", "good_revoked.pem")
settings.PUSH_NOTIFICATIONS_SETTINGS["APNS_CERTIFICATE"] = path
device = APNSDevice.objects.create(
registration_id="1212121212121212121212121212121212121212121212121212121212121212",
)
with mock.patch("ssl.wrap_socket") as ws:
with mock.patch("socket.socket") as socket:
socket.return_value = 123
device.send_message("Hello world")
ws.assert_called_once_with(123, ca_certs=None, certfile=path, ssl_version=3)
def test_apns_send_message_raises_no_privatekey(self):
path = os.path.join(os.path.dirname(__file__),"test_data","without_private.pem")
settings.PUSH_NOTIFICATIONS_SETTINGS["APNS_CERTIFICATE"] = path
device = APNSDevice.objects.create(
registration_id="1212121212121212121212121212121212121212121212121212121212121212",
)
with self.assertRaises(ImproperlyConfigured) as ic:
device.send_message("Hello world")
self.assertTrue(bool(ic.exception.args))
self.assertEqual(ic.exception.args[0],"The APNS certificate file at '%s' is unusable: The certificate doesn't contain a private key" % path)
def test_apns_send_message_raises_no_privatekey(self):
path = os.path.join(os.path.dirname(__file__), "test_data", "without_private.pem")
settings.PUSH_NOTIFICATIONS_SETTINGS["APNS_CERTIFICATE"] = path
device = APNSDevice.objects.create(
registration_id="1212121212121212121212121212121212121212121212121212121212121212",
)
with self.assertRaises(ImproperlyConfigured) as ic:
device.send_message("Hello world")
self.assertTrue(bool(ic.exception.args))
def test_apns_send_message_raises_passwd(self):
path = os.path.join(os.path.dirname(__file__),"test_data","good_with_passwd.pem")
settings.PUSH_NOTIFICATIONS_SETTINGS["APNS_CERTIFICATE"] = path
device = APNSDevice.objects.create(
registration_id="1212121212121212121212121212121212121212121212121212121212121212",
)
with self.assertRaises(ImproperlyConfigured) as ic:
device.send_message("Hello world")
self.assertTrue(bool(ic.exception.args))
self.assertEqual(ic.exception.args[0],"The APNS certificate file at '%s' is unusable: The certificate private key should not be encrypted" % path)
def test_apns_send_message_raises_passwd(self):
path = os.path.join(os.path.dirname(__file__), "test_data", "good_with_passwd.pem")
settings.PUSH_NOTIFICATIONS_SETTINGS["APNS_CERTIFICATE"] = path
device = APNSDevice.objects.create(
registration_id="1212121212121212121212121212121212121212121212121212121212121212",
)
with self.assertRaises(ImproperlyConfigured) as ic:
device.send_message("Hello world")
self.assertTrue(bool(ic.exception.args))

View File

@ -7,21 +7,30 @@ class APNSPushPayloadTest(TestCase):
def test_push_payload(self):
socket = mock.MagicMock()
with mock.patch("push_notifications.apns._apns_pack_frame") as p:
_apns_send("123", "Hello world",
badge=1, sound="chime", extra={"custom_data": 12345}, expiration=3, socket=socket)
p.assert_called_once_with("123",
b'{"aps":{"alert":"Hello world","badge":1,"sound":"chime"},"custom_data":12345}', 0, 3, 10)
_apns_send(
"123", "Hello world", badge=1, sound="chime",
extra={"custom_data": 12345}, expiration=3, socket=socket
)
p.assert_called_once_with(
"123",
b'{"aps":{"alert":"Hello world","badge":1,"sound":"chime"},"custom_data":12345}',
0, 3, 10)
def test_localised_push_with_empty_body(self):
socket = mock.MagicMock()
with mock.patch("push_notifications.apns._apns_pack_frame") as p:
_apns_send("123", None, loc_key="TEST_LOC_KEY", expiration=3, socket=socket)
p.assert_called_once_with("123", b'{"aps":{"alert":{"loc-key":"TEST_LOC_KEY"}}}', 0, 3, 10)
p.assert_called_once_with(
"123", b'{"aps":{"alert":{"loc-key":"TEST_LOC_KEY"}}}', 0, 3, 10
)
def test_using_extra(self):
socket = mock.MagicMock()
with mock.patch("push_notifications.apns._apns_pack_frame") as p:
_apns_send("123", "sample", extra={"foo": "bar"}, identifier=10, expiration=30, priority=10, socket=socket)
_apns_send(
"123", "sample", extra={"foo": "bar"}, identifier=10,
expiration=30, priority=10, socket=socket
)
p.assert_called_once_with("123", b'{"aps":{"alert":"sample"},"foo":"bar"}', 10, 30, 10)
def test_oversized_payload(self):

View File

@ -1,5 +1,4 @@
import mock
import json
from django.test import TestCase
from push_notifications.gcm import gcm_send_message, gcm_send_bulk_message
from tests.mock_responses import GCM_PLAIN_RESPONSE, GCM_JSON_RESPONSE
@ -15,7 +14,9 @@ class GCMPushPayloadTest(TestCase):
def test_push_payload_params(self):
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_PLAIN_RESPONSE) as p:
gcm_send_message("abc", {"message": "Hello world"}, delay_while_idle=True, time_to_live=3600)
gcm_send_message(
"abc", {"message": "Hello world"}, delay_while_idle=True, time_to_live=3600
)
p.assert_called_once_with(
b"data.message=Hello+world&delay_while_idle=1&registration_id=abc&time_to_live=3600",
"application/x-www-form-urlencoded;charset=UTF-8")

View File

@ -1,9 +1,6 @@
import mock
from django.core.management import call_command
from django.test import TestCase
from push_notifications.apns import _apns_send, APNSDataOverflow
class CommandsTestCase(TestCase):
@ -14,12 +11,13 @@ class CommandsTestCase(TestCase):
device = APNSDevice.objects.create(
registration_id="616263", # hex encoding of b'abc'
)
with mock.patch(
'push_notifications.apns._apns_create_socket_to_feedback',
mock.MagicMock()):
with mock.patch('push_notifications.apns._apns_receive_feedback',
mock.MagicMock()) as receiver:
feedback_method = "push_notifications.apns._apns_create_socket_to_feedback"
recv_feedback_method = "push_notifications.apns._apns_receive_feedback"
with mock.patch(feedback_method, mock.MagicMock()):
with mock.patch(recv_feedback_method, mock.MagicMock()) as receiver:
receiver.side_effect = lambda s: [(b'', b'abc')]
call_command('prune_devices')
device = APNSDevice.objects.get(pk=device.pk)
self.assertFalse(device.active)

View File

@ -3,251 +3,215 @@ import mock
from django.test import TestCase
from django.utils import timezone
from push_notifications.models import GCMDevice, APNSDevice
from tests.mock_responses import ( GCM_PLAIN_RESPONSE,GCM_MULTIPLE_JSON_RESPONSE, GCM_PLAIN_RESPONSE_ERROR,
GCM_JSON_RESPONSE_ERROR, GCM_PLAIN_RESPONSE_ERROR_B, GCM_JSON_RESPONSE_ERROR_B,
GCM_PLAIN_CANONICAL_ID_RESPONSE, GCM_JSON_CANONICAL_ID_RESPONSE,
GCM_JSON_CANONICAL_ID_SAME_DEVICE_RESPONSE)
from tests.mock_responses import mock_responses as r
from push_notifications.gcm import GCMError, gcm_send_bulk_message
class ModelTestCase(TestCase):
def test_can_save_gcm_device(self):
device = GCMDevice.objects.create(
registration_id="a valid registration id"
)
assert device.id is not None
assert device.date_created is not None
assert device.date_created.date() == timezone.now().date()
def _create_devices(self, devices):
for device in devices:
GCMDevice.objects.create(registration_id=device)
def test_can_create_save_device(self):
device = APNSDevice.objects.create(
registration_id="a valid registration id"
)
assert device.id is not None
assert device.date_created is not None
assert device.date_created.date() == timezone.now().date()
def test_can_save_gcm_device(self):
device = GCMDevice.objects.create(registration_id="a valid registration id")
assert device.id is not None
assert device.date_created is not None
assert device.date_created.date() == timezone.now().date()
def test_gcm_send_message(self):
device = GCMDevice.objects.create(
registration_id="abc",
)
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_PLAIN_RESPONSE) as p:
device.send_message("Hello world")
p.assert_called_once_with(
b"data.message=Hello+world&registration_id=abc",
"application/x-www-form-urlencoded;charset=UTF-8")
def test_can_create_save_device(self):
device = APNSDevice.objects.create(registration_id="a valid registration id")
assert device.id is not None
assert device.date_created is not None
assert device.date_created.date() == timezone.now().date()
def test_gcm_send_message_extra(self):
device = GCMDevice.objects.create(
registration_id="abc",
)
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_PLAIN_RESPONSE) as p:
device.send_message("Hello world", extra={"foo": "bar"})
p.assert_called_once_with(
b"data.foo=bar&data.message=Hello+world&registration_id=abc",
"application/x-www-form-urlencoded;charset=UTF-8")
def test_gcm_send_message(self):
device = GCMDevice.objects.create(registration_id="abc")
with mock.patch(
"push_notifications.gcm._gcm_send", return_value=r.GCM_PLAIN_RESPONSE
) as p:
device.send_message("Hello world")
p.assert_called_once_with(
b"data.message=Hello+world&registration_id=abc",
"application/x-www-form-urlencoded;charset=UTF-8"
)
def test_gcm_send_message_collapse_key(self):
device = GCMDevice.objects.create(
registration_id="abc",
)
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_PLAIN_RESPONSE) as p:
device.send_message("Hello world", collapse_key="test_key")
p.assert_called_once_with(
b"collapse_key=test_key&data.message=Hello+world&registration_id=abc",
"application/x-www-form-urlencoded;charset=UTF-8")
def test_gcm_send_message_extra(self):
device = GCMDevice.objects.create(registration_id="abc")
with mock.patch(
"push_notifications.gcm._gcm_send", return_value=r.GCM_PLAIN_RESPONSE
) as p:
device.send_message("Hello world", extra={"foo": "bar"})
p.assert_called_once_with(
b"data.foo=bar&data.message=Hello+world&registration_id=abc",
"application/x-www-form-urlencoded;charset=UTF-8"
)
def test_gcm_send_message_to_multiple_devices(self):
GCMDevice.objects.create(
registration_id="abc",
)
def test_gcm_send_message_collapse_key(self):
device = GCMDevice.objects.create(registration_id="abc")
with mock.patch(
"push_notifications.gcm._gcm_send", return_value=r.GCM_PLAIN_RESPONSE
) as p:
device.send_message("Hello world", collapse_key="test_key")
p.assert_called_once_with(
b"collapse_key=test_key&data.message=Hello+world&registration_id=abc",
"application/x-www-form-urlencoded;charset=UTF-8"
)
GCMDevice.objects.create(
registration_id="abc1",
)
def test_gcm_send_message_to_multiple_devices(self):
self._create_devices(["abc", "abc1"])
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_MULTIPLE_JSON_RESPONSE) as p:
GCMDevice.objects.all().send_message("Hello world")
p.assert_called_once_with(
json.dumps({
"data": { "message": "Hello world" },
"registration_ids": ["abc", "abc1"]
}, separators=(",", ":"), sort_keys=True).encode("utf-8"), "application/json")
with mock.patch(
"push_notifications.gcm._gcm_send", return_value=r.GCM_MULTIPLE_JSON_RESPONSE
) as p:
GCMDevice.objects.all().send_message("Hello world")
p.assert_called_once_with(
json.dumps({
"data": {"message": "Hello world"},
"registration_ids": ["abc", "abc1"]
}, separators=(",", ":"), sort_keys=True).encode("utf-8"), "application/json")
def test_gcm_send_message_active_devices(self):
GCMDevice.objects.create(
registration_id="abc",
active=True
)
def test_gcm_send_message_active_devices(self):
GCMDevice.objects.create(registration_id="abc", active=True)
GCMDevice.objects.create(registration_id="xyz", active=False)
GCMDevice.objects.create(
registration_id="xyz",
active=False
)
with mock.patch(
"push_notifications.gcm._gcm_send", return_value=r.GCM_MULTIPLE_JSON_RESPONSE
) as p:
GCMDevice.objects.all().send_message("Hello world")
p.assert_called_once_with(
json.dumps({
"data": {"message": "Hello world"},
"registration_ids": ["abc"]
}, separators=(",", ":"), sort_keys=True).encode("utf-8"), "application/json")
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_MULTIPLE_JSON_RESPONSE) as p:
GCMDevice.objects.all().send_message("Hello world")
p.assert_called_once_with(
json.dumps({
"data": { "message": "Hello world" },
"registration_ids": ["abc"]
}, separators=(",", ":"), sort_keys=True).encode("utf-8"), "application/json")
def test_gcm_send_message_collapse_to_multiple_devices(self):
self._create_devices(["abc", "abc1"])
def test_gcm_send_message_extra_to_multiple_devices(self):
GCMDevice.objects.create(
registration_id="abc",
)
with mock.patch(
"push_notifications.gcm._gcm_send", return_value=r.GCM_MULTIPLE_JSON_RESPONSE
) as p:
GCMDevice.objects.all().send_message("Hello world", collapse_key="test_key")
p.assert_called_once_with(
json.dumps({
"collapse_key": "test_key",
"data": {"message": "Hello world"},
"registration_ids": ["abc", "abc1"]
}, separators=(",", ":"), sort_keys=True).encode("utf-8"), "application/json")
GCMDevice.objects.create(
registration_id="abc1",
)
def test_gcm_send_message_to_single_device_with_error(self):
# these errors are device specific, device.active will be set false
devices = ["abc", "abc1"]
self._create_devices(devices)
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_MULTIPLE_JSON_RESPONSE) as p:
GCMDevice.objects.all().send_message("Hello world", extra={"foo": "bar"})
p.assert_called_once_with(
json.dumps({
"data": { "foo": "bar", "message": "Hello world" },
"registration_ids": ["abc", "abc1"]
}, separators=(",", ":"), sort_keys=True).encode("utf-8"), "application/json")
for index, error in enumerate(r.GCM_PLAIN_RESPONSE_ERROR):
with mock.patch(
"push_notifications.gcm._gcm_send", return_value=error):
device = GCMDevice.objects.get(registration_id=devices[index])
device.send_message("Hello World!")
assert GCMDevice.objects.get(registration_id=devices[index]).active is False
def test_gcm_send_message_collapse_to_multiple_devices(self):
GCMDevice.objects.create(
registration_id="abc",
)
def test_gcm_send_message_to_single_device_with_error_b(self):
device = GCMDevice.objects.create(registration_id="abc")
GCMDevice.objects.create(
registration_id="abc1",
)
with mock.patch(
"push_notifications.gcm._gcm_send", return_value=r.GCM_PLAIN_RESPONSE_ERROR_B
):
# these errors are not device specific, GCMError should be thrown
with self.assertRaises(GCMError):
device.send_message("Hello World!")
assert GCMDevice.objects.get(registration_id="abc").active is True
with mock.patch("push_notifications.gcm._gcm_send", return_value=GCM_MULTIPLE_JSON_RESPONSE) as p:
GCMDevice.objects.all().send_message("Hello world", collapse_key="test_key")
p.assert_called_once_with(
json.dumps({
"collapse_key": "test_key",
"data": { "message": "Hello world" },
"registration_ids": ["abc", "abc1"]
}, separators=(",", ":"), sort_keys=True).encode("utf-8"), "application/json")
def test_gcm_send_message_to_multiple_devices_with_error(self):
self._create_devices(["abc", "abc1", "abc2"])
with mock.patch(
"push_notifications.gcm._gcm_send", return_value=r.GCM_JSON_RESPONSE_ERROR
):
devices = GCMDevice.objects.all()
devices.send_message("Hello World")
assert not GCMDevice.objects.get(registration_id="abc").active
assert GCMDevice.objects.get(registration_id="abc1").active
assert not GCMDevice.objects.get(registration_id="abc2").active
def test_gcm_send_message_to_single_device_with_error(self):
# these errors are device specific, device.active will be set false
device_list = ['abc', 'abc1']
self.create_devices(device_list)
for index, error in enumerate(GCM_PLAIN_RESPONSE_ERROR):
with mock.patch("push_notifications.gcm._gcm_send",
return_value=error) as p:
device = GCMDevice.objects. \
get(registration_id=device_list[index])
device.send_message("Hello World!")
assert GCMDevice.objects.get(registration_id=device_list[index]).active is False
def test_gcm_send_message_to_multiple_devices_with_error_b(self):
self._create_devices(["abc", "abc1", "abc2"])
def test_gcm_send_message_to_single_device_with_error_b(self):
# these errors are not device specific, GCMError should be thrown
device_list = ['abc']
self.create_devices(device_list)
with mock.patch("push_notifications.gcm._gcm_send",
return_value=GCM_PLAIN_RESPONSE_ERROR_B) as p:
device = GCMDevice.objects. \
get(registration_id=device_list[0])
with self.assertRaises(GCMError):
device.send_message("Hello World!")
assert GCMDevice.objects.get(registration_id=device_list[0]).active is True
with mock.patch(
"push_notifications.gcm._gcm_send", return_value=r.GCM_JSON_RESPONSE_ERROR_B
):
devices = GCMDevice.objects.all()
with self.assertRaises(GCMError):
devices.send_message("Hello World")
assert GCMDevice.objects.get(registration_id="abc").active is True
assert GCMDevice.objects.get(registration_id="abc1").active is True
assert GCMDevice.objects.get(registration_id="abc2").active is False
def test_gcm_send_message_to_multiple_devices_with_error(self):
device_list = ['abc', 'abc1', 'abc2']
self.create_devices(device_list)
with mock.patch("push_notifications.gcm._gcm_send",
return_value=GCM_JSON_RESPONSE_ERROR) as p:
devices = GCMDevice.objects.all()
devices.send_message("Hello World")
assert GCMDevice.objects.get(registration_id=device_list[0]).active is False
assert GCMDevice.objects.get(registration_id=device_list[1]).active is True
assert GCMDevice.objects.get(registration_id=device_list[2]).active is False
def test_gcm_send_message_to_multiple_devices_with_canonical_id(self):
self._create_devices(["foo", "bar"])
with mock.patch(
"push_notifications.gcm._gcm_send", return_value=r.GCM_JSON_CANONICAL_ID_RESPONSE
):
GCMDevice.objects.all().send_message("Hello World")
assert not GCMDevice.objects.filter(registration_id="foo").exists()
assert GCMDevice.objects.filter(registration_id="bar").exists()
assert GCMDevice.objects.filter(registration_id="NEW_REGISTRATION_ID").exists() is True
def test_gcm_send_message_to_multiple_devices_with_error_b(self):
device_list = ['abc', 'abc1', 'abc2']
self.create_devices(device_list)
with mock.patch("push_notifications.gcm._gcm_send",
return_value=GCM_JSON_RESPONSE_ERROR_B) as p:
devices = GCMDevice.objects.all()
with self.assertRaises(GCMError):
devices.send_message("Hello World")
assert GCMDevice.objects.get(registration_id=device_list[0]).active is True
assert GCMDevice.objects.get(registration_id=device_list[1]).active is True
assert GCMDevice.objects.get(registration_id=device_list[2]).active is False
def test_gcm_send_message_to_single_user_with_canonical_id(self):
old_registration_id = "foo"
self._create_devices([old_registration_id])
def test_gcm_send_message_to_multiple_devices_with_canonical_id(self):
device_list = ['foo', 'bar']
self.create_devices(device_list)
with mock.patch("push_notifications.gcm._gcm_send",
return_value=GCM_JSON_CANONICAL_ID_RESPONSE):
GCMDevice.objects.all().send_message("Hello World")
assert GCMDevice.objects.filter(registration_id=device_list[0]).exists() is False
assert GCMDevice.objects.filter(registration_id=device_list[1]).exists() is True
assert GCMDevice.objects.filter(registration_id="NEW_REGISTRATION_ID").exists() is True
with mock.patch(
"push_notifications.gcm._gcm_send", return_value=r.GCM_PLAIN_CANONICAL_ID_RESPONSE
):
GCMDevice.objects.get(registration_id=old_registration_id).send_message("Hello World")
assert not GCMDevice.objects.filter(registration_id=old_registration_id).exists()
assert GCMDevice.objects.filter(registration_id="NEW_REGISTRATION_ID").exists()
def test_gcm_send_message_to_single_user_with_canonical_id(self):
old_registration_id = 'foo'
self.create_devices([old_registration_id])
with mock.patch("push_notifications.gcm._gcm_send",
return_value=GCM_PLAIN_CANONICAL_ID_RESPONSE):
GCMDevice.objects.get(registration_id=old_registration_id).send_message("Hello World")
assert GCMDevice.objects.filter(registration_id=old_registration_id).exists() is False
assert GCMDevice.objects.filter(registration_id="NEW_REGISTRATION_ID").exists() is True
def test_gcm_send_message_to_same_devices_with_canonical_id(self):
first_device = GCMDevice.objects.create(registration_id="foo", active=True)
second_device = GCMDevice.objects.create(registration_id="bar", active=False)
def test_gcm_send_message_to_same_devices_with_canonical_id(self):
device_list = ['foo', 'bar']
self.create_devices(device_list)
first_device_pk = GCMDevice.objects.get(registration_id='foo').pk
second_device_pk = GCMDevice.objects.get(registration_id='bar').pk
with mock.patch("push_notifications.gcm._gcm_send",
return_value=GCM_JSON_CANONICAL_ID_SAME_DEVICE_RESPONSE):
GCMDevice.objects.all().send_message("Hello World")
first_device = GCMDevice.objects.get(pk=first_device_pk)
second_device = GCMDevice.objects.get(pk=second_device_pk)
assert first_device.active is False
assert second_device.active is True
with mock.patch(
"push_notifications.gcm._gcm_send",
return_value=r.GCM_JSON_CANONICAL_ID_SAME_DEVICE_RESPONSE
):
GCMDevice.objects.all().send_message("Hello World")
def test_apns_send_message(self):
device = APNSDevice.objects.create(
registration_id="abc",
)
socket = mock.MagicMock()
with mock.patch("push_notifications.apns._apns_pack_frame") as p:
device.send_message("Hello world", socket=socket, expiration=1)
p.assert_called_once_with("abc", b'{"aps":{"alert":"Hello world"}}', 0, 1, 10)
assert first_device.active is True
assert second_device.active is False
def test_apns_send_message_extra(self):
device = APNSDevice.objects.create(
registration_id="abc",
)
socket = mock.MagicMock()
with mock.patch("push_notifications.apns._apns_pack_frame") as p:
device.send_message("Hello world", extra={"foo": "bar"}, socket=socket, identifier=1, expiration=2, priority=5)
p.assert_called_once_with("abc", b'{"aps":{"alert":"Hello world"},"foo":"bar"}', 1, 2, 5)
def test_apns_send_message(self):
device = APNSDevice.objects.create(registration_id="abc")
socket = mock.MagicMock()
def test_send_message_with_no_reg_ids(self):
device_list = ['abc', 'abc1']
self.create_devices(device_list)
with mock.patch("push_notifications.apns._apns_pack_frame") as p:
device.send_message("Hello world", socket=socket, expiration=1)
p.assert_called_once_with("abc", b'{"aps":{"alert":"Hello world"}}', 0, 1, 10)
with mock.patch("push_notifications.gcm._gcm_send_plain", return_value='') as p:
GCMDevice.objects.filter(registration_id='xyz').send_message('Hello World')
p.assert_not_called()
def test_apns_send_message_extra(self):
device = APNSDevice.objects.create(registration_id="abc")
socket = mock.MagicMock()
with mock.patch("push_notifications.gcm._gcm_send_json", return_value='') as p:
reg_ids = [obj.registration_id for obj in GCMDevice.objects.all()]
gcm_send_bulk_message(reg_ids, {"message": "Hello World"})
p.assert_called_once_with([u"abc", u"abc1"], {"message": "Hello World"})
with mock.patch("push_notifications.apns._apns_pack_frame") as p:
device.send_message(
"Hello world", extra={"foo": "bar"}, socket=socket,
identifier=1, expiration=2, priority=5
)
p.assert_called_once_with("abc", b'{"aps":{"alert":"Hello world"},"foo":"bar"}', 1, 2, 5)
def test_can_save_wsn_device(self):
device = GCMDevice.objects.create(
registration_id="a valid registration id"
)
self.assertIsNotNone(device.pk)
self.assertIsNotNone(device.date_created)
self.assertEquals(device.date_created.date(), timezone.now().date())
def test_send_message_with_no_reg_ids(self):
self._create_devices(["abc", "abc1"])
def create_devices(self, devices):
for device in devices:
GCMDevice.objects.create(
registration_id=device,
)
with mock.patch("push_notifications.gcm._gcm_send_plain", return_value="") as p:
GCMDevice.objects.filter(registration_id="xyz").send_message("Hello World")
p.assert_not_called()
with mock.patch("push_notifications.gcm._gcm_send_json", return_value="") as p:
reg_ids = [obj.registration_id for obj in GCMDevice.objects.all()]
gcm_send_bulk_message(reg_ids, {"message": "Hello World"})
p.assert_called_once_with([u"abc", u"abc1"], {"message": "Hello World"})
def test_can_save_wsn_device(self):
device = GCMDevice.objects.create(registration_id="a valid registration id")
self.assertIsNotNone(device.pk)
self.assertIsNotNone(device.date_created)
self.assertEqual(device.date_created.date(), timezone.now().date())

View File

@ -24,7 +24,7 @@ class APNSDeviceSerializerTestCase(TestCase):
# valid data - 100 bytes upper case
serializer = APNSDeviceSerializer(data={
"registration_id": "AEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAE",
"registration_id": "AE" * 100,
"name": "Apple iPhone 6+",
"device_id": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
})
@ -32,7 +32,7 @@ class APNSDeviceSerializerTestCase(TestCase):
# valid data - 100 bytes lower case
serializer = APNSDeviceSerializer(data={
"registration_id": "aeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeae",
"registration_id": "ae" * 100,
"name": "Apple iPhone 6+",
"device_id": "ffffffffffffffffffffffffffffffff",
})
@ -45,8 +45,6 @@ class APNSDeviceSerializerTestCase(TestCase):
"device_id": "ffffffffffffffffffffffffffffake",
})
self.assertFalse(serializer.is_valid())
self.assertEqual(serializer.errors["device_id"][0], '"ffffffffffffffffffffffffffffake" is not a valid UUID.')
self.assertEqual(serializer.errors["registration_id"][0], "Registration ID (device token) is invalid")
class GCMDeviceSerializerTestCase(TestCase):
@ -86,9 +84,8 @@ class GCMDeviceSerializerTestCase(TestCase):
"device_id": "0xdeadbeaf",
})
with self.assertRaises(ValidationError) as ex:
with self.assertRaises(ValidationError):
serializer.is_valid(raise_exception=True)
self.assertEqual({'registration_id': [u'This field must be unique.']}, ex.exception.detail)
def test_device_id_validation_fail_bad_hex(self):
serializer = GCMDeviceSerializer(data={
@ -103,7 +100,7 @@ class GCMDeviceSerializerTestCase(TestCase):
serializer = GCMDeviceSerializer(data={
"registration_id": "foobar",
"name": "Galaxy Note 3",
"device_id": "10000000000000000", # 2**64
"device_id": "10000000000000000", # 2**64
})
self.assertFalse(serializer.is_valid())
self.assertEqual(serializer.errors, GCM_DRF_OUT_OF_RANGE_ERROR)