116 lines
5.9 KiB
Python
116 lines
5.9 KiB
Python
import unittest
|
|
import threading
|
|
from unittest.mock import Mock, patch
|
|
from browser_gateway.platform.notice_polling import NoticePollingSession
|
|
from browser_gateway.test_notice_polling import page, notice, BOUNDARY
|
|
|
|
class EventPushTest(unittest.TestCase):
|
|
def session(self):
|
|
browser = Mock()
|
|
browser.identity.return_value = {'status': 'verified'}
|
|
browser.get.return_value = page([notice('21')])
|
|
return NoticePollingSession(browser, 'safe', '123', BOUNDARY, {})
|
|
|
|
def test_stale_stop_cannot_stop_a_new_receiver_of_the_same_generation(self):
|
|
from browser_gateway.platform.notice_polling import SubscriptionManager
|
|
manager=SubscriptionManager(Mock());session=self.session();session.generation='same'
|
|
manager._items['safe']=session
|
|
with self.assertRaisesRegex(Exception,'stale notification listener session'):
|
|
manager.stop('safe','same','previous-session')
|
|
self.assertFalse(session.stopped)
|
|
manager.stop('safe','same',session.session_id);self.assertTrue(session.stopped)
|
|
|
|
@patch('browser_gateway.platform.notice_polling.NoticePollingSession')
|
|
def test_one_account_start_does_not_block_other_account_registry_reads(self, session_class):
|
|
from browser_gateway.platform.notice_polling import SubscriptionManager
|
|
manager=SubscriptionManager(Mock());manager._items['other']=object()
|
|
entered,release,read=threading.Event(),threading.Event(),threading.Event()
|
|
new=session_class.return_value;new.session_id='new';new.start.side_effect=lambda changed:(entered.set(),release.wait(1))
|
|
starter=threading.Thread(target=lambda:manager.start('safe','123',BOUNDARY,{}),daemon=True);starter.start()
|
|
try:
|
|
self.assertTrue(entered.wait(1))
|
|
reader=threading.Thread(target=lambda:(manager._get('other'),read.set()),daemon=True);reader.start()
|
|
self.assertTrue(read.wait(.1),'account startup held the shared registry lock')
|
|
finally:
|
|
release.set();starter.join(1);reader.join(1)
|
|
|
|
def test_signal_is_queued_without_history_scan(self):
|
|
session = self.session()
|
|
session.publish_notices([notice('9007199254740993123')])
|
|
self.assertEqual(session.pending()[0]['notice']['event_key'], '9007199254740993123')
|
|
session.browser.get.assert_not_called()
|
|
|
|
def test_later_realtime_notice_does_not_block_history_checkpoint(self):
|
|
session = self.session()
|
|
history = session.poll(100, 0)
|
|
session.publish_notices([notice('99')])
|
|
session.ack([item['delivery_id'] for item in history])
|
|
self.assertEqual(session.checkpoints, {'700':'21','960':'21','961':'21'})
|
|
self.assertEqual([item['notice']['event_key'] for item in session.pending()], ['99'])
|
|
|
|
def test_bad_identity_does_not_queue_or_confirm(self):
|
|
session = self.session()
|
|
raw = notice('1'); raw['user_id'] = '999'
|
|
with self.assertRaisesRegex(Exception, 'identity'):
|
|
session.publish_notices([raw])
|
|
self.assertEqual(session.pending(), [])
|
|
|
|
def test_history_backpressure_reserves_room_for_live_notifications(self):
|
|
session = self.session(); session.max_pending = 4
|
|
changed = threading.Event(); session._changed = changed.set
|
|
thread = threading.Thread(target=session._scan, daemon=True); thread.start()
|
|
try:
|
|
self.assertTrue(changed.wait(1))
|
|
history = session.pending(); self.assertEqual(len(history),2)
|
|
session.publish_notices([notice('99')])
|
|
self.assertEqual(session.pending()[0]['notice']['event_key'],'99')
|
|
session.ack([d['delivery_id'] for d in history])
|
|
session.ack([d['delivery_id'] for d in session.pending() if d.get('notice',{}).get('event_key')=='99'])
|
|
thread.join(1); self.assertFalse(thread.is_alive())
|
|
session.ack([d['delivery_id'] for d in session.pending()])
|
|
self.assertEqual(session.checkpoints, {'700':'21','960':'21','961':'21'})
|
|
finally:
|
|
session.stop();thread.join(1)
|
|
|
|
@patch('browser_gateway.platform.notice_signal.NoticeSignal')
|
|
def test_receiver_continues_while_history_is_blocked_and_stops_cleanly(self, bridge):
|
|
session = self.session();started = threading.Event();received = threading.Event()
|
|
signal = bridge.return_value;signal.received_at = BOUNDARY
|
|
first = True
|
|
def receive():
|
|
nonlocal first
|
|
if first:
|
|
first=False;started.wait(1);return [notice('99')]
|
|
session._stop.wait();return []
|
|
signal.receive.side_effect=receive
|
|
session._scan=lambda:(started.set(),session._stop.wait())
|
|
session.start(received.set)
|
|
try:
|
|
self.assertTrue(received.wait(1));self.assertEqual(session.pending()[0]['notice']['event_key'],'99')
|
|
finally:
|
|
session.stop()
|
|
for thread in session._threads:thread.join(1);self.assertFalse(thread.is_alive())
|
|
signal.close.assert_called_once()
|
|
|
|
@patch('browser_gateway.platform.notice_signal.NoticeSignal')
|
|
def test_receiver_error_is_visible_without_fabricating_notifications(self, bridge):
|
|
session = self.session();changed = threading.Event()
|
|
bridge.return_value.receive.side_effect=RuntimeError('signal failed')
|
|
session._scan=lambda:None
|
|
session.start(changed.set)
|
|
try:
|
|
self.assertTrue(changed.wait(1));self.assertEqual(session.error,'signal failed');self.assertEqual(session.pending(),[])
|
|
finally:
|
|
session.stop()
|
|
for thread in session._threads:thread.join(1)
|
|
|
|
def test_overload_is_explicit_and_does_not_drop_pending_events(self):
|
|
session = self.session()
|
|
session.max_pending = 1
|
|
session.publish_notices([notice('1')])
|
|
with self.assertRaisesRegex(Exception, 'queue'):
|
|
session.publish_notices([notice('2')])
|
|
self.assertEqual(session.pending()[0]['notice']['event_key'], '1')
|
|
|
|
if __name__ == '__main__': unittest.main()
|