synapse 2.172.0__py311-none-any.whl → 2.174.0__py311-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of synapse might be problematic. Click here for more details.

Files changed (52) hide show
  1. synapse/axon.py +1 -1
  2. synapse/common.py +19 -5
  3. synapse/cortex.py +46 -10
  4. synapse/lib/agenda.py +6 -0
  5. synapse/lib/ast.py +1 -1
  6. synapse/lib/hiveauth.py +81 -4
  7. synapse/lib/layer.py +8 -8
  8. synapse/lib/lmdbslab.py +11 -1
  9. synapse/lib/modelrev.py +17 -1
  10. synapse/lib/modules.py +1 -0
  11. synapse/lib/msgpack.py +25 -3
  12. synapse/lib/nexus.py +26 -22
  13. synapse/lib/schemas.py +31 -0
  14. synapse/lib/stormsvc.py +30 -11
  15. synapse/lib/stormtypes.py +23 -9
  16. synapse/lib/trigger.py +0 -4
  17. synapse/lib/version.py +2 -2
  18. synapse/lib/view.py +2 -0
  19. synapse/models/crypto.py +22 -0
  20. synapse/models/economic.py +23 -2
  21. synapse/models/entity.py +16 -0
  22. synapse/models/files.py +4 -1
  23. synapse/models/geopol.py +3 -0
  24. synapse/models/orgs.py +7 -5
  25. synapse/models/person.py +5 -2
  26. synapse/models/planning.py +5 -0
  27. synapse/tests/test_cortex.py +13 -0
  28. synapse/tests/test_lib_agenda.py +129 -1
  29. synapse/tests/test_lib_ast.py +21 -0
  30. synapse/tests/test_lib_grammar.py +4 -0
  31. synapse/tests/test_lib_hiveauth.py +139 -1
  32. synapse/tests/test_lib_httpapi.py +1 -0
  33. synapse/tests/test_lib_layer.py +67 -13
  34. synapse/tests/test_lib_lmdbslab.py +16 -1
  35. synapse/tests/test_lib_modelrev.py +57 -0
  36. synapse/tests/test_lib_msgpack.py +58 -8
  37. synapse/tests/test_lib_nexus.py +44 -1
  38. synapse/tests/test_lib_storm.py +7 -7
  39. synapse/tests/test_lib_stormsvc.py +128 -51
  40. synapse/tests/test_lib_stormtypes.py +43 -4
  41. synapse/tests/test_lib_trigger.py +23 -4
  42. synapse/tests/test_model_crypto.py +6 -0
  43. synapse/tests/test_model_economic.py +14 -1
  44. synapse/tests/test_model_geopol.py +3 -0
  45. synapse/tests/test_model_orgs.py +8 -2
  46. synapse/tests/test_model_person.py +2 -0
  47. synapse/tools/changelog.py +236 -0
  48. {synapse-2.172.0.dist-info → synapse-2.174.0.dist-info}/METADATA +1 -1
  49. {synapse-2.172.0.dist-info → synapse-2.174.0.dist-info}/RECORD +52 -50
  50. {synapse-2.172.0.dist-info → synapse-2.174.0.dist-info}/WHEEL +1 -1
  51. {synapse-2.172.0.dist-info → synapse-2.174.0.dist-info}/LICENSE +0 -0
  52. {synapse-2.172.0.dist-info → synapse-2.174.0.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,6 @@ import asyncio
2
2
  import contextlib
3
3
  import synapse.exc as s_exc
4
4
  import synapse.common as s_common
5
- import synapse.cortex as s_cortex
6
5
 
7
6
  import synapse.tests.utils as s_test
8
7
 
@@ -796,56 +795,134 @@ class StormSvcTest(s_test.SynTest):
796
795
 
797
796
  with self.getTestDir() as dirn:
798
797
  async with self.getTestCore(dirn=dirn) as core:
799
- with self.getTestDir() as svcd:
800
- async with await ChangingService.anit(svcd) as chng:
801
- chng.dmon.share('chng', chng)
802
-
803
- root = await chng.auth.getUserByName('root')
804
- await root.setPasswd('root')
805
-
806
- info = await chng.dmon.listen('tcp://127.0.0.1:0/')
807
- host, port = info
808
-
809
- curl = f'tcp://root:root@127.0.0.1:{port}/chng'
810
-
811
- await core.nodes(f'service.add chng {curl}')
812
- await core.nodes('$lib.service.wait(chng)')
813
-
814
- self.nn(core.getStormCmd('oldcmd'))
815
- self.nn(core.getStormCmd('old.bar'))
816
- self.nn(core.getStormCmd('old.baz'))
817
- self.none(core.getStormCmd('new.baz'))
818
- self.none(core.getStormCmd('runtecho'))
819
- self.none(core.getStormCmd('newcmd'))
820
- self.isin('old', core.stormpkgs)
821
- self.isin('old.bar', core.stormmods)
822
- self.isin('old.baz', core.stormmods)
823
- pkg = await core.getStormPkg('old')
824
- self.eq(pkg.get('version'), '0.0.1')
825
-
826
- waiter = core.waiter(1, 'stormsvc:client:unready')
827
-
828
- self.true(await waiter.wait(10))
829
- async with await ChangingService.anit(svcd, {'updated': True}) as chng:
830
- chng.dmon.share('chng', chng)
831
- await chng.dmon.listen(f'tcp://127.0.0.1:{port}/')
832
-
833
- await core.nodes('$lib.service.wait(chng)')
834
-
835
- self.nn(core.getStormCmd('newcmd'))
836
- self.nn(core.getStormCmd('new.baz'))
837
- self.nn(core.getStormCmd('old.bar'))
838
- self.nn(core.getStormCmd('runtecho'))
839
- self.none(core.getStormCmd('oldcmd'))
840
- self.none(core.getStormCmd('old.baz'))
841
- self.isin('old', core.stormpkgs)
842
- self.isin('new', core.stormpkgs)
843
- self.isin('echo', core.stormmods)
844
- self.isin('old.bar', core.stormmods)
845
- self.isin('new.baz', core.stormmods)
846
- self.notin('old.baz', core.stormmods)
847
- pkg = await core.getStormPkg('old')
848
- self.eq(pkg.get('version'), '0.1.0')
798
+ async with core.beholder() as wind:
799
+ with self.getTestDir() as svcd:
800
+ async with await ChangingService.anit(svcd) as chng:
801
+ chng.dmon.share('chng', chng)
802
+
803
+ root = await chng.auth.getUserByName('root')
804
+ await root.setPasswd('root')
805
+
806
+ info = await chng.dmon.listen('tcp://127.0.0.1:0/')
807
+ host, port = info
808
+
809
+ curl = f'tcp://root:root@127.0.0.1:{port}/chng'
810
+
811
+ await core.nodes(f'service.add chng {curl}')
812
+ await core.nodes('$lib.service.wait(chng)')
813
+
814
+ self.nn(core.getStormCmd('oldcmd'))
815
+ self.nn(core.getStormCmd('old.bar'))
816
+ self.nn(core.getStormCmd('old.baz'))
817
+ self.none(core.getStormCmd('new.baz'))
818
+ self.none(core.getStormCmd('runtecho'))
819
+ self.none(core.getStormCmd('newcmd'))
820
+ self.isin('old', core.stormpkgs)
821
+ self.isin('old.bar', core.stormmods)
822
+ self.isin('old.baz', core.stormmods)
823
+ pkg = await core.getStormPkg('old')
824
+ self.eq(pkg.get('version'), '0.0.1')
825
+
826
+ waiter = core.waiter(1, 'stormsvc:client:unready')
827
+
828
+ self.true(await waiter.wait(10))
829
+ async with await ChangingService.anit(svcd, {'updated': True}) as chng:
830
+ chng.dmon.share('chng', chng)
831
+ await chng.dmon.listen(f'tcp://127.0.0.1:{port}/')
832
+
833
+ await core.nodes('$lib.service.wait(chng)')
834
+
835
+ self.nn(core.getStormCmd('newcmd'))
836
+ self.nn(core.getStormCmd('new.baz'))
837
+ self.nn(core.getStormCmd('old.bar'))
838
+ self.nn(core.getStormCmd('runtecho'))
839
+ self.none(core.getStormCmd('oldcmd'))
840
+ self.none(core.getStormCmd('old.baz'))
841
+ self.isin('old', core.stormpkgs)
842
+ self.isin('new', core.stormpkgs)
843
+ self.isin('echo', core.stormmods)
844
+ self.isin('old.bar', core.stormmods)
845
+ self.isin('new.baz', core.stormmods)
846
+ self.notin('old.baz', core.stormmods)
847
+ pkg = await core.getStormPkg('old')
848
+ self.eq(pkg.get('version'), '0.1.0')
849
+
850
+ async with await ChangingService.anit(svcd, {'updated': False}) as chng:
851
+ chng.dmon.share('chng', chng)
852
+ await chng.dmon.listen(f'tcp://127.0.0.1:{port}/')
853
+
854
+ await core.nodes('$lib.service.wait(chng)')
855
+ self.nn(core.getStormCmd('oldcmd'))
856
+ self.nn(core.getStormCmd('old.bar'))
857
+ self.nn(core.getStormCmd('old.baz'))
858
+ self.none(core.getStormCmd('new.baz'))
859
+ self.none(core.getStormCmd('runtecho'))
860
+ self.none(core.getStormCmd('newcmd'))
861
+ self.isin('old', core.stormpkgs)
862
+ self.isin('old.bar', core.stormmods)
863
+ self.isin('old.baz', core.stormmods)
864
+
865
+ self.none(await core.getStormPkg('new'))
866
+
867
+ pkg = await core.getStormPkg('old')
868
+ self.eq(pkg.get('version'), '0.0.1')
869
+
870
+ svcs = await core.callStorm('return($lib.service.list())')
871
+ self.len(1, svcs)
872
+
873
+ async with await ChangingService.anit(svcd, {'updated': True}) as chng:
874
+ chng.dmon.share('chng', chng)
875
+ await chng.dmon.listen(f'tcp://127.0.0.1:{port}/')
876
+
877
+ await core.nodes('$lib.service.wait(chng)')
878
+
879
+ async with await ChangingService.anit(svcd, {'updated': True}) as chng:
880
+ chng.dmon.share('chng', chng)
881
+ await chng.dmon.listen(f'tcp://127.0.0.1:{port}/')
882
+
883
+ await core.nodes('$lib.service.wait(chng)')
884
+
885
+ events = []
886
+ async for m in wind:
887
+ events.append(m)
888
+
889
+ self.len(16, events)
890
+
891
+ # updated = false
892
+ self.eq('svc:set', events[-9]['event'])
893
+ self.eq('chng', events[-9]['info']['name'])
894
+ self.eq((0, 0, 1), events[-9]['info']['version'])
895
+
896
+ self.eq('pkg:del', events[-8]['event'])
897
+ self.eq('old', events[-8]['info']['name'])
898
+
899
+ self.eq('pkg:add', events[-7]['event'])
900
+ self.eq('old', events[-7]['info']['name'])
901
+ self.eq('0.0.1', events[-7]['info']['version'])
902
+
903
+ self.eq('pkg:del', events[-6]['event'])
904
+ self.eq('new', events[-6]['info']['name'])
905
+
906
+ # updated = true
907
+ self.eq('svc:set', events[-5]['event'])
908
+ self.eq('chng', events[-5]['info']['name'])
909
+ self.eq((0, 0, 1), events[-5]['info']['version'])
910
+
911
+ self.eq('pkg:del', events[-4]['event'])
912
+ self.eq('old', events[-4]['info']['name'])
913
+
914
+ self.eq('pkg:add', events[-3]['event'])
915
+ self.eq('old', events[-3]['info']['name'])
916
+ self.eq('0.1.0', events[-3]['info']['version'])
917
+
918
+ self.eq('pkg:add', events[-2]['event'])
919
+ self.eq('new', events[-2]['info']['name'])
920
+
921
+ # we get the set to let us know things are back, not no adds since the pkgs are the same
922
+ # so this is the last
923
+ self.eq('svc:set', events[-1]['event'])
924
+ self.eq('chng', events[-1]['info']['name'])
925
+ self.eq((0, 0, 1), events[-1]['info']['version'])
849
926
 
850
927
  # This test verifies that storm commands loaded from a previously connected service are still available,
851
928
  # even if the service is not available now
@@ -322,6 +322,42 @@ class StormTypesTest(s_test.SynTest):
322
322
  msgs = await core.stormlist('$lib.debug = (1) hehe.haha')
323
323
  self.stormIsInPrint('hehe.haha', msgs)
324
324
 
325
+ async def test_storm_doubleadd_pkg(self):
326
+ async with self.getTestCore() as core:
327
+ async with core.beholder() as wind:
328
+ pkg = {
329
+ 'name': 'hehe',
330
+ 'version': '1.1.1',
331
+ 'modules': [
332
+ {'name': 'hehe', 'storm': 'function getDebug() { return($lib.debug) }'},
333
+ ],
334
+ 'commands': [
335
+ {'name': 'hehe.haha', 'storm': 'if $lib.debug { $lib.print(hehe.haha) }'},
336
+ ],
337
+ }
338
+
339
+ # all but the first of these should bounce
340
+ for i in range(5):
341
+ await core.addStormPkg(pkg)
342
+
343
+ pkg['version'] = '1.2.3'
344
+
345
+ # all but the first of these should bounce
346
+ for i in range(5):
347
+ await core.addStormPkg(pkg)
348
+
349
+ events = []
350
+ async for m in wind:
351
+ events.append(m)
352
+ self.len(2, events)
353
+ self.eq('pkg:add', events[0]['event'])
354
+ self.eq('hehe', events[0]['info']['name'])
355
+ self.eq('1.1.1', events[0]['info']['version'])
356
+
357
+ self.eq('pkg:add', events[1]['event'])
358
+ self.eq('hehe', events[1]['info']['name'])
359
+ self.eq('1.2.3', events[1]['info']['version'])
360
+
325
361
  async def test_storm_private(self):
326
362
  async with self.getTestCore() as core:
327
363
  await core.addStormPkg({
@@ -4642,6 +4678,11 @@ class StormTypesTest(s_test.SynTest):
4642
4678
 
4643
4679
  opts = {'vars': {'iden': iden0}}
4644
4680
 
4681
+ # for coverage...
4682
+ self.false(await core.killCronTask('newp'))
4683
+ self.false(await core._killCronTask('newp'))
4684
+ self.false(await core.callStorm(f'return($lib.cron.get({iden0}).kill())'))
4685
+
4645
4686
  cdef = await core.callStorm('return($lib.cron.get($iden).pack())', opts=opts)
4646
4687
  self.eq('mydoc', cdef.get('doc'))
4647
4688
  self.eq('myname', cdef.get('name'))
@@ -4793,12 +4834,10 @@ class StormTypesTest(s_test.SynTest):
4793
4834
  self.stormIsInErr('does not match', mesgs)
4794
4835
 
4795
4836
  # Make sure the old one didn't run and the new query ran
4837
+ nextlayroffs = await layr.getEditOffs() + 1
4796
4838
  unixtime += 60
4797
- await asyncio.sleep(0)
4839
+ await layr.waitEditOffs(nextlayroffs, timeout=5)
4798
4840
  self.eq(1, await prox.count('meta:note:type=m1'))
4799
- # UNG WTF
4800
- await asyncio.sleep(0)
4801
- await asyncio.sleep(0)
4802
4841
  self.eq(1, await prox.count('meta:note:type=m2'))
4803
4842
 
4804
4843
  # Delete the job
@@ -3,9 +3,6 @@ import json
3
3
  import synapse.exc as s_exc
4
4
  import synapse.common as s_common
5
5
 
6
- from synapse.common import aspin
7
-
8
- import synapse.cortex as s_cortex
9
6
  import synapse.telepath as s_telepath
10
7
  import synapse.tests.utils as s_t_utils
11
8
  import synapse.tools.backup as s_tools_backup
@@ -294,6 +291,18 @@ class TrigTest(s_t_utils.SynTest):
294
291
  with self.raises(s_exc.SchemaViolation):
295
292
  await view.addTrigger({'cond': 'tag:add', 'storm': '[ +#count test:str=$tag ]', 'tag': 'foo&baz'})
296
293
 
294
+ # View iden mismatch
295
+ trigiden = s_common.guid()
296
+ viewiden = s_common.guid()
297
+ tdef = {'iden': trigiden, 'cond': 'node:add', 'storm': 'test:int=4', 'form': 'test:int', 'view': viewiden}
298
+ await view.addTrigger(tdef)
299
+ trigger = await view.getTrigger(trigiden)
300
+ self.eq(trigger.get('view'), view.iden)
301
+ with self.raises(s_exc.BadArg) as exc:
302
+ await view.setTriggerInfo(trigiden, 'view', viewiden)
303
+ self.eq(exc.exception.get('mesg'), 'Invalid key name provided: view')
304
+ await view.delTrigger(trigiden)
305
+
297
306
  # Trigger list
298
307
  triglist = await view.listTriggers()
299
308
  self.len(12, triglist)
@@ -551,6 +560,10 @@ class TrigTest(s_t_utils.SynTest):
551
560
 
552
561
  derp = await core.auth.addUser('derp')
553
562
 
563
+ # This is so we can later update the trigger in a view other than the one which it was created
564
+ viewiden = await core.callStorm('$view = $lib.view.get().fork() return($view.iden)')
565
+ inview = {'view': viewiden}
566
+
554
567
  tdef = {'cond': 'node:add', 'form': 'inet:ipv4', 'storm': '[ +#foo ]'}
555
568
  opts = {'vars': {'tdef': tdef}}
556
569
 
@@ -562,7 +575,7 @@ class TrigTest(s_t_utils.SynTest):
562
575
  self.nn(nodes[0].getTag('foo'))
563
576
 
564
577
  opts = {'vars': {'iden': trig.get('iden'), 'derp': derp.iden}}
565
- await core.callStorm('$lib.trigger.get($iden).set(user, $derp)', opts=opts)
578
+ await core.callStorm('$lib.trigger.get($iden).set(user, $derp)', opts=opts | inview)
566
579
 
567
580
  nodes = await core.nodes('[ inet:ipv4=8.8.8.8 ]')
568
581
  self.len(1, nodes)
@@ -885,3 +898,9 @@ class TrigTest(s_t_utils.SynTest):
885
898
 
886
899
  await core.nodes('for $trig in $lib.trigger.list() { $lib.trigger.del($trig.iden) }')
887
900
  self.len(0, await core.nodes('syn:trigger'))
901
+
902
+ async def test_trigger_viewiden_migration(self):
903
+ async with self.getRegrCore('trigger-viewiden-migration') as core:
904
+ for view in core.views.values():
905
+ for _, trigger in view.triggers.list():
906
+ self.eq(trigger.tdef.get('view'), view.iden)
@@ -49,7 +49,9 @@ class CryptoModelTest(s_t_utils.SynTest):
49
49
  :algorithm=aes256
50
50
  :mode=CBC
51
51
  :iv=41414141
52
+ :iv:text=AAAA
52
53
  :private=00000000
54
+ :private:text=hehe
53
55
  :private:md5=$md5
54
56
  :private:sha1=$sha1
55
57
  :private:sha256=$sha256
@@ -57,6 +59,7 @@ class CryptoModelTest(s_t_utils.SynTest):
57
59
  :public:md5=$md5
58
60
  :public:sha1=$sha1
59
61
  :public:sha256=$sha256
62
+ :public:text=haha
60
63
  :seed:passwd=s3cret
61
64
  :seed:algorithm=pbkdf2 ]
62
65
  }]
