comparison sat_pubsub/test/test_storage.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_storage.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.memory_storage} and L{idavoll.pgsql_storage}.
55 """
56
57 from zope.interface.verify import verifyObject
58 from twisted.trial import unittest
59 from twisted.words.protocols.jabber import jid
60 from twisted.internet import defer
61 from twisted.words.xish import domish
62
63 from sat_pubsub import error, iidavoll, const
64
65 OWNER = jid.JID('owner@example.com/Work')
66 SUBSCRIBER = jid.JID('subscriber@example.com/Home')
67 SUBSCRIBER_NEW = jid.JID('new@example.com/Home')
68 SUBSCRIBER_TO_BE_DELETED = jid.JID('to_be_deleted@example.com/Home')
69 SUBSCRIBER_PENDING = jid.JID('pending@example.com/Home')
70 PUBLISHER = jid.JID('publisher@example.com')
71 ITEM = domish.Element((None, 'item'))
72 ITEM['id'] = 'current'
73 ITEM.addElement(('testns', 'test'), content=u'Test \u2083 item')
74 ITEM_NEW = domish.Element((None, 'item'))
75 ITEM_NEW['id'] = 'new'
76 ITEM_NEW.addElement(('testns', 'test'), content=u'Test \u2083 item')
77 ITEM_UPDATED = domish.Element((None, 'item'))
78 ITEM_UPDATED['id'] = 'current'
79 ITEM_UPDATED.addElement(('testns', 'test'), content=u'Test \u2084 item')
80 ITEM_TO_BE_DELETED = domish.Element((None, 'item'))
81 ITEM_TO_BE_DELETED['id'] = 'to-be-deleted'
82 ITEM_TO_BE_DELETED.addElement(('testns', 'test'), content=u'Test \u2083 item')
83
84 def decode(object):
85 if isinstance(object, str):
86 object = object.decode('utf-8')
87 return object
88
89
90
91 class StorageTests:
92
93 def _assignTestNode(self, node):
94 self.node = node
95
96
97 def setUp(self):
98 d = self.s.getNode('pre-existing')
99 d.addCallback(self._assignTestNode)
100 return d
101
102
103 def test_interfaceIStorage(self):
104 self.assertTrue(verifyObject(iidavoll.IStorage, self.s))
105
106
107 def test_interfaceINode(self):
108 self.assertTrue(verifyObject(iidavoll.INode, self.node))
109
110
111 def test_interfaceILeafNode(self):
112 self.assertTrue(verifyObject(iidavoll.ILeafNode, self.node))
113
114
115 def test_getNode(self):
116 return self.s.getNode('pre-existing')
117
118
119 def test_getNonExistingNode(self):
120 d = self.s.getNode('non-existing')
121 self.assertFailure(d, error.NodeNotFound)
122 return d
123
124
125 def test_getNodeIDs(self):
126 def cb(nodeIdentifiers):
127 self.assertIn('pre-existing', nodeIdentifiers)
128 self.assertNotIn('non-existing', nodeIdentifiers)
129
130 return self.s.getNodeIds().addCallback(cb)
131
132
133 def test_createExistingNode(self):
134 config = self.s.getDefaultConfiguration('leaf')
135 config['pubsub#node_type'] = 'leaf'
136 d = self.s.createNode('pre-existing', OWNER, config)
137 self.assertFailure(d, error.NodeExists)
138 return d
139
140
141 def test_createNode(self):
142 def cb(void):
143 d = self.s.getNode('new 1')
144 return d
145
146 config = self.s.getDefaultConfiguration('leaf')
147 config['pubsub#node_type'] = 'leaf'
148 d = self.s.createNode('new 1', OWNER, config)
149 d.addCallback(cb)
150 return d
151
152
153 def test_createNodeChangingConfig(self):
154 """
155 The configuration passed to createNode must be free to be changed.
156 """
157 def cb(result):
158 node1, node2 = result
159 self.assertTrue(node1.getConfiguration()['pubsub#persist_items'])
160
161 config = {
162 "pubsub#persist_items": True,
163 "pubsub#deliver_payloads": True,
164 "pubsub#send_last_published_item": 'on_sub',
165 "pubsub#node_type": 'leaf',
166 "pubsub#access_model": 'open',
167 const.OPT_PUBLISH_MODEL: const.VAL_PMODEL_OPEN
168 }
169
170 def unsetPersistItems(_):
171 config["pubsub#persist_items"] = False
172
173 d = defer.succeed(None)
174 d.addCallback(lambda _: self.s.createNode('new 1', OWNER, config))
175 d.addCallback(unsetPersistItems)
176 d.addCallback(lambda _: self.s.createNode('new 2', OWNER, config))
177 d.addCallback(lambda _: defer.gatherResults([
178 self.s.getNode('new 1'),
179 self.s.getNode('new 2')]))
180 d.addCallback(cb)
181 return d
182
183
184 def test_deleteNonExistingNode(self):
185 d = self.s.deleteNode('non-existing')
186 self.assertFailure(d, error.NodeNotFound)
187 return d
188
189
190 def test_deleteNode(self):
191 def cb(void):
192 d = self.s.getNode('to-be-deleted')
193 self.assertFailure(d, error.NodeNotFound)
194 return d
195
196 d = self.s.deleteNode('to-be-deleted')
197 d.addCallback(cb)
198 return d
199
200
201 def test_getAffiliations(self):
202 def cb(affiliations):
203 self.assertIn(('pre-existing', 'owner'), affiliations)
204
205 d = self.s.getAffiliations(OWNER)
206 d.addCallback(cb)
207 return d
208
209
210 def test_getSubscriptions(self):
211 def cb(subscriptions):
212 found = False
213 for subscription in subscriptions:
214 if (subscription.nodeIdentifier == 'pre-existing' and
215 subscription.subscriber == SUBSCRIBER and
216 subscription.state == 'subscribed'):
217 found = True
218 self.assertTrue(found)
219
220 d = self.s.getSubscriptions(SUBSCRIBER)
221 d.addCallback(cb)
222 return d
223
224
225 # Node tests
226
227 def test_getType(self):
228 self.assertEqual(self.node.getType(), 'leaf')
229
230
231 def test_getConfiguration(self):
232 config = self.node.getConfiguration()
233 self.assertIn('pubsub#persist_items', config.iterkeys())
234 self.assertIn('pubsub#deliver_payloads', config.iterkeys())
235 self.assertEqual(config['pubsub#persist_items'], True)
236 self.assertEqual(config['pubsub#deliver_payloads'], True)
237
238
239 def test_setConfiguration(self):
240 def getConfig(node):
241 d = node.setConfiguration({'pubsub#persist_items': False})
242 d.addCallback(lambda _: node)
243 return d
244
245 def checkObjectConfig(node):
246 config = node.getConfiguration()
247 self.assertEqual(config['pubsub#persist_items'], False)
248
249 def getNode(void):
250 return self.s.getNode('to-be-reconfigured')
251
252 def checkStorageConfig(node):
253 config = node.getConfiguration()
254 self.assertEqual(config['pubsub#persist_items'], False)
255
256 d = self.s.getNode('to-be-reconfigured')
257 d.addCallback(getConfig)
258 d.addCallback(checkObjectConfig)
259 d.addCallback(getNode)
260 d.addCallback(checkStorageConfig)
261 return d
262
263
264 def test_getMetaData(self):
265 metaData = self.node.getMetaData()
266 for key, value in self.node.getConfiguration().iteritems():
267 self.assertIn(key, metaData.iterkeys())
268 self.assertEqual(value, metaData[key])
269 self.assertIn('pubsub#node_type', metaData.iterkeys())
270 self.assertEqual(metaData['pubsub#node_type'], 'leaf')
271
272
273 def test_getAffiliation(self):
274 def cb(affiliation):
275 self.assertEqual(affiliation, 'owner')
276
277 d = self.node.getAffiliation(OWNER)
278 d.addCallback(cb)
279 return d
280
281
282 def test_getNonExistingAffiliation(self):
283 def cb(affiliation):
284 self.assertEqual(affiliation, None)
285
286 d = self.node.getAffiliation(SUBSCRIBER)
287 d.addCallback(cb)
288 return d
289
290
291 def test_addSubscription(self):
292 def cb1(void):
293 return self.node.getSubscription(SUBSCRIBER_NEW)
294
295 def cb2(subscription):
296 self.assertEqual(subscription.state, 'pending')
297
298 d = self.node.addSubscription(SUBSCRIBER_NEW, 'pending', {})
299 d.addCallback(cb1)
300 d.addCallback(cb2)
301 return d
302
303
304 def test_addExistingSubscription(self):
305 d = self.node.addSubscription(SUBSCRIBER, 'pending', {})
306 self.assertFailure(d, error.SubscriptionExists)
307 return d
308
309
310 def test_getSubscription(self):
311 def cb(subscriptions):
312 self.assertEquals(subscriptions[0].state, 'subscribed')
313 self.assertEquals(subscriptions[1].state, 'pending')
314 self.assertEquals(subscriptions[2], None)
315
316 d = defer.gatherResults([self.node.getSubscription(SUBSCRIBER),
317 self.node.getSubscription(SUBSCRIBER_PENDING),
318 self.node.getSubscription(OWNER)])
319 d.addCallback(cb)
320 return d
321
322
323 def test_removeSubscription(self):
324 return self.node.removeSubscription(SUBSCRIBER_TO_BE_DELETED)
325
326
327 def test_removeNonExistingSubscription(self):
328 d = self.node.removeSubscription(OWNER)
329 self.assertFailure(d, error.NotSubscribed)
330 return d
331
332
333 def test_getNodeSubscriptions(self):
334 def extractSubscribers(subscriptions):
335 return [subscription.subscriber for subscription in subscriptions]
336
337 def cb(subscribers):
338 self.assertIn(SUBSCRIBER, subscribers)
339 self.assertNotIn(SUBSCRIBER_PENDING, subscribers)
340 self.assertNotIn(OWNER, subscribers)
341
342 d = self.node.getSubscriptions('subscribed')
343 d.addCallback(extractSubscribers)
344 d.addCallback(cb)
345 return d
346
347
348 def test_isSubscriber(self):
349 def cb(subscribed):
350 self.assertEquals(subscribed[0][1], True)
351 self.assertEquals(subscribed[1][1], True)
352 self.assertEquals(subscribed[2][1], False)
353 self.assertEquals(subscribed[3][1], False)
354
355 d = defer.DeferredList([self.node.isSubscribed(SUBSCRIBER),
356 self.node.isSubscribed(SUBSCRIBER.userhostJID()),
357 self.node.isSubscribed(SUBSCRIBER_PENDING),
358 self.node.isSubscribed(OWNER)])
359 d.addCallback(cb)
360 return d
361
362
363 def test_storeItems(self):
364 def cb1(void):
365 return self.node.getItemsById("", False, ['new'])
366
367 def cb2(result):
368 self.assertEqual(ITEM_NEW.toXml(), result[0].toXml())
369
370 d = self.node.storeItems([(const.VAL_AMODEL_DEFAULT, {}, ITEM_NEW)], PUBLISHER)
371 d.addCallback(cb1)
372 d.addCallback(cb2)
373 return d
374
375
376 def test_storeUpdatedItems(self):
377 def cb1(void):
378 return self.node.getItemsById("", False, ['current'])
379
380 def cb2(result):
381 self.assertEqual(ITEM_UPDATED.toXml(), result[0].toXml())
382
383 d = self.node.storeItems([(const.VAL_AMODEL_DEFAULT, {}, ITEM_UPDATED)], PUBLISHER)
384 d.addCallback(cb1)
385 d.addCallback(cb2)
386 return d
387
388
389 def test_removeItems(self):
390 def cb1(result):
391 self.assertEqual(['to-be-deleted'], result)
392 return self.node.getItemsById("", False, ['to-be-deleted'])
393
394 def cb2(result):
395 self.assertEqual(0, len(result))
396
397 d = self.node.removeItems(['to-be-deleted'])
398 d.addCallback(cb1)
399 d.addCallback(cb2)
400 return d
401
402
403 def test_removeNonExistingItems(self):
404 def cb(result):
405 self.assertEqual([], result)
406
407 d = self.node.removeItems(['non-existing'])
408 d.addCallback(cb)
409 return d
410
411
412 def test_getItems(self):
413 def cb(result):
414 items = [item.toXml() for item in result]
415 self.assertIn(ITEM.toXml(), items)
416 d = self.node.getItems("", False)
417 d.addCallback(cb)
418 return d
419
420
421 def test_lastItem(self):
422 def cb(result):
423 self.assertEqual(1, len(result))
424 self.assertEqual(ITEM.toXml(), result[0].toXml())
425
426 d = self.node.getItems("", False, 1)
427 d.addCallback(cb)
428 return d
429
430
431 def test_getItemsById(self):
432 def cb(result):
433 self.assertEqual(1, len(result))
434
435 d = self.node.getItemsById("", False, ['current'])
436 d.addCallback(cb)
437 return d
438
439
440 def test_getNonExistingItemsById(self):
441 def cb(result):
442 self.assertEqual(0, len(result))
443
444 d = self.node.getItemsById("", False, ['non-existing'])
445 d.addCallback(cb)
446 return d
447
448
449 def test_purge(self):
450 def cb1(node):
451 d = node.purge()
452 d.addCallback(lambda _: node)
453 return d
454
455 def cb2(node):
456 return node.getItems("", False)
457
458 def cb3(result):
459 self.assertEqual([], result)
460
461 d = self.s.getNode('to-be-purged')
462 d.addCallback(cb1)
463 d.addCallback(cb2)
464 d.addCallback(cb3)
465 return d
466
467
468 def test_getNodeAffilatiations(self):
469 def cb1(node):
470 return node.getAffiliations()
471
472 def cb2(affiliations):
473 affiliations = dict(((a[0].full(), a[1]) for a in affiliations))
474 self.assertEquals(affiliations[OWNER.userhost()], 'owner')
475
476 d = self.s.getNode('pre-existing')
477 d.addCallback(cb1)
478 d.addCallback(cb2)
479 return d
480
481
482
483 class MemoryStorageStorageTestCase(unittest.TestCase, StorageTests):
484
485 def setUp(self):
486 from sat_pubsub.memory_storage import Storage, PublishedItem, LeafNode
487 from sat_pubsub.memory_storage import Subscription
488
489 defaultConfig = Storage.defaultConfig['leaf']
490
491 self.s = Storage()
492 self.s._nodes['pre-existing'] = \
493 LeafNode('pre-existing', OWNER, defaultConfig)
494 self.s._nodes['to-be-deleted'] = \
495 LeafNode('to-be-deleted', OWNER, None)
496 self.s._nodes['to-be-reconfigured'] = \
497 LeafNode('to-be-reconfigured', OWNER, defaultConfig)
498 self.s._nodes['to-be-purged'] = \
499 LeafNode('to-be-purged', OWNER, None)
500
501 subscriptions = self.s._nodes['pre-existing']._subscriptions
502 subscriptions[SUBSCRIBER.full()] = Subscription('pre-existing',
503 SUBSCRIBER,
504 'subscribed')
505 subscriptions[SUBSCRIBER_TO_BE_DELETED.full()] = \
506 Subscription('pre-existing', SUBSCRIBER_TO_BE_DELETED,
507 'subscribed')
508 subscriptions[SUBSCRIBER_PENDING.full()] = \
509 Subscription('pre-existing', SUBSCRIBER_PENDING,
510 'pending')
511
512 item = PublishedItem(ITEM_TO_BE_DELETED, PUBLISHER)
513 self.s._nodes['pre-existing']._items['to-be-deleted'] = item
514 self.s._nodes['pre-existing']._itemlist.append(item)
515 self.s._nodes['to-be-purged']._items['to-be-deleted'] = item
516 self.s._nodes['to-be-purged']._itemlist.append(item)
517 item = PublishedItem(ITEM, PUBLISHER)
518 self.s._nodes['pre-existing']._items['current'] = item
519 self.s._nodes['pre-existing']._itemlist.append(item)
520
521 return StorageTests.setUp(self)
522
523
524
525 class PgsqlStorageStorageTestCase(unittest.TestCase, StorageTests):
526
527 dbpool = None
528
529 def setUp(self):
530 from sat_pubsub.pgsql_storage import Storage
531 from twisted.enterprise import adbapi
532 if self.dbpool is None:
533 self.__class__.dbpool = adbapi.ConnectionPool('psycopg2',
534 database='pubsub_test',
535 cp_reconnect=True,
536 client_encoding='utf-8',
537 connection_factory=NamedTupleConnection,
538 )
539 self.s = Storage(self.dbpool)
540 self.dbpool.start()
541 d = self.dbpool.runInteraction(self.init)
542 d.addCallback(lambda _: StorageTests.setUp(self))
543 return d
544
545
546 def tearDown(self):
547 d = self.dbpool.runInteraction(self.cleandb)
548 return d.addCallback(lambda _: self.dbpool.close())
549
550
551 def init(self, cursor):
552 self.cleandb(cursor)
553 cursor.execute("""INSERT INTO nodes
554 (node, node_type, persist_items)
555 VALUES ('pre-existing', 'leaf', TRUE)""")
556 cursor.execute("""INSERT INTO nodes (node) VALUES ('to-be-deleted')""")
557 cursor.execute("""INSERT INTO nodes (node) VALUES ('to-be-reconfigured')""")
558 cursor.execute("""INSERT INTO nodes (node) VALUES ('to-be-purged')""")
559 cursor.execute("""INSERT INTO entities (jid) VALUES (%s)""",
560 (OWNER.userhost(),))
561 cursor.execute("""INSERT INTO affiliations
562 (node_id, entity_id, affiliation)
563 SELECT node_id, entity_id, 'owner'
564 FROM nodes, entities
565 WHERE node='pre-existing' AND jid=%s""",
566 (OWNER.userhost(),))
567 cursor.execute("""INSERT INTO entities (jid) VALUES (%s)""",
568 (SUBSCRIBER.userhost(),))
569 cursor.execute("""INSERT INTO subscriptions
570 (node_id, entity_id, resource, state)
571 SELECT node_id, entity_id, %s, 'subscribed'
572 FROM nodes, entities
573 WHERE node='pre-existing' AND jid=%s""",
574 (SUBSCRIBER.resource,
575 SUBSCRIBER.userhost()))
576 cursor.execute("""INSERT INTO entities (jid) VALUES (%s)""",
577 (SUBSCRIBER_TO_BE_DELETED.userhost(),))
578 cursor.execute("""INSERT INTO subscriptions
579 (node_id, entity_id, resource, state)
580 SELECT node_id, entity_id, %s, 'subscribed'
581 FROM nodes, entities
582 WHERE node='pre-existing' AND jid=%s""",
583 (SUBSCRIBER_TO_BE_DELETED.resource,
584 SUBSCRIBER_TO_BE_DELETED.userhost()))
585 cursor.execute("""INSERT INTO entities (jid) VALUES (%s)""",
586 (SUBSCRIBER_PENDING.userhost(),))
587 cursor.execute("""INSERT INTO subscriptions
588 (node_id, entity_id, resource, state)
589 SELECT node_id, entity_id, %s, 'pending'
590 FROM nodes, entities
591 WHERE node='pre-existing' AND jid=%s""",
592 (SUBSCRIBER_PENDING.resource,
593 SUBSCRIBER_PENDING.userhost()))
594 cursor.execute("""INSERT INTO entities (jid) VALUES (%s)""",
595 (PUBLISHER.userhost(),))
596 cursor.execute("""INSERT INTO items
597 (node_id, publisher, item, data, created)
598 SELECT node_id, %s, 'to-be-deleted', %s,
599 now() - interval '1 day'
600 FROM nodes
601 WHERE node='pre-existing'""",
602 (PUBLISHER.userhost(),
603 ITEM_TO_BE_DELETED.toXml()))
604 cursor.execute("""INSERT INTO items (node_id, publisher, item, data)
605 SELECT node_id, %s, 'to-be-deleted', %s
606 FROM nodes
607 WHERE node='to-be-purged'""",
608 (PUBLISHER.userhost(),
609 ITEM_TO_BE_DELETED.toXml()))
610 cursor.execute("""INSERT INTO items (node_id, publisher, item, data)
611 SELECT node_id, %s, 'current', %s
612 FROM nodes
613 WHERE node='pre-existing'""",
614 (PUBLISHER.userhost(),
615 ITEM.toXml()))
616
617
618 def cleandb(self, cursor):
619 cursor.execute("""DELETE FROM nodes WHERE node in
620 ('non-existing', 'pre-existing', 'to-be-deleted',
621 'new 1', 'new 2', 'new 3', 'to-be-reconfigured',
622 'to-be-purged')""")
623 cursor.execute("""DELETE FROM entities WHERE jid=%s""",
624 (OWNER.userhost(),))
625 cursor.execute("""DELETE FROM entities WHERE jid=%s""",
626 (SUBSCRIBER.userhost(),))
627 cursor.execute("""DELETE FROM entities WHERE jid=%s""",
628 (SUBSCRIBER_NEW.userhost(),))
629 cursor.execute("""DELETE FROM entities WHERE jid=%s""",
630 (SUBSCRIBER_TO_BE_DELETED.userhost(),))
631 cursor.execute("""DELETE FROM entities WHERE jid=%s""",
632 (SUBSCRIBER_PENDING.userhost(),))
633 cursor.execute("""DELETE FROM entities WHERE jid=%s""",
634 (PUBLISHER.userhost(),))
635
636
637 try:
638 import psycopg2
639 psycopg2
640 from psycopg2.extras import NamedTupleConnection
641 except ImportError:
642 PgsqlStorageStorageTestCase.skip = "psycopg2 not available"