Add Tempest API tests for inspection rules

As of the 2025.1 "Epoxy" release, inspection rules have been migrated
from the ironic-inspector project into Ironic itself.

This commit introduces Tempest tests to validate the creation, updation
and deletion of new inspection rules functionality within Ironic.

Current focus is on covering the important CRUD operations for
inspection rules to ensure robustness and correctness of
the Inspection rules API.

I guess this is the starting point, as Inspection rules evolves
more tests should be added to ensure the continued relevance
effectiveness of the API test suite.

closes-bug: 2105478

Change-Id: I3f6de02acee8d8c3764d3b1465b92292be3b690c
Signed-off-by: abhibongale <abhishekbongale@outlook.com>
diff --git a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
index 2c0d713..2cc2f5c 100644
--- a/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
+++ b/ironic_tempest_plugin/services/baremetal/v1/json/baremetal_client.py
@@ -326,7 +326,6 @@
         :param description: The description of the chassis.
             Default: test-chassis
         :return: A tuple with the server response and the created chassis.
-
         """
         chassis = {'description': kwargs.get('description', 'test-chassis')}
 
@@ -1055,3 +1054,38 @@
     def list_portgroups_details_by_node(self, node_ident):
         """List detailed portgroups filtered by node."""
         return self._list_request(f'nodes/{node_ident}/portgroups/detail')
+
+    @base.handle_errors
+    def create_inspection_rule(self, payload):
+        """Create Inspection rule.
+
+        :param payload: Inspection rule JSON
+        """
+        return self._create_request('inspection_rules', payload)
+
+    @base.handle_errors
+    def show_inspection_rule(self, rule_uuid):
+        """Show Inspection rule."""
+        return self._show_request('inspection_rules', rule_uuid)
+
+    @base.handle_errors
+    def list_inspection_rule(self, **kwargs):
+        """List all Inspection rules."""
+        return self._list_request('inspection_rules', **kwargs)
+
+    @base.handle_errors
+    def update_inspection_rule(self, rule_uuid, patch):
+        """Update the specified Inspection rule.
+
+        :param rule_uuid: The unique identifier of the inspection rule.
+        :param patch: List of dicts representing json patches.
+        """
+        return self._patch_request('inspection_rules', rule_uuid, patch)
+
+    @base.handle_errors
+    def delete_inspection_rule(self, rule_uuid):
+        """Delete Inspection rule.
+
+        :param rule_uuid: uuid of the inspection rule.
+        """
+        return self._delete_request('inspection_rules', rule_uuid)
diff --git a/ironic_tempest_plugin/tests/api/admin/test_inspection_rules.py b/ironic_tempest_plugin/tests/api/admin/test_inspection_rules.py
new file mode 100644
index 0000000..3d00ce3
--- /dev/null
+++ b/ironic_tempest_plugin/tests/api/admin/test_inspection_rules.py
@@ -0,0 +1,172 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
+from ironic_tempest_plugin.tests.api import base
+
+
+class TestInspectionRules(base.BaseBaremetalTest):
+    """API tests for Inspection Rules endpoints"""
+
+    # Inspection rules API was introduced in microversion 1.96
+    # We will be skipping this test for the older version.
+    min_microversion = '1.96'
+
+    def setUp(self):
+        super(TestInspectionRules, self).setUp()
+
+        _, self.node = self.create_node(None)
+
+        self.useFixture(
+            api_microversion_fixture.APIMicroversionFixture('1.96'))
+
+    def _create_inspection_rule_payload(self, **kwargs):
+        """Create a Inspection rule payload."""
+        payload = {
+            "description": "Inspection rule to log node UUID",
+            "conditions": [],
+            "actions": [
+                {
+                    "op": "log",
+                    "args": {
+                        "msg": "Node with UUID {node.uuid} is being inspected"
+                    }
+                }
+            ],
+            "phase": "main",
+            "priority": 0,
+            "sensitive": False
+        }
+
+        payload.update(kwargs)
+
+        return payload
+
+    @decorators.idempotent_id('7fb771cd-b011-409e-a255-3c71cf7251e8')
+    def test_create_rule_sensitive_true(self):
+        """Test creating rule with sensitive=True."""
+        rule_uuid = data_utils.rand_uuid()
+        payload = self._create_inspection_rule_payload(sensitive=True)
+
+        self.create_inspection_rule(rule_uuid, payload)
+
+        _, fetched_rule = self.client.show_inspection_rule(rule_uuid)
+
+        self.assertTrue(fetched_rule.get('sensitive'))
+        self.assertIsNone(fetched_rule.get('conditions'))
+        self.assertIsNone(fetched_rule.get('actions'))
+
+    @decorators.idempotent_id('e60b4513-7c3d-4b2c-b485-17443bf6485f')
+    def test_create_rule_complex_logging_conditions_actions(self):
+        """Test creating rule with loop conditions and actions"""
+        complex_log_conditions = [
+            {
+                "op": "eq",
+                "args": [
+                    "{inventory.system.product_name}",
+                    "{item}"
+                ],
+                "loop": [
+                    "product_name_1",
+                    "product_name_2",
+                    "product_name_3"
+                ],
+                "multiple": "any"
+            }
+        ]
+
+        complex_log_actions = [
+            {
+                "op": "set-attribute",
+                "args": [
+                    "{item[path]}",
+                    "{item[value]}"
+                ],
+                "loop": [
+                    {
+                        "path": "/driver_info/ipmi_username",
+                        "value": "admin"
+                    },
+                    {
+                        "path": "/driver_info/ipmi_password",
+                        "value": "password"
+                    },
+                    {
+                        "path": "/driver_info/ipmi_address",
+                        "value": "{inventory[bmc_address]}"
+                    }
+                ]
+            }
+        ]
+
+        payload = self._create_inspection_rule_payload(
+            conditions=complex_log_conditions,
+            actions=complex_log_actions,
+        )
+
+        _, created_rule = self.create_inspection_rule(None, payload)
+
+        self.assertEqual(complex_log_conditions,
+                         created_rule.get('conditions'))
+        self.assertEqual(complex_log_actions,
+                         created_rule.get('actions'))
+
+    @decorators.idempotent_id('a786a4ec-1e43-4fb9-8fc3-c53aa4e1f52f')
+    def test_patch_conditions_actions_priority(self):
+        """Test Updating rule'si priority, condition and actions"""
+        payload = self._create_inspection_rule_payload()
+
+        patch = [
+            {
+                "op": "replace",
+                "path": "/priority",
+                "value": 200
+            },
+            {
+                "op": "replace",
+                "path": "/conditions",
+                "value": [
+                    {
+                        "op": "eq",
+                        "args": ["{{ inventory.cpu.count }}", 8]
+                    }
+                ]
+            },
+            {
+                "op": "replace",
+                "path": "/actions",
+                "value": [
+                    {
+                        "op": "set-attribute",
+                        "args": ["{{ /properties/cpu_model }}", "cpu_xyz"]
+                    },
+                    {
+                        "op": "log",
+                        "args": ["CPU model updated via rule."]
+                    }
+                ]
+            }
+        ]
+
+        _, created_rule = self.create_inspection_rule(None, payload)
+        _, fetched_rule = self.client.update_inspection_rule(
+            created_rule.get('uuid'), patch)
+
+        self.assertEqual(fetched_rule.get('priority'),
+                         patch[0]['value'])
+        self.assertEqual(fetched_rule.get('conditions'),
+                         patch[1]['value'])
+        self.assertEqual(fetched_rule.get('actions'),
+                         patch[2]['value'])
diff --git a/ironic_tempest_plugin/tests/api/admin/test_inspection_rules_negatives.py b/ironic_tempest_plugin/tests/api/admin/test_inspection_rules_negatives.py
new file mode 100644
index 0000000..a92728b
--- /dev/null
+++ b/ironic_tempest_plugin/tests/api/admin/test_inspection_rules_negatives.py
@@ -0,0 +1,79 @@
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from ironic_tempest_plugin.tests.api.admin import api_microversion_fixture
+from ironic_tempest_plugin.tests.api import base
+
+
+class TestInspectionRulesNegative(base.BaseBaremetalTest):
+    """Negative Inspection Rules test"""
+
+    # Inspection rules API was introduced in microversion 1.96
+    # We will be skipping this test for the older version.
+    min_microversion = '1.96'
+
+    def setUp(self):
+        super(TestInspectionRulesNegative, self).setUp()
+
+        _, self.node = self.create_node(None)
+
+        self.useFixture(
+            api_microversion_fixture.APIMicroversionFixture('1.96'))
+
+    def _create_inspection_rule_payload(self, **kwargs):
+        """Create a Inspection rule payload."""
+        payload = {
+            "description": "Inspection rule to log node UUID",
+            "conditions": [],
+            "actions": [
+                {
+                    "op": "log",
+                    "args": {
+                        "msg": "Node with UUID {node.uuid} is being inspected"
+                    }
+                }
+            ],
+            "phase": "main",
+            "priority": 0,
+            "sensitive": False
+        }
+
+        payload.update(kwargs)
+
+        return payload
+
+    @decorators.idempotent_id('55403d94-53ce-41ab-989a-da3399314c9d')
+    @decorators.attr(type=['negative'])
+    def test_create_invalid_priority_fails(self):
+        """Test to create Inspection rule with invalid priorities"""
+        invalid_priorities = [-1, 10000, 5000.50]
+
+        for priority_val in invalid_priorities:
+            payload = self._create_inspection_rule_payload(
+                priority=priority_val)
+
+            self.assertRaises(lib_exc.BadRequest,
+                              self.create_inspection_rule,
+                              rule_uuid=None, payload=payload)
+
+    @decorators.idempotent_id('cf9615b3-904e-4456-b00a-622d39892b88')
+    @decorators.attr(type=['negative'])
+    def test_delete_by_wrong_uiid(self):
+        """Test to delete Inspection Rule with wrong uuid"""
+        rule_uuid = data_utils.rand_uuid()
+        self.assertRaises(lib_exc.NotFound,
+                          self.delete_inspection_rule,
+                          rule_uuid=rule_uuid)
diff --git a/ironic_tempest_plugin/tests/api/base.py b/ironic_tempest_plugin/tests/api/base.py
index c07137b..7744aee 100644
--- a/ironic_tempest_plugin/tests/api/base.py
+++ b/ironic_tempest_plugin/tests/api/base.py
@@ -38,7 +38,8 @@
 # NOTE(jroll): resources must be deleted in a specific order, this list
 # defines the resource types to clean up, and the correct order.
 RESOURCE_TYPES = ['port', 'portgroup', 'node', 'volume_connector',
-                  'volume_target', 'chassis', 'deploy_template', 'runbook']
+                  'volume_target', 'chassis', 'deploy_template',
+                  'runbook', 'inspection_rule']
 
 
 def creates(resource):
@@ -520,6 +521,32 @@
         resp, body = cls.client.create_allocation(resource_class, **kwargs)
         return resp, body
 
+    @classmethod
+    @creates('inspection_rule')
+    def create_inspection_rule(cls, rule_uuid, payload):
+        """Wrapper utility for creating Inspection rule.
+
+        :param rule_uuid: UUID of the Inspection rule.
+        :param payload: Inspection rule other fields.
+        :return: Server response.
+        """
+        if rule_uuid is not None:
+            payload['uuid'] = rule_uuid
+
+        resp, body = cls.client.create_inspection_rule(payload)
+
+        return resp, body
+
+    @classmethod
+    def delete_inspection_rule(cls, rule_uuid):
+        """Delete a inspection rules having the specified UUID.
+
+        :param rule_uuid: UUID of the Inspection rule.
+        """
+        resp, body = cls.client.delete_inspection_rule(rule_uuid)
+
+        return resp
+
 
 class BaseBaremetalRBACTest(BaseBaremetalTest):