1
0

test_ssdp.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. # -*- coding: utf-8 -*-
  2. '''
  3. :codeauthor: :email:`Bo Maryniuk <bo@suse.de>`
  4. '''
  5. from __future__ import absolute_import, print_function, unicode_literals
  6. import datetime
  7. from tests.support.unit import TestCase, skipIf
  8. from tests.support.mock import (
  9. MagicMock,
  10. patch)
  11. from salt.ext.six.moves import zip
  12. from salt.ext import six
  13. import salt.utils.ssdp as ssdp
  14. import salt.utils.stringutils
  15. try:
  16. import pytest
  17. except ImportError:
  18. pytest = None
  19. class Mocks(object):
  20. def get_socket_mock(self, expected_ip, expected_hostname):
  21. '''
  22. Get a mock of a socket
  23. :return:
  24. '''
  25. sck = MagicMock()
  26. sck.getsockname = MagicMock(return_value=(expected_ip, 123456))
  27. sock_mock = MagicMock()
  28. sock_mock.socket = MagicMock(return_value=sck)
  29. sock_mock.gethostname = MagicMock(return_value=expected_hostname)
  30. sock_mock.gethostbyname = MagicMock(return_value=expected_ip)
  31. return sock_mock
  32. def get_ssdp_factory(self, expected_ip=None, expected_hostname=None, **config):
  33. if expected_ip is None:
  34. expected_ip = '127.0.0.1'
  35. if expected_hostname is None:
  36. expected_hostname = 'localhost'
  37. sock_mock = self.get_socket_mock(expected_ip, expected_hostname)
  38. with patch('salt.utils.ssdp.socket', sock_mock):
  39. factory = ssdp.SSDPFactory(**config)
  40. return factory
  41. def get_ssdp_discovery_client(self, expected_ip=None, expected_hostname=None, **config):
  42. if expected_ip is None:
  43. expected_ip = '127.0.0.1'
  44. if expected_hostname is None:
  45. expected_hostname = 'localhost'
  46. sock_mock = self.get_socket_mock(expected_ip, expected_hostname)
  47. with patch('salt.utils.ssdp.socket', sock_mock):
  48. factory = ssdp.SSDPDiscoveryClient(**config)
  49. return factory
  50. def get_ssdp_discovery_server(self, expected_ip=None, expected_hostname=None, **config):
  51. if expected_ip is None:
  52. expected_ip = '127.0.0.1'
  53. if expected_hostname is None:
  54. expected_hostname = 'localhost'
  55. sock_mock = self.get_socket_mock(expected_ip, expected_hostname)
  56. with patch('salt.utils.ssdp.socket', sock_mock):
  57. factory = ssdp.SSDPDiscoveryServer(**config)
  58. return factory
  59. @skipIf(pytest is None, 'PyTest is missing')
  60. class SSDPBaseTestCase(TestCase, Mocks):
  61. '''
  62. TestCase for SSDP-related parts.
  63. '''
  64. @staticmethod
  65. def exception_generic(*args, **kwargs):
  66. '''
  67. Side effect
  68. :return:
  69. '''
  70. raise Exception('some network error')
  71. @staticmethod
  72. def exception_attr_error(*args, **kwargs):
  73. '''
  74. Side effect
  75. :return:
  76. '''
  77. raise AttributeError('attribute error: {0}. {1}'.format(args, kwargs))
  78. @patch('salt.utils.ssdp._json', None)
  79. @patch('salt.utils.ssdp.asyncio', None)
  80. def test_base_avail(self):
  81. '''
  82. Test SSDP base class availability method.
  83. :return:
  84. '''
  85. base = ssdp.SSDPBase()
  86. assert not base._is_available()
  87. with patch('salt.utils.ssdp._json', True):
  88. assert not base._is_available()
  89. with patch('salt.utils.ssdp.asyncio', True):
  90. assert not base._is_available()
  91. with patch('salt.utils.ssdp._json', True), patch('salt.utils.ssdp.asyncio', True):
  92. assert base._is_available()
  93. def test_base_protocol_settings(self):
  94. '''
  95. Tests default constants data.
  96. :return:
  97. '''
  98. base = ssdp.SSDPBase()
  99. v_keys = ['signature', 'answer', 'port', 'listen_ip', 'timeout']
  100. v_vals = ['__salt_master_service', {}, 4520, '0.0.0.0', 3]
  101. for key in v_keys:
  102. assert key in base.DEFAULTS
  103. for key in base.DEFAULTS:
  104. assert key in v_keys
  105. for key, value in zip(v_keys, v_vals):
  106. assert base.DEFAULTS[key] == value
  107. def test_base_self_ip(self):
  108. '''
  109. Test getting self IP method.
  110. :return:
  111. '''
  112. base = ssdp.SSDPBase()
  113. expected_ip = '192.168.1.10'
  114. expected_host = 'oxygen'
  115. sock_mock = self.get_socket_mock(expected_ip, expected_host)
  116. with patch('salt.utils.ssdp.socket', sock_mock):
  117. assert base.get_self_ip() == expected_ip
  118. sock_mock.socket().getsockname.side_effect = SSDPBaseTestCase.exception_generic
  119. with patch('salt.utils.ssdp.socket', sock_mock):
  120. assert base.get_self_ip() == expected_ip
  121. @skipIf(pytest is None, 'PyTest is missing')
  122. class SSDPFactoryTestCase(TestCase, Mocks):
  123. '''
  124. Test socket protocol
  125. '''
  126. def test_attr_check(self):
  127. '''
  128. Tests attributes are set to the base class
  129. :return:
  130. '''
  131. config = {
  132. ssdp.SSDPBase.SIGNATURE: '-signature-',
  133. ssdp.SSDPBase.ANSWER: {'this-is': 'the-answer'}
  134. }
  135. expected_ip = '10.10.10.10'
  136. factory = self.get_ssdp_factory(expected_ip=expected_ip, **config)
  137. for attr in [ssdp.SSDPBase.SIGNATURE, ssdp.SSDPBase.ANSWER]:
  138. assert hasattr(factory, attr)
  139. assert getattr(factory, attr) == config[attr]
  140. assert not factory.disable_hidden
  141. assert factory.my_ip == expected_ip
  142. def test_transport_sendto_success(self):
  143. '''
  144. Test transport send_to.
  145. :return:
  146. '''
  147. transport = MagicMock()
  148. log = MagicMock()
  149. factory = self.get_ssdp_factory()
  150. with patch.object(factory, 'transport', transport), patch.object(factory, 'log', log):
  151. data = {'some': 'data'}
  152. addr = '10.10.10.10'
  153. factory._sendto(data=data, addr=addr)
  154. assert factory.transport.sendto.called
  155. assert factory.transport.sendto.mock_calls[0][1][0]['some'] == 'data'
  156. assert factory.transport.sendto.mock_calls[0][2]['addr'] == '10.10.10.10'
  157. assert factory.log.debug.called
  158. assert factory.log.debug.mock_calls[0][1][0] == 'Sent successfully'
  159. def test_transport_sendto_retry(self):
  160. '''
  161. Test transport send_to.
  162. :return:
  163. '''
  164. with patch('salt.utils.ssdp.time.sleep', MagicMock()):
  165. transport = MagicMock()
  166. transport.sendto = MagicMock(side_effect=SSDPBaseTestCase.exception_attr_error)
  167. log = MagicMock()
  168. factory = self.get_ssdp_factory()
  169. with patch.object(factory, 'transport', transport), patch.object(factory, 'log', log):
  170. data = {'some': 'data'}
  171. addr = '10.10.10.10'
  172. factory._sendto(data=data, addr=addr)
  173. assert factory.transport.sendto.called
  174. assert ssdp.time.sleep.called
  175. assert ssdp.time.sleep.call_args[0][0] > 0 and ssdp.time.sleep.call_args[0][0] < 0.5
  176. assert factory.log.debug.called
  177. assert 'Permission error' in factory.log.debug.mock_calls[0][1][0]
  178. def test_datagram_signature_bad(self):
  179. '''
  180. Test datagram_received on bad signature
  181. :return:
  182. '''
  183. factory = self.get_ssdp_factory()
  184. data = 'nonsense'
  185. addr = '10.10.10.10', 'foo.suse.de'
  186. with patch.object(factory, 'log', MagicMock()):
  187. factory.datagram_received(data=data, addr=addr)
  188. assert factory.log.debug.called
  189. assert 'Received bad signature from' in factory.log.debug.call_args[0][0]
  190. assert factory.log.debug.call_args[0][1] == addr[0]
  191. assert factory.log.debug.call_args[0][2] == addr[1]
  192. def test_datagram_signature_wrong_timestamp_quiet(self):
  193. '''
  194. Test datagram receives a wrong timestamp (no reply).
  195. :return:
  196. '''
  197. factory = self.get_ssdp_factory()
  198. data = '{}nonsense'.format(ssdp.SSDPBase.DEFAULTS[ssdp.SSDPBase.SIGNATURE])
  199. addr = '10.10.10.10', 'foo.suse.de'
  200. with patch.object(factory, 'log', MagicMock()), patch.object(factory, '_sendto', MagicMock()):
  201. factory.datagram_received(data=data, addr=addr)
  202. assert factory.log.debug.called
  203. assert 'Received invalid timestamp in package' in factory.log.debug.call_args[0][0]
  204. assert not factory._sendto.called
  205. def test_datagram_signature_wrong_timestamp_reply(self):
  206. '''
  207. Test datagram receives a wrong timestamp.
  208. :return:
  209. '''
  210. factory = self.get_ssdp_factory()
  211. factory.disable_hidden = True
  212. signature = ssdp.SSDPBase.DEFAULTS[ssdp.SSDPBase.SIGNATURE]
  213. data = '{}nonsense'.format(signature)
  214. addr = '10.10.10.10', 'foo.suse.de'
  215. with patch.object(factory, 'log', MagicMock()), patch.object(factory, '_sendto', MagicMock()):
  216. factory.datagram_received(data=data, addr=addr)
  217. assert factory.log.debug.called
  218. assert 'Received invalid timestamp in package' in factory.log.debug.call_args[0][0]
  219. assert factory._sendto.called
  220. assert '{}:E:Invalid timestamp'.format(signature) == factory._sendto.call_args[0][0]
  221. def test_datagram_signature_outdated_timestamp_quiet(self):
  222. '''
  223. Test if datagram processing reacts on outdated message (more than 20 seconds). Quiet mode.
  224. :return:
  225. '''
  226. factory = self.get_ssdp_factory()
  227. signature = ssdp.SSDPBase.DEFAULTS[ssdp.SSDPBase.SIGNATURE]
  228. data = '{}{}'.format(signature, '1516623820')
  229. addr = '10.10.10.10', 'foo.suse.de'
  230. ahead_dt = datetime.datetime.fromtimestamp(1516623841)
  231. curnt_dt = datetime.datetime.fromtimestamp(1516623820)
  232. delta = datetime.timedelta(0, 20)
  233. with patch.object(factory, 'log', MagicMock()), patch.object(factory, '_sendto'), \
  234. patch('salt.utils.ssdp.datetime.datetime', MagicMock()), \
  235. patch('salt.utils.ssdp.datetime.datetime.now', MagicMock(return_value=ahead_dt)), \
  236. patch('salt.utils.ssdp.datetime.datetime.fromtimestamp', MagicMock(return_value=curnt_dt)), \
  237. patch('salt.utils.ssdp.datetime.timedelta', MagicMock(return_value=delta)):
  238. factory.datagram_received(data=data, addr=addr)
  239. assert factory.log.debug.called
  240. assert not factory.disable_hidden
  241. assert not factory._sendto.called
  242. assert 'Received outdated package' in factory.log.debug.call_args[0][0]
  243. def test_datagram_signature_outdated_timestamp_reply(self):
  244. '''
  245. Test if datagram processing reacts on outdated message (more than 20 seconds). Reply mode.
  246. :return:
  247. '''
  248. factory = self.get_ssdp_factory()
  249. factory.disable_hidden = True
  250. signature = ssdp.SSDPBase.DEFAULTS[ssdp.SSDPBase.SIGNATURE]
  251. data = '{}{}'.format(signature, '1516623820')
  252. addr = '10.10.10.10', 'foo.suse.de'
  253. ahead_dt = datetime.datetime.fromtimestamp(1516623841)
  254. curnt_dt = datetime.datetime.fromtimestamp(1516623820)
  255. delta = datetime.timedelta(0, 20)
  256. with patch.object(factory, 'log', MagicMock()), patch.object(factory, '_sendto'), \
  257. patch('salt.utils.ssdp.datetime.datetime', MagicMock()), \
  258. patch('salt.utils.ssdp.datetime.datetime.now', MagicMock(return_value=ahead_dt)), \
  259. patch('salt.utils.ssdp.datetime.datetime.fromtimestamp', MagicMock(return_value=curnt_dt)), \
  260. patch('salt.utils.ssdp.datetime.timedelta', MagicMock(return_value=delta)):
  261. factory.datagram_received(data=data, addr=addr)
  262. assert factory.log.debug.called
  263. assert factory.disable_hidden
  264. assert factory._sendto.called
  265. assert factory._sendto.call_args[0][0] == '{}:E:Timestamp is too old'.format(signature)
  266. assert 'Received outdated package' in factory.log.debug.call_args[0][0]
  267. def test_datagram_signature_correct_timestamp_reply(self):
  268. '''
  269. Test if datagram processing sends out correct reply within 20 seconds.
  270. :return:
  271. '''
  272. factory = self.get_ssdp_factory()
  273. factory.disable_hidden = True
  274. signature = ssdp.SSDPBase.DEFAULTS[ssdp.SSDPBase.SIGNATURE]
  275. data = '{}{}'.format(signature, '1516623820')
  276. addr = '10.10.10.10', 'foo.suse.de'
  277. ahead_dt = datetime.datetime.fromtimestamp(1516623840)
  278. curnt_dt = datetime.datetime.fromtimestamp(1516623820)
  279. delta = datetime.timedelta(0, 20)
  280. with patch.object(factory, 'log', MagicMock()), patch.object(factory, '_sendto'), \
  281. patch('salt.utils.ssdp.datetime.datetime', MagicMock()), \
  282. patch('salt.utils.ssdp.datetime.datetime.now', MagicMock(return_value=ahead_dt)), \
  283. patch('salt.utils.ssdp.datetime.datetime.fromtimestamp', MagicMock(return_value=curnt_dt)), \
  284. patch('salt.utils.ssdp.datetime.timedelta', MagicMock(return_value=delta)):
  285. factory.datagram_received(data=data, addr=addr)
  286. assert factory.log.debug.called
  287. assert factory.disable_hidden
  288. assert factory._sendto.called
  289. assert factory._sendto.call_args[0][0] == salt.utils.stringutils.to_bytes("{}:@:{{}}".format(signature))
  290. assert 'Received "%s" from %s:%s' in factory.log.debug.call_args[0][0]
  291. @skipIf(pytest is None, 'PyTest is missing')
  292. class SSDPServerTestCase(TestCase, Mocks):
  293. '''
  294. Server-related test cases
  295. '''
  296. def test_config_detached(self):
  297. '''
  298. Test if configuration is not a reference.
  299. :return:
  300. '''
  301. old_ip = '10.10.10.10'
  302. new_ip = '20.20.20.20'
  303. config = {'answer': {'master': old_ip}}
  304. with patch('salt.utils.ssdp.SSDPDiscoveryServer.get_self_ip', MagicMock(return_value=new_ip)):
  305. srv = ssdp.SSDPDiscoveryServer(**config)
  306. assert srv._config['answer']['master'] == new_ip
  307. assert config['answer']['master'] == old_ip
  308. def test_run(self):
  309. '''
  310. Test server runner.
  311. :return:
  312. '''
  313. with patch('salt.utils.ssdp.SSDPFactory', MagicMock()):
  314. config = {'answer': {'master': '10.10.10.10'},
  315. ssdp.SSDPBase.LISTEN_IP: '10.10.10.10',
  316. ssdp.SSDPBase.PORT: 12345}
  317. srv = self.get_ssdp_discovery_server(**config)
  318. srv.create_datagram_endpoint = MagicMock()
  319. srv.log = MagicMock()
  320. trnsp = MagicMock()
  321. proto = MagicMock()
  322. loop = MagicMock()
  323. loop.run_until_complete = MagicMock(return_value=(trnsp, proto))
  324. io = MagicMock()
  325. io.ported = False
  326. io.get_event_loop = MagicMock(return_value=loop)
  327. with patch('salt.utils.ssdp.asyncio', io):
  328. srv.run()
  329. cde_args = io.get_event_loop().create_datagram_endpoint.call_args[1]
  330. cfg_ip_addr, cfg_port = cde_args['local_addr']
  331. assert io.get_event_loop.called
  332. assert io.get_event_loop().run_until_complete.called
  333. assert io.get_event_loop().create_datagram_endpoint.called
  334. assert io.get_event_loop().run_forever.called
  335. assert trnsp.close.called
  336. assert loop.close.called
  337. assert srv.log.info.called
  338. assert srv.log.info.call_args[0][0] == 'Stopping service discovery listener.'
  339. assert 'allow_broadcast' in cde_args
  340. assert cde_args['allow_broadcast']
  341. assert 'local_addr' in cde_args
  342. assert not cfg_ip_addr == ssdp.SSDPBase.DEFAULTS[ssdp.SSDPBase.LISTEN_IP] and cfg_ip_addr == '10.10.10.10'
  343. assert not cfg_port == ssdp.SSDPBase.DEFAULTS[ssdp.SSDPBase.PORT] and cfg_port == 12345
  344. @skipIf(pytest is None, 'PyTest is missing')
  345. class SSDPClientTestCase(TestCase, Mocks):
  346. '''
  347. Client-related test cases
  348. '''
  349. class Resource(object):
  350. '''
  351. Fake network reader
  352. '''
  353. def __init__(self):
  354. self.pool = [('some', '10.10.10.10'),
  355. ('data', '20.20.20.20'),
  356. ('data', '10.10.10.10'),
  357. (None, None)]
  358. def read(self, *args, **kwargs):
  359. return self.pool.pop(0)
  360. def test_config_passed(self):
  361. '''
  362. Test if the configuration is passed.
  363. :return:
  364. '''
  365. config = {ssdp.SSDPBase.SIGNATURE: 'SUSE Enterprise Server',
  366. ssdp.SSDPBase.TIMEOUT: 5, ssdp.SSDPBase.PORT: 12345}
  367. clnt = self.get_ssdp_discovery_client(**config)
  368. assert clnt._config[ssdp.SSDPBase.SIGNATURE] == config[ssdp.SSDPBase.SIGNATURE]
  369. assert clnt._config[ssdp.SSDPBase.PORT] == config[ssdp.SSDPBase.PORT]
  370. assert clnt._config[ssdp.SSDPBase.TIMEOUT] == config[ssdp.SSDPBase.TIMEOUT]
  371. def test_config_detached(self):
  372. '''
  373. Test if the passed configuration is not a reference.
  374. :return:
  375. '''
  376. config = {ssdp.SSDPBase.SIGNATURE: 'SUSE Enterprise Server', }
  377. clnt = self.get_ssdp_discovery_client(**config)
  378. clnt._config['foo'] = 'bar'
  379. assert 'foo' in clnt._config
  380. assert 'foo' not in config
  381. def test_query(self):
  382. '''
  383. Test if client queries the broadcast
  384. :return:
  385. '''
  386. config = {ssdp.SSDPBase.SIGNATURE: 'SUSE Enterprise Server',
  387. ssdp.SSDPBase.PORT: 4000}
  388. f_time = 1111
  389. _socket = MagicMock()
  390. with patch('salt.utils.ssdp.socket', _socket),\
  391. patch('salt.utils.ssdp.time.time', MagicMock(return_value=f_time)):
  392. clnt = ssdp.SSDPDiscoveryClient(**config)
  393. clnt._query()
  394. assert clnt._socket.sendto.called
  395. message, target = clnt._socket.sendto.call_args[0]
  396. assert message == salt.utils.stringutils.to_bytes(
  397. '{}{}'.format(config[ssdp.SSDPBase.SIGNATURE], f_time)
  398. )
  399. assert target[0] == '<broadcast>'
  400. assert target[1] == config[ssdp.SSDPBase.PORT]
  401. def test_get_masters_map(self):
  402. '''
  403. Test getting map of the available masters on the network
  404. :return:
  405. '''
  406. _socket = MagicMock()
  407. response = {}
  408. with patch('salt.utils.ssdp.socket', _socket):
  409. clnt = ssdp.SSDPDiscoveryClient()
  410. clnt._socket.recvfrom = SSDPClientTestCase.Resource().read
  411. clnt.log = MagicMock()
  412. clnt._collect_masters_map(response=response)
  413. assert '10.10.10.10' in response
  414. assert '20.20.20.20' in response
  415. assert response['10.10.10.10'] == ['some', 'data']
  416. assert response['20.20.20.20'] == ['data']
  417. def test_get_masters_map_error_handling(self):
  418. '''
  419. Test getting map handles timeout network exception
  420. :return:
  421. '''
  422. _socket = MagicMock()
  423. response = {}
  424. error_msg = 'fake testing timeout just had happened'
  425. with patch('salt.utils.ssdp.socket', _socket):
  426. clnt = ssdp.SSDPDiscoveryClient()
  427. clnt._socket.recvfrom = MagicMock(side_effect=Exception(error_msg))
  428. clnt.log = MagicMock()
  429. clnt._collect_masters_map(response=response)
  430. assert clnt.log.error.called
  431. assert 'Discovery master collection failure' in clnt.log.error.call_args[0][0]
  432. assert error_msg == six.text_type(clnt.log.error.call_args[0][1])
  433. assert not response
  434. def test_discover_no_masters(self):
  435. '''
  436. Test discover available master on the network (none found).
  437. :return:
  438. '''
  439. clnt = self.get_ssdp_discovery_client()
  440. clnt._query = MagicMock()
  441. clnt._collect_masters_map = MagicMock()
  442. clnt.log = MagicMock()
  443. clnt.discover()
  444. assert clnt.log.info.called
  445. assert clnt.log.info.call_args[0][0] == 'No master has been discovered.'
  446. def test_discover_general_error(self):
  447. '''
  448. Test discover available master on the network (erroneous found)
  449. :return:
  450. '''
  451. _socket = MagicMock()
  452. error = 'Admins on strike due to broken coffee machine'
  453. signature = ssdp.SSDPBase.DEFAULTS[ssdp.SSDPBase.SIGNATURE]
  454. fake_resource = SSDPClientTestCase.Resource()
  455. fake_resource.pool = [('{}:E:{}'.format(signature, error), '10.10.10.10'),
  456. (None, None)]
  457. with patch('salt.utils.ssdp.socket', _socket):
  458. clnt = ssdp.SSDPDiscoveryClient()
  459. clnt._socket.recvfrom = fake_resource.read
  460. clnt._query = MagicMock()
  461. clnt.log = MagicMock()
  462. clnt.discover()
  463. assert len(clnt.log.error.mock_calls) == 1
  464. assert 'Error response from the service publisher' in clnt.log.error.call_args[0][0]
  465. assert '10.10.10.10' == clnt.log.error.call_args[0][1]
  466. assert clnt.log.error.call_args[1] == {}
  467. assert clnt.log.error.call_args[0][2] == error
  468. def test_discover_timestamp_error(self):
  469. '''
  470. Test discover available master on the network (outdated timestamp)
  471. :return:
  472. '''
  473. _socket = MagicMock()
  474. error = 'We only support a 1200 bps connection. Routing timestamp problems on neural net.'
  475. signature = ssdp.SSDPBase.DEFAULTS[ssdp.SSDPBase.SIGNATURE]
  476. fake_resource = SSDPClientTestCase.Resource()
  477. fake_resource.pool = [('{}:E:{}'.format(signature, error), '10.10.10.10'),
  478. (None, None)]
  479. with patch('salt.utils.ssdp.socket', _socket):
  480. clnt = ssdp.SSDPDiscoveryClient()
  481. clnt._socket.recvfrom = fake_resource.read
  482. clnt._query = MagicMock()
  483. clnt.log = MagicMock()
  484. clnt.discover()
  485. assert len(clnt.log.error.mock_calls) == 2
  486. assert 'Error response from the service publisher' in clnt.log.error.mock_calls[0][1][0]
  487. assert clnt.log.error.mock_calls[0][1][2] == error
  488. assert clnt.log.error.mock_calls[0][2] == {}
  489. assert 'Publisher sent shifted timestamp' in clnt.log.error.mock_calls[1][1][0]
  490. assert clnt.log.error.mock_calls[1][1][1] == clnt.log.error.mock_calls[0][1][1] == '10.10.10.10'