1# -*- coding: utf-8 -*- 2# 3# Copyright (C) 2014-2021 Edgewall Software 4# All rights reserved. 5# 6# This software is licensed as described in the file COPYING, which 7# you should have received as part of this distribution. The terms 8# are also available at https://trac.edgewall.org/wiki/TracLicense. 9# 10# This software consists of voluntary contributions made by many 11# individuals. For the exact contribution history, see the revision 12# history and logs, available at https://trac.edgewall.org/log/. 13 14import os 15import tempfile 16import textwrap 17import unittest 18 19from trac.config import ConfigurationError 20from trac.perm import PermissionSystem 21from trac.test import EnvironmentStub, MockRequest 22from trac.ticket.api import TicketSystem 23from trac.ticket.batch import BatchModifyModule 24from trac.ticket.default_workflow import ConfigurableTicketWorkflow 25from trac.ticket.model import Component, Ticket 26from trac.ticket.test import insert_ticket 27from trac.ticket.web_ui import TicketModule 28from trac.util import create_file 29from trac.util.datefmt import to_utimestamp 30from trac.web.api import RequestDone 31from tracopt.perm.authz_policy import AuthzPolicy 32 33 34class ConfigurableTicketWorkflowTestCase(unittest.TestCase): 35 36 def setUp(self): 37 self.env = EnvironmentStub() 38 config = self.env.config 39 config.set('ticket-workflow', 'change_owner', 'new -> new') 40 config.set('ticket-workflow', 'change_owner.operations', 'set_owner') 41 self.ctlr = TicketSystem(self.env).action_controllers[0] 42 self.ticket_module = TicketModule(self.env) 43 44 def tearDown(self): 45 self.env.reset_db() 46 47 def _add_component(self, name='test', owner='owner1'): 48 component = Component(self.env) 49 component.name = name 50 component.owner = owner 51 component.insert() 52 53 def _reload_workflow(self): 54 self.ctlr.actions = self.ctlr.get_all_actions() 55 56 def test_get_all_actions_custom_attribute(self): 57 """Custom attribute in ticket-workflow.""" 58 config = self.env.config['ticket-workflow'] 59 config.set('resolve.set_milestone', 'reject') 60 all_actions = self.ctlr.get_all_actions() 61 62 resolve_action = None 63 for name, attrs in all_actions.items(): 64 if name == 'resolve': 65 resolve_action = attrs 66 67 self.assertIsNotNone(resolve_action) 68 self.assertIn('set_milestone', list(resolve_action)) 69 self.assertEqual('reject', resolve_action['set_milestone']) 70 71 def test_owner_from_component(self): 72 """Verify that the owner of a new ticket is set to the owner 73 of the component. 74 """ 75 self._add_component('component3', 'cowner3') 76 77 req = MockRequest(self.env, method='POST', args={ 78 'field_reporter': 'reporter1', 79 'field_summary': 'the summary', 80 'field_component': 'component3', 81 }) 82 self.assertRaises(RequestDone, self.ticket_module.process_request, req) 83 ticket = Ticket(self.env, 1) 84 85 self.assertEqual('component3', ticket['component']) 86 self.assertEqual('cowner3', ticket['owner']) 87 88 def test_component_change(self): 89 """New ticket owner is updated when the component is changed. 90 """ 91 self._add_component('component3', 'cowner3') 92 self._add_component('component4', 'cowner4') 93 94 ticket = insert_ticket(self.env, reporter='reporter1', 95 summary='the summary', component='component3', 96 owner='cowner3', status='new') 97 98 req = MockRequest(self.env, method='POST', args={ 99 'id': ticket.id, 100 'field_component': 'component4', 101 'submit': True, 102 'action': 'leave', 103 'view_time': str(to_utimestamp(ticket['changetime'])), 104 }) 105 self.assertRaises(RequestDone, self.ticket_module.process_request, req) 106 ticket = Ticket(self.env, ticket.id) 107 108 self.assertEqual('component4', ticket['component']) 109 self.assertEqual('cowner4', ticket['owner']) 110 111 def test_component_change_and_owner_change(self): 112 """New ticket owner is not updated if owner is explicitly 113 changed. 114 """ 115 self._add_component('component3', 'cowner3') 116 self._add_component('component4', 'cowner4') 117 118 ticket = insert_ticket(self.env, reporter='reporter1', 119 summary='the summary', component='component3', 120 status='new') 121 122 req = MockRequest(self.env, method='POST', args={ 123 'id': ticket.id, 124 'field_component': 'component4', 125 'submit': True, 126 'action': 'change_owner', 127 'action_change_owner_reassign_owner': 'owner1', 128 'view_time': str(to_utimestamp(ticket['changetime'])), 129 }) 130 self.assertRaises(RequestDone, self.ticket_module.process_request, req) 131 ticket = Ticket(self.env, ticket.id) 132 133 self.assertEqual('component4', ticket['component']) 134 self.assertEqual('owner1', ticket['owner']) 135 136 def test_old_owner_not_old_component_owner(self): 137 """New ticket owner is not updated if old owner is not the owner 138 of the old component. 139 """ 140 self._add_component('component3', 'cowner3') 141 self._add_component('component4', 'cowner4') 142 143 ticket = insert_ticket(self.env, reporter='reporter1', 144 summary='the summary', component='component3', 145 owner='owner1', status='new') 146 147 req = MockRequest(self.env, method='POST', args={ 148 'id': ticket.id, 149 'field_component': 'component4', 150 'submit': True, 151 'action': 'leave', 152 'view_time': str(to_utimestamp(ticket['changetime'])), 153 }) 154 self.assertRaises(RequestDone, self.ticket_module.process_request, req) 155 ticket = Ticket(self.env, ticket.id) 156 157 self.assertEqual('component4', ticket['component']) 158 self.assertEqual('owner1', ticket['owner']) 159 160 def test_new_component_has_no_owner(self): 161 """Ticket is not disowned when the component is changed to a 162 component with no owner. 163 """ 164 self._add_component('component3', 'cowner3') 165 self._add_component('component4', '') 166 167 ticket = insert_ticket(self.env, reporter='reporter1', 168 summary='the summary', component='component3', 169 owner='cowner3', status='new') 170 171 req = MockRequest(self.env, method='POST', args={ 172 'id': ticket.id, 173 'field_component': 'component4', 174 'submit': True, 175 'action': 'leave', 176 'view_time': str(to_utimestamp(ticket['changetime'])), 177 }) 178 self.assertRaises(RequestDone, self.ticket_module.process_request, req) 179 ticket = Ticket(self.env, ticket.id) 180 181 self.assertEqual('component4', ticket['component']) 182 self.assertEqual('cowner3', ticket['owner']) 183 184 def _test_get_allowed_owners(self): 185 ticket = insert_ticket(self.env, summary='Ticket 1') 186 self.env.insert_users([('user1', None, None, 1), 187 ('user2', None, None, 1), 188 ('user3', None, None, 1)]) 189 ps = PermissionSystem(self.env) 190 for user in ('user1', 'user3'): 191 ps.grant_permission(user, 'TICKET_MODIFY') 192 self.env.config.set('ticket', 'restrict_owner', True) 193 return ticket 194 195 def test_get_allowed_owners_returns_set_owner_list(self): 196 """Users specified in `set_owner` for the action are returned.""" 197 req = None 198 action = {'set_owner': ['user4', 'user5']} 199 ticket = self._test_get_allowed_owners() 200 self.assertEqual(['user4', 'user5'], 201 self.ctlr.get_allowed_owners(req, ticket, action)) 202 203 def test_get_allowed_owners_returns_user_with_ticket_modify(self): 204 """Users with TICKET_MODIFY are are returned if `set_owner` is 205 not specified for the action. 206 """ 207 req = None 208 action = {} 209 ticket = self._test_get_allowed_owners() 210 self.assertEqual(['user1', 'user3'], 211 self.ctlr.get_allowed_owners(req, ticket, action)) 212 213 def test_status_change_with_operation(self): 214 """Status change with operation.""" 215 ticket = Ticket(self.env) 216 ticket['new'] = 'status1' 217 ticket['owner'] = 'user1' 218 ticket.insert() 219 req = MockRequest(self.env, path_info='/ticket', authname='user2', 220 method='POST') 221 222 label, control, hints = \ 223 self.ctlr.render_ticket_action_control(req, ticket, 'accept') 224 225 self.assertEqual('accept', label) 226 self.assertEqual('', str(control)) 227 self.assertEqual("The owner will be <span class=\"trac-author-user\">" 228 "user2</span>. The status will be 'accepted'.", 229 str(hints)) 230 231 def test_status_change_with_no_operation(self): 232 """Existing ticket status change with no operation.""" 233 config = self.env.config 234 config.set('ticket-workflow', 'change_status', 'status1 -> status2') 235 self._reload_workflow() 236 ticket = Ticket(self.env) 237 ticket['status'] = 'status1' 238 ticket.insert() 239 req = MockRequest(self.env, path_info='/ticket', method='POST') 240 241 label, control, hints = \ 242 self.ctlr.render_ticket_action_control(req, ticket, 243 'change_status') 244 245 self.assertEqual('change status', label) 246 self.assertEqual('', str(control)) 247 self.assertEqual("Next status will be 'status2'.", str(hints)) 248 249 def test_new_ticket_status_change_with_no_operation(self): 250 """New ticket status change with no operation.""" 251 config = self.env.config 252 config.set('ticket-workflow', 'change_status', '<none> -> status1') 253 self._reload_workflow() 254 ticket = Ticket(self.env) 255 req = MockRequest(self.env, path_info='/newticket', method='POST') 256 257 label, control, hints = \ 258 self.ctlr.render_ticket_action_control(req, ticket, 259 'change_status') 260 261 self.assertEqual('change status', label) 262 self.assertEqual('', str(control)) 263 self.assertEqual("The status will be 'status1'.", str(hints)) 264 265 def test_operation_with_no_status_change(self): 266 """Operation with no status change.""" 267 config = self.env.config 268 config.set('ticket-workflow', 'change_owner', 'closed -> closed') 269 config.set('ticket-workflow', 'change_owner.operations', 'set_owner') 270 271 self._reload_workflow() 272 ticket = Ticket(self.env) 273 ticket['status'] = 'closed' 274 ticket['owner'] = 'user2' 275 ticket.insert() 276 req = MockRequest(self.env, path_info='/ticket', method='POST', 277 authname='user1') 278 279 label, control, hints = \ 280 self.ctlr.render_ticket_action_control(req, ticket, 281 'change_owner') 282 283 self.assertEqual('change owner', label) 284 self.assertEqual( 285 'to <input id="action_change_owner_reassign_owner" ' 286 'name="action_change_owner_reassign_owner" type="text" ' 287 'value="user1" />', str(control)) 288 self.assertEqual( 289 'The owner will be changed from <span class="trac-author">' 290 'user2</span> to the specified user.', str(hints)) 291 292 def test_transition_to_star(self): 293 """Action not rendered by CTW for transition to * 294 295 AdvancedTicketWorkflow uses the behavior for the triage operation 296 (see #12823) 297 """ 298 config = self.env.config 299 config.set('ticket-workflow', 'create_and_triage', '<none> -> *') 300 config.set('ticket-workflow', 'create_and_triage.operations', 'triage') 301 self._reload_workflow() 302 ticket = Ticket(self.env) 303 req = MockRequest(self.env, path_info='/newticket', method='POST') 304 305 actions = self.ctlr.get_ticket_actions(req, ticket) 306 307 # create_and_triage not in actions 308 self.assertEqual({(1, 'create'), (0, 'create_and_assign')}, 309 set(actions)) 310 311 def test_transition_to_star_with_leave_operation(self): 312 """Action is rendered by CTW for transition to * with leave_status 313 """ 314 config = self.env.config 315 config.set('ticket-workflow', 'change_owner', 'assigned,closed -> *') 316 config.set('ticket-workflow', 'change_owner.operations', 317 'leave_status,set_owner') 318 self._reload_workflow() 319 status = ['assigned', 'closed'] 320 for s in status: 321 ticket = Ticket(self.env) 322 ticket['status'] = s 323 ticket['owner'] = 'user2' 324 ticket.insert() 325 req = MockRequest(self.env, path_info='/ticket', method='POST', 326 authname='user1') 327 328 label, control, hints = \ 329 self.ctlr.render_ticket_action_control(req, ticket, 330 'change_owner') 331 self.assertEqual('change owner', label) 332 self.assertEqual( 333 'to <input id="action_change_owner_reassign_owner" ' 334 'name="action_change_owner_reassign_owner" type="text" ' 335 'value="user1" />', str(control)) 336 self.assertEqual( 337 'The owner will be changed from <span class="trac-author">' 338 'user2</span> to the specified user.', str(hints)) 339 340 def test_leave_operation(self): 341 ticket = Ticket(self.env) 342 ticket['status'] = 'assigned' 343 ticket['owner'] = 'user2' 344 ticket.insert() 345 req = MockRequest(self.env, path_info='/ticket', method='POST', 346 authname='user1') 347 348 label, control, hints = \ 349 self.ctlr.render_ticket_action_control(req, ticket, 'leave') 350 351 self.assertEqual('leave', label) 352 self.assertEqual('as assigned', str(control)) 353 self.assertEqual('The owner will remain <span class="trac-author">' 354 'user2</span>.', str(hints)) 355 356 def test_get_actions_by_operation_for_req(self): 357 """Request with no permission checking.""" 358 req = MockRequest(self.env, path_info='/ticket/1') 359 ticket = insert_ticket(self.env, status='new') 360 actions = self.ctlr.get_actions_by_operation_for_req(req, ticket, 361 'set_owner') 362 self.assertEqual([(0, 'change_owner'), (0, 'reassign')], 363 sorted(actions)) 364 365 def test_get_actions_by_operation_for_req_with_ticket_modify(self): 366 """User without TICKET_MODIFY won't have reassign action.""" 367 req = MockRequest(self.env, authname='user1', path_info='/ticket/1') 368 ticket = insert_ticket(self.env, status='new') 369 actions = self.ctlr.get_actions_by_operation_for_req(req, ticket, 370 'set_owner') 371 self.assertEqual([(0, 'change_owner')], sorted(actions)) 372 373 def test_get_actions_by_operation_for_req_without_ticket_modify(self): 374 """User with TICKET_MODIFY will have reassign action.""" 375 PermissionSystem(self.env).grant_permission('user1', 'TICKET_MODIFY') 376 req = MockRequest(self.env, authname='user1', path_info='/ticket/1') 377 ticket = insert_ticket(self.env, status='new') 378 actions = self.ctlr.get_actions_by_operation_for_req(req, ticket, 379 'set_owner') 380 self.assertEqual([(0, 'change_owner'), (0, 'reassign')], 381 sorted(actions)) 382 383 384class ResetActionTestCase(unittest.TestCase): 385 386 def setUp(self): 387 self.env = EnvironmentStub(default_data=True) 388 self.perm_sys = PermissionSystem(self.env) 389 self.ctlr = TicketSystem(self.env).action_controllers[0] 390 self.req1 = MockRequest(self.env, authname='user1') 391 self.req2 = MockRequest(self.env, authname='user2') 392 self.ticket = insert_ticket(self.env, status='invalid') 393 394 def tearDown(self): 395 self.env.reset_db() 396 397 def _reload_workflow(self): 398 self.ctlr.actions = self.ctlr.get_all_actions() 399 400 def test_default_reset_action(self): 401 """Default reset action.""" 402 self.perm_sys.grant_permission('user2', 'TICKET_ADMIN') 403 self._reload_workflow() 404 405 actions1 = self.ctlr.get_ticket_actions(self.req1, self.ticket) 406 actions2 = self.ctlr.get_ticket_actions(self.req2, self.ticket) 407 chgs2 = self.ctlr.get_ticket_changes(self.req2, self.ticket, '_reset') 408 409 self.assertEqual(1, len(actions1)) 410 self.assertNotIn((0, '_reset'), actions1) 411 self.assertEqual(2, len(actions2)) 412 self.assertIn((0, '_reset'), actions2) 413 self.assertEqual('new', chgs2['status']) 414 415 def test_default_reset_action_without_new_state(self): 416 """Default reset action not available when no new state.""" 417 self.perm_sys.grant_permission('user2', 'TICKET_ADMIN') 418 config = self.env.config 419 # Replace 'new' state with 'untriaged' 420 config.set('ticket-workflow', 'create', 421 '<none> -> untriaged') 422 config.set('ticket-workflow', 'accept', 423 'untriaged,assigned,accepted,reopened -> accepted') 424 config.set('ticket-workflow', 'resolve', 425 'untriaged,assigned,accepted,reopened -> closed') 426 config.set('ticket-workflow', 'reassign', 427 'untriaged,assigned,accepted,reopened -> assigned') 428 self._reload_workflow() 429 430 actions = self.ctlr.get_ticket_actions(self.req2, self.ticket) 431 432 self.assertEqual(1, len(actions)) 433 self.assertNotIn((0, '_reset'), actions) 434 435 def test_custom_reset_action(self): 436 """Custom reset action in [ticket-workflow] section.""" 437 config = self.env.config['ticket-workflow'] 438 config.set('_reset', '-> review') 439 config.set('_reset.operations', 'reset_workflow') 440 config.set('_reset.permissions', 'TICKET_BATCH_MODIFY') 441 config.set('_reset.default', 2) 442 self.perm_sys.grant_permission('user2', 'TICKET_BATCH_MODIFY') 443 self._reload_workflow() 444 445 actions1 = self.ctlr.get_ticket_actions(self.req1, self.ticket) 446 actions2 = self.ctlr.get_ticket_actions(self.req2, self.ticket) 447 chgs2 = self.ctlr.get_ticket_changes(self.req2, self.ticket, '_reset') 448 449 self.assertEqual(1, len(actions1)) 450 self.assertNotIn((2, '_reset'), actions1) 451 self.assertEqual(2, len(actions2)) 452 self.assertIn((2, '_reset'), actions2) 453 self.assertEqual('review', chgs2['status']) 454 455 456class SetOwnerAttributeTestCase(unittest.TestCase): 457 458 def setUp(self): 459 self.env = EnvironmentStub(default_data=True) 460 self.perm_sys = PermissionSystem(self.env) 461 self.ctlr = TicketSystem(self.env).action_controllers[0] 462 self.ticket = insert_ticket(self.env, status='new') 463 self.env.insert_users([ 464 (user, None, None) for user in ('user1', 'user2', 'user3', 'user4') 465 ]) 466 permissions = [ 467 ('user1', 'TICKET_EDIT_CC'), 468 ('user2', 'TICKET_EDIT_CC'), 469 ('user2', 'TICKET_BATCH_MODIFY'), 470 ('user3', 'TICKET_ADMIN'), 471 ('user4', 'TICKET_VIEW'), 472 ('user1', 'group1'), 473 ('user2', 'group1'), 474 ('user2', 'group2'), 475 ('user3', 'group2'), 476 ('user4', 'group3') 477 ] 478 for perm in permissions: 479 self.perm_sys.grant_permission(*perm) 480 self.req = MockRequest(self.env, authname='user1') 481 self.expected = """\ 482to <select id="action_reassign_reassign_owner" \ 483name="action_reassign_reassign_owner"><option selected="selected" \ 484value="user1">user1</option><option value="user2">user2</option>\ 485<option value="user3">user3</option></select>""" 486 487 def _reload_workflow(self): 488 self.ctlr.actions = self.ctlr.get_all_actions() 489 490 def tearDown(self): 491 self.env.reset_db() 492 493 def test_users(self): 494 self.env.config.set('ticket-workflow', 'reassign.set_owner', 495 'user1, user2, user3') 496 self._reload_workflow() 497 498 args = self.req, self.ticket, 'reassign' 499 label, control, hints = self.ctlr.render_ticket_action_control(*args) 500 501 self.assertEqual(self.expected, str(control)) 502 503 def test_groups(self): 504 self.env.config.set('ticket-workflow', 'reassign.set_owner', 505 'group1, group2') 506 self._reload_workflow() 507 508 args = self.req, self.ticket, 'reassign' 509 label, control, hints = self.ctlr.render_ticket_action_control(*args) 510 511 self.assertEqual(self.expected, str(control)) 512 513 def test_permission(self): 514 self.env.config.set('ticket-workflow', 'reassign.set_owner', 515 'TICKET_EDIT_CC, TICKET_BATCH_MODIFY') 516 self._reload_workflow() 517 518 args = self.req, self.ticket, 'reassign' 519 label, control, hints = self.ctlr.render_ticket_action_control(*args) 520 521 self.assertEqual(self.expected, str(control)) 522 523 524class SetOwnerToSelfAttributeTestCase(unittest.TestCase): 525 526 def setUp(self): 527 self.env = EnvironmentStub(default_data=True) 528 self.ctlr = TicketSystem(self.env).action_controllers[0] 529 self.req = MockRequest(self.env, authname='user1') 530 ps = PermissionSystem(self.env) 531 for user in ('user1', 'user2'): 532 ps.grant_permission(user, 'TICKET_MODIFY') 533 self.env.insert_users([('user1', 'User 1', None), 534 ('user2', 'User 2', None)]) 535 536 def _get_ticket_actions(self, req, ticket): 537 return [action[1] for action 538 in self.ctlr.get_ticket_actions(req, ticket)] 539 540 def _reload_workflow(self): 541 self.ctlr.actions = self.ctlr.get_all_actions() 542 543 def _insert_ticket(self, status, owner, resolution=None): 544 ticket = Ticket(self.env) 545 ticket['status'] = status 546 ticket['owner'] = owner 547 if resolution: 548 ticket['resolution'] = resolution 549 ticket.insert() 550 return ticket 551 552 def test_owner_is_other(self): 553 """Ticket owner is not auth'ed user. 554 555 The workflow action is shown when the state will be changed by 556 the action. 557 """ 558 ticket = self._insert_ticket('accepted', 'user2') 559 args = self.req, ticket, 'accept' 560 561 label, control, hints = self.ctlr.render_ticket_action_control(*args) 562 ticket_actions = self._get_ticket_actions(*args[0:2]) 563 564 self.assertIn('accept', ticket_actions) 565 self.assertEqual(label, 'accept') 566 self.assertEqual('', str(control)) 567 self.assertEqual('The owner will be changed from ' 568 '<span class="trac-author">User 2</span> to ' 569 '<span class="trac-author-user">User 1</span>.', 570 str(hints)) 571 572 def test_owner_is_self_and_state_change(self): 573 """Ticket owner is auth'ed user with state change. 574 575 The workflow action is shown when the state will be changed by the 576 action, even when the ticket owner is the authenticated user. 577 """ 578 ticket = self._insert_ticket('new', 'user1') 579 args = self.req, ticket, 'accept' 580 581 label, control, hints = self.ctlr.render_ticket_action_control(*args) 582 ticket_actions = self._get_ticket_actions(*args[0:2]) 583 584 self.assertIn('accept', ticket_actions) 585 self.assertEqual(label, 'accept') 586 self.assertEqual('', str(control)) 587 self.assertEqual('The owner will remain <span class="trac-author-user">' 588 'User 1</span>. Next status will be \'accepted\'.', 589 str(hints)) 590 591 def test_owner_is_self_and_no_state_change(self): 592 """Ticket owner is the auth'ed user and no state change. 593 594 The ticket action is not in the list of available actions 595 when the state will not be changed by the action and the ticket 596 owner is the authenticated user. 597 """ 598 ticket = self._insert_ticket('accepted', 'user1') 599 args = self.req, ticket, 'accept' 600 601 ticket_actions = self._get_ticket_actions(*args[0:2]) 602 603 self.assertNotIn('accept', ticket_actions) 604 605 def test_owner_is_self_state_change_and_multiple_operations(self): 606 """Ticket owner is auth'ed user, state change and multiple ops. 607 608 The set_owner_to_self workflow hint is shown when the ticket status 609 is changed by the action, even when the ticket owner is the 610 authenticated user. 611 """ 612 ticket = self._insert_ticket('new', 'user1') 613 workflow = self.env.config['ticket-workflow'] 614 workflow.set('resolve_as_owner', '* -> closed') 615 workflow.set('resolve_as_owner.operations', 616 'set_owner_to_self, set_resolution') 617 workflow.set('resolve_as_owner.set_resolution', 'fixed') 618 self._reload_workflow() 619 args = self.req, ticket, 'resolve_as_owner' 620 621 label, control, hints = self.ctlr.render_ticket_action_control(*args) 622 ticket_actions = self._get_ticket_actions(*args[0:2]) 623 624 self.assertIn('resolve_as_owner', ticket_actions) 625 self.assertEqual(label, 'resolve as owner') 626 self.assertEqual( 627 'as fixed<input id="action_resolve_as_owner_resolve_resolution" ' 628 'name="action_resolve_as_owner_resolve_resolution" type="hidden" ' 629 'value="fixed" />', str(control)) 630 self.assertEqual( 631 "The owner will remain <span class=\"trac-author-user\">User 1" 632 "</span>. The resolution will be set to fixed. Next status will " 633 "be 'closed'.", str(hints)) 634 635 def test_owner_is_self_no_state_change_and_multiple_operations(self): 636 """Ticket owner is auth'ed user, no state change and multiple ops. 637 638 The set_owner_to_self workflow hint is not shown when the ticket 639 state is not changed by the action and the ticket owner is the 640 authenticated user. 641 """ 642 ticket = self._insert_ticket('closed', 'user1', 'fixed') 643 workflow = self.env.config['ticket-workflow'] 644 workflow.set('fix_resolution', 'closed -> closed') 645 workflow.set('fix_resolution.operations', 646 'set_owner_to_self, set_resolution') 647 workflow.set('fix_resolution.set_resolution', 'invalid') 648 self._reload_workflow() 649 args = self.req, ticket, 'fix_resolution' 650 651 label, control, hints = self.ctlr.render_ticket_action_control(*args) 652 ticket_actions = self._get_ticket_actions(*args[0:2]) 653 654 self.assertIn('fix_resolution', ticket_actions) 655 self.assertEqual(label, 'fix resolution') 656 self.assertEqual( 657 'as invalid<input id="action_fix_resolution_resolve_resolution" ' 658 'name="action_fix_resolution_resolve_resolution" type="hidden" ' 659 'value="invalid" />', str(control)) 660 self.assertEqual("The resolution will be set to invalid.", 661 str(hints)) 662 663 664class RestrictOwnerTestCase(unittest.TestCase): 665 666 def setUp(self): 667 tmpdir = os.path.realpath(tempfile.gettempdir()) 668 self.env = EnvironmentStub(enable=['trac.*', AuthzPolicy], path=tmpdir) 669 self.env.config.set('trac', 'permission_policies', 670 'AuthzPolicy, DefaultPermissionPolicy') 671 self.env.config.set('ticket', 'restrict_owner', True) 672 673 self.perm_sys = PermissionSystem(self.env) 674 self.env.insert_users([('user1', 'User C', 'user1@example.org'), 675 ('user2', 'User A', 'user2@example.org'), 676 ('user3', 'User D', 'user3@example.org'), 677 ('user4', 'User B', 'user4@example.org')]) 678 self.perm_sys.grant_permission('user1', 'TICKET_MODIFY') 679 self.perm_sys.grant_permission('user2', 'TICKET_VIEW') 680 self.perm_sys.grant_permission('user3', 'TICKET_MODIFY') 681 self.perm_sys.grant_permission('user4', 'TICKET_MODIFY') 682 self.authz_file = os.path.join(tmpdir, 'trac-authz-policy') 683 create_file(self.authz_file) 684 self.env.config.set('authz_policy', 'authz_file', self.authz_file) 685 self.ctlr = TicketSystem(self.env).action_controllers[0] 686 self.req1 = MockRequest(self.env, authname='user1') 687 self.ticket = insert_ticket(self.env, status='new') 688 689 def tearDown(self): 690 self.env.reset_db() 691 os.remove(self.authz_file) 692 693 def _reload_workflow(self): 694 self.ctlr.actions = self.ctlr.get_all_actions() 695 696 def test_set_owner(self): 697 """Restricted owners list contains users with TICKET_MODIFY. 698 """ 699 self.env.config.set('trac', 'show_full_names', False) 700 701 ctrl = self.ctlr.render_ticket_action_control(self.req1, self.ticket, 702 'reassign') 703 704 self.assertEqual('reassign', ctrl[0]) 705 self.assertIn('value="user1">user1</option>', str(ctrl[1])) 706 self.assertNotIn('value="user2">user2</option>', str(ctrl[1])) 707 self.assertIn('value="user3">user3</option>', str(ctrl[1])) 708 self.assertIn('value="user4">user4</option>', str(ctrl[1])) 709 710 def test_set_owner_fine_grained_permissions(self): 711 """Fine-grained permission checks when populating the restricted 712 owners list (#10833). 713 """ 714 self.env.config.set('trac', 'show_full_names', False) 715 create_file(self.authz_file, textwrap.dedent("""\ 716 [ticket:1] 717 user4 = !TICKET_MODIFY 718 """)) 719 720 ctrl = self.ctlr.render_ticket_action_control(self.req1, self.ticket, 721 'reassign') 722 723 self.assertEqual('reassign', ctrl[0]) 724 self.assertIn('value="user1">user1</option>', str(ctrl[1])) 725 self.assertNotIn('value="user2">user2</option>', str(ctrl[1])) 726 self.assertIn('value="user3">user3</option>', str(ctrl[1])) 727 self.assertNotIn('value="user4">user4</option>', str(ctrl[1])) 728 729 def test_set_owner_show_fullnames(self): 730 """Full names are sorted when [trac] show_full_names = True.""" 731 ctrl = self.ctlr.render_ticket_action_control(self.req1, self.ticket, 732 'reassign') 733 734 self.assertEqual('reassign', ctrl[0]) 735 self.assertEqual("""\ 736to <select id="action_reassign_reassign_owner"\ 737 name="action_reassign_reassign_owner">\ 738<option value="user4">User B</option>\ 739<option selected="selected" value="user1">User C</option>\ 740<option value="user3">User D</option></select>\ 741""", str(ctrl[1])) 742 743 744class SetResolutionAttributeTestCase(unittest.TestCase): 745 746 def setUp(self): 747 self.env = EnvironmentStub(default_data=True) 748 for ctlr in TicketSystem(self.env).action_controllers: 749 if isinstance(ctlr, ConfigurableTicketWorkflow): 750 self.ctlr = ctlr 751 752 def _reload_workflow(self): 753 self.ctlr.actions = self.ctlr.get_all_actions() 754 755 def test_empty_set_resolution(self): 756 config = self.env.config['ticket-workflow'] 757 config.set('resolve.set_resolution', '') 758 self._reload_workflow() 759 ticket = Ticket(self.env) 760 ticket.populate({'summary': '#12882', 'status': 'new'}) 761 ticket.insert() 762 req = MockRequest(self.env, path_info='/ticket/%d' % ticket.id) 763 try: 764 self.ctlr.render_ticket_action_control(req, ticket, 'resolve') 765 self.fail('ConfigurationError not raised') 766 except ConfigurationError as e: 767 self.assertIn('but none is defined', str(e)) 768 769 def test_undefined_resolutions(self): 770 config = self.env.config['ticket-workflow'] 771 ticket = Ticket(self.env) 772 ticket.populate({'summary': '#12882', 'status': 'new'}) 773 ticket.insert() 774 req = MockRequest(self.env, path_info='/ticket/%d' % ticket.id) 775 776 config.set('resolve.set_resolution', 777 'fixed,invalid,wontfix,,duplicate,worksforme,,,,,') 778 self._reload_workflow() 779 self.ctlr.render_ticket_action_control(req, ticket, 'resolve') 780 781 config.set('resolve.set_resolution', 'undefined,fixed') 782 self._reload_workflow() 783 try: 784 self.ctlr.render_ticket_action_control(req, ticket, 'resolve') 785 self.fail('ConfigurationError not raised') 786 except ConfigurationError as e: 787 self.assertIn('but uses undefined resolutions', str(e)) 788 789 790def test_suite(): 791 suite = unittest.TestSuite() 792 suite.addTest(unittest.makeSuite(ConfigurableTicketWorkflowTestCase)) 793 suite.addTest(unittest.makeSuite(ResetActionTestCase)) 794 suite.addTest(unittest.makeSuite(SetOwnerAttributeTestCase)) 795 suite.addTest(unittest.makeSuite(SetOwnerToSelfAttributeTestCase)) 796 suite.addTest(unittest.makeSuite(RestrictOwnerTestCase)) 797 suite.addTest(unittest.makeSuite(SetResolutionAttributeTestCase)) 798 return suite 799 800 801if __name__ == '__main__': 802 unittest.main(defaultTest='test_suite') 803