# Copyright 2009 - Participatory Culture Foundation # # This file is part of djpubsubhubbub. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import urllib from datetime import datetime, timedelta from django.test import TestCase from django.urls import reverse from apps.push.models import PushSubscription, PushSubscriptionManager from apps.push.signals import pre_subscribe, updated, verified class MockResponse(object): def __init__(self, status, data=None): self.status = status self.data = data def info(self): return self def read(self): if self.data is None: return "" data, self.data = self.data, None return data class PSHBTestBase: urls = "apps.push.urls" def setUp(self): self._old_send_request = PushSubscriptionManager._send_request PushSubscriptionManager._send_request = self._send_request self.responses = [] self.requests = [] self.signals = [] for connecter in pre_subscribe, verified, updated: def callback(signal=None, **kwargs): self.signals.append((signal, kwargs)) connecter.connect(callback, dispatch_uid=connecter, weak=False) def tearDown(self): PushSubscriptionManager._send_request = self._old_send_request del self._old_send_request for signal in pre_subscribe, verified: signal.disconnect(dispatch_uid=signal) def _send_request(self, url, data): self.requests.append((url, data)) return self.responses.pop() class Test_PSHBSubscriptionManagerTest(PSHBTestBase, TestCase): def test_sync_verify(self): """ If the hub returns a 204 response, the subscription is verified and active. """ self.responses.append(MockResponse(204)) sub = PushSubscription.objects.subscribe("topic", "hub", "callback", 2000) self.assertEquals(len(self.signals), 2) self.assertEquals(self.signals[0], (pre_subscribe, {"sender": sub, "created": True})) self.assertEquals(self.signals[1], (verified, {"sender": sub})) self.assertEquals(sub.hub, "hub") self.assertEquals(sub.topic, "topic") self.assertEquals(sub.verified, True) rough_expires = datetime.now() + timedelta(seconds=2000) self.assert_(abs(sub.lease_expires - rough_expires).seconds < 5, "lease more than 5 seconds off") self.assertEquals(len(self.requests), 1) request = self.requests[0] self.assertEquals(request[0], "hub") self.assertEquals(request[1]["mode"], "subscribe") self.assertEquals(request[1]["topic"], "topic") self.assertEquals(request[1]["callback"], "callback") self.assertEquals(request[1]["verify"], ("async", "sync")) self.assertEquals(request[1]["verify_token"], sub.verify_token) self.assertEquals(request[1]["lease_seconds"], 2000) def test_async_verify(self): """ If the hub returns a 202 response, we should not assume the subscription is verified. """ self.responses.append(MockResponse(202)) sub = PushSubscription.objects.subscribe("topic", "hub", "callback", 2000) self.assertEquals(len(self.signals), 1) self.assertEquals(self.signals[0], (pre_subscribe, {"sender": sub, "created": True})) self.assertEquals(sub.hub, "hub") self.assertEquals(sub.topic, "topic") self.assertEquals(sub.verified, False) rough_expires = datetime.now() + timedelta(seconds=2000) self.assert_(abs(sub.lease_expires - rough_expires).seconds < 5, "lease more than 5 seconds off") self.assertEquals(len(self.requests), 1) request = self.requests[0] self.assertEquals(request[0], "hub") self.assertEquals(request[1]["mode"], "subscribe") self.assertEquals(request[1]["topic"], "topic") self.assertEquals(request[1]["callback"], "callback") self.assertEquals(request[1]["verify"], ("async", "sync")) self.assertEquals(request[1]["verify_token"], sub.verify_token) self.assertEquals(request[1]["lease_seconds"], 2000) def test_least_seconds_default(self): """ If the number of seconds to lease the subscription is not specified, it should default to 2592000 (30 days). """ self.responses.append(MockResponse(202)) sub = PushSubscription.objects.subscribe("topic", "hub", "callback") rough_expires = datetime.now() + timedelta(seconds=2592000) self.assert_(abs(sub.lease_expires - rough_expires).seconds < 5, "lease more than 5 seconds off") self.assertEquals(len(self.requests), 1) request = self.requests[0] self.assertEquals(request[1]["lease_seconds"], 2592000) def test_error_on_subscribe_raises_URLError(self): """ If a non-202/204 status is returned, raise a URLError. """ self.responses.append(MockResponse(500, "error data")) try: PushSubscription.objects.subscribe("topic", "hub", "callback") except urllib.error.URLError as e: self.assertEquals(e.reason, "error subscribing to topic on hub:\nerror data") else: self.fail("subscription did not raise URLError exception") class Test_PSHBCallbackViewCase(PSHBTestBase, TestCase): def test_verify(self): """ Getting the callback from the server should verify the subscription. """ sub = PushSubscription.objects.create(topic="topic", hub="hub", verified=False) verify_token = sub.generate_token("subscribe") response = self.client.get( reverse("pubsubhubbub_callback", args=(sub.pk,)), { "hub.mode": "subscribe", "hub.topic": sub.topic, "hub.challenge": "challenge", "hub.lease_seconds": 2000, "hub.verify_token": verify_token, }, ) self.assertEquals(response.status_code, 200) self.assertEquals(response.content, "challenge") sub = PushSubscription.objects.get(pk=sub.pk) self.assertEquals(sub.verified, True) self.assertEquals(len(self.signals), 1) self.assertEquals(self.signals[0], (verified, {"sender": sub})) def test_404(self): """ Various things sould return a 404: * invalid primary key in the URL * token doesn't start with 'subscribe' * subscription doesn't exist * token doesn't match the subscription """ sub = PushSubscription.objects.create(topic="topic", hub="hub", verified=False) verify_token = sub.generate_token("subscribe") response = self.client.get( reverse("pubsubhubbub_callback", args=(0,)), { "hub.mode": "subscribe", "hub.topic": sub.topic, "hub.challenge": "challenge", "hub.lease_seconds": 2000, "hub.verify_token": verify_token[1:], }, ) self.assertEquals(response.status_code, 404) self.assertEquals(len(self.signals), 0) response = self.client.get( reverse("pubsubhubbub_callback", args=(sub.pk,)), { "hub.mode": "subscribe", "hub.topic": sub.topic, "hub.challenge": "challenge", "hub.lease_seconds": 2000, "hub.verify_token": verify_token[1:], }, ) self.assertEquals(response.status_code, 404) self.assertEquals(len(self.signals), 0) response = self.client.get( reverse("pubsubhubbub_callback", args=(sub.pk,)), { "hub.mode": "subscribe", "hub.topic": sub.topic + "extra", "hub.challenge": "challenge", "hub.lease_seconds": 2000, "hub.verify_token": verify_token, }, ) self.assertEquals(response.status_code, 404) self.assertEquals(len(self.signals), 0) response = self.client.get( reverse("pubsubhubbub_callback", args=(sub.pk,)), { "hub.mode": "subscribe", "hub.topic": sub.topic, "hub.challenge": "challenge", "hub.lease_seconds": 2000, "hub.verify_token": verify_token[:-5], }, ) self.assertEquals(response.status_code, 404) self.assertEquals(len(self.signals), 0) class Test_PSHBUpdateCase(PSHBTestBase, TestCase): def test_update(self): # this data comes from # http://pubsubhubbub.googlecode.com/svn/trunk/pubsubhubbub-core-0.1.html#anchor3 update_data = """ 2008-08-11T02:15:01Z Heathcliff http://publisher.example.com/happycat25.xml 2008-08-11T02:15:01Z What a happy cat. Full content goes here. Heathcliff http://publisher.example.com/happycat25.xml 2008-08-11T02:15:01Z What a happy cat! Garfield http://publisher.example.com/happycat25.xml 2008-08-11T02:15:01Z Nermal http://publisher.example.com/happycat25.xml 2008-07-10T12:28:13Z """ sub = PushSubscription.objects.create( hub="http://myhub.example.com/endpoint", topic="http://publisher.example.com/happycats.xml" ) callback_data = [] updated.connect( lambda sender=None, update=None, **kwargs: callback_data.append((sender, update)), weak=False ) response = self.client.post( reverse("pubsubhubbub_callback", args=(sub.pk,)), update_data, "application/atom+xml" ) self.assertEquals(response.status_code, 200) self.assertEquals(len(callback_data), 1) sender, update = callback_data[0] self.assertEquals(sender, sub) self.assertEquals(len(update.entries), 4) self.assertEquals(update.entries[0].id, "http://publisher.example.com/happycat25.xml") self.assertEquals(update.entries[1].id, "http://publisher.example.com/happycat25.xml") self.assertEquals(update.entries[2].id, "http://publisher.example.com/happycat25.xml") self.assertEquals(update.entries[3].id, "http://publisher.example.com/happycat25.xml") def test_update_with_changed_hub(self): update_data = """ 2008-08-11T02:15:01Z Heathcliff http://publisher.example.com/happycat25.xml 2008-08-11T02:15:01Z What a happy cat. Full content goes here. """ sub = PushSubscription.objects.create( hub="hub", topic="http://publisher.example.com/happycats.xml", lease_expires=datetime.now() + timedelta(days=1), ) callback_data = [] updated.connect( lambda sender=None, update=None, **kwargs: callback_data.append((sender, update)), weak=False ) self.responses.append(MockResponse(204)) response = self.client.post( reverse("pubsubhubbub_callback", args=(sub.pk,)), update_data, "application/atom+xml" ) self.assertEquals(response.status_code, 200) self.assertEquals( PushSubscription.objects.filter( hub="http://myhub.example.com/endpoint", topic="http://publisher.example.com/happycats.xml", verified=True, ).count(), 1, ) self.assertEquals(len(self.requests), 1) self.assertEquals(self.requests[0][0], "http://myhub.example.com/endpoint") self.assertEquals(self.requests[0][1]["callback"], "http://test.nb.local.com/1/") self.assert_((self.requests[0][1]["lease_seconds"] - 86400) < 5) def test_update_with_changed_self(self): update_data = """ 2008-08-11T02:15:01Z Heathcliff http://publisher.example.com/happycat25.xml 2008-08-11T02:15:01Z What a happy cat. Full content goes here. """ sub = PushSubscription.objects.create( hub="http://myhub.example.com/endpoint", topic="topic", lease_expires=datetime.now() + timedelta(days=1), ) callback_data = [] updated.connect( lambda sender=None, update=None, **kwargs: callback_data.append((sender, update)), weak=False ) self.responses.append(MockResponse(204)) response = self.client.post( reverse("pubsubhubbub_callback", kwargs={"push_id": sub.pk}), update_data, "application/atom+xml" ) self.assertEquals(response.status_code, 200) self.assertEquals( PushSubscription.objects.filter( hub="http://myhub.example.com/endpoint", topic="http://publisher.example.com/happycats.xml", verified=True, ).count(), 1, ) self.assertEquals(len(self.requests), 1) self.assertEquals(self.requests[0][0], "http://myhub.example.com/endpoint") self.assertEquals(self.requests[0][1]["callback"], "http://test.nb.local.com/1/") self.assert_((self.requests[0][1]["lease_seconds"] - 86400) < 5) def test_update_with_changed_hub_and_self(self): update_data = """ 2008-08-11T02:15:01Z Heathcliff http://publisher.example.com/happycat25.xml 2008-08-11T02:15:01Z What a happy cat. Full content goes here. """ sub = PushSubscription.objects.create( hub="hub", topic="topic", lease_expires=datetime.now() + timedelta(days=1) ) callback_data = [] updated.connect( lambda sender=None, update=None, **kwargs: callback_data.append((sender, update)), weak=False ) self.responses.append(MockResponse(204)) response = self.client.post( reverse("pubsubhubbub_callback", args=(sub.pk,)), update_data, "application/atom+xml" ) self.assertEquals(response.status_code, 200) self.assertEquals( PushSubscription.objects.filter( hub="http://myhub.example.com/endpoint", topic="http://publisher.example.com/happycats.xml", verified=True, ).count(), 1, ) self.assertEquals(len(self.requests), 1) self.assertEquals(self.requests[0][0], "http://myhub.example.com/endpoint") self.assertEquals(self.requests[0][1]["callback"], "http://test.nb.local.com/1/") self.assert_((self.requests[0][1]["lease_seconds"] - 86400) < 5)