class RepeaterConnectionSettingsTests(TestCase): def setUp(self): self.rep = FormRepeater( domain=DOMAIN, url="https://spam.example.com/api/", auth_type=BASIC_AUTH, username="******", password="******", notify_addresses_str="*****@*****.**", format=FormRepeaterXMLPayloadGenerator.format_name, ) def tearDown(self): if self.rep.connection_settings_id: ConnectionSettings.objects.filter( pk=self.rep.connection_settings_id).delete() self.rep.delete() def test_create_connection_settings(self): self.assertIsNone(self.rep.connection_settings_id) conn = self.rep.connection_settings self.assertIsNotNone(self.rep.connection_settings_id) self.assertEqual(conn.name, self.rep.url) self.assertEqual(self.rep.plaintext_password, conn.plaintext_password) # rep.password was saved decrypted; conn.password is not: self.assertNotEqual(self.rep.password, conn.password)
class IgnoreDocumentTest(BaseRepeaterTest): domain = 'ignore-doc' @classmethod def setUpClass(cls): super(IgnoreDocumentTest, cls).setUpClass() class NewFormGenerator(BasePayloadGenerator): format_name = 'new_format' format_label = 'XML' def get_payload(self, repeat_record, payload_doc): raise IgnoreDocument RegisterGenerator.get_collection(FormRepeater).add_new_format( NewFormGenerator) def setUp(self): super().setUp() self.connx = ConnectionSettings.objects.create( domain=self.domain, url='form-repeater-url', ) self.repeater = FormRepeater(domain=self.domain, connection_settings_id=self.connx.id, format='new_format') self.repeater.save() def tearDown(self): self.repeater.delete() self.connx.delete() FormProcessorTestUtils.delete_all_cases_forms_ledgers(self.domain) delete_all_repeat_records() @run_with_all_backends def test_ignore_document(self): """ When get_payload raises IgnoreDocument, fire should call update_success """ repeat_records = RepeatRecord.all(domain=self.domain, ) for repeat_record_ in repeat_records: repeat_record_.fire() self.assertIsNone(repeat_record_.next_check) self.assertTrue(repeat_record_.succeeded)
class IgnoreDocumentTest(BaseRepeaterTest): @classmethod def setUpClass(cls): super(IgnoreDocumentTest, cls).setUpClass() class NewFormGenerator(BasePayloadGenerator): format_name = 'new_format' format_label = 'XML' def get_payload(self, repeat_record, payload_doc): raise IgnoreDocument RegisterGenerator.get_collection(FormRepeater).add_new_format(NewFormGenerator) def setUp(self): super(IgnoreDocumentTest, self).setUp() self.domain = "test-domain" create_domain(self.domain) self.repeater = FormRepeater( domain=self.domain, url='form-repeater-url', format='new_format' ) self.repeater.save() def tearDown(self): self.repeater.delete() delete_all_repeat_records() super(IgnoreDocumentTest, self).tearDown() @run_with_all_backends def test_ignore_document(self): """ When get_payload raises IgnoreDocument, fire should call update_success """ repeat_records = RepeatRecord.all( domain=self.domain, ) for repeat_record_ in repeat_records: repeat_record_.fire() self.assertIsNone(repeat_record_.next_check) self.assertTrue(repeat_record_.succeeded)
class TestRepeatRecordsUpdateRepeater(TestCase): def setUp(self): self.rep = FormRepeater( domain=DOMAIN, url='https://server.example.com/api/', ) self.rep.save() def tearDown(self): self.rep.delete() def test_success(self): one_minute = timedelta(minutes=1) rec = RepeatRecord( domain=DOMAIN, repeater_id=self.rep.get_id, ) attempt = RepeatRecordAttempt( datetime=datetime.utcnow() - one_minute, success_response='w00t', succeeded=True, ) rec.add_attempt(attempt) self.assertEqual(rec.repeater.last_success_at, attempt.datetime) self.assertEqual(rec.repeater.failure_streak, 0) def test_failure(self): one_minute = timedelta(minutes=1) rec = RepeatRecord( domain=DOMAIN, repeater_id=self.rep.get_id, ) attempt = RepeatRecordAttempt( datetime=datetime.utcnow() - one_minute, failure_reason='oops', succeeded=False, ) rec.add_attempt(attempt) self.assertIsNone(rec.repeater.last_success_at) self.assertEqual(rec.repeater.failure_streak, 1)
class RepeaterTest(BaseRepeaterTest): def setUp(self): super(RepeaterTest, self).setUp() self.domain = "test-domain" create_domain(self.domain) self.case_repeater = CaseRepeater( domain=self.domain, url='case-repeater-url', ) self.case_repeater.save() self.form_repeater = FormRepeater( domain=self.domain, url='form-repeater-url', ) self.form_repeater.save() self.log = [] self.post_xml(self.xform_xml, self.domain) def tearDown(self): self.case_repeater.delete() self.form_repeater.delete() FormProcessorTestUtils.delete_all_cases_forms_ledgers(self.domain) delete_all_repeat_records() super(RepeaterTest, self).tearDown() @run_with_all_backends def test_skip_device_logs(self): devicelog_xml = XFORM_XML_TEMPLATE.format(DEVICE_LOG_XMLNS, USER_ID, '1234', '') self.post_xml(devicelog_xml, self.domain) repeat_records = RepeatRecord.all(domain=self.domain) for repeat_record in repeat_records: self.assertNotEqual(repeat_record.payload_id, '1234') @run_with_all_backends def test_skip_duplicates(self): """ Ensure that submitting a duplicate form does not create extra RepeatRecords """ self.assertEqual(len(RepeatRecord.all()), 2) # this form is already submitted during setUp so a second submission should be a duplicate form = self.post_xml(self.xform_xml, self.domain).xform self.assertTrue(form.is_duplicate) self.assertEqual(len(RepeatRecord.all()), 2) @run_with_all_backends def test_repeater_failed_sends(self): """ This tests records that fail are requeued later """ def now(): return datetime.utcnow() repeat_records = RepeatRecord.all(domain=self.domain, due_before=now()) self.assertEqual(len(repeat_records), 2) for repeat_record in repeat_records: with patch('corehq.motech.repeaters.models.simple_post', return_value=MockResponse( status_code=404, reason='Not Found')) as mock_post: repeat_record.fire() self.assertEqual(mock_post.call_count, 1) next_check_time = now() + timedelta(minutes=60) repeat_records = RepeatRecord.all( domain=self.domain, due_before=now() + timedelta(minutes=15), ) self.assertEqual(len(repeat_records), 0) repeat_records = RepeatRecord.all( domain=self.domain, due_before=next_check_time, ) self.assertEqual(len(repeat_records), 2) @run_with_all_backends def test_update_failure_next_check(self): now = datetime.utcnow() record = RepeatRecord(domain=self.domain, next_check=now) self.assertIsNone(record.last_checked) attempt = record.make_set_next_try_attempt(None) record.add_attempt(attempt) self.assertTrue(record.last_checked > now) self.assertEqual(record.next_check, record.last_checked + MIN_RETRY_WAIT) @run_with_all_backends def test_repeater_successful_send(self): repeat_records = RepeatRecord.all(domain=self.domain, due_before=datetime.utcnow()) for repeat_record in repeat_records: with patch('corehq.motech.repeaters.models.simple_post', return_value=MockResponse( status_code=200, reason='No Reason')) as mock_post: repeat_record.fire() self.assertEqual(mock_post.call_count, 1) mock_post.assert_any_call( repeat_record.get_payload(), repeat_record.repeater.get_url(repeat_record), headers=repeat_record.repeater.get_headers(repeat_record), timeout=POST_TIMEOUT, auth=repeat_record.repeater.get_auth(), verify=repeat_record.repeater.verify, ) # The following is pretty fickle and depends on which of # - corehq.motech.repeaters.signals # - casexml.apps.case.signals # gets loaded first. # This is deterministic but easily affected by minor code changes repeat_records = RepeatRecord.all( domain=self.domain, due_before=datetime.utcnow(), ) for repeat_record in repeat_records: self.assertEqual(repeat_record.succeeded, True) self.assertEqual(repeat_record.next_check, None) self.assertEqual(len(self.repeat_records(self.domain)), 0) self.post_xml(self.update_xform_xml, self.domain) self.assertEqual(len(self.repeat_records(self.domain)), 2) @run_with_all_backends def test_check_repeat_records(self): self.assertEqual(len(RepeatRecord.all()), 2) with patch('corehq.motech.repeaters.models.simple_post') as mock_fire: check_repeaters() self.assertEqual(mock_fire.call_count, 2) with patch('corehq.motech.repeaters.models.simple_post') as mock_fire: check_repeaters() self.assertEqual(mock_fire.call_count, 0) @run_with_all_backends def test_repeat_record_status_check(self): self.assertEqual(len(RepeatRecord.all()), 2) # Do not trigger cancelled records for repeat_record in RepeatRecord.all(): repeat_record.cancelled = True repeat_record.save() with patch('corehq.motech.repeaters.models.simple_post') as mock_fire: check_repeaters() self.assertEqual(mock_fire.call_count, 0) # trigger force send records if not cancelled and tries not exhausted for repeat_record in RepeatRecord.all(): with patch('corehq.motech.repeaters.models.simple_post', return_value=MockResponse(status_code=200, reason='')) as mock_fire: repeat_record.fire(force_send=True) self.assertEqual(mock_fire.call_count, 1) # all records should be in SUCCESS state after force try for repeat_record in RepeatRecord.all(): self.assertEqual(repeat_record.state, RECORD_SUCCESS_STATE) self.assertEqual(repeat_record.overall_tries, 1) # not trigger records succeeded triggered after cancellation with patch('corehq.motech.repeaters.models.simple_post') as mock_fire: check_repeaters() self.assertEqual(mock_fire.call_count, 0) for repeat_record in RepeatRecord.all(): self.assertEqual(repeat_record.state, RECORD_SUCCESS_STATE) @run_with_all_backends def test_process_repeat_record_locking(self): self.assertEqual(len(RepeatRecord.all()), 2) with patch('corehq.motech.repeaters.tasks.process_repeat_record' ) as mock_process: check_repeaters() self.assertEqual(mock_process.delay.call_count, 2) with patch('corehq.motech.repeaters.tasks.process_repeat_record' ) as mock_process: check_repeaters() self.assertEqual(mock_process.delay.call_count, 0) records = RepeatRecord.all() # Saving should unlock them again by changing the rev for record in records: record.save() with patch('corehq.motech.repeaters.tasks.process_repeat_record' ) as mock_process: check_repeaters() self.assertEqual(mock_process.delay.call_count, 2) @run_with_all_backends def test_automatic_cancel_repeat_record(self): repeat_record = self.case_repeater.register( CaseAccessors(self.domain).get_case(CASE_ID)) repeat_record.overall_tries = 1 with patch('corehq.motech.repeaters.models.simple_post', side_effect=Exception('Boom!')): repeat_record.fire() self.assertEqual(2, repeat_record.overall_tries) with patch('corehq.motech.repeaters.models.simple_post', side_effect=Exception('Boom!')): repeat_record.fire() self.assertEqual(True, repeat_record.cancelled) repeat_record.requeue() self.assertEqual(0, repeat_record.overall_tries) self.assertNotEqual(None, repeat_record.next_check)
class RepeaterTest(BaseRepeaterTest): def setUp(self): super(RepeaterTest, self).setUp() self.domain = "test-domain" create_domain(self.domain) self.case_repeater = CaseRepeater( domain=self.domain, url='case-repeater-url', ) self.case_repeater.save() self.form_repeater = FormRepeater( domain=self.domain, url='form-repeater-url', ) self.form_repeater.save() self.log = [] with patch('corehq.motech.repeaters.models.simple_post') as mock_fire: self.post_xml(self.xform_xml, self.domain) self.initial_fire_call_count = mock_fire.call_count def tearDown(self): self.case_repeater.delete() self.form_repeater.delete() FormProcessorTestUtils.delete_all_cases_forms_ledgers(self.domain) delete_all_repeat_records() super(RepeaterTest, self).tearDown() def repeat_records(self): return super(RepeaterTest, self).repeat_records(self.domain) @run_with_all_backends def test_skip_device_logs(self): devicelog_xml = XFORM_XML_TEMPLATE.format(DEVICE_LOG_XMLNS, USER_ID, '1234', '') self.post_xml(devicelog_xml, self.domain) for repeat_record in self.repeat_records(): self.assertNotEqual(repeat_record.payload_id, '1234') @run_with_all_backends def test_skip_duplicates(self): """ Ensure that submitting a duplicate form does not create extra RepeatRecords """ self.assertEqual(len(self.repeat_records()), 2) # this form is already submitted during setUp so a second submission should be a duplicate form = self.post_xml(self.xform_xml, self.domain).xform self.assertTrue(form.is_duplicate) self.assertEqual(len(self.repeat_records()), 2) @run_with_all_backends def test_repeater_failed_sends(self): """ This tests records that fail are requeued later """ def now(): return datetime.utcnow() repeat_records = self.repeat_records() self.assertEqual(len(repeat_records), 2) for repeat_record in repeat_records: with patch( 'corehq.motech.repeaters.models.simple_post', return_value=MockResponse(status_code=404, reason='Not Found')) as mock_post: repeat_record.fire() self.assertEqual(mock_post.call_count, 1) # Enqueued repeat records have next_check incremented by 48 hours next_check_time = now() + timedelta(minutes=60) + timedelta(hours=48) repeat_records = RepeatRecord.all( domain=self.domain, due_before=now() + timedelta(minutes=15), ) self.assertEqual(len(repeat_records), 0) repeat_records = RepeatRecord.all( domain=self.domain, due_before=next_check_time, ) self.assertEqual(len(repeat_records), 2) @run_with_all_backends def test_update_failure_next_check(self): now = datetime.utcnow() record = RepeatRecord(domain=self.domain, next_check=now) self.assertIsNone(record.last_checked) attempt = record.make_set_next_try_attempt(None) record.add_attempt(attempt) self.assertTrue(record.last_checked > now) self.assertEqual(record.next_check, record.last_checked + MIN_RETRY_WAIT) @run_with_all_backends def test_repeater_successful_send(self): repeat_records = self.repeat_records() for repeat_record in repeat_records: with patch( 'corehq.motech.repeaters.models.simple_post', return_value=MockResponse(status_code=200, reason='No Reason')) as mock_post: repeat_record.fire() self.assertEqual(mock_post.call_count, 1) mock_post.assert_any_call( repeat_record.get_payload(), repeat_record.repeater.get_url(repeat_record), headers=repeat_record.repeater.get_headers(repeat_record), timeout=POST_TIMEOUT, auth=repeat_record.repeater.get_auth(), verify=repeat_record.repeater.verify, ) # The following is pretty fickle and depends on which of # - corehq.motech.repeaters.signals # - casexml.apps.case.signals # gets loaded first. # This is deterministic but easily affected by minor code changes repeat_records = self.repeat_records() for repeat_record in repeat_records: self.assertEqual(repeat_record.succeeded, True) self.assertEqual(repeat_record.next_check, None) self.assertEqual(len(self.repeat_records()), 0) self.post_xml(self.update_xform_xml, self.domain) self.assertEqual(len(self.repeat_records()), 2) @run_with_all_backends def test_check_repeat_records(self): self.assertEqual(len(self.repeat_records()), 2) self.assertEqual(self.initial_fire_call_count, 2) with patch('corehq.motech.repeaters.models.simple_post') as mock_fire: check_repeaters() self.assertEqual(mock_fire.call_count, 0) @run_with_all_backends def test_repeat_record_status_check(self): self.assertEqual(len(self.repeat_records()), 2) # Do not trigger cancelled records for repeat_record in self.repeat_records(): repeat_record.cancelled = True repeat_record.save() with patch('corehq.motech.repeaters.models.simple_post') as mock_fire: check_repeaters() self.assertEqual(mock_fire.call_count, 0) # trigger force send records if not cancelled and tries not exhausted for repeat_record in self.repeat_records(): with patch('corehq.motech.repeaters.models.simple_post', return_value=MockResponse(status_code=200, reason='') ) as mock_fire: repeat_record.fire(force_send=True) self.assertEqual(mock_fire.call_count, 1) # all records should be in SUCCESS state after force try for repeat_record in self.repeat_records(): self.assertEqual(repeat_record.state, RECORD_SUCCESS_STATE) self.assertEqual(repeat_record.overall_tries, 1) # not trigger records succeeded triggered after cancellation with patch('corehq.motech.repeaters.models.simple_post') as mock_fire: check_repeaters() self.assertEqual(mock_fire.call_count, 0) for repeat_record in self.repeat_records(): self.assertEqual(repeat_record.state, RECORD_SUCCESS_STATE) @run_with_all_backends def test_process_repeat_record_locking(self): self.assertEqual(len(self.repeat_records()), 2) with patch('corehq.motech.repeaters.tasks.process_repeat_record') as mock_process: check_repeaters() self.assertEqual(mock_process.delay.call_count, 0) for record in self.repeat_records(): # Resetting next_check should allow them to be requeued record.next_check = datetime.utcnow() record.save() with patch('corehq.motech.repeaters.tasks.process_repeat_record') as mock_process: check_repeaters() self.assertEqual(mock_process.delay.call_count, 2) @run_with_all_backends def test_automatic_cancel_repeat_record(self): repeat_record = self.case_repeater.register(CaseAccessors(self.domain).get_case(CASE_ID)) self.assertEqual(1, repeat_record.overall_tries) with patch('corehq.motech.repeaters.models.simple_post', side_effect=Exception('Boom!')): for __ in range(repeat_record.max_possible_tries - repeat_record.overall_tries): repeat_record.fire() self.assertEqual(True, repeat_record.cancelled) repeat_record.requeue() self.assertEqual(0, repeat_record.overall_tries) self.assertNotEqual(None, repeat_record.next_check)
class RepeaterTest(BaseRepeaterTest): domain = "repeater-test" def setUp(self): super(RepeaterTest, self).setUp() self.case_connx = ConnectionSettings.objects.create( domain=self.domain, url='case-repeater-url', ) self.case_repeater = CaseRepeater( domain=self.domain, connection_settings_id=self.case_connx.id, ) self.case_repeater.save() self.form_connx = ConnectionSettings.objects.create( domain=self.domain, url='form-repeater-url', ) self.form_repeater = FormRepeater( domain=self.domain, connection_settings_id=self.form_connx.id, ) self.form_repeater.save() self.log = [] with patch('corehq.motech.repeaters.models.simple_post', return_value=MockResponse(status_code=500, reason="Borked")) as mock_fire: self.post_xml(self.xform_xml, self.domain) self.initial_fire_call_count = mock_fire.call_count def tearDown(self): self.case_repeater.delete() self.case_connx.delete() self.form_repeater.delete() self.form_connx.delete() FormProcessorTestUtils.delete_all_cases_forms_ledgers(self.domain) delete_all_repeat_records() super(RepeaterTest, self).tearDown() def repeat_records(self): return super(RepeaterTest, self).repeat_records(self.domain) @run_with_all_backends def test_skip_device_logs(self): devicelog_xml = XFORM_XML_TEMPLATE.format(DEVICE_LOG_XMLNS, USER_ID, '1234', '') self.post_xml(devicelog_xml, self.domain) for repeat_record in self.repeat_records(): self.assertNotEqual(repeat_record.payload_id, '1234') @run_with_all_backends def test_skip_duplicates(self): """ Ensure that submitting a duplicate form does not create extra RepeatRecords """ self.assertEqual(len(self.repeat_records()), 2) # this form is already submitted during setUp so a second submission should be a duplicate form = self.post_xml(self.xform_xml, self.domain).xform self.assertTrue(form.is_duplicate) self.assertEqual(len(self.repeat_records()), 2) @run_with_all_backends def test_repeater_failed_sends(self): """ This tests records that fail are requeued later """ def now(): return datetime.utcnow() repeat_records = self.repeat_records() self.assertEqual(len(repeat_records), 2) for repeat_record in repeat_records: with patch('corehq.motech.repeaters.models.simple_post', return_value=MockResponse( status_code=404, reason='Not Found')) as mock_post: repeat_record.fire() self.assertEqual(mock_post.call_count, 1) # Enqueued repeat records have next_check incremented by 48 hours next_check_time = now() + timedelta(minutes=60) + timedelta(hours=48) repeat_records = RepeatRecord.all( domain=self.domain, due_before=now() + timedelta(minutes=15), ) self.assertEqual(len(repeat_records), 0) repeat_records = RepeatRecord.all( domain=self.domain, due_before=next_check_time, ) self.assertEqual(len(repeat_records), 2) @run_with_all_backends def test_update_failure_next_check(self): now = datetime.utcnow() record = RepeatRecord( domain=self.domain, repeater_id=self.case_repeater.get_id, next_check=now, ) self.assertIsNone(record.last_checked) attempt = record.make_set_next_try_attempt(None) record.add_attempt(attempt) self.assertTrue(record.last_checked > now) self.assertEqual(record.next_check, record.last_checked + MIN_RETRY_WAIT) @run_with_all_backends def test_repeater_successful_send(self): repeat_records = self.repeat_records() for repeat_record in repeat_records: with patch('corehq.motech.repeaters.models.simple_post') as mock_post, \ patch.object(ConnectionSettings, 'get_auth_manager') as mock_manager: mock_post.return_value.status_code = 200 mock_manager.return_value = 'MockAuthManager' repeat_record.fire() self.assertEqual(mock_post.call_count, 1) mock_post.assert_called_with( self.domain, repeat_record.repeater.get_url(repeat_record), repeat_record.get_payload(), headers=repeat_record.repeater.get_headers(repeat_record), auth_manager='MockAuthManager', verify=repeat_record.repeater.verify, notify_addresses=[], payload_id=repeat_record.payload_id, ) # The following is pretty fickle and depends on which of # - corehq.motech.repeaters.signals # - casexml.apps.case.signals # gets loaded first. # This is deterministic but easily affected by minor code changes repeat_records = self.repeat_records() for repeat_record in repeat_records: self.assertEqual(repeat_record.succeeded, True) self.assertEqual(repeat_record.next_check, None) self.assertEqual(len(self.repeat_records()), 0) self.post_xml(self.update_xform_xml, self.domain) self.assertEqual(len(self.repeat_records()), 2) @run_with_all_backends def test_check_repeat_records(self): self.assertEqual(len(self.repeat_records()), 2) self.assertEqual(self.initial_fire_call_count, 2) with patch('corehq.motech.repeaters.models.simple_post') as mock_fire: check_repeaters() self.assertEqual(mock_fire.call_count, 0) @run_with_all_backends def test_repeat_record_status_check(self): self.assertEqual(len(self.repeat_records()), 2) # Do not trigger cancelled records for repeat_record in self.repeat_records(): repeat_record.cancelled = True repeat_record.save() with patch('corehq.motech.repeaters.models.simple_post') as mock_fire: check_repeaters() self.assertEqual(mock_fire.call_count, 0) # trigger force send records if not cancelled and tries not exhausted for repeat_record in self.repeat_records(): with patch('corehq.motech.repeaters.models.simple_post', return_value=MockResponse(status_code=200, reason='')) as mock_fire: repeat_record.fire(force_send=True) self.assertEqual(mock_fire.call_count, 1) # all records should be in SUCCESS state after force try for repeat_record in self.repeat_records(): self.assertEqual(repeat_record.state, RECORD_SUCCESS_STATE) self.assertEqual(repeat_record.overall_tries, 1) # not trigger records succeeded triggered after cancellation with patch('corehq.motech.repeaters.models.simple_post') as mock_fire: check_repeaters() self.assertEqual(mock_fire.call_count, 0) for repeat_record in self.repeat_records(): self.assertEqual(repeat_record.state, RECORD_SUCCESS_STATE) @run_with_all_backends def test_retry_process_repeat_record_locking(self): self.assertEqual(len(self.repeat_records()), 2) with patch('corehq.motech.repeaters.tasks.retry_process_repeat_record' ) as mock_process: check_repeaters() self.assertEqual(mock_process.delay.call_count, 0) for record in self.repeat_records(): # Resetting next_check should allow them to be requeued record.next_check = datetime.utcnow() record.save() with patch('corehq.motech.repeaters.tasks.retry_process_repeat_record' ) as mock_process: check_repeaters() self.assertEqual(mock_process.delay.call_count, 2) @run_with_all_backends def test_automatic_cancel_repeat_record(self): repeat_record = self.case_repeater.register( CaseAccessors(self.domain).get_case(CASE_ID)) self.assertEqual(1, repeat_record.overall_tries) with patch('corehq.motech.repeaters.models.simple_post', side_effect=Exception('Boom!')): for __ in range(repeat_record.max_possible_tries - repeat_record.overall_tries): repeat_record.fire() self.assertEqual(True, repeat_record.cancelled) repeat_record.requeue() self.assertEqual(0, repeat_record.overall_tries) self.assertNotEqual(None, repeat_record.next_check)