comparison sat_pubsub/test/test_backend.py @ 405:c56a728412f1

file organisation + setup refactoring: - `/src` has been renamed to `/sat_pubsub`, this is the recommended naming convention - revamped `setup.py` on the basis of SàT's `setup.py` - added a `VERSION` which is the unique place where version number will now be set - use same trick as in SàT to specify dev version (`D` at the end) - use setuptools_scm to retrieve Mercurial hash when in dev version
author Goffi <goffi@goffi.org>
date Fri, 16 Aug 2019 12:00:02 +0200
parents src/test/test_backend.py@aa3a464df605
children ccb2a22ea0fc
comparison
equal deleted inserted replaced
404:105a0772eedd 405:c56a728412f1
1 #!/usr/bin/python
2 #-*- coding: utf-8 -*-
3
4 # Copyright (c) 2003-2011 Ralph Meijer
5 # Copyright (c) 2012-2019 Jérôme Poisson
6
7
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Affero General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Affero General Public License for more details.
17
18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 # --
21
22 # This program is based on Idavoll (http://idavoll.ik.nu/),
23 # originaly written by Ralph Meijer (http://ralphm.net/blog/)
24 # It is sublicensed under AGPL v3 (or any later version) as allowed by the original
25 # license.
26
27 # --
28
29 # Here is a copy of the original license:
30
31 # Copyright (c) 2003-2011 Ralph Meijer
32
33 # Permission is hereby granted, free of charge, to any person obtaining
34 # a copy of this software and associated documentation files (the
35 # "Software"), to deal in the Software without restriction, including
36 # without limitation the rights to use, copy, modify, merge, publish,
37 # distribute, sublicense, and/or sell copies of the Software, and to
38 # permit persons to whom the Software is furnished to do so, subject to
39 # the following conditions:
40
41 # The above copyright notice and this permission notice shall be
42 # included in all copies or substantial portions of the Software.
43
44 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
45 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
46 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
47 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
48 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
49 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
50 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
51
52
53 """
54 Tests for L{idavoll.backend}.
55 """
56
57 from zope.interface import implements
58 from zope.interface.verify import verifyObject
59
60 from twisted.internet import defer
61 from twisted.trial import unittest
62 from twisted.words.protocols.jabber import jid
63 from twisted.words.protocols.jabber.error import StanzaError
64
65 from wokkel import iwokkel, pubsub
66
67 from sat_pubsub import backend, error, iidavoll, const
68
69 OWNER = jid.JID('owner@example.com')
70 OWNER_FULL = jid.JID('owner@example.com/home')
71 SERVICE = jid.JID('test.example.org')
72 NS_PUBSUB = 'http://jabber.org/protocol/pubsub'
73
74 class BackendTest(unittest.TestCase):
75
76 def test_interfaceIBackend(self):
77 self.assertTrue(verifyObject(iidavoll.IBackendService,
78 backend.BackendService(None)))
79
80
81 def test_deleteNode(self):
82 class TestNode:
83 nodeIdentifier = 'to-be-deleted'
84 def getAffiliation(self, entity):
85 if entity.userhostJID() == OWNER:
86 return defer.succeed('owner')
87
88 class TestStorage:
89 def __init__(self):
90 self.deleteCalled = []
91
92 def getNode(self, nodeIdentifier):
93 return defer.succeed(TestNode())
94
95 def deleteNode(self, nodeIdentifier):
96 if nodeIdentifier in ['to-be-deleted']:
97 self.deleteCalled.append(nodeIdentifier)
98 return defer.succeed(None)
99 else:
100 return defer.fail(error.NodeNotFound())
101
102 def preDelete(data):
103 self.assertFalse(self.storage.deleteCalled)
104 preDeleteCalled.append(data)
105 return defer.succeed(None)
106
107 def cb(result):
108 self.assertEquals(1, len(preDeleteCalled))
109 data = preDeleteCalled[-1]
110 self.assertEquals('to-be-deleted', data['node'].nodeIdentifier)
111 self.assertTrue(self.storage.deleteCalled)
112
113 self.storage = TestStorage()
114 self.backend = backend.BackendService(self.storage)
115
116 preDeleteCalled = []
117
118 self.backend.registerPreDelete(preDelete)
119 d = self.backend.deleteNode('to-be-deleted', OWNER_FULL)
120 d.addCallback(cb)
121 return d
122
123
124 def test_deleteNodeRedirect(self):
125 uri = 'xmpp:%s?;node=test2' % (SERVICE.full(),)
126
127 class TestNode:
128 nodeIdentifier = 'to-be-deleted'
129 def getAffiliation(self, entity):
130 if entity.userhostJID() == OWNER:
131 return defer.succeed('owner')
132
133 class TestStorage:
134 def __init__(self):
135 self.deleteCalled = []
136
137 def getNode(self, nodeIdentifier):
138 return defer.succeed(TestNode())
139
140 def deleteNode(self, nodeIdentifier):
141 if nodeIdentifier in ['to-be-deleted']:
142 self.deleteCalled.append(nodeIdentifier)
143 return defer.succeed(None)
144 else:
145 return defer.fail(error.NodeNotFound())
146
147 def preDelete(data):
148 self.assertFalse(self.storage.deleteCalled)
149 preDeleteCalled.append(data)
150 return defer.succeed(None)
151
152 def cb(result):
153 self.assertEquals(1, len(preDeleteCalled))
154 data = preDeleteCalled[-1]
155 self.assertEquals('to-be-deleted', data['node'].nodeIdentifier)
156 self.assertEquals(uri, data['redirectURI'])
157 self.assertTrue(self.storage.deleteCalled)
158
159 self.storage = TestStorage()
160 self.backend = backend.BackendService(self.storage)
161
162 preDeleteCalled = []
163
164 self.backend.registerPreDelete(preDelete)
165 d = self.backend.deleteNode('to-be-deleted', OWNER, redirectURI=uri)
166 d.addCallback(cb)
167 return d
168
169
170 def test_createNodeNoID(self):
171 """
172 Test creation of a node without a given node identifier.
173 """
174 class TestStorage:
175 def getDefaultConfiguration(self, nodeType):
176 return {}
177
178 def createNode(self, nodeIdentifier, requestor, config):
179 self.nodeIdentifier = nodeIdentifier
180 return defer.succeed(None)
181
182 self.storage = TestStorage()
183 self.backend = backend.BackendService(self.storage)
184 self.storage.backend = self.backend
185
186 def checkID(nodeIdentifier):
187 self.assertNotIdentical(None, nodeIdentifier)
188 self.assertIdentical(self.storage.nodeIdentifier, nodeIdentifier)
189
190 d = self.backend.createNode(None, OWNER_FULL)
191 d.addCallback(checkID)
192 return d
193
194 class NodeStore:
195 """
196 I just store nodes to pose as an L{IStorage} implementation.
197 """
198 def __init__(self, nodes):
199 self.nodes = nodes
200
201 def getNode(self, nodeIdentifier):
202 try:
203 return defer.succeed(self.nodes[nodeIdentifier])
204 except KeyError:
205 return defer.fail(error.NodeNotFound())
206
207
208 def test_getNotifications(self):
209 """
210 Ensure subscribers show up in the notification list.
211 """
212 item = pubsub.Item()
213 sub = pubsub.Subscription('test', OWNER, 'subscribed')
214
215 class TestNode:
216 def getSubscriptions(self, state=None):
217 return [sub]
218
219 def cb(result):
220 self.assertEquals(1, len(result))
221 subscriber, subscriptions, items = result[-1]
222
223 self.assertEquals(OWNER, subscriber)
224 self.assertEquals({sub}, subscriptions)
225 self.assertEquals([item], items)
226
227 self.storage = self.NodeStore({'test': TestNode()})
228 self.backend = backend.BackendService(self.storage)
229 d = self.backend.getNotifications('test', [item])
230 d.addCallback(cb)
231 return d
232
233 def test_getNotificationsRoot(self):
234 """
235 Ensure subscribers to the root node show up in the notification list
236 for leaf nodes.
237
238 This assumes a flat node relationship model with exactly one collection
239 node: the root node. Each leaf node is automatically a child node
240 of the root node.
241 """
242 item = pubsub.Item()
243 subRoot = pubsub.Subscription('', OWNER, 'subscribed')
244
245 class TestNode:
246 def getSubscriptions(self, state=None):
247 return []
248
249 class TestRootNode:
250 def getSubscriptions(self, state=None):
251 return [subRoot]
252
253 def cb(result):
254 self.assertEquals(1, len(result))
255 subscriber, subscriptions, items = result[-1]
256 self.assertEquals(OWNER, subscriber)
257 self.assertEquals({subRoot}, subscriptions)
258 self.assertEquals([item], items)
259
260 self.storage = self.NodeStore({'test': TestNode(),
261 '': TestRootNode()})
262 self.backend = backend.BackendService(self.storage)
263 d = self.backend.getNotifications('test', [item])
264 d.addCallback(cb)
265 return d
266
267
268 def test_getNotificationsMultipleNodes(self):
269 """
270 Ensure that entities that subscribe to a leaf node as well as the
271 root node get exactly one notification.
272 """
273 item = pubsub.Item()
274 sub = pubsub.Subscription('test', OWNER, 'subscribed')
275 subRoot = pubsub.Subscription('', OWNER, 'subscribed')
276
277 class TestNode:
278 def getSubscriptions(self, state=None):
279 return [sub]
280
281 class TestRootNode:
282 def getSubscriptions(self, state=None):
283 return [subRoot]
284
285 def cb(result):
286 self.assertEquals(1, len(result))
287 subscriber, subscriptions, items = result[-1]
288
289 self.assertEquals(OWNER, subscriber)
290 self.assertEquals({sub, subRoot}, subscriptions)
291 self.assertEquals([item], items)
292
293 self.storage = self.NodeStore({'test': TestNode(),
294 '': TestRootNode()})
295 self.backend = backend.BackendService(self.storage)
296 d = self.backend.getNotifications('test', [item])
297 d.addCallback(cb)
298 return d
299
300
301 def test_getDefaultConfiguration(self):
302 """
303 L{backend.BackendService.getDefaultConfiguration} should return
304 a deferred that fires a dictionary with configuration values.
305 """
306
307 class TestStorage:
308 def getDefaultConfiguration(self, nodeType):
309 return {
310 "pubsub#persist_items": True,
311 "pubsub#deliver_payloads": True}
312
313 def cb(options):
314 self.assertIn("pubsub#persist_items", options)
315 self.assertEqual(True, options["pubsub#persist_items"])
316
317 self.backend = backend.BackendService(TestStorage())
318 d = self.backend.getDefaultConfiguration('leaf')
319 d.addCallback(cb)
320 return d
321
322
323 def test_getNodeConfiguration(self):
324 class testNode:
325 nodeIdentifier = 'node'
326 def getConfiguration(self):
327 return {'pubsub#deliver_payloads': True,
328 'pubsub#persist_items': False}
329
330 class testStorage:
331 def getNode(self, nodeIdentifier):
332 return defer.succeed(testNode())
333
334 def cb(options):
335 self.assertIn("pubsub#deliver_payloads", options)
336 self.assertEqual(True, options["pubsub#deliver_payloads"])
337 self.assertIn("pubsub#persist_items", options)
338 self.assertEqual(False, options["pubsub#persist_items"])
339
340 self.storage = testStorage()
341 self.backend = backend.BackendService(self.storage)
342 self.storage.backend = self.backend
343
344 d = self.backend.getNodeConfiguration('node')
345 d.addCallback(cb)
346 return d
347
348
349 def test_setNodeConfiguration(self):
350 class testNode:
351 nodeIdentifier = 'node'
352 def getAffiliation(self, entity):
353 if entity.userhostJID() == OWNER:
354 return defer.succeed('owner')
355 def setConfiguration(self, options):
356 self.options = options
357
358 class testStorage:
359 def __init__(self):
360 self.nodes = {'node': testNode()}
361 def getNode(self, nodeIdentifier):
362 return defer.succeed(self.nodes[nodeIdentifier])
363
364 def checkOptions(node):
365 options = node.options
366 self.assertIn("pubsub#deliver_payloads", options)
367 self.assertEqual(True, options["pubsub#deliver_payloads"])
368 self.assertIn("pubsub#persist_items", options)
369 self.assertEqual(False, options["pubsub#persist_items"])
370
371 def cb(result):
372 d = self.storage.getNode('node')
373 d.addCallback(checkOptions)
374 return d
375
376 self.storage = testStorage()
377 self.backend = backend.BackendService(self.storage)
378 self.storage.backend = self.backend
379
380 options = {'pubsub#deliver_payloads': True,
381 'pubsub#persist_items': False}
382
383 d = self.backend.setNodeConfiguration('node', options, OWNER_FULL)
384 d.addCallback(cb)
385 return d
386
387
388 def test_publishNoID(self):
389 """
390 Test publish request with an item without a node identifier.
391 """
392 class TestNode:
393 nodeType = 'leaf'
394 nodeIdentifier = 'node'
395 def getAffiliation(self, entity):
396 if entity.userhostJID() == OWNER:
397 return defer.succeed('owner')
398 def getConfiguration(self):
399 return {'pubsub#deliver_payloads': True,
400 'pubsub#persist_items': False,
401 const.OPT_PUBLISH_MODEL: const.VAL_PMODEL_OPEN}
402
403 class TestStorage:
404 def getNode(self, nodeIdentifier):
405 return defer.succeed(TestNode())
406
407 def checkID(notification):
408 self.assertNotIdentical(None, notification['items'][0][2]['id'])
409
410 self.storage = TestStorage()
411 self.backend = backend.BackendService(self.storage)
412 self.storage.backend = self.backend
413
414 self.backend.registerNotifier(checkID)
415
416 items = [pubsub.Item()]
417 d = self.backend.publish('node', items, OWNER_FULL)
418 return d
419
420
421 def test_notifyOnSubscription(self):
422 """
423 Test notification of last published item on subscription.
424 """
425 ITEM = "<item xmlns='%s' id='1'/>" % NS_PUBSUB
426
427 class TestNode:
428 implements(iidavoll.ILeafNode)
429 nodeIdentifier = 'node'
430 nodeType = 'leaf'
431 def getAffiliation(self, entity):
432 if entity is OWNER:
433 return defer.succeed('owner')
434 def getConfiguration(self):
435 return {'pubsub#deliver_payloads': True,
436 'pubsub#persist_items': False,
437 'pubsub#send_last_published_item': 'on_sub',
438 const.OPT_ACCESS_MODEL: const.VAL_AMODEL_OPEN}
439 def getItems(self, authorized_groups, unrestricted, maxItems):
440 return defer.succeed([(ITEM, const.VAL_AMODEL_OPEN, None)])
441 def addSubscription(self, subscriber, state, options):
442 self.subscription = pubsub.Subscription('node', subscriber,
443 state, options)
444 return defer.succeed(None)
445 def getSubscription(self, subscriber):
446 return defer.succeed(self.subscription)
447 def getNodeOwner(self):
448 return defer.succeed(OWNER)
449
450 class TestStorage:
451 def getNode(self, nodeIdentifier):
452 return defer.succeed(TestNode())
453
454 def cb(data):
455 self.assertEquals('node', data['node'].nodeIdentifier)
456 self.assertEquals([ITEM], data['items'])
457 self.assertEquals(OWNER, data['subscription'].subscriber)
458
459 self.storage = TestStorage()
460 self.backend = backend.BackendService(self.storage)
461 self.storage.backend = self.backend
462
463 class Roster(object):
464 def getRoster(self, owner):
465 return {}
466 self.backend.roster = Roster()
467
468 d1 = defer.Deferred()
469 d1.addCallback(cb)
470 self.backend.registerNotifier(d1.callback)
471 d2 = self.backend.subscribe('node', OWNER, OWNER_FULL)
472 return defer.gatherResults([d1, d2])
473
474 test_notifyOnSubscription.timeout = 2
475
476
477
478 class BaseTestBackend(object):
479 """
480 Base class for backend stubs.
481 """
482
483 def supportsPublisherAffiliation(self):
484 return True
485
486
487 def supportsOutcastAffiliation(self):
488 return True
489
490
491 def supportsPersistentItems(self):
492 return True
493
494
495 def supportsInstantNodes(self):
496 return True
497
498 def supportsItemAccess(self):
499 return True
500
501 def supportsAutoCreate(self):
502 return True
503
504 def supportsCreatorCheck(self):
505 return True
506
507 def supportsGroupBlog(self):
508 return True
509
510 def registerNotifier(self, observerfn, *args, **kwargs):
511 return
512
513
514 def registerPreDelete(self, preDeleteFn):
515 return
516
517
518
519 class PubSubResourceFromBackendTest(unittest.TestCase):
520
521 def test_interface(self):
522 resource = backend.PubSubResourceFromBackend(BaseTestBackend())
523 self.assertTrue(verifyObject(iwokkel.IPubSubResource, resource))
524
525
526 def test_preDelete(self):
527 """
528 Test pre-delete sending out notifications to subscribers.
529 """
530
531 class TestBackend(BaseTestBackend):
532 preDeleteFn = None
533
534 def registerPreDelete(self, preDeleteFn):
535 self.preDeleteFn = preDeleteFn
536
537 def getSubscribers(self, nodeIdentifier):
538 return defer.succeed([OWNER])
539
540 def notifyDelete(service, nodeIdentifier, subscribers,
541 redirectURI=None):
542 self.assertEqual(SERVICE, service)
543 self.assertEqual('test', nodeIdentifier)
544 self.assertEqual([OWNER], subscribers)
545 self.assertIdentical(None, redirectURI)
546 d1.callback(None)
547
548 d1 = defer.Deferred()
549 resource = backend.PubSubResourceFromBackend(TestBackend())
550 resource.serviceJID = SERVICE
551 resource.pubsubService = pubsub.PubSubService()
552 resource.pubsubService.notifyDelete = notifyDelete
553 self.assertTrue(verifyObject(iwokkel.IPubSubResource, resource))
554 self.assertNotIdentical(None, resource.backend.preDeleteFn)
555
556 class TestNode:
557 implements(iidavoll.ILeafNode)
558 nodeIdentifier = 'test'
559 nodeType = 'leaf'
560
561 data = {'node': TestNode()}
562 d2 = resource.backend.preDeleteFn(data)
563 return defer.DeferredList([d1, d2], fireOnOneErrback=1)
564
565
566 def test_preDeleteRedirect(self):
567 """
568 Test pre-delete sending out notifications to subscribers.
569 """
570
571 uri = 'xmpp:%s?;node=test2' % (SERVICE.full(),)
572
573 class TestBackend(BaseTestBackend):
574 preDeleteFn = None
575
576 def registerPreDelete(self, preDeleteFn):
577 self.preDeleteFn = preDeleteFn
578
579 def getSubscribers(self, nodeIdentifier):
580 return defer.succeed([OWNER])
581
582 def notifyDelete(service, nodeIdentifier, subscribers,
583 redirectURI=None):
584 self.assertEqual(SERVICE, service)
585 self.assertEqual('test', nodeIdentifier)
586 self.assertEqual([OWNER], subscribers)
587 self.assertEqual(uri, redirectURI)
588 d1.callback(None)
589
590 d1 = defer.Deferred()
591 resource = backend.PubSubResourceFromBackend(TestBackend())
592 resource.serviceJID = SERVICE
593 resource.pubsubService = pubsub.PubSubService()
594 resource.pubsubService.notifyDelete = notifyDelete
595 self.assertTrue(verifyObject(iwokkel.IPubSubResource, resource))
596 self.assertNotIdentical(None, resource.backend.preDeleteFn)
597
598 class TestNode:
599 implements(iidavoll.ILeafNode)
600 nodeIdentifier = 'test'
601 nodeType = 'leaf'
602
603 data = {'node': TestNode(),
604 'redirectURI': uri}
605 d2 = resource.backend.preDeleteFn(data)
606 return defer.DeferredList([d1, d2], fireOnOneErrback=1)
607
608
609 def test_unsubscribeNotSubscribed(self):
610 """
611 Test unsubscription request when not subscribed.
612 """
613
614 class TestBackend(BaseTestBackend):
615 def unsubscribe(self, nodeIdentifier, subscriber, requestor):
616 return defer.fail(error.NotSubscribed())
617
618 def cb(e):
619 self.assertEquals('unexpected-request', e.condition)
620
621 resource = backend.PubSubResourceFromBackend(TestBackend())
622 request = pubsub.PubSubRequest()
623 request.sender = OWNER
624 request.recipient = SERVICE
625 request.nodeIdentifier = 'test'
626 request.subscriber = OWNER
627 d = resource.unsubscribe(request)
628 self.assertFailure(d, StanzaError)
629 d.addCallback(cb)
630 return d
631
632
633 def test_getInfo(self):
634 """
635 Test retrieving node information.
636 """
637
638 class TestBackend(BaseTestBackend):
639 def getNodeType(self, nodeIdentifier):
640 return defer.succeed('leaf')
641
642 def getNodeMetaData(self, nodeIdentifier):
643 return defer.succeed({'pubsub#persist_items': True})
644
645 def cb(info):
646 self.assertIn('type', info)
647 self.assertEquals('leaf', info['type'])
648 self.assertIn('meta-data', info)
649 self.assertEquals({'pubsub#persist_items': True}, info['meta-data'])
650
651 resource = backend.PubSubResourceFromBackend(TestBackend())
652 d = resource.getInfo(OWNER, SERVICE, 'test')
653 d.addCallback(cb)
654 return d
655
656
657 def test_getConfigurationOptions(self):
658 class TestBackend(BaseTestBackend):
659 nodeOptions = {
660 "pubsub#persist_items":
661 {"type": "boolean",
662 "label": "Persist items to storage"},
663 "pubsub#deliver_payloads":
664 {"type": "boolean",
665 "label": "Deliver payloads with event notifications"}
666 }
667
668 resource = backend.PubSubResourceFromBackend(TestBackend())
669 options = resource.getConfigurationOptions()
670 self.assertIn("pubsub#persist_items", options)
671
672
673 def test_default(self):
674 class TestBackend(BaseTestBackend):
675 def getDefaultConfiguration(self, nodeType):
676 options = {"pubsub#persist_items": True,
677 "pubsub#deliver_payloads": True,
678 "pubsub#send_last_published_item": 'on_sub',
679 }
680 return defer.succeed(options)
681
682 def cb(options):
683 self.assertEquals(True, options["pubsub#persist_items"])
684
685 resource = backend.PubSubResourceFromBackend(TestBackend())
686 request = pubsub.PubSubRequest()
687 request.sender = OWNER
688 request.recipient = SERVICE
689 request.nodeType = 'leaf'
690 d = resource.default(request)
691 d.addCallback(cb)
692 return d