@@ -72,6 +75,9 @@ class CryptoModelTest(s_t_utils.SynTest):
72
75
  +:mode=cbc
73
76
  +:iv=41414141
74
77
  '''))
78
+ self.len(1, await core.nodes('it:dev:str=AAAA -> crypto:key'))
79
+ self.len(1, await core.nodes('it:dev:str=hehe -> crypto:key'))
80
+ self.len(1, await core.nodes('it:dev:str=haha -> crypto:key'))
75
81
  self.len(1, await core.nodes('inet:passwd=s3cret -> crypto:key -> crypto:currency:address'))
76
82
 
77
83
  self.len(2, await core.nodes('crypto:key -> hash:md5'))
@@ -129,8 +129,21 @@ class EconTest(s_utils.SynTest):
129
129
 
130
130
  :time=20180202
131
131
  :purchase={perc.ndef[1]}
132
+
133
+ :place=*
134
+ :place:loc=us.ny.brooklyn
135
+ :place:name=myhouse
136
+ :place:address="123 main street, brooklyn, ny, 11223"
137
+ :place:latlong=(90,80)
132
138
  ]'''
133
- await core.nodes(text)
139
+ nodes = await core.nodes(text)
140
+
141
+ self.eq('myhouse', nodes[0].get('place:name'))
142
+ self.eq((90, 80), nodes[0].get('place:latlong'))
143
+ self.eq('us.ny.brooklyn', nodes[0].get('place:loc'))
144
+ self.eq('123 main street, brooklyn, ny, 11223', nodes[0].get('place:address'))
145
+
146
+ self.len(1, await core.nodes('econ:acct:payment -> geo:place'))
134
147
 
