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()