class EnvironmentUpgradeTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub() def tearDown(self): self.env.reset_db() def test_multiple_upgrade_participants(self): class Participant1(Component): implements(IEnvironmentSetupParticipant) def environment_created(self): pass def environment_needs_upgrade(self): return True def upgrade_environment(self): insert_value('value1', 1) class Participant2(Component): implements(IEnvironmentSetupParticipant) def environment_created(self): pass def environment_needs_upgrade(self): return True def upgrade_environment(self): insert_value('value2', 2) def insert_value(name, value): self.env.db_transaction( """ INSERT INTO system (name, value) VALUES (%s, %s) """, (name, value)) def select_value(name): for value, in self.env.db_query( """ SELECT value FROM system WHERE name=%s """, (name, )): return value self.env.enable_component(Participant1) self.env.enable_component(Participant2) self.assertTrue(self.env.needs_upgrade()) self.assertTrue(self.env.upgrade()) self.assertEqual('1', select_value('value1')) self.assertEqual('2', select_value('value2'))
class RepositoryAdminPanelTestCase(unittest.TestCase): RepositoryConnector = None @classmethod def setUpClass(cls): class RepositoryConnector(Component): implements(IRepositoryConnector) def get_supported_types(self): yield 'RepositoryConnector', 1 def get_repository(self, repos_type, repos_dir, params): pass cls.RepositoryConnector = RepositoryConnector def setUp(self): self.env = EnvironmentStub(enable=('trac.versioncontrol.admin.*', )) def tearDown(self): self.env.reset_db() @classmethod def tearDownClass(cls): from trac.core import ComponentMeta ComponentMeta.deregister(cls.RepositoryConnector) def test_panel_not_exists_when_no_repository_connectors(self): """Repositories admin panel is not present when there are no repository connectors enabled. """ req = MockRequest(self.env) rap = RepositoryAdminPanel(self.env) panels = [panel for panel in rap.get_admin_panels(req)] self.assertEqual(0, len(panels)) def test_panel_exists_when_repository_connectors(self): """Repositories admin panel is present when there are repository connectors enabled. """ self.env.enable_component(self.RepositoryConnector) req = MockRequest(self.env) rap = RepositoryAdminPanel(self.env) panels = [panel for panel in rap.get_admin_panels(req)] self.assertEqual(1, len(panels))
class SystemInfoProviderTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub() self.env.clear_component_registry() class SystemInfoProvider1(Component): implements(ISystemInfoProvider) def get_system_info(self): yield 'pkg1', 1.0 yield 'pkg2', 2.0 class SystemInfoProvider2(Component): implements(ISystemInfoProvider) def get_system_info(self): yield 'pkg1', 1.0 self.env.enable_component(SystemInfoProvider1) self.env.enable_component(SystemInfoProvider2) def tearDown(self): self.env.restore_component_registry() def test_system_info_property(self): """The system_info property returns a list of all tuples generated by ISystemInfoProvider implementations. """ system_info = self.env.system_info self.assertEqual(system_info, self.env.get_systeminfo()) self.assertEqual(2, len(system_info)) self.assertIn(('pkg1', 1.0), system_info) self.assertIn(('pkg2', 2.0), system_info) def test_duplicate_entries_are_removed(self): """Duplicate entries are removed.""" system_info = self.env.system_info self.assertIn(('pkg1', 1.0), system_info) self.assertEqual(len(system_info), len(set(system_info)))
class BatchModifyTestCase(unittest.TestCase): ticket_manipulators = None @classmethod def setUpClass(cls): class TicketValidator1(Component): implements(api.ITicketManipulator) def prepare_ticket(self, req, ticket, fields, actions): pass def validate_ticket(self, req, ticket): errors = [] if ticket['component'] == 'component3': errors.append(('component', 'Invalid Component')) return errors class TicketValidator2(Component): implements(api.ITicketManipulator) def prepare_ticket(self, req, ticket, fields, actions): pass def validate_ticket(self, req, ticket): return [] def validate_comment(self, req, comment): if 'badword' in comment: yield "Word is not allowed in comment" cls.ticket_manipulators = [TicketValidator1, TicketValidator2] @classmethod def tearDownClass(cls): from trac.core import ComponentMeta for manipulator in cls.ticket_manipulators: ComponentMeta.deregister(manipulator) def setUp(self): self.env = EnvironmentStub( default_data=True, enable=[ default_workflow.ConfigurableTicketWorkflow, DefaultPermissionPolicy, DefaultPermissionStore, BatchModifyModule, api.TicketSystem, web_ui.TicketModule ]) self.env.config.set('trac', 'permission_policies', 'DefaultPermissionPolicy') self.env.config.set('ticket-custom', 'text1', 'text') self.env.config.set('ticket-custom', 'text1.max_size', 5) self.env.config.set('ticket-custom', 'time1', 'time') self.env.config.set('ticket-custom', 'time1.format', 'date') self.env.config.set('ticket-workflow', 'acknowledge', '* -> acknowledged') ps = PermissionSystem(self.env) ps.grant_permission('has_ta_&_bm', 'TICKET_ADMIN') ps.grant_permission('has_bm', 'TICKET_BATCH_MODIFY') ps.grant_permission('has_ta_&_bm', 'TICKET_BATCH_MODIFY') session = DetachedSession(self.env, 'has_ta_&_bm') session.set('query_href', '') session.save() session = DetachedSession(self.env, 'has_bm') session.set('query_href', '') session.save() self._insert_ticket('Ticket 1', reporter='user1', component='component1', description='the desc', keywords='foo one', status='new') self._insert_ticket('Ticket 2', reporter='user1', component='component2', description='the desc', keywords='baz two', status='new') def tearDown(self): self.env.reset_db() def assertCommentAdded(self, ticket_id, comment): ticket = model.Ticket(self.env, int(ticket_id)) changes = ticket.get_changelog() comment_change = [c for c in changes if c[2] == 'comment'][-1] self.assertEqual(comment_change[4], comment) def assertFieldValue(self, ticket_id, field, new_value): ticket = model.Ticket(self.env, int(ticket_id)) self.assertEqual(ticket[field], new_value) def _insert_ticket(self, summary, **kw): """Helper for inserting a ticket into the database""" ticket = insert_ticket(self.env, summary=summary, **kw) return ticket.id def _insert_component(self, name): component = model.Component(self.env) component.name = name component.insert() def test_require_post_method(self): """Request must use POST method.""" module = BatchModifyModule(self.env) req = MockRequest(self.env, method='GET', path_info='/batchmodify') req.session['query_href'] = req.href.query() self.assertTrue(module.match_request(req)) with self.assertRaises(HTTPBadRequest): module.process_request(req) req = MockRequest(self.env, method='POST', path_info='/batchmodify', args={'selected_tickets': ''}) req.session['query_href'] = req.href.query() self.assertTrue(module.match_request(req)) with self.assertRaises(RequestDone): module.process_request(req) def test_redirect_to_query_href_in_req_args(self): redirect_listener_args = [] def redirect_listener(req, url, permanent): redirect_listener_args[:] = (url, permanent) module = BatchModifyModule(self.env) req = MockRequest(self.env, method='POST', path_info='/batchmodify') query_opened_tickets = req.href.query(status='!closed') query_default = req.href.query() req.args = {'selected_tickets': '', 'query_href': query_opened_tickets} req.session['query_href'] = query_default req.add_redirect_listener(redirect_listener) self.assertTrue(module.match_request(req)) self.assertRaises(RequestDone, module.process_request, req) self.assertEqual([query_opened_tickets, False], redirect_listener_args) def test_save_comment(self): """Comments are saved to all selected tickets.""" req = MockRequest(self.env, method='POST', authname='has_bm', path_info='/batchmodify', args={ 'batchmod_value_comment': 'the comment', 'action': 'leave', 'selected_tickets': '1,2', }) batch = BatchModifyModule(self.env) self.assertTrue(batch.match_request(req)) with self.assertRaises(RequestDone): batch.process_request(req) self.assertCommentAdded(1, 'the comment') self.assertCommentAdded(2, 'the comment') def test_save_values(self): """Changed values are saved to all tickets.""" req = MockRequest(self.env, method='POST', authname='has_bm', path_info='/batchmodify', args={ 'batchmod_value_component': 'component1', 'batchmod_value_comment': '', 'action': 'leave', 'selected_tickets': '1,2', }) batch = BatchModifyModule(self.env) self.assertTrue(batch.match_request(req)) with self.assertRaises(RequestDone): batch.process_request(req) self.assertFieldValue(1, 'component', 'component1') self.assertFieldValue(2, 'component', 'component1') def test_list_fields_add(self): req = MockRequest(self.env, method='POST', authname='has_bm', path_info='/batchmodify', args={ 'batchmod_mode_keywords': '+', 'batchmod_primary_keywords': 'baz new', 'batchmod_secondary_keywords': '*****', 'action': 'leave', 'selected_tickets': '1,2', }) batch = BatchModifyModule(self.env) self.assertTrue(batch.match_request(req)) with self.assertRaises(RequestDone): batch.process_request(req) self.assertFieldValue(1, 'keywords', 'foo, one, baz, new') self.assertFieldValue(2, 'keywords', 'baz, two, new') def test_list_fields_addrem(self): req = MockRequest(self.env, method='POST', authname='has_bm', path_info='/batchmodify', args={ 'batchmod_mode_keywords': '+-', 'batchmod_primary_keywords': 'one three four', 'batchmod_secondary_keywords': 'baz missing', 'action': 'leave', 'selected_tickets': '1,2', }) batch = BatchModifyModule(self.env) self.assertTrue(batch.match_request(req)) with self.assertRaises(RequestDone): batch.process_request(req) self.assertFieldValue(1, 'keywords', 'foo, one, three, four') self.assertFieldValue(2, 'keywords', 'two, one, three, four') def test_list_fields_rem(self): req = MockRequest(self.env, method='POST', authname='has_bm', path_info='/batchmodify', args={ 'batchmod_mode_keywords': '-', 'batchmod_primary_keywords': 'foo two', 'batchmod_secondary_keywords': '*****', 'action': 'leave', 'selected_tickets': '1,2', }) batch = BatchModifyModule(self.env) self.assertTrue(batch.match_request(req)) with self.assertRaises(RequestDone): batch.process_request(req) self.assertFieldValue(1, 'keywords', 'one') self.assertFieldValue(2, 'keywords', 'baz') def test_list_fields_set(self): req = MockRequest(self.env, method='POST', authname='has_bm', path_info='/batchmodify', args={ 'batchmod_mode_keywords': '=', 'batchmod_primary_keywords': 'orange', 'batchmod_secondary_keywords': '*****', 'action': 'leave', 'selected_tickets': '1,2', }) batch = BatchModifyModule(self.env) self.assertTrue(batch.match_request(req)) with self.assertRaises(RequestDone): batch.process_request(req) self.assertFieldValue(1, 'keywords', 'orange') self.assertFieldValue(2, 'keywords', 'orange') def test_action_with_state_change(self): """Actions can have change status.""" req = MockRequest(self.env, method='POST', authname='has_bm', path_info='/batchmodify', args={ 'action': 'acknowledge', 'batchmod_value_comment': '', 'selected_tickets': '1,2', }) batch = BatchModifyModule(self.env) self.assertTrue(batch.match_request(req)) with self.assertRaises(RequestDone): batch.process_request(req) self.assertFieldValue(1, 'status', 'acknowledged') self.assertFieldValue(2, 'status', 'acknowledged') def test_action_with_side_effects(self): """Actions can have operations with side effects.""" req = MockRequest(self.env, method='POST', authname='has_bm', path_info='/batchmodify', args={ 'action': 'reassign', 'action_reassign_reassign_owner': 'user3', 'batchmod_value_comment': '', 'selected_tickets': '1,2', }) batch = BatchModifyModule(self.env) self.assertTrue(batch.match_request(req)) with self.assertRaises(RequestDone): batch.process_request(req) self.assertFieldValue(1, 'owner', 'user3') self.assertFieldValue(2, 'owner', 'user3') self.assertFieldValue(1, 'status', 'assigned') self.assertFieldValue(2, 'status', 'assigned') def test_timeline_events(self): """Regression test for #11288""" req1 = MockRequest(self.env) tktmod = web_ui.TicketModule(self.env) now = datetime_now(utc) start = now - timedelta(hours=1) stop = now + timedelta(hours=1) events = tktmod.get_timeline_events(req1, start, stop, ['ticket_details']) self.assertTrue(all(ev[0] != 'batchmodify' for ev in events)) prio_ids = {} for i in xrange(20): priority = ('', 'minor', 'major', 'critical')[i % 4] t = insert_ticket(self.env, summary='Ticket %d' % i, priority=priority) prio_ids.setdefault(t['priority'], []).append(t.id) tktids = prio_ids['critical'] + prio_ids['major'] + \ prio_ids['minor'] + prio_ids[''] req2 = MockRequest(self.env, method='POST', authname='has_ta_&_bm', path_info='/batchmodify', args={ 'batchmod_value_summary': 'batch updated ticket', 'batchmod_value_owner': 'ticket11288', 'batchmod_value_reporter': 'ticket11288', 'action': 'leave', 'selected_tickets': ','.join(str(t) for t in tktids), }) batch = BatchModifyModule(self.env) self.assertTrue(batch.match_request(req2)) with self.assertRaises(RequestDone): batch.process_request(req2) # shuffle ticket_change records with self.env.db_transaction as db: rows = db('SELECT * FROM ticket_change') db.execute('DELETE FROM ticket_change') rows = rows[0::4] + rows[1::4] + rows[2::4] + rows[3::4] db.executemany( 'INSERT INTO ticket_change VALUES (%s)' % ','.join( ('%s', ) * len(rows[0])), rows) events = tktmod.get_timeline_events(req1, start, stop, ['ticket_details']) events = [ev for ev in events if ev[0] == 'batchmodify'] self.assertEqual(1, len(events)) batch_ev = events[0] self.assertEqual('has_ta_&_bm', batch_ev[2]) self.assertEqual(tktids, batch_ev[3][0]) self.assertEqual('updated', batch_ev[3][1]) context = web_context(req2) self.assertEqual( req2.href.query(id=','.join(str(t) for t in tktids)), tktmod.render_timeline_event(context, 'url', batch_ev)) def test_modify_summary_and_description(self): """The ticket summary and description cannot be modified.""" req = MockRequest(self.env, authname='has_ta_&_bm', method='POST', path_info='/batchmodify', args={ 'batchmod_value_summary': 'the new summary', 'batchmod_value_description': 'the new description', 'batchmod_value_comment': '', 'action': 'leave', 'selected_tickets': '1,2', }) module = BatchModifyModule(self.env) self.assertTrue(module.match_request(req)) with self.assertRaises(RequestDone): module.process_request(req) self.assertFieldValue(1, 'description', 'the desc') self.assertFieldValue(1, 'summary', 'Ticket 1') self.assertFieldValue(2, 'description', 'the desc') self.assertFieldValue(2, 'summary', 'Ticket 2') def test_modify_reporter_with_ticket_admin(self): """User with TICKET_ADMIN can batch modify the reporter.""" req = MockRequest(self.env, method='POST', authname='has_ta_&_bm', path_info='/batchmodify', args={ 'batchmod_value_reporter': 'user2', 'batchmod_value_comment': '', 'action': 'leave', 'selected_tickets': '1,2', }) module = BatchModifyModule(self.env) self.assertTrue(module.match_request(req)) with self.assertRaises(RequestDone): module.process_request(req) self.assertFieldValue(1, 'reporter', 'user2') self.assertFieldValue(2, 'reporter', 'user2') def test_modify_reporter_without_ticket_admin(self): """User without TICKET_ADMIN cannot batch modify the reporter.""" req = MockRequest(self.env, method='POST', authname='has_bm', path_info='/batchmodify', args={ 'batchmod_value_reporter': 'user2', 'batchmod_value_comment': '', 'action': 'leave', 'selected_tickets': '1,2', }) module = BatchModifyModule(self.env) self.assertTrue(module.match_request(req)) with self.assertRaises(RequestDone): module.process_request(req) self.assertFieldValue(1, 'reporter', 'user1') self.assertFieldValue(2, 'reporter', 'user1') def test_validate_ticket_comment_size(self): """The [ticket] max_comment_size value is enforced.""" module = BatchModifyModule(self.env) self.env.config.set('ticket', 'max_comment_size', 5) req1 = MockRequest(self.env, authname='has_bm', method='POST', path_info='/batchmodify', args={ 'batchmod_value_comment': '12345', 'action': 'leave', 'selected_tickets': '1,2', }) req2 = MockRequest(self.env, authname='has_bm', method='POST', path_info='/batchmodify', args={ 'batchmod_value_comment': '123456', 'action': 'leave', 'selected_tickets': '1,2', }) self.assertTrue(module.match_request(req1)) with self.assertRaises(RequestDone): module.process_request(req1) self.assertEqual([], req1.chrome['warnings']) self.assertCommentAdded(1, '12345') self.assertCommentAdded(2, '12345') self.assertTrue(module.match_request(req2)) with self.assertRaises(RequestDone): module.process_request(req2) self.assertEqual(1, len(req2.chrome['warnings'])) self.assertEqual( "The ticket comment is invalid: Must be less than or " "equal to 5 characters", unicode(req2.chrome['warnings'][0])) self.assertEqual(1, len(model.Ticket(self.env, 1).get_changelog())) self.assertEqual(1, len(model.Ticket(self.env, 2).get_changelog())) def test_validate_select_fields(self): """The select field values are validated.""" req = MockRequest(self.env, authname='has_bm', method='POST', path_info='/batchmodify', args={ 'batchmod_value_component': 'component3', 'action': 'leave', 'selected_tickets': '1,2', }) module = BatchModifyModule(self.env) self.assertTrue(module.match_request(req)) with self.assertRaises(RequestDone): module.process_request(req) self.assertEqual(1, len(req.chrome['warnings'])) self.assertEqual( 'The ticket field <strong>component</strong> is ' 'invalid: "component3" is not a valid value', unicode(req.chrome['warnings'][0])) def test_validate_ticket_custom_field_max_size(self): """The [ticket-custom] max_size attribute is enforced.""" module = BatchModifyModule(self.env) req1 = MockRequest(self.env, authname='has_bm', method='POST', path_info='/batchmodify', args={ 'batchmod_value_text1': '12345', 'action': 'leave', 'selected_tickets': '1,2', }) req2 = MockRequest(self.env, authname='has_bm', method='POST', path_info='/batchmodify', args={ 'batchmod_value_text1': '123456', 'action': 'leave', 'selected_tickets': '1,2', }) self.assertTrue(module.match_request(req1)) with self.assertRaises(RequestDone): module.process_request(req1) self.assertEqual([], req1.chrome['warnings']) self.assertEqual('12345', model.Ticket(self.env, 1)['text1']) self.assertEqual('12345', model.Ticket(self.env, 2)['text1']) self.assertTrue(module.match_request(req2)) with self.assertRaises(RequestDone): module.process_request(req2) self.assertEqual(1, len(req2.chrome['warnings'])) self.assertEqual( "The ticket field <strong>Text1</strong> is " "invalid: Must be less than or equal to 5 " "characters", unicode(req2.chrome['warnings'][0])) self.assertEqual('12345', model.Ticket(self.env, 1)['text1']) self.assertEqual('12345', model.Ticket(self.env, 2)['text1']) def test_validate_time_fields(self): """The time fields are validated.""" module = BatchModifyModule(self.env) req1 = MockRequest(self.env, authname='has_bm', method='POST', path_info='/batchmodify', args={ 'batchmod_value_time1': '2016-01-02T12:34:56Z', 'action': 'leave', 'selected_tickets': '1,2', }) req2 = MockRequest(self.env, authname='has_bm', method='POST', path_info='/batchmodify', args={ 'batchmod_value_time1': 'invalid', 'action': 'leave', 'selected_tickets': '1,2', }) dt = datetime(2016, 1, 2, 12, 34, 56, tzinfo=utc) self.assertTrue(module.match_request(req1)) with self.assertRaises(RequestDone): module.process_request(req1) self.assertEqual(dt, model.Ticket(self.env, 1)['time1']) self.assertEqual(dt, model.Ticket(self.env, 2)['time1']) self.assertEqual([], req1.chrome['warnings']) self.assertTrue(module.match_request(req2)) with self.assertRaises(RequestDone): module.process_request(req2) self.assertEqual(1, len(req2.chrome['warnings'])) self.assertRegexpMatches( unicode(req2.chrome['warnings'][0]), 'The ticket field <strong>Time1</strong> is invalid: "invalid" ' 'is an invalid date, or the date format is not known. ' 'Try "[^"]+" or "[^"]+" instead.') self.assertEqual(dt, model.Ticket(self.env, 1)['time1']) self.assertEqual(dt, model.Ticket(self.env, 2)['time1']) def test_ticket_manipulators(self): """The ticket manipulators are called to valid the ticket.""" module = BatchModifyModule(self.env) self._insert_component('component3') self._insert_component('component4') self.env.enable_component(self.ticket_manipulators[0]) self.env.enable_component(self.ticket_manipulators[1]) req1 = MockRequest(self.env, authname='has_bm', method='POST', path_info='/batchmodify', args={ 'batchmod_value_component': 'component3', 'action': 'leave', 'selected_tickets': '1,2', }) self.assertTrue(module.match_request(req1)) with self.assertRaises(RequestDone): module.process_request(req1) self.assertEqual(1, len(req1.chrome['warnings'])) self.assertEqual( "The ticket field <strong>component</strong> is " "invalid: Invalid Component", unicode(req1.chrome['warnings'][0])) self.assertFieldValue(1, 'component', 'component1') self.assertFieldValue(2, 'component', 'component2') req2 = MockRequest(self.env, authname='has_bm', method='POST', path_info='/batchmodify', args={ 'batchmod_value_component': 'component4', 'action': 'leave', 'selected_tickets': '1,2', }) self.assertTrue(module.match_request(req2)) with self.assertRaises(RequestDone): module.process_request(req2) self.assertEqual([], req2.chrome['warnings']) self.assertFieldValue(1, 'component', 'component4') self.assertFieldValue(2, 'component', 'component4') req3 = MockRequest(self.env, authname='has_bm', method='POST', path_info='/batchmodify', args={ 'batchmod_value_comment': 'this comment has the badword!', 'batchmod_value_component': 'component3', 'action': 'leave', 'selected_tickets': '1,2', }) self.assertTrue(module.match_request(req3)) with self.assertRaises(RequestDone): module.process_request(req3) self.assertEqual( "The ticket comment is invalid: Word is not allowed " "in comment", unicode(req3.chrome['warnings'][0])) self.assertFieldValue(1, 'component', 'component4') self.assertFieldValue(2, 'component', 'component4') def test_post_process_request_add_template_data(self): """Template data added by post_process_request.""" self._insert_ticket("Ticket 1", status='new') self._insert_ticket("Ticket 2", status='new') req = MockRequest(self.env, path_info='/query') req.session['query_href'] = '/query?status=!closed' batch = BatchModifyModule(self.env) data_in = {'tickets': [{'id': 1}, {'id': 2}]} data_out = batch.post_process_request(req, 'query.html', data_in, 'text/html')[1] self.assertTrue(data_out['batch_modify']) self.assertEqual( ['leave', 'resolve', 'reassign', 'acknowledge', 'accept'], [a[0] for a in data_out['action_controls']]) def test_actions_added_by_additional_ticket_action_controllers(self): """Actions added by custom ticket action controller. Regression test for #12938. """ class TestOperation(Component): """TicketActionController that directly provides an action.""" implements(api.ITicketActionController) def get_ticket_actions(self, req, ticket): return [(0, 'test')] def get_all_status(self): return [] def render_ticket_action_control(self, req, ticket, action): return "test", '', "This is a null action." def get_ticket_changes(self, req, ticket, action): return {} def apply_action_side_effects(self, req, ticket, action): pass self._insert_ticket("Ticket 1", status='new') self._insert_ticket("Ticket 2", status='new') req = MockRequest(self.env, path_info='/query') req.session['query_href'] = '/query?status=!closed' batch = BatchModifyModule(self.env) data_in = {'tickets': [{'id': 1}, {'id': 2}]} self.env.config.set('ticket', 'workflow', 'ConfigurableTicketWorkflow, TestOperation') self.env.enable_component(TestOperation) data_out = batch.post_process_request(req, 'query.html', data_in, 'text/html')[1] self.assertEqual( ['leave', 'test', 'resolve', 'reassign', 'acknowledge', 'accept'], [a[0] for a in data_out['action_controls']]) def test_post_process_request_error_handling(self): """Exception not raised in post_process_request error handling. """ module = BatchModifyModule(self.env) req = MockRequest(self.env, path_info='/query') self.assertEqual((None, None, None), module.post_process_request(req, None, None, None))
class RenderResourceLinkTestCase(unittest.TestCase): class FakeResourceManager(Component): implements(resource.IResourceManager) def get_resource_realms(self): yield 'fake' def resource_exists(self, resource): return False if resource.id == 'missing' else True def setUp(self): self.env = EnvironmentStub(default_data=True) self.env.enable_component(self.FakeResourceManager) self.req = Mock(perm=MockPerm(), href=Href('/trac.cgi')) self.context = web_context(self.req) def tearDown(self): self.env.reset_db() def test_resource_exists_default_format(self): res = resource.Resource('fake', 'exists', version=1) link = resource.render_resource_link(self.env, self.context, res) html = tag.a('fake:exists', class_='fake', href='/trac.cgi/fake/exists?version=1') self.assertEqual(unicode(html), unicode(link)) def test_resource_exists_summary_format(self): res = resource.Resource('fake', 'exists', version=1) link = resource.render_resource_link(self.env, self.context, res, 'summary') html = tag.a('fake:exists at version 1', class_='fake', href='/trac.cgi/fake/exists?version=1') self.assertEqual(unicode(html), unicode(link)) def test_resource_missing_default_format(self): res = resource.Resource('fake', 'missing', version=1) link = resource.render_resource_link(self.env, self.context, res) html = tag.a('fake:missing', class_='fake missing', rel='nofollow', href='/trac.cgi/fake/missing?version=1') self.assertEqual(unicode(html), unicode(link)) def test_resource_missing_summary_format(self): res = resource.Resource('fake', 'missing', version=1) link = resource.render_resource_link(self.env, self.context, res, 'summary') html = tag.a('fake:missing at version 1', class_='fake missing', rel='nofollow', href='/trac.cgi/fake/missing?version=1') self.assertEqual(unicode(html), unicode(link)) def test_resource_has_no_manager_default_format(self): res = resource.Resource('unmanaged', 'exists', version=1) link = resource.render_resource_link(self.env, self.context, res) html = tag.a('unmanaged:exists', class_='unmanaged', href='/trac.cgi/unmanaged/exists?version=1') self.assertEqual(unicode(html), unicode(link)) def test_resource_has_no_manager_summary_format(self): res = resource.Resource('unmanaged', 'exists', version=1) link = resource.render_resource_link(self.env, self.context, res, 'summary') html = tag.a('unmanaged:exists at version 1', class_='unmanaged', href='/trac.cgi/unmanaged/exists?version=1') self.assertEqual(unicode(html), unicode(link))
class AuthenticateTestCase(unittest.TestCase): authenticators = {} request_handlers = [] @classmethod def setUpClass(cls): class UnsuccessfulAuthenticator(Component): implements(IAuthenticator) def authenticate(self, req): return None class RaisingAuthenticator(Component): implements(IAuthenticator) def authenticate(self, req): raise TracError("Bad attempt") class SuccessfulAuthenticator1(Component): implements(IAuthenticator) def authenticate(self, req): return 'user1' class SuccessfulAuthenticator2(Component): implements(IAuthenticator) def authenticate(self, req): return 'user2' class AuthenticateRequestHandler(Component): implements(IRequestHandler) def __init__(self): self.calls = 0 def match_request(self, req): return bool(req.perm) def process_request(self, req): self.calls += 1 req.authname req.send('') cls.authenticators['success1'] = SuccessfulAuthenticator1 cls.authenticators['success2'] = SuccessfulAuthenticator2 cls.authenticators['unsuccess'] = UnsuccessfulAuthenticator cls.authenticators['raising'] = RaisingAuthenticator cls.request_handlers = [AuthenticateRequestHandler] @classmethod def tearDownClass(cls): from trac.core import ComponentMeta for component in cls.authenticators.values() + cls.request_handlers: ComponentMeta.deregister(component) def setUp(self): self.env = EnvironmentStub(enable=('trac.web.main.*', )) self.req = MockRequest(self.env) self.request_dispatcher = RequestDispatcher(self.env) def test_authenticate_returns_first_successful(self): self.env.enable_component(self.authenticators['success1']) self.env.enable_component(self.authenticators['success2']) self.assertEqual(2, len(self.request_dispatcher.authenticators)) self.assertIsInstance(self.request_dispatcher.authenticators[0], self.authenticators['success1']) self.assertIsInstance(self.request_dispatcher.authenticators[1], self.authenticators['success2']) self.assertEqual('user1', self.request_dispatcher.authenticate(self.req)) def test_authenticate_skips_unsuccessful(self): self.env.enable_component(self.authenticators['unsuccess']) self.env.enable_component(self.authenticators['success1']) self.assertEqual(2, len(self.request_dispatcher.authenticators)) self.assertIsInstance(self.request_dispatcher.authenticators[0], self.authenticators['unsuccess']) self.assertIsInstance(self.request_dispatcher.authenticators[1], self.authenticators['success1']) self.assertEqual('user1', self.request_dispatcher.authenticate(self.req)) def test_authenticate_raises(self): self.env.enable_component(self.authenticators['raising']) self.env.enable_component(self.authenticators['success1']) self.assertEqual(2, len(self.request_dispatcher.authenticators)) self.assertIsInstance(self.request_dispatcher.authenticators[0], self.authenticators['raising']) self.assertIsInstance(self.request_dispatcher.authenticators[1], self.authenticators['success1']) self.assertEqual('anonymous', self.request_dispatcher.authenticate(self.req)) self.assertEqual(1, len(self.req.chrome['warnings'])) expected = "Can't authenticate using RaisingAuthenticator: " for level, message in self.env.log_messages: if expected in message.split('\n'): self.assertEqual('ERROR', level) break else: self.fail("Expected log message not found: \"%s\"" % expected) def test_authenticate_once(self): self.env.enable_component(self.authenticators['success1']) self.env.enable_component(self.request_handlers[0]) self.env.config.set('trac', 'default_handler', 'AuthenticateRequestHandler') self.request_dispatcher.set_default_callbacks(self.req) with self.assertRaises(RequestDone): self.request_dispatcher.dispatch(self.req) self.assertEqual(1, len(self.request_dispatcher.authenticators)) self.assertEqual(1, len(self.request_dispatcher.handlers)) self.assertEqual(1, self.request_dispatcher.handlers[0].calls)
class PostProcessRequestTestCase(unittest.TestCase): """Test cases for handling of the optional `method` argument in RequestDispatcher._post_process_request.""" request_filter = {} @classmethod def setUpClass(cls): class RequestFilter4Arg(Component): implements(IRequestFilter) def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, metadata): return template, data, metadata class RequestFilter5Arg(Component): implements(IRequestFilter) def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, metadata, method=None): return template, data, metadata, method class RequestFilter5ArgXml(Component): implements(IRequestFilter) def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, metadata, method=None): return template, data, metadata, 'xml' class RequestFilterRedirectOnPermError(Component): implements(IRequestHandler, IRequestFilter) def match_request(self, req): return re.match(r'/perm-error', req.path_info) def process_request(self, req): req.entered_process_request = True raise PermissionError("No permission to view") def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, content_type): if (template, data, content_type) == (None, None, None): req.entered_post_process_request = True req.redirect(req.href('/redirect-target')) return template, data, content_type cls.request_filter['4Arg'] = RequestFilter4Arg cls.request_filter['5Arg'] = RequestFilter5Arg cls.request_filter['5ArgXml'] = RequestFilter5ArgXml cls.request_filter['RedirectOnPermError'] = \ RequestFilterRedirectOnPermError @classmethod def tearDownClass(cls): from trac.core import ComponentMeta for component in cls.request_filter.values(): ComponentMeta.deregister(component) def setUp(self): self.env = EnvironmentStub(enable=('trac.web.main.*', )) self.req = MockRequest(self.env) def test_no_request_filters_request_handler_returns_method_false(self): """IRequestHandler doesn't return `method` and no IRequestFilters are registered. The `method` is set to `None`. """ args = ('template.html', {}, 'text/html') request_dispatcher = RequestDispatcher(self.env) resp = request_dispatcher._post_process_request(self.req, *args) self.assertEqual(0, len(request_dispatcher.filters)) self.assertEqual(4, len(resp)) self.assertEqual(args + (None, ), resp) # TODO (1.5.1) remove old API (genshi style) args = ('template.html', {}, {'content_type': 'text/html'}) request_dispatcher = RequestDispatcher(self.env) resp = request_dispatcher._post_process_request(self.req, *args) self.assertEqual(0, len(request_dispatcher.filters)) self.assertEqual(4, len(resp)) self.assertEqual(args + (None, ), resp) def test_no_request_filters_request_handler_returns_method_true(self): """IRequestHandler returns `method` and no IRequestFilters are registered. The `method` is forwarded. """ args = ('template.html', {}, 'text/html', 'xhtml') request_dispatcher = RequestDispatcher(self.env) resp = request_dispatcher._post_process_request(self.req, *args) self.assertEqual(0, len(request_dispatcher.filters)) self.assertEqual(4, len(resp)) self.assertEqual(args, resp) # TODO (1.5.1) remove old API (genshi style) args = ('template.html', {}, {'content_type': 'text/html'}, 'xhtml') resp = request_dispatcher._post_process_request(self.req, *args) self.assertEqual(0, len(request_dispatcher.filters)) self.assertEqual(4, len(resp)) self.assertEqual(args, resp) def test_4arg_post_process_request_request_handler_returns_method_false( self): """IRequestHandler doesn't return `method` and IRequestFilter doesn't accept `method` as an argument. The `method` is set to `None`. """ self.env.enable_component(self.request_filter['4Arg']) request_dispatcher = RequestDispatcher(self.env) args = ('template.html', {}, 'text/html') resp = request_dispatcher._post_process_request(self.req, *args) self.assertEqual(1, len(request_dispatcher.filters)) self.assertEqual(4, len(resp)) self.assertEqual(args + (None, ), resp) # TODO (1.5.1) remove old API (genshi style) args = ('template.html', {}, {'content_type': 'text/html'}) resp = request_dispatcher._post_process_request(self.req, *args) self.assertEqual(1, len(request_dispatcher.filters)) self.assertEqual(4, len(resp)) self.assertEqual(args + (None, ), resp) def test_4arg_post_process_request_request_handler_returns_method_true( self): """IRequestHandler returns `method` and IRequestFilter doesn't accept the argument. The `method` argument is forwarded over IRequestFilter implementations that don't accept the argument. """ self.env.enable_component(self.request_filter['4Arg']) request_dispatcher = RequestDispatcher(self.env) args = ('template.html', {}, 'text/html', 'xhtml') resp = request_dispatcher._post_process_request(self.req, *args) self.assertEqual(1, len(request_dispatcher.filters)) self.assertEqual(4, len(resp)) self.assertEqual(args, resp) # TODO (1.5.1) remove old API (genshi style) args = ('template.html', {}, {'content_type': 'text/html'}, 'xhtml') resp = request_dispatcher._post_process_request(self.req, *args) self.assertEqual(1, len(request_dispatcher.filters)) self.assertEqual(4, len(resp)) self.assertEqual(args, resp) def test_5arg_post_process_request_request_handler_returns_method_false( self): """IRequestHandler doesn't return `method` and IRequestFilter accepts `method` as an argument. The `method` is set to `None`. """ self.env.enable_component(self.request_filter['5Arg']) request_dispatcher = RequestDispatcher(self.env) args = ('template.html', {}, 'text/html') resp = request_dispatcher._post_process_request(self.req, *args) self.assertEqual(1, len(request_dispatcher.filters)) self.assertEqual(4, len(resp)) self.assertEqual(args[:3] + (None, ), resp) def test_5arg_post_process_request_request_handler_returns_method_true( self): """IRequestHandler returns `method` and IRequestFilter accepts the argument. The `method` argument is passed through IRequestFilter implementations. """ self.env.enable_component(self.request_filter['5Arg']) request_dispatcher = RequestDispatcher(self.env) args = ('template.html', {}, 'text/html', 'xhtml') resp = request_dispatcher._post_process_request(self.req, *args) self.assertEqual(1, len(request_dispatcher.filters)) self.assertEqual(4, len(resp)) self.assertEqual(args, resp) # TODO (1.5.1) remove old API (genshi style) args = ('template.html', {}, {'content_type': 'text/html'}, 'xhtml') resp = request_dispatcher._post_process_request(self.req, *args) self.assertEqual(1, len(request_dispatcher.filters)) self.assertEqual(4, len(resp)) self.assertEqual(args, resp) def test_5arg_post_process_request_request_handler_adds_method(self): """IRequestFilter adds `method` not returned by IRequestHandler. """ self.env.enable_component(self.request_filter['5ArgXml']) args = ('template.html', {}, 'text/html') request_dispatcher = RequestDispatcher(self.env) resp = request_dispatcher._post_process_request(self.req, *args) self.assertEqual(1, len(request_dispatcher.filters)) self.assertEqual(4, len(resp)) self.assertEqual(args[:3] + ('xml', ), resp) # TODO (1.5.1) remove old API (genshi style) args = ('template.html', {}, {'content_type': 'text/html'}) resp = request_dispatcher._post_process_request(self.req, *args) self.assertEqual(1, len(request_dispatcher.filters)) self.assertEqual(4, len(resp)) self.assertEqual(args[:3] + ('xml', ), resp) def test_5arg_post_process_request_request_handler_modifies_method(self): """IRequestFilter modifies `method` returned by IRequestHandler. """ self.env.enable_component(self.request_filter['5ArgXml']) args = ('template.html', {}, 'text/html', 'xhtml') request_dispatcher = RequestDispatcher(self.env) resp = request_dispatcher._post_process_request(self.req, *args) self.assertEqual(1, len(request_dispatcher.filters)) self.assertEqual(4, len(resp)) self.assertEqual(args[:3] + ('xml', ), resp) # TODO (1.5.1) remove old API (genshi style) args = ('template.html', {}, {'content_type': 'text/html'}, 'xhtml') resp = request_dispatcher._post_process_request(self.req, *args) self.assertEqual(1, len(request_dispatcher.filters)) self.assertEqual(4, len(resp)) self.assertEqual(args[:3] + ('xml', ), resp) def test_redirect_on_permission_error(self): """The post_process_request method can redirect during exception handling from an exception raised in process_request. """ self.env.enable_component(self.request_filter['RedirectOnPermError']) dispatcher = RequestDispatcher(self.env) req = MockRequest(self.env, method='GET', path_info='/perm-error') req.entered_process_request = False req.entered_post_process_request = False try: dispatcher.dispatch(req) except RequestDone: pass else: self.fail("RequestDone not raised") self.assertTrue(req.entered_process_request) self.assertTrue(req.entered_post_process_request)
class UpgradeTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(path=mkdtemp()) self.env.config.filename = os.path.join(self.env.path, 'trac.ini') self.env.config.set('trac', 'repository_sync_per_request', 'repos1, repos3, repos5') def tearDown(self): self.env.reset_db_and_disk() def test_saves_backup(self): """Backup file is saved during upgrade.""" config = self.env.config db32.do_upgrade(self.env, VERSION, None) self.assertTrue(os.path.exists(config.filename + '.db32.bak')) def test_repository_sync_per_request_default_value(self): """The default repository sync_per_request attribute is set to true when repository_sync_per_request is not set in trac.ini. """ self.env.config.remove('trac', 'repository_sync_per_request') repositories = self.env.config['repositories'] repositories.set('.dir', '/var/svn') repositories.set('.type', 'svn') repositories.set('git.dir', '/var/git') repositories.set('git.type', 'git') db32.do_upgrade(self.env, VERSION, None) repositories = self.env.config['repositories'] self.assertIn('.sync_per_request', repositories) self.assertTrue(repositories.getbool('.sync_per_request')) self.assertNotIn('git.sync_per_request', repositories) self.assertFalse(repositories.getbool('git.sync_per_request')) def test_repository_sync_per_request_default_value_with_db(self): """The default repository sync_per_request attribute is set to true when repository_sync_per_request is not set in trac.ini. """ self.env.config.remove('trac', 'repository_sync_per_request') # directly insert repository records instead of DbRepositoryProvider # to avoid a TracError "The repository type 'svn' is not supported" with self.env.db_transaction as db: db.executemany( """INSERT INTO repository (id,name,value) VALUES (%s,%s,%s)""", [(1, 'name', ''), (1, 'dir', '/var/svn'), (1, 'type', 'svn'), (2, 'name', 'git'), (2, 'dir', '/var/git'), (2, 'type', 'git')]) db32.do_upgrade(self.env, VERSION, None) repos = RepositoryManager(self.env).get_all_repositories() self.assertIn('', repos) self.assertTrue(repos['']['sync_per_request']) self.assertEqual( '1', self.env.db_query(""" SELECT value FROM repository WHERE id=1 AND name='sync_per_request'""")[0][0]) self.assertIn('git', repos) self.assertFalse(repos['git']['sync_per_request']) self.assertIsNone( self.env.db_query(""" SELECT value FROM repository WHERE id=2 AND name='sync_per_request'""")[0][0]) def test_gitweb_configuration_moved(self): """The Gitweb configuration is moved from the [git] section to the [gitweb-repositories] section. """ projects_list = os.path.join(self.env.path, 'projects_list') projects_base = os.path.dirname(projects_list) projects_url = 'http://localhost/%s' with open(projects_list, 'w') as f: f.write(""" repos1 user1+<*****@*****.**> repos2 """) config = self.env.config['git'] config.set('projects_list', projects_list) config.set('projects_base', projects_base) config.set('projects_url', projects_url) repos1_dir = os.path.join(projects_base, 'repos1') repos2_dir = os.path.join(projects_base, 'repos2') db32.do_upgrade(self.env, VERSION, None) repos = RepositoryManager(self.env).get_all_repositories() self.assertIn('repos1', repos) self.assertTrue(repos['repos1']['sync_per_request']) self.assertEqual(repos1_dir, repos['repos1']['dir']) self.assertEqual('http://localhost/repos1', repos['repos1']['url']) self.assertIn('repos2', repos) self.assertFalse(repos['repos2']['sync_per_request']) self.assertEqual(repos2_dir, repos['repos2']['dir']) self.assertEqual('http://localhost/repos2', repos['repos2']['url']) config = self.env.config['gitweb-repositories'] self.assertNotIn('projects_list', self.env.config) self.assertNotIn('projects_base', self.env.config) self.assertNotIn('projects_url', self.env.config) self.assertNotIn('repository_sync_per_request', self.env.config) self.assertEqual(projects_list, config.get('projects_list')) self.assertEqual(projects_base, config.get('projects_base')) self.assertEqual(projects_url, config.get('projects_url')) self.assertEqual('repos1', config.get('sync_per_request')) def test_repository_providers_disabled(self): """Repository configuration is rewritten when repository providers are disabled. """ projects_list = os.path.join(self.env.path, 'projects_list') projects_base = os.path.dirname(projects_list) projects_url = 'http://localhost/%s' with open(projects_list, 'w') as f: f.write(""" repos1 user1+<*****@*****.**> repos2 """) config = self.env.config['git'] config.set('projects_list', projects_list) config.set('projects_base', projects_base) config.set('projects_url', projects_url) db_provider = DbRepositoryProvider(self.env) db_provider.add_repository('repos3', '/var/git/repos3', 'git') db_provider.add_repository('repos4', '/var/git/repos4', 'git') config = self.env.config['repositories'] config.set('repos5.dir', '/var/svn/repos4') config.set('repos5.type', 'svn') config.set('repos6.dir', '/var/svn/repos5') config.set('repos6.type', 'svn') self.env.disable_component(GitwebProjectsRepositoryProvider) self.env.disable_component(DbRepositoryProvider) self.env.disable_component(RepositoryManager) db32.do_upgrade(self.env, VERSION, None) self.env.enable_component(GitwebProjectsRepositoryProvider) self.env.enable_component(DbRepositoryProvider) self.env.enable_component(RepositoryManager) repos = RepositoryManager(self.env).get_all_repositories() config = self.env.config['gitweb-repositories'] self.assertEqual(projects_list, config.get('projects_list')) self.assertEqual(projects_base, config.get('projects_base')) self.assertEqual(projects_url, config.get('projects_url')) self.assertEqual('repos1', config.get('sync_per_request')) self.assertIn('repos1', repos) self.assertTrue(repos['repos1']['sync_per_request']) self.assertIn('repos2', repos) self.assertFalse(repos['repos2']['sync_per_request']) self.assertIn('repos3', repos) self.assertTrue(repos['repos3']['sync_per_request']) self.assertIn('repos4', repos) self.assertFalse(repos['repos4']['sync_per_request']) self.assertIn('repos5', repos) self.assertTrue(repos['repos5']['sync_per_request']) self.assertIn('repos6', repos) self.assertFalse(repos['repos6']['sync_per_request'])
class EnvironmentUpgradeTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub() def tearDown(self): self.env.reset_db() def test_multiple_upgrade_participants(self): class Participant1(Component): implements(IEnvironmentSetupParticipant) def environment_created(self): pass def environment_needs_upgrade(self): return True def upgrade_environment(self): insert_value('value1', 1) class Participant2(Component): implements(IEnvironmentSetupParticipant) def environment_created(self): pass def environment_needs_upgrade(self): return True def upgrade_environment(self): insert_value('value2', 2) def insert_value(name, value): with self.env.db_transaction as db: db( """ INSERT INTO {0} (name, value) VALUES (%s, %s) """.format(db.quote('system')), (name, value)) def select_value(name): with self.env.db_query as db: for value, in db( """ SELECT value FROM {0} WHERE name=%s """.format(db.quote('system')), (name, )): return value self.env.enable_component(Participant1) self.env.enable_component(Participant2) self.assertTrue(self.env.needs_upgrade()) self.assertTrue(self.env.upgrade()) self.assertEqual('1', select_value('value1')) self.assertEqual('2', select_value('value2')) def test_upgrade_environment(self): """EnvironmentSetupParticipants are called only if environment_needs_upgrade returns True for the participant. """ class SetupParticipantA(Component): implements(IEnvironmentSetupParticipant) called = False def environment_created(self): pass def environment_needs_upgrade(self): return True def upgrade_environment(self): self.called = True class SetupParticipantB(Component): implements(IEnvironmentSetupParticipant) called = False def environment_created(self): pass def environment_needs_upgrade(self): return False def upgrade_environment(self): self.called = True self.env.enable_component(SetupParticipantA) self.env.enable_component(SetupParticipantB) participant_a = SetupParticipantA(self.env) participant_b = SetupParticipantB(self.env) self.assertTrue(self.env.needs_upgrade()) self.env.upgrade() self.assertTrue(participant_a.called) self.assertFalse(participant_b.called)
class AttachmentTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub() self.env.path = tempfile.mkdtemp(prefix='trac-tempenv-') self.attachments_dir = os.path.join(self.env.path, 'files', 'attachments') self.env.enable_component(TicketOnlyViewsTicket) self.env.config.set('trac', 'permission_policies', 'TicketOnlyViewsTicket, LegacyAttachmentPolicy') self.env.config.set('attachment', 'max_size', 512) self.perm = PermissionCache(self.env) self.datetime = datetime(2001, 1, 1, 1, 1, 1, 0, utc) with self.env.db_transaction as db: db("INSERT INTO wiki (name,version) VALUES ('WikiStart',1)") db("INSERT INTO wiki (name,version) VALUES ('SomePage',1)") db("INSERT INTO ticket (id) VALUES (42)") db("INSERT INTO ticket (id) VALUES (43)") db("INSERT INTO attachment VALUES (%s,%s,%s,%s,%s,%s,%s,%s)", ('ticket', '43', 'foo.txt', 8, to_utimestamp( self.datetime), 'A comment', 'joe', '::1')) def tearDown(self): self.env.reset_db_and_disk() def test_new_attachment(self): attachment = Attachment(self.env, 'ticket', 42) self.assertEqual(None, attachment.filename) self.assertEqual(None, attachment.description) self.assertEqual(None, attachment.size) self.assertEqual(None, attachment.date) self.assertEqual(None, attachment.author) self.assertEqual(None, attachment.ipnr) self.assertEqual('<Attachment None>', repr(attachment)) def test_existing_attachment(self): attachment = Attachment(self.env, 'ticket', 43, 'foo.txt') self.assertEqual('foo.txt', attachment.filename) self.assertEqual('A comment', attachment.description) self.assertEqual(8, attachment.size) self.assertEqual(self.datetime, attachment.date) self.assertEqual('joe', attachment.author) self.assertEqual('::1', attachment.ipnr) self.assertEqual("<Attachment u'foo.txt'>", repr(attachment)) def test_existing_attachment_from_resource(self): resource = Resource('ticket', 43).child('attachment', 'foo.txt') attachment = Attachment(self.env, resource) self.assertEqual('foo.txt', attachment.filename) self.assertEqual('A comment', attachment.description) self.assertEqual(8, attachment.size) self.assertEqual(self.datetime, attachment.date) self.assertEqual('joe', attachment.author) self.assertEqual('::1', attachment.ipnr) self.assertEqual("<Attachment u'foo.txt'>", repr(attachment)) def test_get_path(self): attachment = Attachment(self.env, 'ticket', 42) attachment.filename = 'foo.txt' self.assertEqual( os.path.join(self.attachments_dir, 'ticket', hashes['42'][0:3], hashes['42'], hashes['foo.txt'] + '.txt'), attachment.path) attachment = Attachment(self.env, 'wiki', 'SomePage') attachment.filename = 'bar.jpg' self.assertEqual( os.path.join(self.attachments_dir, 'wiki', hashes['SomePage'][0:3], hashes['SomePage'], hashes['bar.jpg'] + '.jpg'), attachment.path) def test_path_extension(self): attachment = Attachment(self.env, 'ticket', 42) attachment.filename = 'Foo.Mp3' self.assertEqual( os.path.join(self.attachments_dir, 'ticket', hashes['42'][0:3], hashes['42'], hashes['Foo.Mp3'] + '.Mp3'), attachment.path) attachment = Attachment(self.env, 'wiki', 'SomePage') attachment.filename = 'bar.7z' self.assertEqual( os.path.join(self.attachments_dir, 'wiki', hashes['SomePage'][0:3], hashes['SomePage'], hashes['bar.7z'] + '.7z'), attachment.path) attachment = Attachment(self.env, 'ticket', 42) attachment.filename = 'foo.$$$' self.assertEqual( os.path.join(self.attachments_dir, 'ticket', hashes['42'][0:3], hashes['42'], hashes['foo.$$$']), attachment.path) attachment = Attachment(self.env, 'wiki', 'SomePage') attachment.filename = u'bar.aäc' self.assertEqual( os.path.join(self.attachments_dir, 'wiki', hashes['SomePage'][0:3], hashes['SomePage'], hashes[u'bar.aäc']), attachment.path) def test_get_path_encoded(self): attachment = Attachment(self.env, 'ticket', 42) attachment.filename = 'Teh foo.txt' self.assertEqual( os.path.join(self.attachments_dir, 'ticket', hashes['42'][0:3], hashes['42'], hashes['Teh foo.txt'] + '.txt'), attachment.path) attachment = Attachment(self.env, 'wiki', u'ÜberSicht') attachment.filename = 'Teh bar.jpg' self.assertEqual( os.path.join(self.attachments_dir, 'wiki', hashes[u'ÜberSicht'][0:3], hashes[u'ÜberSicht'], hashes['Teh bar.jpg'] + '.jpg'), attachment.path) def test_select_empty(self): self.assertRaises(StopIteration, Attachment.select(self.env, 'ticket', 42).next) self.assertRaises(StopIteration, Attachment.select(self.env, 'wiki', 'SomePage').next) def test_insert(self): attachment = Attachment(self.env, 'ticket', 42) attachment.insert('foo.txt', StringIO(''), 0, 1) attachment = Attachment(self.env, 'ticket', 42) attachment.insert('bar.jpg', StringIO(''), 0, 2) attachments = Attachment.select(self.env, 'ticket', 42) self.assertEqual('foo.txt', attachments.next().filename) self.assertEqual('bar.jpg', attachments.next().filename) self.assertRaises(StopIteration, attachments.next) def test_insert_unique(self): attachment = Attachment(self.env, 'ticket', 42) attachment.insert('foo.txt', StringIO(''), 0) self.assertEqual('foo.txt', attachment.filename) attachment = Attachment(self.env, 'ticket', 42) attachment.insert('foo.txt', StringIO(''), 0) self.assertEqual('foo.2.txt', attachment.filename) self.assertEqual( os.path.join(self.attachments_dir, 'ticket', hashes['42'][0:3], hashes['42'], hashes['foo.2.txt'] + '.txt'), attachment.path) self.assertTrue(os.path.exists(attachment.path)) def test_insert_outside_attachments_dir(self): attachment = Attachment(self.env, '../../../../../sth/private', 42) self.assertRaises(TracError, attachment.insert, 'foo.txt', StringIO(''), 0) def test_delete(self): attachment1 = Attachment(self.env, 'wiki', 'SomePage') attachment1.insert('foo.txt', StringIO(''), 0) attachment2 = Attachment(self.env, 'wiki', 'SomePage') attachment2.insert('bar.jpg', StringIO(''), 0) attachments = Attachment.select(self.env, 'wiki', 'SomePage') self.assertEqual(2, len(list(attachments))) attachment1.delete() attachment2.delete() self.assertFalse(os.path.exists(attachment1.path)) self.assertFalse(os.path.exists(attachment2.path)) attachments = Attachment.select(self.env, 'wiki', 'SomePage') self.assertEqual(0, len(list(attachments))) def test_delete_file_gone(self): """ Verify that deleting an attachment works even if the referenced file doesn't exist for some reason. """ attachment = Attachment(self.env, 'wiki', 'SomePage') attachment.insert('foo.txt', StringIO(''), 0) os.unlink(attachment.path) attachment.delete() def test_reparent(self): attachment1 = Attachment(self.env, 'wiki', 'SomePage') attachment1.insert('foo.txt', StringIO(''), 0) path1 = attachment1.path attachment2 = Attachment(self.env, 'wiki', 'SomePage') attachment2.insert('bar.jpg', StringIO(''), 0) attachments = Attachment.select(self.env, 'wiki', 'SomePage') self.assertEqual(2, len(list(attachments))) attachments = Attachment.select(self.env, 'ticket', 123) self.assertEqual(0, len(list(attachments))) self.assertTrue( os.path.exists(path1) and os.path.exists(attachment2.path)) attachment1.reparent('ticket', 123) self.assertEqual('ticket', attachment1.parent_realm) self.assertEqual('ticket', attachment1.resource.parent.realm) self.assertEqual('123', attachment1.parent_id) self.assertEqual('123', attachment1.resource.parent.id) attachments = Attachment.select(self.env, 'wiki', 'SomePage') self.assertEqual(1, len(list(attachments))) attachments = Attachment.select(self.env, 'ticket', 123) self.assertEqual(1, len(list(attachments))) self.assertFalse( os.path.exists(path1) and os.path.exists(attachment1.path)) self.assertTrue(os.path.exists(attachment2.path)) def test_legacy_permission_on_parent(self): """Ensure that legacy action tests are done on parent. As `ATTACHMENT_VIEW` maps to `TICKET_VIEW`, the `TICKET_VIEW` is tested against the ticket's resource.""" attachment = Attachment(self.env, 'ticket', 42) self.assertTrue('ATTACHMENT_VIEW' in self.perm(attachment.resource)) def test_resource_exists(self): att = Attachment(self.env, 'wiki', 'WikiStart') att.insert('file.txt', StringIO(''), 1) self.assertTrue(resource_exists(self.env, att.resource))
class EnvironmentUpgradeTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub() def tearDown(self): self.env.reset_db() def test_multiple_upgrade_participants(self): class Participant1(Component): implements(IEnvironmentSetupParticipant) def environment_created(self): pass def environment_needs_upgrade(self, db): return True def upgrade_environment(self, db): insert_value('value1', 1) class Participant2(Component): implements(IEnvironmentSetupParticipant) def environment_created(self): pass def environment_needs_upgrade(self, db): return True def upgrade_environment(self, db): insert_value('value2', 2) def insert_value(name, value): self.env.db_transaction( """ INSERT INTO system (name, value) VALUES (%s, %s) """, (name, value)) def select_value(name): for value, in self.env.db_query( """ SELECT value FROM system WHERE name=%s """, (name, )): return value self.env.enable_component(Participant1) self.env.enable_component(Participant2) self.assertTrue(self.env.needs_upgrade()) self.assertTrue(self.env.upgrade()) self.assertEqual('1', select_value('value1')) self.assertEqual('2', select_value('value2')) def test_needs_upgrade_legacy_participant(self): """For backward compatibility with plugin, environment_needs_upgrade with a `db` argument is deprecated but still allowed.""" participants = self.env.setup_participants needs_upgrade = self.env.needs_upgrade() class LegacyParticipant(Component): implements(IEnvironmentSetupParticipant) def environment_created(self): pass def environment_needs_upgrade(self, db): return True def upgrade_environment(self, db): pass self.env.enable_component(LegacyParticipant) self.assertFalse(needs_upgrade) self.assertEqual( len(participants) + 1, len(self.env.setup_participants)) self.assertTrue(self.env.needs_upgrade()) def test_upgrade_legacy_participant(self): """For backward compatibility with plugin, upgrade with a `db` argument is deprecated but still allowed.""" participants = self.env.setup_participants class LegacyParticipant(Component): implements(IEnvironmentSetupParticipant) def environment_created(self): pass def environment_needs_upgrade(self, db): return True def upgrade_environment(self, db): pass self.env.enable_component(LegacyParticipant) self.assertEqual( len(participants) + 1, len(self.env.setup_participants)) self.assertTrue(self.env.needs_upgrade()) self.assertTrue(self.env.upgrade())
class PostProcessRequestTestCase(unittest.TestCase): """Test cases for handling of the optional `method` argument in RequestDispatcher._post_process_request.""" request_filter = {} @classmethod def setUpClass(cls): class RequestFilterReturns2Args(Component): implements(IRequestFilter) def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, metadata): if metadata is not None: metadata['text'] = True return template, data class RequestFilterReturns3Args(Component): implements(IRequestFilter) def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, metadata): if metadata is not None: metadata['domain'] = 'en_US' return template, data, metadata class RequestFilterRedirectOnPermError(Component): implements(IRequestHandler, IRequestFilter) def match_request(self, req): return re.match(r'/perm-error', req.path_info) def process_request(self, req): req.entered_process_request = True raise PermissionError("No permission to view") def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, content_type): if (template, data, content_type) == (None, None, None): req.entered_post_process_request = True req.redirect(req.href('/redirect-target')) return template, data, content_type cls.request_filter['2Arg'] = RequestFilterReturns2Args cls.request_filter['3Arg'] = RequestFilterReturns3Args cls.request_filter['RedirectOnPermError'] = \ RequestFilterRedirectOnPermError @classmethod def tearDownClass(cls): from trac.core import ComponentMeta for component in cls.request_filter.values(): ComponentMeta.deregister(component) def setUp(self): self.env = EnvironmentStub(enable=('trac.web.main.*', self.request_filter['2Arg'], self.request_filter['3Arg'])) self.req = MockRequest(self.env) def test_post_process_request_error_handling(self): """The post_process_request methods are called with a triple of `None` values when an exception is raised in process_request or post_process_request, or an empty response is returned by process_request. """ request_dispatcher = RequestDispatcher(self.env) args = (None, ) * 3 resp = request_dispatcher._post_process_request(self.req, *args) self.assertEqual(2, len(request_dispatcher.filters)) self.assertEqual((None, None, None), resp) def test_post_process_request_with_2_args(self): request_dispatcher = RequestDispatcher(self.env) args = ('template.html', {}) resp = request_dispatcher._post_process_request(self.req, *args) self.assertEqual(2, len(request_dispatcher.filters)) self.assertEqual(3, len(resp)) self.assertEqual(2, len(resp[2])) self.assertEqual('en_US', resp[2]['domain']) self.assertTrue(resp[2]['text']) def test_post_process_request_with_3_args(self): request_dispatcher = RequestDispatcher(self.env) args = ('template.html', {}, {'content_type': 'text/html'}) resp = request_dispatcher._post_process_request(self.req, *args) self.assertEqual(2, len(request_dispatcher.filters)) self.assertEqual(3, len(resp)) self.assertEqual('text/html', resp[2]['content_type']) self.assertEqual('en_US', resp[2]['domain']) self.assertTrue(resp[2]['text']) self.assertEqual(args, resp) def test_redirect_on_permission_error(self): """The post_process_request method can redirect during exception handling from an exception raised in process_request. """ self.env.enable_component(self.request_filter['RedirectOnPermError']) dispatcher = RequestDispatcher(self.env) req = MockRequest(self.env, method='GET', path_info='/perm-error') req.entered_process_request = False req.entered_post_process_request = False try: dispatcher.dispatch(req) except RequestDone: pass else: self.fail("RequestDone not raised") self.assertTrue(req.entered_process_request) self.assertTrue(req.entered_post_process_request)
class AttachmentTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub() self.env.path = tempfile.mkdtemp(prefix='trac-tempenv-') self.attachments_dir = os.path.join(self.env.path, 'files', 'attachments') self.env.enable_component(TicketOnlyViewsTicket) self.env.config.set('trac', 'permission_policies', 'TicketOnlyViewsTicket, LegacyAttachmentPolicy') self.env.config.set('attachment', 'max_size', 512) self.perm = PermissionCache(self.env) self.datetime = datetime(2001, 1, 1, 1, 1, 1, 0, utc) with self.env.db_transaction as db: db("INSERT INTO wiki (name,version) VALUES ('WikiStart',1)") db("INSERT INTO wiki (name,version) VALUES ('SomePage',1)") db("INSERT INTO ticket (id) VALUES (42)") db("INSERT INTO ticket (id) VALUES (43)") db("INSERT INTO attachment VALUES (%s,%s,%s,%s,%s,%s,%s,%s)", ('ticket', '43', 'foo.txt', 8, to_utimestamp(self.datetime), 'A comment', 'joe', '::1')) def tearDown(self): shutil.rmtree(self.env.path) self.env.reset_db() def test_new_attachment(self): attachment = Attachment(self.env, 'ticket', 42) self.assertEqual(None, attachment.filename) self.assertEqual(None, attachment.description) self.assertEqual(None, attachment.size) self.assertEqual(None, attachment.date) self.assertEqual(None, attachment.author) self.assertEqual(None, attachment.ipnr) self.assertEqual('<Attachment None>', repr(attachment)) def test_existing_attachment(self): attachment = Attachment(self.env, 'ticket', 43, 'foo.txt') self.assertEqual('foo.txt', attachment.filename) self.assertEqual('A comment', attachment.description) self.assertEqual(8, attachment.size) self.assertEqual(self.datetime, attachment.date) self.assertEqual('joe', attachment.author) self.assertEqual('::1', attachment.ipnr) self.assertEqual("<Attachment u'foo.txt'>", repr(attachment)) def test_existing_attachment_from_resource(self): resource = Resource('ticket', 43).child('attachment', 'foo.txt') attachment = Attachment(self.env, resource) self.assertEqual('foo.txt', attachment.filename) self.assertEqual('A comment', attachment.description) self.assertEqual(8, attachment.size) self.assertEqual(self.datetime, attachment.date) self.assertEqual('joe', attachment.author) self.assertEqual('::1', attachment.ipnr) self.assertEqual("<Attachment u'foo.txt'>", repr(attachment)) def test_get_path(self): attachment = Attachment(self.env, 'ticket', 42) attachment.filename = 'foo.txt' self.assertEqual(os.path.join(self.attachments_dir, 'ticket', hashes['42'][0:3], hashes['42'], hashes['foo.txt'] + '.txt'), attachment.path) attachment = Attachment(self.env, 'wiki', 'SomePage') attachment.filename = 'bar.jpg' self.assertEqual(os.path.join(self.attachments_dir, 'wiki', hashes['SomePage'][0:3], hashes['SomePage'], hashes['bar.jpg'] + '.jpg'), attachment.path) def test_path_extension(self): attachment = Attachment(self.env, 'ticket', 42) attachment.filename = 'Foo.Mp3' self.assertEqual(os.path.join(self.attachments_dir, 'ticket', hashes['42'][0:3], hashes['42'], hashes['Foo.Mp3'] + '.Mp3'), attachment.path) attachment = Attachment(self.env, 'wiki', 'SomePage') attachment.filename = 'bar.7z' self.assertEqual(os.path.join(self.attachments_dir, 'wiki', hashes['SomePage'][0:3], hashes['SomePage'], hashes['bar.7z'] + '.7z'), attachment.path) attachment = Attachment(self.env, 'ticket', 42) attachment.filename = 'foo.$$$' self.assertEqual(os.path.join(self.attachments_dir, 'ticket', hashes['42'][0:3], hashes['42'], hashes['foo.$$$']), attachment.path) attachment = Attachment(self.env, 'wiki', 'SomePage') attachment.filename = u'bar.aäc' self.assertEqual(os.path.join(self.attachments_dir, 'wiki', hashes['SomePage'][0:3], hashes['SomePage'], hashes[u'bar.aäc']), attachment.path) def test_get_path_encoded(self): attachment = Attachment(self.env, 'ticket', 42) attachment.filename = 'Teh foo.txt' self.assertEqual(os.path.join(self.attachments_dir, 'ticket', hashes['42'][0:3], hashes['42'], hashes['Teh foo.txt'] + '.txt'), attachment.path) attachment = Attachment(self.env, 'wiki', u'ÜberSicht') attachment.filename = 'Teh bar.jpg' self.assertEqual(os.path.join(self.attachments_dir, 'wiki', hashes[u'ÜberSicht'][0:3], hashes[u'ÜberSicht'], hashes['Teh bar.jpg'] + '.jpg'), attachment.path) def test_select_empty(self): self.assertRaises(StopIteration, Attachment.select(self.env, 'ticket', 42).next) self.assertRaises(StopIteration, Attachment.select(self.env, 'wiki', 'SomePage').next) def test_insert(self): attachment = Attachment(self.env, 'ticket', 42) attachment.insert('foo.txt', StringIO(''), 0, 1) attachment = Attachment(self.env, 'ticket', 42) attachment.insert('bar.jpg', StringIO(''), 0, 2) attachments = Attachment.select(self.env, 'ticket', 42) self.assertEqual('foo.txt', attachments.next().filename) self.assertEqual('bar.jpg', attachments.next().filename) self.assertRaises(StopIteration, attachments.next) def test_insert_unique(self): attachment = Attachment(self.env, 'ticket', 42) attachment.insert('foo.txt', StringIO(''), 0) self.assertEqual('foo.txt', attachment.filename) attachment = Attachment(self.env, 'ticket', 42) attachment.insert('foo.txt', StringIO(''), 0) self.assertEqual('foo.2.txt', attachment.filename) self.assertEqual(os.path.join(self.attachments_dir, 'ticket', hashes['42'][0:3], hashes['42'], hashes['foo.2.txt'] + '.txt'), attachment.path) self.assertTrue(os.path.exists(attachment.path)) def test_insert_outside_attachments_dir(self): attachment = Attachment(self.env, '../../../../../sth/private', 42) self.assertRaises(TracError, attachment.insert, 'foo.txt', StringIO(''), 0) def test_delete(self): attachment1 = Attachment(self.env, 'wiki', 'SomePage') attachment1.insert('foo.txt', StringIO(''), 0) attachment2 = Attachment(self.env, 'wiki', 'SomePage') attachment2.insert('bar.jpg', StringIO(''), 0) attachments = Attachment.select(self.env, 'wiki', 'SomePage') self.assertEqual(2, len(list(attachments))) attachment1.delete() attachment2.delete() self.assertFalse(os.path.exists(attachment1.path)) self.assertFalse(os.path.exists(attachment2.path)) attachments = Attachment.select(self.env, 'wiki', 'SomePage') self.assertEqual(0, len(list(attachments))) def test_delete_file_gone(self): """ Verify that deleting an attachment works even if the referenced file doesn't exist for some reason. """ attachment = Attachment(self.env, 'wiki', 'SomePage') attachment.insert('foo.txt', StringIO(''), 0) os.unlink(attachment.path) attachment.delete() def test_reparent(self): attachment1 = Attachment(self.env, 'wiki', 'SomePage') attachment1.insert('foo.txt', StringIO(''), 0) path1 = attachment1.path attachment2 = Attachment(self.env, 'wiki', 'SomePage') attachment2.insert('bar.jpg', StringIO(''), 0) attachments = Attachment.select(self.env, 'wiki', 'SomePage') self.assertEqual(2, len(list(attachments))) attachments = Attachment.select(self.env, 'ticket', 123) self.assertEqual(0, len(list(attachments))) self.assertTrue(os.path.exists(path1) and os.path.exists(attachment2.path)) attachment1.reparent('ticket', 123) self.assertEqual('ticket', attachment1.parent_realm) self.assertEqual('ticket', attachment1.resource.parent.realm) self.assertEqual('123', attachment1.parent_id) self.assertEqual('123', attachment1.resource.parent.id) attachments = Attachment.select(self.env, 'wiki', 'SomePage') self.assertEqual(1, len(list(attachments))) attachments = Attachment.select(self.env, 'ticket', 123) self.assertEqual(1, len(list(attachments))) self.assertFalse(os.path.exists(path1) and os.path.exists(attachment1.path)) self.assertTrue(os.path.exists(attachment2.path)) def test_legacy_permission_on_parent(self): """Ensure that legacy action tests are done on parent. As `ATTACHMENT_VIEW` maps to `TICKET_VIEW`, the `TICKET_VIEW` is tested against the ticket's resource.""" attachment = Attachment(self.env, 'ticket', 42) self.assertTrue('ATTACHMENT_VIEW' in self.perm(attachment.resource)) def test_resource_doesnt_exist(self): r = Resource('wiki', 'WikiStart').child('attachment', 'file.txt') self.assertFalse(AttachmentModule(self.env).resource_exists(r)) def test_resource_exists(self): att = Attachment(self.env, 'wiki', 'WikiStart') att.insert('file.txt', StringIO(''), 1) self.assertTrue(resource_exists(self.env, att.resource))
class RecursivePolicyTestCase(unittest.TestCase): """Test case for policies that perform recursive permission checks.""" def setUp(self): self.env = EnvironmentStub() self.env.clear_component_registry() decisions = [] self.decisions = decisions class PermissionPolicy1(Component): implements(perm.IPermissionPolicy) def __init__(self): self.call_count = 0 def check_permission(self, action, username, resource, perm): self.call_count += 1 decision = None if 'ACTION_2' in perm(resource): decision = None elif action == 'ACTION_1': decision = username == 'user1' decisions.append(('policy1', action, decision)) return decision class PermissionPolicy2(Component): implements(perm.IPermissionPolicy) def __init__(self): self.call_count = 0 def check_permission(self, action, username, resource, perm): self.call_count += 1 decision = None if action == 'ACTION_2': decision = username == 'user2' decisions.append(('policy2', action, decision)) return decision self.env.enable_component(PermissionPolicy1) self.env.enable_component(PermissionPolicy2) self.env.config.set('trac', 'permission_policies', 'PermissionPolicy1, PermissionPolicy2') self.ps = perm.PermissionSystem(self.env) def tearDown(self): self.env.restore_component_registry() self.env.reset_db() def test_user1_allowed_by_policy1(self): """policy1 consulted for ACTION_1. policy1 and policy2 consulted for ACTION_2. """ perm_cache = perm.PermissionCache(self.env, 'user1') self.assertTrue('ACTION_1' in perm_cache) self.assertEqual(2, self.ps.policies[0].call_count) self.assertEqual(1, self.ps.policies[1].call_count) self.assertEqual([ ('policy1', 'ACTION_2', None), ('policy2', 'ACTION_2', False), ('policy1', 'ACTION_1', True), ], self.decisions) def test_user2_denied_by_no_decision(self): """policy1 and policy2 consulted for ACTION_1. policy1 and policy2 consulted for ACTION_2. """ perm_cache = perm.PermissionCache(self.env, 'user2') self.assertFalse('ACTION_1' in perm_cache) self.assertEqual(2, self.ps.policies[0].call_count) self.assertEqual(2, self.ps.policies[1].call_count) self.assertEqual([ ('policy1', 'ACTION_2', None), ('policy2', 'ACTION_2', True), ('policy1', 'ACTION_1', None), ('policy2', 'ACTION_1', None), ], self.decisions) def test_user1_denied_by_policy2(self): """policy1 consulted for ACTION_2. policy2 consulted for ACTION_2. """ perm_cache = perm.PermissionCache(self.env, 'user1') self.assertFalse('ACTION_2' in perm_cache) self.assertEqual(1, self.ps.policies[0].call_count) self.assertEqual(1, self.ps.policies[1].call_count) self.assertEqual([ ('policy1', 'ACTION_2', None), ('policy2', 'ACTION_2', False), ], self.decisions) def test_user1_allowed_by_policy2(self): """policy1 consulted for ACTION_2. policy2 consulted for ACTION_2. """ perm_cache = perm.PermissionCache(self.env, 'user2') self.assertTrue('ACTION_2' in perm_cache) self.assertEqual(1, self.ps.policies[0].call_count) self.assertEqual(1, self.ps.policies[1].call_count) self.assertEqual([ ('policy1', 'ACTION_2', None), ('policy2', 'ACTION_2', True), ], self.decisions)