135
148
  self.len(1, await core.nodes('econ:acct:payment +:time@=(2017,2019) +{-> econ:pay:card +:name="bob smith"}'))
136
149
 
@@ -14,6 +14,7 @@ class GeoPolModelTest(s_t_utils.SynTest):
14
14
  :iso2=vi
15
15
  :iso3=vis
16
16
  :isonum=31337
17
+ :currencies=(usd, vcoins, PESOS, USD)
17
18
  ]
18
19
  ''')
19
20
  self.len(1, nodes)
@@ -24,7 +25,9 @@ class GeoPolModelTest(s_t_utils.SynTest):
24
25
  self.eq('vi', nodes[0].get('iso2'))
25
26
  self.eq('vis', nodes[0].get('iso3'))
26
27
  self.eq(31337, nodes[0].get('isonum'))
28
+ self.eq(('pesos', 'usd', 'vcoins'), nodes[0].get('currencies'))
27
29
  self.len(2, await core.nodes('pol:country -> geo:name'))
30
+ self.len(3, await core.nodes('pol:country -> econ:currency'))
28
31
 
29
32
  nodes = await core.nodes('''
30
33
  [ pol:vitals=*
@@ -375,19 +375,25 @@ class OuModelTest(s_t_utils.SynTest):
375
375
  props = {
376
376
  'org': guid0,
377
377
  'name': 'arrowcon 2018',
378
+ 'names': ('Arrow Conference 2018', 'ArrCon18', 'ArrCon18'),
378
379
  'base': 'arrowcon',
379
380
  'start': '20180301',
380
381
  'end': '20180303',
381
382
  'place': place0,
382
383
  'url': 'http://arrowcon.org/2018',
383
384
  }
384
- q = '''[(ou:conference=$valu :org=$p.org :name=$p.name :base=$p.base
385
- :start=$p.start :end=$p.end :place=$p.place :url=$p.url)]'''
385
+ q = '''[
386
+ ou:conference=$valu
387
+ :org=$p.org :name=$p.name :names=$p.names
388
+ :base=$p.base :start=$p.start :end=$p.end
389
+ :place=$p.place :url=$p.url
390
+ ]'''
386
391
  nodes = await core.nodes(q, opts={'vars': {'valu': c0, 'p': props}})
387
392
  self.len(1, nodes)
388
393
  node = nodes[0]
389
394
  self.eq(node.ndef[1], c0)
390
395
  self.eq(node.get('name'), 'arrowcon 2018')
396
+ self.eq(node.get('names'), ('arrcon18', 'arrow conference 2018',))
391
397
  self.eq(node.get('base'), 'arrowcon')
392
398
  self.eq(node.get('org'), guid0)
393
399
  self.eq(node.get('start'), 1519862400000)
@@ -153,6 +153,7 @@ class PsModelTest(s_t_utils.SynTest):
153
153
  :org=$p.org :asof=$p.asof :person=$p.person
154
154
  :place=$p.place :place:name=$p."place:name" :name=$p.name
155
155
  :title=$p.title :orgname=$p.orgname :user=$p.user
156
+ :titles=('hehe', 'hehe', 'haha')
156
157
  :web:acct=$p."web:acct" :web:group=$p."web:group"
157
158
  :dob=$p.dob :dod=$p.dod :url=$p.url
158
159
  :email=$p.email :email:work=$p."email:work"
@@ -177,6 +178,7 @@ class PsModelTest(s_t_utils.SynTest):
177
178
  self.eq(node.get('place:name'), 'the shire')
178
179
  self.eq(node.get('name'), 'tony stark')
179
180
  self.eq(node.get('title'), 'ceo')
181
+ self.eq(node.get('titles'), ('haha', 'hehe'))
180
182
  self.eq(node.get('orgname'), 'stark industries, inc')
181
183
  self.eq(node.get('user'), 'ironman')
182
184
  self.eq(node.get('web:acct'), ('twitter.com', 'ironman'))