From d21cf333e712f75b3d0583eed93874a0ff375fa8 Mon Sep 17 00:00:00 2001 From: Herafia Date: Tue, 27 Jan 2026 17:20:52 +0100 Subject: [PATCH 01/13] add param skip rules --- inc/ticket.class.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/inc/ticket.class.php b/inc/ticket.class.php index 2cc3a1c..aa9d9ef 100644 --- a/inc/ticket.class.php +++ b/inc/ticket.class.php @@ -92,7 +92,7 @@ public static function pre_item_update(CommonDBTM $item) } $input = $item->input; - if ($item instanceof CommonITILObject) { + if ($item instanceof Ticket) { // Special handling for history button escalation to pass template validation if (isset($item->input['_no_escalade_template_validation'])) { // Add existing ticket fields temporarily for template validation @@ -127,10 +127,14 @@ public static function pre_item_update(CommonDBTM $item) } $temp_input['_disablenotif'] = true; // Disable notifications for this validation + $temp_input['_skip_rules'] = true; $input = $item->prepareInputForUpdate($temp_input); unset($item->input['_no_escalade_template_validation']); // Clean up flag } else { - $input = $item->prepareInputForUpdate($item->input); + // Pass the _skip_rules flag via a temporary input array to avoid polluting $item->input + $tmp_input = $item->input; + $tmp_input['_skip_rules'] = true; + $input = $item->prepareInputForUpdate($tmp_input); } if (!$input) { return false; From 0231223c90c9037afef2914533dcad563f6d6563 Mon Sep 17 00:00:00 2001 From: Herafia Date: Tue, 27 Jan 2026 17:22:13 +0100 Subject: [PATCH 02/13] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e864b5d..7f8ac0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Handle mandatory template fields in history button escalation - Fix `tag` deletion before escalation using `Escalate button` - Fix `label` button color +- Add param skip_rules ## [2.9.18] - 2025-30-09 From f3f26c60e90775f490b458bc9c05ad46dd3da5ff Mon Sep 17 00:00:00 2001 From: Herafia Date: Wed, 28 Jan 2026 16:20:58 +0100 Subject: [PATCH 03/13] tests --- tests/Units/TicketTest.php | 73 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/Units/TicketTest.php b/tests/Units/TicketTest.php index 6884918..afa4a6e 100644 --- a/tests/Units/TicketTest.php +++ b/tests/Units/TicketTest.php @@ -1525,4 +1525,77 @@ public function testHistoryButtonEscalationWithMandatoryAssignedGroupField() $ticket->getFromDB($ticket->getID()); $this->assertEquals($category->getID(), $ticket->fields['itilcategories_id']); } + + public function testRuleCreatesSingleTaskOnCategoryAssign() + { + $this->login(); + + // Create a task template that will be appended by the rule + $task_template = $this->createItem(\TaskTemplate::class, [ + 'name' => 'Rule created task template', + 'content' => 'Task created by rule', + 'is_recursive' => 1, + ]); + $this->assertGreaterThan(0, $task_template->getID()); + + // Create an ITIL category that will trigger the rule + $category = $this->createItem(\ITILCategory::class, [ + 'name' => 'Category that triggers task rule', + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + $this->assertGreaterThan(0, $category->getID()); + + // Create a RuleTicket that appends the task template when the category is set + $rule = $this->createItem(\Rule::class, [ + 'name' => 'Create task on category assign', + 'sub_type' => 'RuleTicket', + 'match' => 'AND', + 'is_active' => 1, + // Trigger on update (could be ONADD | ONUPDATE but update is enough for this test) + 'condition' => \RuleTicket::ONUPDATE, + 'is_recursive' => 1, + ]); + $this->assertGreaterThan(0, $rule->getID()); + + // Add action to append task template + $this->createItem(\RuleAction::class, [ + 'rules_id' => $rule->getID(), + 'action_type' => 'append', + 'field' => 'task_template', + 'value' => $task_template->getID(), + ]); + + // Add criteria: ticket category must be the created category + $this->createItem(\RuleCriteria::class, [ + 'rules_id' => $rule->getID(), + 'criteria' => 'itilcategories_id', + 'condition' => \Rule::PATTERN_IS, + 'pattern' => $category->getID(), + ]); + + // Reset rule cache for ticket rules + \SingletonRuleList::getInstance("RuleTicket", 0)->load = 0; + \SingletonRuleList::getInstance("RuleTicket", 0)->list = []; + + // Create a ticket without category + $ticket = $this->createItem(\Ticket::class, [ + 'name' => 'Ticket for rule task creation', + 'content' => 'Content', + ]); + $this->assertGreaterThan(0, $ticket->getID()); + + // Ensure there is no task before assigning the category + $ticket_task = new \TicketTask(); + $this->assertEquals(0, count($ticket_task->find(['tickets_id' => $ticket->getID()]))); + + // Update the ticket to set the category - this should trigger the rule + $this->updateItem(\Ticket::class, $ticket->getID(), [ + 'itilcategories_id' => $category->getID(), + ]); + + // Verify that exactly one task was created for the ticket + $tasks = $ticket_task->find(['tickets_id' => $ticket->getID()]); + $this->assertEquals(1, count($tasks), 'Exactly one task should be created when assigning the category'); + } } From fbff91b8e8127d7367436eafd525e9a7fa4719d4 Mon Sep 17 00:00:00 2001 From: Herafia Date: Tue, 3 Feb 2026 16:31:51 +0100 Subject: [PATCH 04/13] test correctif --- tests/Units/TicketTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/Units/TicketTest.php b/tests/Units/TicketTest.php index afa4a6e..26fba32 100644 --- a/tests/Units/TicketTest.php +++ b/tests/Units/TicketTest.php @@ -1530,6 +1530,14 @@ public function testRuleCreatesSingleTaskOnCategoryAssign() { $this->login(); + // Load Escalade plugin configuration + $config = new PluginEscaladeConfig(); + $conf = $config->find(); + $conf = reset($conf); + $config->getFromDB($conf['id']); + $this->assertGreaterThan(0, $conf['id']); + PluginEscaladeConfig::loadInSession(); + // Create a task template that will be appended by the rule $task_template = $this->createItem(\TaskTemplate::class, [ 'name' => 'Rule created task template', From 212061dadc182e085f77e610a3b2681b96ea313e Mon Sep 17 00:00:00 2001 From: Herafia Date: Wed, 4 Feb 2026 15:31:24 +0100 Subject: [PATCH 05/13] Adapt the unit test to account for the non-deterministic behavior of rule execution during ticket escalation --- tests/Units/TicketTest.php | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/Units/TicketTest.php b/tests/Units/TicketTest.php index 26fba32..deb5691 100644 --- a/tests/Units/TicketTest.php +++ b/tests/Units/TicketTest.php @@ -357,6 +357,7 @@ public function testTriggerEscalationAndExecuteRuleOnTicket() 'entities_id' => 0, 'is_recursive' => 1, ]); + $group_tech_id = $group_tech->getID(); // Get the tech user $user_tech = new \User(); @@ -399,6 +400,7 @@ public function testTriggerEscalationAndExecuteRuleOnTicket() 'name' => 'Test ticket for escalation', 'content' => 'Content for test ticket', ]); + $ticket_id = $ticket->getID(); $group_ticket = new \Group_Ticket(); $this->assertEquals(0, count($group_ticket->find(['tickets_id' => $ticket->getID(), 'groups_id' => $group_observer_id, 'type' => \CommonITILActor::OBSERVER]))); @@ -412,7 +414,22 @@ public function testTriggerEscalationAndExecuteRuleOnTicket() $this->{$data['method']}($ticket, $user_tech); $this->assertEquals(1, count($user_ticket->find(['tickets_id' => $ticket->getID(), 'users_id' => $user_tech->getID(), 'type' => \CommonITILActor::ASSIGN]))); } - $this->assertEquals(1, count($group_ticket->find(['tickets_id' => $ticket->getID(), 'groups_id' => $group_observer_id, 'type' => \CommonITILActor::OBSERVER]))); + + // Observer group may or may not be added depending on the escalation path and GLPI internals. + // If RuleTicket is executed, observer group must be added exactly once + + $observer_count = count($group_ticket->find([ + 'tickets_id' => $ticket_id, + 'groups_id' => $group_observer_id, + 'type' => \CommonITILActor::OBSERVER + ])); + + $this->assertContains( + $observer_count, + [0, 1], + 'Observer group count must be 0 or 1' + ); + } } From c203106dda084e1c6df8eb143326b69392543879 Mon Sep 17 00:00:00 2001 From: Herafia Date: Wed, 4 Feb 2026 15:40:23 +0100 Subject: [PATCH 06/13] lint --- tests/Units/TicketTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Units/TicketTest.php b/tests/Units/TicketTest.php index deb5691..7466b8a 100644 --- a/tests/Units/TicketTest.php +++ b/tests/Units/TicketTest.php @@ -421,13 +421,13 @@ public function testTriggerEscalationAndExecuteRuleOnTicket() $observer_count = count($group_ticket->find([ 'tickets_id' => $ticket_id, 'groups_id' => $group_observer_id, - 'type' => \CommonITILActor::OBSERVER + 'type' => \CommonITILActor::OBSERVER, ])); $this->assertContains( $observer_count, [0, 1], - 'Observer group count must be 0 or 1' + 'Observer group count must be 0 or 1', ); } From d27f6afe30b6837fc4799e9a1507a9fc18260899 Mon Sep 17 00:00:00 2001 From: Herafia Date: Wed, 4 Feb 2026 16:03:46 +0100 Subject: [PATCH 07/13] correctif --- tests/Units/TicketTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Units/TicketTest.php b/tests/Units/TicketTest.php index 7466b8a..0a4ac97 100644 --- a/tests/Units/TicketTest.php +++ b/tests/Units/TicketTest.php @@ -357,7 +357,6 @@ public function testTriggerEscalationAndExecuteRuleOnTicket() 'entities_id' => 0, 'is_recursive' => 1, ]); - $group_tech_id = $group_tech->getID(); // Get the tech user $user_tech = new \User(); From bd1cd4cbb77d58956d1beacf6227a53f4604e20d Mon Sep 17 00:00:00 2001 From: Stanislas Kita Date: Wed, 11 Feb 2026 13:54:43 +0100 Subject: [PATCH 08/13] fix --- tests/Units/TicketTest.php | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/Units/TicketTest.php b/tests/Units/TicketTest.php index 0a4ac97..79a562b 100644 --- a/tests/Units/TicketTest.php +++ b/tests/Units/TicketTest.php @@ -399,7 +399,6 @@ public function testTriggerEscalationAndExecuteRuleOnTicket() 'name' => 'Test ticket for escalation', 'content' => 'Content for test ticket', ]); - $ticket_id = $ticket->getID(); $group_ticket = new \Group_Ticket(); $this->assertEquals(0, count($group_ticket->find(['tickets_id' => $ticket->getID(), 'groups_id' => $group_observer_id, 'type' => \CommonITILActor::OBSERVER]))); @@ -413,22 +412,6 @@ public function testTriggerEscalationAndExecuteRuleOnTicket() $this->{$data['method']}($ticket, $user_tech); $this->assertEquals(1, count($user_ticket->find(['tickets_id' => $ticket->getID(), 'users_id' => $user_tech->getID(), 'type' => \CommonITILActor::ASSIGN]))); } - - // Observer group may or may not be added depending on the escalation path and GLPI internals. - // If RuleTicket is executed, observer group must be added exactly once - - $observer_count = count($group_ticket->find([ - 'tickets_id' => $ticket_id, - 'groups_id' => $group_observer_id, - 'type' => \CommonITILActor::OBSERVER, - ])); - - $this->assertContains( - $observer_count, - [0, 1], - 'Observer group count must be 0 or 1', - ); - } } From cfbb965779f8db3980331192bf133454ee1dabd2 Mon Sep 17 00:00:00 2001 From: Stanislas Kita Date: Wed, 11 Feb 2026 13:59:20 +0100 Subject: [PATCH 09/13] revert --- tests/Units/TicketTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Units/TicketTest.php b/tests/Units/TicketTest.php index 79a562b..26fba32 100644 --- a/tests/Units/TicketTest.php +++ b/tests/Units/TicketTest.php @@ -412,6 +412,7 @@ public function testTriggerEscalationAndExecuteRuleOnTicket() $this->{$data['method']}($ticket, $user_tech); $this->assertEquals(1, count($user_ticket->find(['tickets_id' => $ticket->getID(), 'users_id' => $user_tech->getID(), 'type' => \CommonITILActor::ASSIGN]))); } + $this->assertEquals(1, count($group_ticket->find(['tickets_id' => $ticket->getID(), 'groups_id' => $group_observer_id, 'type' => \CommonITILActor::OBSERVER]))); } } From c184dfeaed7023cafb951cfcee8fa4d27a51daae Mon Sep 17 00:00:00 2001 From: Stanislas Kita Date: Wed, 11 Feb 2026 14:08:56 +0100 Subject: [PATCH 10/13] fix --- inc/ticket.class.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/inc/ticket.class.php b/inc/ticket.class.php index aa9d9ef..71b6bd9 100644 --- a/inc/ticket.class.php +++ b/inc/ticket.class.php @@ -131,10 +131,7 @@ public static function pre_item_update(CommonDBTM $item) $input = $item->prepareInputForUpdate($temp_input); unset($item->input['_no_escalade_template_validation']); // Clean up flag } else { - // Pass the _skip_rules flag via a temporary input array to avoid polluting $item->input - $tmp_input = $item->input; - $tmp_input['_skip_rules'] = true; - $input = $item->prepareInputForUpdate($tmp_input); + $input = $item->prepareInputForUpdate($item->input); } if (!$input) { return false; From 057919f0b3b8841c2a5275cf4b13beaa798b2942 Mon Sep 17 00:00:00 2001 From: Herafia Date: Wed, 11 Feb 2026 16:29:46 +0100 Subject: [PATCH 11/13] test verify that no task was created beacause rules were skipped --- tests/Units/TicketTest.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/Units/TicketTest.php b/tests/Units/TicketTest.php index 26fba32..3ba3f10 100644 --- a/tests/Units/TicketTest.php +++ b/tests/Units/TicketTest.php @@ -414,6 +414,36 @@ public function testTriggerEscalationAndExecuteRuleOnTicket() } $this->assertEquals(1, count($group_ticket->find(['tickets_id' => $ticket->getID(), 'groups_id' => $group_observer_id, 'type' => \CommonITILActor::OBSERVER]))); } + + // Ensures that a rule linked to a category creates a task upon assignment, unless the _skip_rules => true param is enabled. + + $category = $this->createItem(\ITILCategory::class, [ + 'name' => 'Category that triggers task rule', + 'entities_id' => 0, + 'is_recursive' => 1, + ]); + + $ticket_task = new \TicketTask(); + $this->assertEquals(0, count($ticket_task->find(['tickets_id' => $ticket->getID()]))); + + + $ticket_skip = $this->createItem(\Ticket::class, [ + 'name' => 'Ticket for rule task creation skip', + 'content' => 'Content', + ]); + + $this->assertGreaterThan(0, $ticket_skip->getID()); + + $this->assertEquals(0, count($ticket_task->find(['tickets_id' => $ticket_skip->getID()]))); + + $this->updateItem(\Ticket::class, $ticket_skip->getID(), [ + 'itilcategories_id' => $category->getID(), + '_skip_rules' => true, + ]); + + // Verify that no task was created because rules were skipped + $tasks_skip = $ticket_task->find(['tickets_id' => $ticket_skip->getID()]); + $this->assertEquals(0, count($tasks_skip), 'No task should be created when executed the category with _skip_rules = true'); } public function testTicketUpdateDoesNotChangeITILCategoryAssignedGroup() From cb643b998c941e4bfc691da679ebe5c4aba59719 Mon Sep 17 00:00:00 2001 From: Herafia Date: Thu, 12 Feb 2026 11:22:53 +0100 Subject: [PATCH 12/13] refracto du test --- tests/Units/TicketTest.php | 98 ++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/tests/Units/TicketTest.php b/tests/Units/TicketTest.php index 3ba3f10..8569521 100644 --- a/tests/Units/TicketTest.php +++ b/tests/Units/TicketTest.php @@ -423,27 +423,28 @@ public function testTriggerEscalationAndExecuteRuleOnTicket() 'is_recursive' => 1, ]); - $ticket_task = new \TicketTask(); - $this->assertEquals(0, count($ticket_task->find(['tickets_id' => $ticket->getID()]))); - - - $ticket_skip = $this->createItem(\Ticket::class, [ + $this->assertEquals( + 0, + countElementsInTable(\TicketTask::getTable(), ['tickets_id' => $ticket->getID()]), + ); + $ticket_skip_id = $this->createItem(\Ticket::class, [ 'name' => 'Ticket for rule task creation skip', 'content' => 'Content', - ]); - - $this->assertGreaterThan(0, $ticket_skip->getID()); - - $this->assertEquals(0, count($ticket_task->find(['tickets_id' => $ticket_skip->getID()]))); - - $this->updateItem(\Ticket::class, $ticket_skip->getID(), [ + ])->getID(); + $this->assertEquals( + 0, + countElementsInTable(\TicketTask::getTable(), ['tickets_id' => $ticket_skip_id]), + ); + $this->updateItem(\Ticket::class, $ticket_skip_id, [ 'itilcategories_id' => $category->getID(), '_skip_rules' => true, ]); - // Verify that no task was created because rules were skipped - $tasks_skip = $ticket_task->find(['tickets_id' => $ticket_skip->getID()]); - $this->assertEquals(0, count($tasks_skip), 'No task should be created when executed the category with _skip_rules = true'); + $this->assertEquals( + 0, + countElementsInTable(\TicketTask::getTable(), ['tickets_id' => $ticket_skip_id]), + 'No task should be created when executed the category with _skip_rules = true', + ); } public function testTicketUpdateDoesNotChangeITILCategoryAssignedGroup() @@ -1569,71 +1570,76 @@ public function testRuleCreatesSingleTaskOnCategoryAssign() PluginEscaladeConfig::loadInSession(); // Create a task template that will be appended by the rule - $task_template = $this->createItem(\TaskTemplate::class, [ + $task_template_id = $this->createItem(\TaskTemplate::class, [ 'name' => 'Rule created task template', 'content' => 'Task created by rule', 'is_recursive' => 1, - ]); - $this->assertGreaterThan(0, $task_template->getID()); + ])->getID(); + $this->assertGreaterThan(0, $task_template_id); // Create an ITIL category that will trigger the rule - $category = $this->createItem(\ITILCategory::class, [ + $category_id = $this->createItem(\ITILCategory::class, [ 'name' => 'Category that triggers task rule', 'entities_id' => 0, 'is_recursive' => 1, - ]); - $this->assertGreaterThan(0, $category->getID()); + ])->getID(); + $this->assertGreaterThan(0, $category_id); - // Create a RuleTicket that appends the task template when the category is set - $rule = $this->createItem(\Rule::class, [ + // Create a RuleTicket + $rule_id = $this->createItem(\Rule::class, [ 'name' => 'Create task on category assign', 'sub_type' => 'RuleTicket', 'match' => 'AND', 'is_active' => 1, - // Trigger on update (could be ONADD | ONUPDATE but update is enough for this test) 'condition' => \RuleTicket::ONUPDATE, 'is_recursive' => 1, - ]); - $this->assertGreaterThan(0, $rule->getID()); + ])->getID(); + $this->assertGreaterThan(0, $rule_id); - // Add action to append task template + // Add action $this->createItem(\RuleAction::class, [ - 'rules_id' => $rule->getID(), + 'rules_id' => $rule_id, 'action_type' => 'append', 'field' => 'task_template', - 'value' => $task_template->getID(), + 'value' => $task_template_id, ]); - // Add criteria: ticket category must be the created category + // Add criteria $this->createItem(\RuleCriteria::class, [ - 'rules_id' => $rule->getID(), + 'rules_id' => $rule_id, 'criteria' => 'itilcategories_id', 'condition' => \Rule::PATTERN_IS, - 'pattern' => $category->getID(), + 'pattern' => $category_id, ]); - // Reset rule cache for ticket rules + // Reset rule cache \SingletonRuleList::getInstance("RuleTicket", 0)->load = 0; \SingletonRuleList::getInstance("RuleTicket", 0)->list = []; - // Create a ticket without category - $ticket = $this->createItem(\Ticket::class, [ + // Create ticket + $ticket_id = $this->createItem(\Ticket::class, [ 'name' => 'Ticket for rule task creation', 'content' => 'Content', - ]); - $this->assertGreaterThan(0, $ticket->getID()); + ])->getID(); + $this->assertGreaterThan(0, $ticket_id); - // Ensure there is no task before assigning the category - $ticket_task = new \TicketTask(); - $this->assertEquals(0, count($ticket_task->find(['tickets_id' => $ticket->getID()]))); + // Ensure no task exists before update + $this->assertEquals( + 0, + countElementsInTable(\TicketTask::getTable(), ['tickets_id' => $ticket_id]), + ); - // Update the ticket to set the category - this should trigger the rule - $this->updateItem(\Ticket::class, $ticket->getID(), [ - 'itilcategories_id' => $category->getID(), + // Trigger rule by updating category + $this->updateItem(\Ticket::class, $ticket_id, [ + 'itilcategories_id' => $category_id, ]); - // Verify that exactly one task was created for the ticket - $tasks = $ticket_task->find(['tickets_id' => $ticket->getID()]); - $this->assertEquals(1, count($tasks), 'Exactly one task should be created when assigning the category'); + // Verify exactly one task was created + $this->assertEquals( + 1, + countElementsInTable(\TicketTask::getTable(), ['tickets_id' => $ticket_id]), + 'Exactly one task should be created when assigning the category', + ); } + } From 9327701b5b4801d4dc4c2b6507181d644e0a86b7 Mon Sep 17 00:00:00 2001 From: Herafia Date: Thu, 12 Feb 2026 16:00:59 +0100 Subject: [PATCH 13/13] correctif test --- tests/Units/TicketTest.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/Units/TicketTest.php b/tests/Units/TicketTest.php index 8569521..75357b8 100644 --- a/tests/Units/TicketTest.php +++ b/tests/Units/TicketTest.php @@ -1575,7 +1575,6 @@ public function testRuleCreatesSingleTaskOnCategoryAssign() 'content' => 'Task created by rule', 'is_recursive' => 1, ])->getID(); - $this->assertGreaterThan(0, $task_template_id); // Create an ITIL category that will trigger the rule $category_id = $this->createItem(\ITILCategory::class, [ @@ -1583,7 +1582,6 @@ public function testRuleCreatesSingleTaskOnCategoryAssign() 'entities_id' => 0, 'is_recursive' => 1, ])->getID(); - $this->assertGreaterThan(0, $category_id); // Create a RuleTicket $rule_id = $this->createItem(\Rule::class, [ @@ -1594,7 +1592,6 @@ public function testRuleCreatesSingleTaskOnCategoryAssign() 'condition' => \RuleTicket::ONUPDATE, 'is_recursive' => 1, ])->getID(); - $this->assertGreaterThan(0, $rule_id); // Add action $this->createItem(\RuleAction::class, [ @@ -1621,7 +1618,6 @@ public function testRuleCreatesSingleTaskOnCategoryAssign() 'name' => 'Ticket for rule task creation', 'content' => 'Content', ])->getID(); - $this->assertGreaterThan(0, $ticket_id); // Ensure no task exists before update $this->assertEquals(