from odoo.exceptions import AccessError, ValidationError from odoo.tests.common import Form from .common import TestTowerCommon class TestTowerServerTemplate(TestTowerCommon): """ Test the server template model """ @classmethod def setUpClass(cls): super().setUpClass() # Create two "Manager" group users cls.manager1 = cls.Users.create( { "name": "Manager 1", "login": "manager1", "email": "manager1@example.com", "groups_id": [(6, 0, [cls.group_manager.id])], } ) cls.manager2 = cls.Users.create( { "name": "Manager 2", "login": "manager2", "email": "manager2@example.com", "groups_id": [(6, 0, [cls.group_manager.id])], } ) def test_create_server_from_template(self): """ Create new server from template """ self.assertFalse( self.Server.search( [("server_template_id", "=", self.server_template_sample.id)] ), "The servers shouldn't exist", ) # add variable values to server template self.VariableValue.create( { "variable_id": self.variable_version.id, "server_template_id": self.server_template_sample.id, "value_char": "test", } ) # add delete flight plan self.server_template_sample.plan_delete_id = self.plan_1.id # add server logs to template command_for_log = self.Command.create( {"name": "Get system info", "code": "uname -a"} ) server_template_log = self.ServerLog.create( { "name": "Log from server template", "server_template_id": self.server_template_sample.id, "log_type": "command", "command_id": command_for_log.id, } ) self.assertEqual( len(self.variable_version.value_ids), 1, "The variable must have one value only", ) server_log = self.ServerLog.search([("command_id", "=", command_for_log.id)]) self.assertEqual(len(server_log), 1, "Server log must be one") # create new server from template new_server = self.ServerTemplate.create_server_from_template( self.server_template_sample.reference, "server_from_template", ipv4="0.0.0.0", ) server = self.Server.search( [("server_template_id", "=", self.server_template_sample.id)] ) self.assertEqual(new_server, server, "Servers must be the same") self.assertEqual( new_server.name, "server_from_template", "Server name must be server_from_template", ) self.assertEqual( new_server.ip_v4_address, "0.0.0.0", "Server IP must be 0.0.0.0" ) self.assertEqual( new_server.os_id, self.os_debian_10, "Server os must be Debian" ) self.assertEqual(new_server.ssh_port, 22, "Server SSH Port must be 22") self.assertEqual( new_server.ssh_username, "admin", "Server SSH Username must be 'admin'" ) self.assertEqual( new_server._get_secret_value("ssh_password"), "password", "Server SSH Password must be 'password'", ) self.assertEqual( new_server.ssh_auth_mode, "p", "Server SSH Auth Mode must be 'p'" ) self.assertEqual( len(self.variable_version.value_ids), 2, "The variable must have two value only", ) self.assertEqual( new_server.plan_delete_id, self.plan_1, "Server On Delete Plan must be 'Test plan 1'", ) server_log = self.ServerLog.search([("command_id", "=", command_for_log.id)]) self.assertEqual(len(server_log), 2, "Server log must be two") server_log = server_log.filtered(lambda rec: rec.server_id == new_server) self.assertNotEqual(server_log, server_template_log) def test_create_server_from_template_wizard(self): """ Create new server from template from wizard """ action = self.server_template_sample.action_create_server() wizard = ( self.env["cx.tower.server.template.create.wizard"] # pylint: disable=context-overridden we need a new clean context .with_context(action["context"]) .new({}) ) self.assertEqual( self.server_template_sample, wizard.server_template_id, "Server Templates must be the same", ) self.assertFalse( self.Server.search( [("server_template_id", "=", self.server_template_sample.id)] ), "The servers shouldn't exist", ) wizard.update( { "name": "test", "ip_v4_address": "0.0.0.0", "use_sudo": "n", "partner_id": self.user_bob.partner_id.id, "os_id": self.os_debian_10.id, "tag_ids": [(4, self.tag_test_production.id)], } ) action = wizard.action_confirm() server = self.Server.search( [("server_template_id", "=", self.server_template_sample.id)] ) self.assertEqual(action["res_id"], server.id, "Server ids must be the same") self.assertEqual( server.partner_id, self.user_bob.partner_id, "Partner must be the same" ) self.assertEqual(server.os_id, self.os_debian_10, "OS must be the same") self.assertEqual( server.tag_ids, self.tag_test_production, "Tag must be the same" ) self.assertEqual(server.use_sudo, "n", "Use sudo must be the same") self.assertEqual(server.ip_v4_address, "0.0.0.0", "IP must be the same") self.assertEqual(server.name, "test", "Name must be the same") def test_create_server_from_template_action(self): """ Create new server from action """ name = "server from template" self.assertFalse( self.Server.search([("name", "=", name)]), "Server should not exist", ) # add variable values to server template self.VariableValue.create( { "variable_id": self.variable_version.id, "server_template_id": self.server_template_sample.id, "value_char": "test template version", } ) self.VariableValue.create( { "variable_id": self.variable_url.id, "server_template_id": self.server_template_sample.id, "value_char": "test template url", } ) # add variable option variable_url_option = self.VariableOption.create( { "name": "localhost", "value_char": "localhost", "variable_id": self.variable_url.id, } ) # create new server with new variable self.ServerTemplate.create_server_from_template( self.server_template_sample.reference, "server from template", ipv4="localhost", ssh_username="test", ssh_password="test", plan_delete_id=self.plan_1.id, configuration_variables={ self.variable_version.reference: "test server version", "new_variable": "new_value", }, configuration_variable_options={ self.variable_url.reference: variable_url_option.reference, }, ) new_server = self.Server.search([("name", "=", name)]) self.assertTrue(new_server, "Server must exist!") self.assertFalse(new_server.plan_delete_id, "On Delete Plan must be empty!") self.assertEqual( len(new_server.variable_value_ids), 3, "Should be 3 variable values!" ) # check variable values var_version_value = new_server.variable_value_ids.filtered( lambda rec: rec.variable_id == self.variable_version ) self.assertEqual( var_version_value.value_char, "test server version", "Version variable values should be with new values for " "server from template", ) var_url_value = new_server.variable_value_ids.filtered( lambda rec: rec.variable_id == self.variable_url ) self.assertEqual( var_url_value.value_char, variable_url_option.value_char, "Url variable values should be same as option value", ) var_new_value = new_server.variable_value_ids.filtered( lambda rec: rec.variable_id.reference == "new_variable" ) self.assertTrue(var_new_value, "New variable should exist on the server") self.assertEqual( var_new_value.value_char, "new_value", "New variable values should be 'new_values'", ) def test_server_template_copy(self): """ Test duplicating a Server Template with variable values and server logs """ # A server template server_template = self.server_template_sample # Add variable values to the server template original_variable_value = self.VariableValue.create( { "variable_id": self.variable_version.id, "server_template_id": server_template.id, "value_char": "test", } ) # Create a command for the server log command_for_log = self.Command.create( { "name": "Get system info", "code": "uname -a", } ) # Add server logs to the template original_log = self.ServerLog.create( { "name": "Log from server template", "server_template_id": server_template.id, "log_type": "command", "command_id": command_for_log.id, } ) # Duplicate the server template copied_template = server_template.copy() # Ensure the new server template was created with a new ID self.assertNotEqual( copied_template.id, server_template.id, "Copied server template should have a different ID from the original", ) # Check that the copied template has the same number of variable values self.assertEqual( len(copied_template.variable_value_ids), len(server_template.variable_value_ids), ( "Copied template should have the same " "number of variable values as the original" ), ) # Ensure the variable itself was copied (check variable_id) copied_variable_value = copied_template.variable_value_ids self.assertEqual( copied_variable_value.variable_id.id, original_variable_value.variable_id.id, "Variable ID should be the same in the copied template", ) self.assertEqual( copied_variable_value.value_char, original_variable_value.value_char, "Variable value should be the same in the copied template", ) # Check that the copied template has the same number of server logs self.assertEqual( len(copied_template.server_log_ids), len(server_template.server_log_ids), ( "Copied template should have the same " "number of server logs as the original" ), ) # Ensure the first server log in the copied template matches the original copied_log = copied_template.server_log_ids self.assertEqual( copied_log.name, original_log.name, "Server log name should be the same in the copied template", ) self.assertEqual( copied_log.command_id.id, original_log.command_id.id, "Command ID should be the same in the copied server log", ) self.assertEqual( copied_log.command_id.code, original_log.command_id.code, "Command code should be the same in the copied server log", ) def test_required_attribute_in_wizard_field(self): """ Test that the 'required' attribute is correctly applied to the 'value_char' field in the wizard when the variable is marked as required. """ # Create a required variable self.VariableValue.create( { "variable_id": self.variable_version.id, "server_template_id": self.server_template_sample.id, "value_char": "Test Value", "required": True, } ) # Open the wizard wizard = self.env["cx.tower.server.template.create.wizard"].create( { "server_template_id": self.server_template_sample.id, "name": "Test Server", "ssh_username": "admin", } ) # Checking that the 'required' flag is passed to the form context required_fields = [ line.required for line in wizard.line_ids if line.variable_id == self.variable_version ] self.assertTrue( all(required_fields), "The 'required' attribute should be correctly " "applied to the 'value_char' field for required variables.", ) def test_successful_server_creation_with_required_variables(self): """ Test that a server is successfully created when all required variables are filled in the wizard. """ # Add manager as user of template self.server_template_sample.user_ids = self.manager # Adding a required variable self.VariableValue.create( { "variable_id": self.variable_version.id, "server_template_id": self.server_template_sample.id, "value_char": "", "required": True, } ) # Open the wizard and fill in the data as manager wizard = ( self.env["cx.tower.server.template.create.wizard"] .with_user(self.manager) .create( { "server_template_id": self.server_template_sample.id, "name": "Test Server With Required Variables", "ssh_username": "admin", "line_ids": [ ( 0, 0, { "variable_id": self.variable_version.id, "required": True, }, ) ], } ) ) # Fill in the value for the required variable with Form(wizard) as wizard_form: with wizard_form.line_ids.edit(0) as line: line.value_char = "Test Value" wizard_form.save() # Checking the successful creation of the server action = wizard.action_confirm() self.assertTrue(action, "Server should be created successfully.") # Checking that the server has been created server = self.Server.search( [ ("name", "=", "Test Server With Required Variables"), ("server_template_id", "=", self.server_template_sample.id), ] ) self.assertTrue(server, "Server should exist.") self.assertEqual( server.variable_value_ids.filtered( lambda v: v.variable_id == self.variable_version ).value_char, "Test Value", "The variable value should be saved correctly.", ) def test_optional_variable_with_empty_value(self): """ Test that an optional variable with an empty value is saved correctly in the wizard and does not block server creation. """ # Adding an optional variable self.VariableValue.create( { "variable_id": self.variable_url.id, "server_template_id": self.server_template_sample.id, "value_char": "", "required": False, } ) # Open the wizard wizard = self.env["cx.tower.server.template.create.wizard"].create( { "server_template_id": self.server_template_sample.id, "name": "Server With Optional Variable", "ssh_username": "admin", "line_ids": [ ( 0, 0, { "variable_id": self.variable_url.id, "value_char": "", "required": False, }, ) ], } ) # Checking that the wizard is saved without errors wizard.action_confirm() # Checking that the server has been created server = self.Server.search( [ ("name", "=", "Server With Optional Variable"), ("server_template_id", "=", self.server_template_sample.id), ] ) self.assertTrue( server, "Server should be created successfully with optional variables." ) # Checking that an optional variable is saved with an empty value variable = server.variable_value_ids.filtered( lambda v: v.variable_id == self.variable_url ) self.assertTrue(variable, "Optional variable should be attached to the server.") self.assertEqual( variable.value_char, "", "Optional variable should have an empty value." ) def test_wizard_without_variables(self): """ Test that the wizard does not display any variables if the server template has none. """ # Removing all variables from the template self.VariableValue.search( [("server_template_id", "=", self.server_template_sample.id)] ).unlink() # Open the wizard wizard = self.env["cx.tower.server.template.create.wizard"].create( { "server_template_id": self.server_template_sample.id, "name": "Server Without Variables", "ssh_username": "admin", } ) # Checking that the wizard does not contain variables self.assertFalse(wizard.line_ids, "Wizard should not display any variables.") def test_update_required_variable_value(self): """ Test that the value of a required variable can be updated in the wizard and saved correctly. """ # Adding a required variable self.VariableValue.create( { "variable_id": self.variable_version.id, "server_template_id": self.server_template_sample.id, "value_char": "Old Value", "required": True, } ) # Open the wizard and update the variable value wizard = self.env["cx.tower.server.template.create.wizard"].create( { "server_template_id": self.server_template_sample.id, "name": "Server With Updated Variable", "ssh_username": "admin", "line_ids": [ ( 0, 0, { "variable_id": self.variable_version.id, "value_char": "New Value", "required": True, }, ) ], } ) wizard.action_confirm() # Checking that the variable value has been updated server = self.Server.search([("name", "=", "Server With Updated Variable")]) variable = server.variable_value_ids.filtered( lambda v: v.variable_id == self.variable_version ) self.assertEqual( variable.value_char, "New Value", "The variable value should be updated correctly.", ) def test_optional_variable_handling(self): """ Test that optional variables do not block server creation, even if their values are empty or missing. """ # Adding an optional variable to the template self.VariableValue.create( { "variable_id": self.variable_url.id, "server_template_id": self.server_template_sample.id, "value_char": "", "required": False, } ) # Specify an optional variable with an empty value values = self.server_template_sample._prepare_server_values( configuration_variables={self.variable_url.reference: ""} ) # Checking that the optional variable is processed correctly variable_data = next( ( v for v in values["variable_value_ids"] if v[2]["variable_id"] == self.variable_url.id ), None, ) self.assertIsNotNone( variable_data, "The optional variable should be included " "in the server values even if empty.", ) self.assertEqual( variable_data[2]["value_char"], "", "Optional variable should have an empty value.", ) def test_server_creation_with_all_required_variables_removed(self): """ Test that server creation fails if all required variables are removed in the wizard. Steps: 1. Create a server template with required variables. 2. Open the server creation wizard. 3. Remove all required variables from the wizard. 4. Attempt to create the server. Expected Result: - ValidationError is raised with a clear message listing missing variables. """ # Create a server template with mandatory variables template = self.ServerTemplate.create( { "name": "Template with required variables", "ssh_port": 22, "ssh_username": "admin", "ssh_auth_mode": "p", "os_id": self.os_debian_10.id, "variable_value_ids": [ ( 0, 0, { "variable_id": self.variable_path.id, "value_char": "/var/log", "required": True, }, ), ( 0, 0, { "variable_id": self.variable_dir.id, "value_char": "logs", "required": True, }, ), ], } ) # Simulating the launch of a wizard with the removal of all variables configuration_variables = {} # All variables removed # Checking that the server cannot be created with self.assertRaises(ValidationError) as cm: template._create_new_server( name="Server with missing variables", configuration_variables=configuration_variables, ) # Checking that the error message contains all removed variables error_message = str(cm.exception) self.assertIn("Please resolve the following issues", error_message) self.assertIn("Missing variables: test_path_, test_dir", error_message) def test_partial_required_variables_provided(self): """ Test that server creation fails if only some required variables are provided, and the error message includes both missing and empty variables. """ # Create a template with mandatory variables template = self.ServerTemplate.create( { "name": "Template with partial variables", "variable_value_ids": [ ( 0, 0, { "variable_id": self.variable_path.id, "value_char": "/var/log", "required": False, }, ), ( 0, 0, { "variable_id": self.variable_dir.id, "required": True, }, ), ], } ) # Launch the wizard and specify only some of the required variables configuration_variables = {"test_path_": "/var/log"} # test_dir skipped # Checking that the server is not being created with self.assertRaises(ValidationError) as cm: template._create_new_server( name="Server with partial variables", configuration_variables=configuration_variables, ) # Checking the error message error_message = str(cm.exception) self.assertIn("Missing variables: test_dir", error_message) self.assertNotIn("test_path_", error_message) # test_path_ provided def test_empty_values_for_required_variables(self): """ Test that server creation fails if required variables have empty values, and the error message includes these variables. """ # Create a template with mandatory variables template = self.ServerTemplate.create( { "name": "Template with empty values", "variable_value_ids": [ ( 0, 0, { "variable_id": self.variable_path.id, "value_char": "", "required": True, }, ), ( 0, 0, { "variable_id": self.variable_dir.id, "value_char": "", "required": True, }, ), ], } ) # Run the wizard with empty values for all variables configuration_variables = {"test_path_": "", "test_dir": ""} # Checking that the server is not being created with self.assertRaises(ValidationError) as cm: template._create_new_server( name="Server with empty variables", configuration_variables=configuration_variables, ) # Checking the error message error_message = str(cm.exception) self.assertIn("Empty values for variables: test_path_, test_dir", error_message) def test_with_partial_removed_variables_from_wizard(self): """ Test that server creation only with specified variables from wizard and option """ # create new variable option test_variable = self.Variable.create( { "name": "Test Variable", "variable_type": "s", } ) option = self.VariableOption.create( { "name": "test", "value_char": "test", "variable_id": test_variable.id, } ) # template with variables self.server_template_sample.write( { "variable_value_ids": [ ( 0, 0, { "variable_id": self.variable_path.id, "value_char": "/var/log", "required": False, }, ), ( 0, 0, { "variable_id": test_variable.id, "option_id": option.id, "required": False, }, ), ], } ) action = self.server_template_sample.action_create_server() # Open the wizard and fill in the data wizard = ( self.env["cx.tower.server.template.create.wizard"] # pylint: disable=context-overridden we new need a new clean context .with_context(action["context"]) .create( { "name": "Server from Template", "ip_v4_address": "localhost", "server_template_id": self.server_template_sample.id, } ) ) with Form(wizard) as wizard_form: wizard_form.line_ids.remove(0) wizard_form.save() wizard.action_confirm() server = self.server_template_sample.server_ids self.assertEqual( len(server.variable_value_ids), 1, "Server variable must be 1!" ) self.assertEqual( server.variable_value_ids.value_char, option.value_char, "The variable value must be equal to the value from the option", ) def test_manager_access_rights(self): """ Test manager access rights for Server Template records: - Read: user is in user_ids or manager_ids - Write: user is in manager_ids """ record = self.ServerTemplate.create( { "name": "Manager Access Test", "ssh_port": 22, "ssh_username": "admin", "ssh_auth_mode": "p", "os_id": self.os_debian_10.id, "user_ids": [(5, 0, 0)], "manager_ids": [(5, 0, 0)], } ) # Case 1: No access rights records = self.ServerTemplate.with_user(self.manager1).search( [("id", "=", record.id)] ) self.assertEqual( len(records), 0, "Manager should not see the record if not added to user_ids or manager_ids", ) # Case 2: Read access through user_ids record.write({"user_ids": [(4, self.manager1.id)]}) records = self.ServerTemplate.with_user(self.manager1).search( [("id", "=", record.id)] ) self.assertEqual( len(records), 1, "Manager should see the record when added to user_ids", ) # Write access should still be forbidden with self.assertRaises(AccessError): record.with_user(self.manager1).write({"name": "Updated Name"}) # Case 3: Full access through manager_ids record.write( { "user_ids": [(5, 0, 0)], "manager_ids": [(4, self.manager1.id)], } ) records = self.ServerTemplate.with_user(self.manager1).search( [("id", "=", record.id)] ) self.assertEqual( len(records), 1, "Manager should see the record when added to manager_ids", ) # Write access should now work try: record.with_user(self.manager1).write({"name": "Updated Name"}) except AccessError: self.fail("Manager should be able to update the record when in manager_ids") def test_manager_create_access(self): """ Test that a manager can only create a Server Template record if they add themselves to manager_ids. """ # Try to create without adding to manager_ids with self.assertRaises(AccessError): self.ServerTemplate.with_user(self.manager1).create( { "name": "Create Access Test - Should Fail", "ssh_port": 22, "ssh_username": "admin", "ssh_auth_mode": "p", "os_id": self.os_debian_10.id, "manager_ids": [(5, 0, 0)], } ) # Create with manager_ids - should succeed record = self.ServerTemplate.with_user(self.manager1).create( { "name": "Create Access Test - Should Succeed", "ssh_port": 22, "ssh_username": "admin", "ssh_auth_mode": "p", "os_id": self.os_debian_10.id, "manager_ids": [(4, self.manager1.id)], } ) self.assertEqual( len(self.ServerTemplate.search([("id", "=", record.id)])), 1, "Manager should be able to create record when added to manager_ids", ) def test_manager_delete_access(self): """ Test that a manager can only delete a Server Template record if: - They are in manager_ids - They created the record """ # Scenario 1: Manager1 creates and tries to delete their own record record = self.ServerTemplate.with_user(self.manager1).create( { "name": "Delete Access Test - Own Record", "ssh_port": 22, "ssh_username": "admin", "ssh_auth_mode": "p", "os_id": self.os_debian_10.id, "manager_ids": [(4, self.manager1.id)], } ) try: record.with_user(self.manager1).unlink() except AccessError: self.fail( "Manager should be able to delete their own record if in manager_ids" ) # Scenario 2: Manager2 creates record, Manager1 tries to delete record2 = self.ServerTemplate.with_user(self.manager2).create( { "name": "Delete Access Test - Other's Record", "ssh_port": 22, "ssh_username": "admin", "ssh_auth_mode": "p", "os_id": self.os_debian_10.id, "manager_ids": [(6, 0, [self.manager1.id, self.manager2.id])], } ) # Manager1 should not be able to delete Manager2's record with self.assertRaises(AccessError): record2.with_user(self.manager1).unlink() # Remove Manager2 from manager_ids record2.write({"manager_ids": [(5, 0, 0)]}) # Manager2 should not be able to delete their record now with self.assertRaises(AccessError): record2.with_user(self.manager2).unlink() # Scenario 3: Manager1 creates record but is later removed from manager_ids record3 = self.ServerTemplate.with_user(self.manager1).create( { "name": "Delete Access Test - Removed Manager", "ssh_port": 22, "ssh_username": "admin", "ssh_auth_mode": "p", "os_id": self.os_debian_10.id, "manager_ids": [(4, self.manager1.id)], } ) # Remove Manager1 from manager_ids record3.write({"manager_ids": [(5, 0, 0)]}) # Manager1 should not be able to delete their record after being removed with self.assertRaises(AccessError): record3.with_user(self.manager1).unlink() def test_server_template_reference_update(self): """Test server template reference update cascades to dependent models""" # 1. Add a variable value to server_template_sample variable_value = self.VariableValue.create( { "variable_id": self.variable_os.id, "value_char": "Ubuntu 20.04", "server_template_id": self.server_template_sample.id, } ) # Store original references for comparison original_template_reference = self.server_template_sample.reference original_variable_value_reference = variable_value.reference # 2. Change the reference for server_template_sample to "super_template" self.server_template_sample.write({"reference": "super_template"}) # 3. Verify that references are updated for dependent models # Invalidate models to refresh all references self.env["cx.tower.server.template"].invalidate_model(["reference"]) self.env["cx.tower.variable.value"].invalidate_model(["reference"]) # Check that server template reference was updated self.assertEqual(self.server_template_sample.reference, "super_template") self.assertNotEqual( self.server_template_sample.reference, original_template_reference ) # Check that variable value reference was updated # to include the new template reference self.assertIn("super_template", variable_value.reference) self.assertNotEqual(variable_value.reference, original_variable_value_reference) # Verify the reference pattern for variable value follows the expected format: # ___ # noqa: E501 expected_variable_pattern = ( f"{self.variable_os.reference}_variable_value_server_template_" f"{self.server_template_sample.reference}" ) self.assertEqual(variable_value.reference, expected_variable_pattern)