Tower: upload cetmix_tower_server 18.0.2.0.0 (was 18.0.2.0.0, via marketplace)

This commit is contained in:
2026-05-03 18:54:38 +00:00
parent 5880120a84
commit c83da26305
235 changed files with 89704 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
from . import test_server
from . import test_command
from . import test_file
from . import test_file_template
from . import test_plan
from . import test_plan_line
from . import test_plan_line_action
from . import test_command_log
from . import test_plan_log
from . import test_server_log
from . import test_server_template
from . import test_variable
from . import test_variable_value
from . import test_variable_option
from . import test_command_wizard
from . import test_reference_mixin
from . import test_scheduled_task
from . import test_update_related_variable_names
from . import test_key
from . import test_cetmix_tower
from . import test_tag
from . import test_shortcut
from . import test_tools
from . import test_partner_server_btn
from . import test_vault_mixin
from . import test_tag_mixin
from . import test_jet_template
from . import test_jet_template_access
from . import test_jet_template_dependency_access
from . import test_jet_template_install
from . import test_jet_template_install_access
from . import test_jet_template_install_line_access
from . import test_jet_access
from . import test_jet_dependency_access
from . import test_jet_action_access
from . import test_jet_create_wizard
from . import test_jet_state
from . import test_jet
from . import test_server_jet_action_command
from . import test_jet_waypoint
from . import test_jet_waypoint_template_access
from . import test_jet_waypoint_access

View File

@@ -0,0 +1,515 @@
# Copyright (C) 2022 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import os
from unittest.mock import MagicMock, patch
from odoo import _
from odoo.exceptions import ValidationError
from odoo.addons.base.tests.common import BaseCommon
from ..models.constants import GENERAL_ERROR
from ..ssh.ssh import SftpService, SSHConnection
class TestTowerCommon(BaseCommon):
"""
Common test class for Cetmix Tower.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Disable transaction commit to avoid race conditions
cls.env = cls.env["base"].with_context(cetmix_tower_no_commit=True).env
# ----------------------------------------------
# -- Create core elements invoked in the tests
# ----------------------------------------------
# Group XML records
cls.group_user = cls.env.ref("cetmix_tower_server.group_user")
cls.group_manager = cls.env.ref("cetmix_tower_server.group_manager")
cls.group_root = cls.env.ref("cetmix_tower_server.group_root")
# Cetmix Tower helper model
cls.CetmixTower = cls.env["cetmix.tower"]
# Tags
cls.Tag = cls.env["cx.tower.tag"]
cls.tag_test_staging = cls.Tag.create({"name": "Test Staging"})
cls.tag_test_production = cls.Tag.create({"name": "Test Production"})
# Users
cls.Users = cls.env["res.users"]
cls.user_bob = cls.Users.create(
{
"name": "Bob",
"login": "bob",
"groups_id": [(4, cls.env.ref("base.group_user").id)],
}
)
cls.user = cls.Users.create(
{
"name": "Test User",
"login": "test_user",
"email": "test_user@example.com",
"groups_id": [
(6, 0, [cls.group_user.id, cls.env.ref("base.group_user").id])
],
}
)
cls.manager = cls.Users.create(
{
"name": "Test Manager",
"login": "test_manager",
"email": "test_manager@example.com",
"groups_id": [
(6, 0, [cls.group_manager.id, cls.env.ref("base.group_user").id])
],
}
)
cls.root = cls.Users.create(
{
"name": "Test Root",
"login": "test_root",
"email": "test_root@example.com",
"groups_id": [
(6, 0, [cls.group_root.id, cls.env.ref("base.group_user").id])
],
}
)
# OS
cls.os_debian_10 = cls.env["cx.tower.os"].create({"name": "Test Debian 10"})
# Server
cls.Server = cls.env["cx.tower.server"]
cls.server_test_1 = cls.Server.create(
{
"name": "Test 1",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"host_key": "test_key",
"os_id": cls.os_debian_10.id,
}
)
# Server Template
cls.ServerTemplate = cls.env["cx.tower.server.template"]
cls.server_template_sample = cls.ServerTemplate.create(
{
"name": "Sample Template",
"ssh_port": 22,
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"os_id": cls.os_debian_10.id,
}
)
# Server log
cls.ServerLog = cls.env["cx.tower.server.log"]
# Variable
cls.Variable = cls.env["cx.tower.variable"]
cls.VariableValue = cls.env["cx.tower.variable.value"]
cls.VariableOption = cls.env["cx.tower.variable.option"]
cls.variable_path = cls.Variable.create({"name": "test_path_"})
cls.variable_dir = cls.Variable.create({"name": "test_dir"})
cls.variable_os = cls.Variable.create({"name": "test_os"})
cls.variable_url = cls.Variable.create({"name": "test_url"})
cls.variable_version = cls.Variable.create({"name": "test_version"})
# Key
cls.Key = cls.env["cx.tower.key"]
cls.KeyValue = cls.env["cx.tower.key.value"]
cls.key_1 = cls.Key.create(
{"name": "Test Key 1", "key_type": "k", "secret_value": "much key"}
)
cls.secret_2 = cls.Key.create(
{"name": "Test Key 2", "key_type": "s", "secret_value": "secret top"}
)
# Command
cls.sudo_prefix = "sudo -S -p ''"
cls.Command = cls.env["cx.tower.command"]
cls.command_create_dir = cls.Command.create(
{
"name": "Test create directory",
"path": "/home/{{ tower.server.username }}",
"code": "cd {{ test_path_ }} && mkdir {{ test_dir }}",
}
)
cls.command_list_dir = cls.Command.create(
{
"name": "Test create directory",
"path": "/home/{{ tower.server.username }}",
"code": "cd {{ test_path_ }} && ls -l",
}
)
cls.template_file_tower = cls.env["cx.tower.file.template"].create(
{
"name": "Test file template",
"file_name": "test_os.txt",
"source": "tower",
"server_dir": "/home/{{ tower.server.username }}",
"code": "Hello, world!",
}
)
cls.template_file_server = cls.env["cx.tower.file.template"].create(
{
"name": "Test file template",
"file_name": "test_os.txt",
"source": "server",
"server_dir": "/home/{{ tower.server.username }}",
}
)
cls.command_create_file_with_template_tower_source = cls.Command.create(
{
"name": "Test create file with template with tower source",
"path": "/home/{{ tower.server.username }}",
"action": "file_using_template",
"file_template_id": cls.template_file_tower.id,
"if_file_exists": "raise",
}
)
cls.command_create_file_with_template_server_source = cls.Command.create(
{
"name": "Test create file with template with server source",
"path": "/home/{{ tower.server.username }}",
"action": "file_using_template",
"file_template_id": cls.template_file_server.id,
"if_file_exists": "raise",
}
)
# Command log
cls.CommandLog = cls.env["cx.tower.command.log"]
# File template
cls.FileTemplate = cls.env["cx.tower.file.template"]
# File
cls.File = cls.env["cx.tower.file"]
# Flight Plans
cls.Plan = cls.env["cx.tower.plan"]
cls.plan_line = cls.env["cx.tower.plan.line"]
cls.plan_line_action = cls.env["cx.tower.plan.line.action"]
cls.plan_1 = cls.Plan.create(
{
"name": "Test plan 1",
"note": "Create directory and list its content",
"tag_ids": [(6, 0, [cls.tag_test_staging.id])],
}
)
cls.plan_line_1 = cls.plan_line.create(
{
"sequence": 5,
"plan_id": cls.plan_1.id,
"command_id": cls.command_create_dir.id,
"path": "/such/much/path",
}
)
cls.plan_line_2 = cls.plan_line.create(
{
"sequence": 20,
"plan_id": cls.plan_1.id,
"command_id": cls.command_list_dir.id,
}
)
cls.plan_line_1_action_1 = cls.plan_line_action.create(
{
"line_id": cls.plan_line_1.id,
"sequence": 1,
"condition": "==",
"value_char": "0",
}
)
cls.plan_line_1_action_2 = cls.plan_line_action.create(
{
"line_id": cls.plan_line_1.id,
"sequence": 2,
"condition": ">",
"value_char": "0",
"action": "ec",
"custom_exit_code": 255,
}
)
cls.plan_line_2_action_1 = cls.plan_line_action.create(
{
"line_id": cls.plan_line_2.id,
"sequence": 1,
"condition": "==",
"value_char": "-1",
"action": "ec",
"custom_exit_code": 100,
}
)
cls.plan_line_2_action_2 = cls.plan_line_action.create(
{
"line_id": cls.plan_line_2.id,
"sequence": 2,
"condition": ">=",
"value_char": "3",
"action": "n",
}
)
# Flight plan log
cls.PlanLog = cls.env["cx.tower.plan.log"]
# Shortcut
cls.Shortcut = cls.env["cx.tower.shortcut"]
# Model references
cls.OS = cls.env["cx.tower.os"]
cls.PlanLineAction = cls.env["cx.tower.plan.line.action"]
# Scheduled task
cls.ScheduledTask = cls.env["cx.tower.scheduled.task"]
cls.ScheduledTaskCv = cls.env["cx.tower.scheduled.task.cv"]
# Jet State
cls.JetState = cls.env["cx.tower.jet.state"]
# Jet Action
cls.JetAction = cls.env["cx.tower.jet.action"]
# Jet Template Install
cls.JetTemplateInstall = cls.env["cx.tower.jet.template.install"]
# Jet Template Install Line
cls.JetTemplateInstallLine = cls.env["cx.tower.jet.template.install.line"]
# Jet Template Dependency
cls.JetTemplateDependency = cls.env["cx.tower.jet.template.dependency"]
# Jet Template
cls.JetTemplate = cls.env["cx.tower.jet.template"]
cls.jet_template_sample = cls.JetTemplate.create(
{
"name": "Sample Jet Template",
"server_ids": [(4, cls.server_test_1.id)],
"variable_value_ids": [
(
0,
0,
{
"variable_id": cls.variable_path.id,
"value_char": "/jets/templates/template1",
},
),
(
0,
0,
{"variable_id": cls.variable_os.id, "value_char": "Debian 10"},
),
(
0,
0,
{
"variable_id": cls.variable_url.id,
"value_char": "https://jets.example.com",
},
),
(
0,
0,
{
"variable_id": cls.variable_dir.id,
"value_char": "jet_templates",
},
),
],
}
)
# Jets
cls.Jet = cls.env["cx.tower.jet"]
cls.jet_sample = cls.Jet.create(
{
"name": "Sample Jet",
"jet_template_id": cls.jet_template_sample.id,
"server_id": cls.server_test_1.id,
"variable_value_ids": [
(
0,
0,
{
"variable_id": cls.variable_path.id,
"value_char": "/jets/jet1",
},
)
],
}
)
# apply ssh connection patches
cls.apply_patches()
@classmethod
def apply_patches(cls):
"""
Apply mock patches for SSH-related methods to simulate various
scenarios during testing.
Patches:
1. SSHConnection.connect:
- Returns a mock connection with a fake exec_command method,
which returns a successful or unsuccessful result depending on the
command content.
2. SftpService.download_file:
- Returns b"ok\x00" for files with the .zip extension and
b"ok" for the rest.
3. SftpService.upload_file:
- Returns MagicMock, simulating file upload.
4. SftpService.delete_file:
- Returns MagicMock, simulating file deletion.
"""
# Patch connection SSH method
def ssh_connect(self):
connection_mock = MagicMock()
# set up stdin with a condition for error simulation
def exec_command_side_effect(command, *args, **kwargs):
# Create mocks for stdin, stdout, and stderr
stdin_mock = MagicMock()
stdout_mock = MagicMock()
stderr_mock = MagicMock()
if "fail" in command:
# Simulate failure
stdout_mock.channel.recv_exit_status.return_value = GENERAL_ERROR
stdout_mock.readlines.return_value = []
stderr_mock.readlines.return_value = ["error"]
return stdin_mock, stdout_mock, stderr_mock
elif "raise" in command:
# Simulate an exception
raise Exception("error") # pylint: disable=broad-exception-raised
else:
# Simulate success
stdout_mock.channel.recv_exit_status.return_value = 0
stdout_mock.readlines.return_value = ["ok"]
stderr_mock.readlines.return_value = []
return stdin_mock, stdout_mock, stderr_mock
# Apply side effect to exec_command
connection_mock.exec_command.side_effect = exec_command_side_effect
return connection_mock
connect_patch = patch.object(SSHConnection, "connect", new=ssh_connect)
connect_patch.start()
cls.addClassCleanup(connect_patch.stop)
# Patch file manipulation methods for testing
def ssh_download_file(self, remote_path):
if hasattr(self, "env"):
error = self.env.context.get("raise_download_error")
if error:
raise ValidationError(error)
_, extension = os.path.splitext(remote_path)
if extension == ".zip":
return b"ok\x00"
return b"ok"
download_patch = patch.object(
SftpService, "download_file", new=ssh_download_file
)
download_patch.start()
cls.addClassCleanup(download_patch.stop)
def ssh_upload_file(self, file, remote_path):
if hasattr(self, "env"):
error = self.env.context.get("raise_upload_error")
if error:
raise ValidationError(error)
return MagicMock()
upload_patch = patch.object(SftpService, "upload_file", new=ssh_upload_file)
upload_patch.start()
cls.addClassCleanup(upload_patch.stop)
def ssh_delete_file(self, remote_path):
return MagicMock()
delete_patch = patch.object(SftpService, "delete_file", new=ssh_delete_file)
delete_patch.start()
cls.addClassCleanup(delete_patch.stop)
@classmethod
def add_to_group(cls, user, group_refs):
"""Add user to groups
Args:
user (res.users): User record
group_refs (list): Group ref OR List of group references
eg ['base.group_user', 'some_module.some_group'...]
"""
if isinstance(group_refs, str):
group = cls.env.ref(group_refs, raise_if_not_found=False)
if not group:
raise ValidationError(_("Group reference %s not found!") % group_refs)
action = [(4, group.id)]
elif isinstance(group_refs, list):
action = []
for group_ref in group_refs:
group = cls.env.ref(group_ref, raise_if_not_found=False)
if not group:
raise ValidationError(
_("Group reference %s not found!") % group_ref
)
action.append((4, group.id))
else:
raise ValidationError(_("groups_ref must be string or list of strings!"))
user.write({"groups_id": action})
@classmethod
def remove_from_group(cls, user, group_refs):
"""Remove user from groups
Args:
user (res.users): User record
group_refs (list): List of group references
eg ['base.group_user', 'some_module.some_group'...]
"""
if isinstance(group_refs, str):
group = cls.env.ref(group_refs, raise_if_not_found=False)
if not group:
raise ValidationError(_("Group reference %s not found!") % group_refs)
action = [(3, group.id)]
elif isinstance(group_refs, list):
action = []
for group_ref in group_refs:
group = cls.env.ref(group_ref, raise_if_not_found=False)
if not group:
raise ValidationError(
_("Group reference %s not found!") % group_ref
)
action.append((3, group.id))
else:
raise ValidationError(_("groups_ref must be string or list of strings!"))
user.write({"groups_id": action})
@classmethod
def write_and_invalidate(cls, records, **values):
"""Write values and invalidate cache
Args:
records (recordset): recordset to save values
**values (dict): values to set
"""
if values:
records.write(values)
records.invalidate_recordset(values.keys())

View File

@@ -0,0 +1,732 @@
# Copyright (C) 2024 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.exceptions import AccessError
from odoo.tools import LazyTranslate
from .common import TestTowerCommon
_lt = LazyTranslate(__name__, default_lang="en_US")
class TestTowerJetsCommon(TestTowerCommon):
"""
Common test class for Jet and JetTemplate models with shared test data
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create jet states for testing
cls.state_initial = cls.JetState.create(
{
"name": "Test Initial",
"reference": "test_initial",
"sequence": 10,
"color": 1,
}
)
cls.state_running = cls.JetState.create(
{
"name": "Test Running",
"reference": "test_running",
"sequence": 20,
"color": 2,
}
)
cls.state_stopped = cls.JetState.create(
{
"name": "Test Stopped",
"reference": "test_stopped",
"sequence": 30,
"color": 3,
}
)
cls.state_error = cls.JetState.create(
{
"name": "Test Error",
"reference": "test_error",
"sequence": 40,
"color": 4,
}
)
# Create transit states
cls.state_starting = cls.JetState.create(
{
"name": "Test Starting",
"reference": "test_starting",
"sequence": 15,
"color": 5,
}
)
cls.state_stopping = cls.JetState.create(
{
"name": "Test Stopping",
"reference": "test_stopping",
"sequence": 25,
"color": 6,
}
)
# Create test states for pathfinding and adjacency tests
cls.state_a = cls.JetState.create(
{
"name": "Test State A",
"reference": "test_state_a",
"sequence": 30,
}
)
cls.state_b = cls.JetState.create(
{
"name": "Test State B",
"reference": "test_state_b",
"sequence": 31,
}
)
cls.state_c = cls.JetState.create(
{
"name": "Test State C",
"reference": "test_state_c",
"sequence": 32,
}
)
cls.state_d = cls.JetState.create(
{
"name": "Test State D",
"reference": "test_state_d",
"sequence": 33,
}
)
# Create jet template for testing
cls.jet_template_test = cls.JetTemplate.create(
{
"name": "Test Jet Template",
"reference": "test_jet_template",
}
)
# Create dependency hierarchy for testing:
# Odoo -> Postgres, Nginx -> Docker -> Tower Core
# Level 1: Base dependencies
cls.jet_template_tower_core = cls.JetTemplate.create(
{
"name": "Tower Core",
"reference": "tower_core",
}
)
# Level 2: Infrastructure
cls.jet_template_docker = cls.JetTemplate.create(
{
"name": "Docker",
"reference": "docker",
}
)
# Docker requires Tower Core to be running
cls._create_jet_template_dependency(
template=cls.jet_template_docker,
template_required=cls.jet_template_tower_core,
state_required_id=cls.state_running.id,
)
# Level 3: Services
cls.jet_template_nginx = cls.JetTemplate.create(
{
"name": "Nginx",
"reference": "nginx",
}
)
# Nginx requires Docker to be running
cls._create_jet_template_dependency(
template=cls.jet_template_nginx,
template_required=cls.jet_template_docker,
state_required_id=cls.state_running.id,
)
# Level 3: Database
cls.jet_template_postgres = cls.JetTemplate.create(
{
"name": "Postgres",
"reference": "postgres",
}
)
# Postgres requires Docker to be running
cls._create_jet_template_dependency(
template=cls.jet_template_postgres,
template_required=cls.jet_template_docker,
state_required_id=cls.state_running.id,
)
cls.jet_template_mariadb = cls.JetTemplate.create(
{
"name": "MariaDB",
"reference": "mariadb",
}
)
# MariaDB requires Docker to be running
cls._create_jet_template_dependency(
template=cls.jet_template_mariadb,
template_required=cls.jet_template_docker,
state_required_id=cls.state_running.id,
)
# Level 5: Applications
cls.jet_template_odoo = cls.JetTemplate.create(
{
"name": "Odoo",
"reference": "odoo",
}
)
# Odoo requires Postgres to be running
cls._create_jet_template_dependency(
template=cls.jet_template_odoo,
template_required=cls.jet_template_postgres,
state_required_id=cls.state_running.id,
)
# Odoo requires Nginx to be running
cls._create_jet_template_dependency(
template=cls.jet_template_odoo,
template_required=cls.jet_template_nginx,
state_required_id=cls.state_running.id,
)
cls.jet_template_wordpress = cls.JetTemplate.create(
{
"name": "WordPress",
"reference": "wordpress",
}
)
# WordPress requires MariaDB to be running
cls._create_jet_template_dependency(
template=cls.jet_template_wordpress,
template_required=cls.jet_template_mariadb,
state_required_id=cls.state_running.id,
)
# WordPress requires Nginx to be running
cls._create_jet_template_dependency(
template=cls.jet_template_wordpress,
template_required=cls.jet_template_nginx,
state_required_id=cls.state_running.id,
)
# Level 6: E-commerce Integration
cls.jet_template_woocommerce_odoo = cls.JetTemplate.create(
{
"name": "WooCommerce with Odoo",
"reference": "woocommerce_odoo",
}
)
# WooCommerce requires WordPress to be running
cls._create_jet_template_dependency(
template=cls.jet_template_woocommerce_odoo,
template_required=cls.jet_template_wordpress,
state_required_id=cls.state_running.id,
)
# WooCommerce requires Odoo to be running
cls._create_jet_template_dependency(
template=cls.jet_template_woocommerce_odoo,
template_required=cls.jet_template_odoo,
state_required_id=cls.state_running.id,
)
# Create test jets for different templates
cls.jet_test = cls._create_jet(
name="Test Jet",
reference="test_jet",
template=cls.jet_template_test,
server=cls.server_test_1,
)
cls.jet_odoo = cls._create_jet(
name="Odoo Jet",
reference="odoo_jet",
template=cls.jet_template_odoo,
server=cls.server_test_1,
)
cls.jet_wordpress = cls._create_jet(
name="WordPress Jet",
reference="wordpress_jet",
template=cls.jet_template_wordpress,
server=cls.server_test_1,
)
cls.jet_woocommerce = cls._create_jet(
name="WooCommerce Jet",
reference="woocommerce_jet",
template=cls.jet_template_woocommerce_odoo,
server=cls.server_test_1,
)
# Add some dependencies with different state requirements for testing
# Create a monitoring template that requires services to be in "running" state
cls.jet_template_monitoring = cls.JetTemplate.create(
{
"name": "Monitoring",
"reference": "monitoring",
}
)
# Monitoring requires Odoo to be running (for business metrics)
cls._create_jet_template_dependency(
template=cls.jet_template_monitoring,
template_required=cls.jet_template_odoo,
state_required_id=cls.state_running.id,
)
# Create a backup template that requires services to be in "stopped" state
cls.jet_template_backup = cls.JetTemplate.create(
{
"name": "Backup",
"reference": "backup",
}
)
# Backup requires Postgres to be stopped for safe backup
cls._create_jet_template_dependency(
template=cls.jet_template_backup,
template_required=cls.jet_template_postgres,
state_required_id=cls.state_stopped.id,
)
# Create common actions for testing
cls.action_running_to_stopped = cls.JetAction.create(
{
"name": "Stop Action",
"reference": "stop_action",
"jet_template_id": cls.jet_template_test.id,
"state_from_id": cls.state_running.id,
"state_to_id": cls.state_stopped.id,
"state_transit_id": cls.state_stopping.id,
"priority": 10,
}
)
cls.action_stopped_to_running = cls.JetAction.create(
{
"name": "Start Action",
"reference": "start_action",
"jet_template_id": cls.jet_template_test.id,
"state_from_id": cls.state_stopped.id,
"state_to_id": cls.state_running.id,
"state_transit_id": cls.state_starting.id,
"priority": 10,
}
)
cls.action_running_to_error = cls.JetAction.create(
{
"name": "Error Action",
"reference": "error_action",
"jet_template_id": cls.jet_template_test.id,
"state_from_id": cls.state_running.id,
"state_to_id": cls.state_error.id,
"state_transit_id": cls.state_error.id,
"priority": 20,
}
)
cls.action_error_to_running = cls.JetAction.create(
{
"name": "Recover Action",
"reference": "recover_action",
"jet_template_id": cls.jet_template_test.id,
"state_from_id": cls.state_error.id,
"state_to_id": cls.state_running.id,
"state_transit_id": cls.state_starting.id,
"priority": 10,
}
)
cls.action_initial_to_running = cls.JetAction.create(
{
"name": "Initialize Action",
"reference": "initialize_action",
"jet_template_id": cls.jet_template_test.id,
"state_from_id": cls.state_initial.id,
"state_to_id": cls.state_running.id,
"state_transit_id": cls.state_starting.id,
"priority": 5,
}
)
# Create actions for pathfinding tests (A -> B -> C -> D)
cls.action_a_to_b = cls.JetAction.create(
{
"name": "Action A to B",
"reference": "action_a_to_b",
"jet_template_id": cls.jet_template_test.id,
"state_from_id": cls.state_a.id,
"state_to_id": cls.state_b.id,
"state_transit_id": cls.state_starting.id,
"priority": 10,
}
)
cls.action_b_to_c = cls.JetAction.create(
{
"name": "Action B to C",
"reference": "action_b_to_c",
"jet_template_id": cls.jet_template_test.id,
"state_from_id": cls.state_b.id,
"state_to_id": cls.state_c.id,
"state_transit_id": cls.state_stopping.id,
"priority": 10,
}
)
cls.action_c_to_d = cls.JetAction.create(
{
"name": "Action C to D",
"reference": "action_c_to_d",
"jet_template_id": cls.jet_template_test.id,
"state_from_id": cls.state_c.id,
"state_to_id": cls.state_d.id,
"state_transit_id": cls.state_stopping.id,
"priority": 10,
}
)
cls.action_a_to_c = cls.JetAction.create(
{
"name": "Action A to C (direct)",
"reference": "action_a_to_c",
"jet_template_id": cls.jet_template_test.id,
"state_from_id": cls.state_a.id,
"state_to_id": cls.state_c.id,
"state_transit_id": cls.state_stopping.id,
"priority": 10,
}
)
# Create border actions (create and destroy)
cls.action_create = cls.JetAction.create(
{
"name": "Create Action",
"reference": "create_action",
"jet_template_id": cls.jet_template_test.id,
"state_from_id": False, # No initial state
"state_to_id": cls.state_running.id,
"state_transit_id": cls.state_starting.id,
"priority": 1,
}
)
cls.action_destroy = cls.JetAction.create(
{
"name": "Destroy Action",
"reference": "destroy_action",
"jet_template_id": cls.jet_template_test.id,
"state_from_id": cls.state_running.id,
"state_to_id": False, # No final state
"state_transit_id": cls.state_stopping.id,
"priority": 1,
}
)
# Create a clean template for tests that need isolation from common actions
cls.clean_template = cls.JetTemplate.create(
{
"name": "Clean Template",
"reference": "clean_template",
}
)
# Create waypoint template for testing
cls.waypoint_template = cls.env["cx.tower.jet.waypoint.template"].create(
{
"name": "Test Waypoint Template",
"jet_template_id": cls.jet_template_test.id,
}
)
cls.waypoint_template_2 = cls.env["cx.tower.jet.waypoint.template"].create(
{
"name": "Test Waypoint Template 2",
"jet_template_id": cls.jet_template_test.id,
}
)
# Create waypoint for testing
cls.waypoint = cls.env["cx.tower.jet.waypoint"].create(
{
"name": "Test Waypoint",
"jet_id": cls.jet_test.id,
"waypoint_template_id": cls.waypoint_template.id,
}
)
# Model references reused by helpers
cls.JetDependency = cls.env["cx.tower.jet.dependency"]
cls.JetWaypointTemplate = cls.env["cx.tower.jet.waypoint.template"]
cls.JetWaypoint = cls.env["cx.tower.jet.waypoint"]
@classmethod
def _create_jet(
cls,
name,
reference,
template=None,
server=None,
user_ids=None,
manager_ids=None,
server_user_ids=None,
server_manager_ids=None,
with_user=None,
):
"""
Helper method to create a jet
with specified access configuration
Args:
name (str): Name of the jet
reference (str): Reference of the jet
template (cx.tower.jet.template): Template for the jet
(if None, defaults to jet_template_test)
server (cx.tower.server): Server for the jet
(if None, defaults to server_test_1)
user_ids (list): List of user IDs for the jet
manager_ids (list): List of manager IDs for the jet
server_user_ids (list): List of user IDs for the server
server_manager_ids (list): List of manager IDs for the server
with_user (res.users): Optional user
to create the jet as (for access rule testing)
Returns:
cx.tower.jet: Created jet record
"""
if template is None:
template = cls.jet_template_test
if server is None:
server = cls.server_test_1
# Configure server access
if server_user_ids is not None or server_manager_ids is not None:
server.write(
{
"user_ids": server_user_ids
if server_user_ids is not None
else [(5, 0, 0)],
"manager_ids": server_manager_ids
if server_manager_ids is not None
else [(5, 0, 0)],
}
)
# Create jet with access configuration
jet_vals = {
"name": name,
"reference": reference,
"jet_template_id": template.id,
"server_id": server.id,
"user_ids": user_ids if user_ids is not None else [(5, 0, 0)],
"manager_ids": manager_ids if manager_ids is not None else [(5, 0, 0)],
}
jet_model = cls.Jet.with_user(with_user) if with_user else cls.Jet
jet = jet_model.create(jet_vals)
return jet
@classmethod
def _create_jet_dependency(
cls,
jet_name,
jet_reference,
depends_on_name,
depends_on_reference,
jet_user_ids=None,
jet_manager_ids=None,
depends_on_user_ids=None,
depends_on_manager_ids=None,
jet_server_user_ids=None,
jet_server_manager_ids=None,
depends_on_server_user_ids=None,
depends_on_server_manager_ids=None,
with_user=None,
jet_template=None,
depends_on_template=None,
):
"""Helper method to create a dependency between two jets
Args:
jet_name (str): Name of the main jet
jet_reference (str): Reference of the main jet
depends_on_name (str): Name of the jet this depends on
depends_on_reference (str): Reference of the jet this depends on
jet_user_ids (list): User IDs for the main jet
jet_manager_ids (list): Manager IDs for the main jet
depends_on_user_ids (list): User IDs for the depends_on jet
depends_on_manager_ids (list): Manager IDs for the depends_on jet
jet_server_user_ids (list): User IDs for the main jet's server
jet_server_manager_ids (list): Manager IDs for the main jet's server
depends_on_server_user_ids (list): User IDs for the depends_on jet's server
depends_on_server_manager_ids (list): Manager IDs for the depends_on
jet's server (if None, defaults to server_test_1)
with_user (res.users): Optional user to create the dependency as
(for access rule testing)
jet_template: Optional template for the main jet
(if None, defaults to jet_template_test)
depends_on_template: Optional template for the depends_on jet
(if None, defaults to jet_template_tower_core)
Returns:
tuple: (jet, depends_on_jet, dependency)
"""
# Use different templates to avoid self-dependency error
# Default to jet_template_test for the main jet and
# jet_template_tower_core for depends_on
jet_template = jet_template or cls.jet_template_test
depends_on_template = depends_on_template or cls.jet_template_tower_core
# Check if template dependency already exists, if so reuse it
template_dep = cls.JetTemplateDependency.search(
[
("template_id", "=", jet_template.id),
("template_required_id", "=", depends_on_template.id),
],
limit=1,
)
if not template_dep:
# Create template dependency first
# to ensure templates are different
(
_template,
_required_template,
template_dep,
) = cls._create_jet_template_dependency(
template=jet_template,
template_required=depends_on_template,
)
# Create first jet
# (always create as root to ensure proper setup)
jet = cls._create_jet(
jet_name,
jet_reference,
template=jet_template,
user_ids=jet_user_ids,
manager_ids=jet_manager_ids,
server_user_ids=jet_server_user_ids,
server_manager_ids=jet_server_manager_ids,
with_user=None, # Create as root to ensure proper setup
)
# Create second jet (depended on)
# (also create as root to ensure proper setup)
depends_on_jet = cls._create_jet(
depends_on_name,
depends_on_reference,
template=depends_on_template,
user_ids=depends_on_user_ids,
manager_ids=depends_on_manager_ids,
server_user_ids=depends_on_server_user_ids,
server_manager_ids=depends_on_server_manager_ids,
with_user=None, # Create as root to ensure proper setup,
)
# If creating dependency with a user context, verify access first
if with_user:
# Verify manager can access both jets by searching in their context
# This ensures the access rule domain can evaluate correctly
# when creating the dependency
jet_search = cls.Jet.with_user(with_user).search([("id", "=", jet.id)])
depends_search = cls.Jet.with_user(with_user).search(
[("id", "=", depends_on_jet.id)]
)
if not jet_search or not depends_search:
raise AccessError(
_lt("Manager must have access to both jets before creating")
)
# Force cache refresh to ensure Many2one relations are accessible,
jet.invalidate_recordset(["manager_ids", "user_ids"])
depends_on_jet.invalidate_recordset(["user_ids", "manager_ids"])
# Create dependency
dependency_vals = {
"jet_id": jet.id,
"jet_depends_on_id": depends_on_jet.id,
"jet_template_dependency_id": template_dep.id,
}
dependency_model = (
cls.JetDependency.with_user(with_user) if with_user else cls.JetDependency
)
dependency = dependency_model.create(dependency_vals)
return jet, depends_on_jet, dependency
@classmethod
def _create_jet_template_dependency(
cls,
template_name=None,
template_reference=None,
access_level="2",
user_ids=None,
manager_ids=None,
template=None,
template_required=None,
state_required_id=None,
with_user=None,
):
"""Helper method to create a dependency between two templates
Args:
template_name (str, optional): Name of the template (if creating new)
template_reference (str, optional): Reference of the template
(if creating new)
access_level (str): Access level for the template
(if creating new, defaults to "2")
user_ids (list): List of user IDs for the template
manager_ids (list): List of manager IDs for the template
template: Existing template record or None to create new
(if None, defaults to jet_template_test)
template_required: Existing required template record or None to create new
(if None, defaults to jet_template_tower_core)
state_required_id: Optional state required ID for the dependency
Returns:
tuple: (template, required_template, dependency)
"""
# Create or use existing template
if template is None:
template_vals = {
"name": template_name,
"reference": template_reference,
"access_level": access_level,
"user_ids": user_ids if user_ids is not None else [(5, 0, 0)],
"manager_ids": manager_ids if manager_ids is not None else [(5, 0, 0)],
}
template = cls.JetTemplate.create(template_vals)
# Create or use existing required template
if template_required is None:
required_template = cls.JetTemplate.create(
{
"name": "Required Template",
"reference": "required_template",
"access_level": "2",
}
)
else:
required_template = template_required
# Create dependency
dependency_vals = {
"template_id": template.id if hasattr(template, "id") else template,
"template_required_id": required_template.id
if hasattr(required_template, "id")
else required_template,
"state_required_id": state_required_id
if state_required_id is not None
else cls.state_running.id,
}
dependency_model = (
cls.JetTemplateDependency.with_user(with_user)
if with_user
else cls.JetTemplateDependency
)
dependency = dependency_model.create(dependency_vals)
return template, required_template, dependency

View File

@@ -0,0 +1,244 @@
# Copyright (C) 2024 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from unittest.mock import patch
from odoo.tools import mute_logger
from ..models.constants import GENERAL_ERROR, NOT_FOUND, SSH_CONNECTION_ERROR
from .common import TestTowerCommon
class TestCetmixTower(TestTowerCommon):
"""
Tests for the 'cetmix.tower' helper model
"""
@mute_logger("odoo.addons.cetmix_tower_server.models.cetmix_tower")
def test_server_set_variable_value(self):
"""Test plan line action naming"""
# -- 1--
# Create new variable
variable_meme = self.Variable.create(
{"name": "Meme Variable", "reference": "meme_variable"}
)
# Set variable for Server 1
result = self.CetmixTower.server_set_variable_value(
server_reference=self.server_test_1.reference,
variable_reference=variable_meme.reference,
value="Doge",
)
# Check exit code
self.assertEqual(result["exit_code"], 0, "Exit code must be equal to 0")
# Check variable value
variable_value = self.VariableValue.search(
[("variable_id", "=", variable_meme.id)]
)
self.assertEqual(len(variable_value), 1, "Must be 1 result")
self.assertEqual(variable_value.value_char, "Doge", "Must be Doge!")
# -- 2 --
# Update existing variable value
# Set variable for Server 1
result = self.CetmixTower.server_set_variable_value(
server_reference=self.server_test_1.reference,
variable_reference=variable_meme.reference,
value="Pepe",
)
# Check exit code
self.assertEqual(result["exit_code"], 0, "Exit code must be equal to 0")
# Check variable value
variable_value = self.VariableValue.search(
[("variable_id", "=", variable_meme.id)]
)
self.assertEqual(len(variable_value), 1, "Must be 1 result")
self.assertEqual(variable_value.value_char, "Pepe", "Must be Pepe!")
@mute_logger("odoo.addons.cetmix_tower_server.models.cetmix_tower")
def test_server_get_variable_value(self):
"""Test getting value for server"""
variable_meme = self.Variable.create(
{"name": "Meme Variable", "reference": "meme_variable"}
)
global_value = self.VariableValue.create(
{"variable_id": variable_meme.id, "value_char": "Memes Globalvs"}
)
# -- 1 -- Get value for Server with no server value defined
value = self.CetmixTower.server_get_variable_value(
self.server_test_1.reference, variable_meme.reference
)
self.assertEqual(value, global_value.value_char)
# -- 2 -- Add server value and try again
server_value = self.VariableValue.create(
{
"variable_id": variable_meme.id,
"value_char": "Memes Servervs",
"server_id": self.server_test_1.id,
}
)
value = self.CetmixTower.server_get_variable_value(
self.server_test_1.reference, variable_meme.reference
)
self.assertEqual(value, server_value.value_char)
@mute_logger("odoo.addons.cetmix_tower_server.models.cetmix_tower")
def test_server_check_ssh_connection(self):
"""
Test SSH connection check with a mocked function that
either returns a dictionary or raises an exception.
"""
# Test successful connection
result = self.env["cetmix.tower"].server_check_ssh_connection(
self.server_test_1.reference,
)
self.assertEqual(result["exit_code"], 0, "SSH connection should be successful.")
def test_ssh_connection(this, *args, **kwargs):
return {"status": GENERAL_ERROR}
with patch.object(
self.registry["cx.tower.server"], "test_ssh_connection", test_ssh_connection
):
# Test connection timeout after max attempts
result = self.env["cetmix.tower"].server_check_ssh_connection(
self.server_test_1.reference,
attempts=2,
wait_time=1,
)
self.assertEqual(
result["exit_code"],
SSH_CONNECTION_ERROR,
"SSH connection should timeout after maximum attempts.",
)
@mute_logger("odoo.addons.cetmix_tower_server.models.cetmix_tower")
def test_server_run_command(self):
"""Test running command on server"""
# Create test command
command = self.Command.create(
{
"name": "Test Command",
"reference": "test_command",
"code": "echo 'Hello World'",
"action": "ssh_command",
}
)
# -- 1 -- Test with non-existent server
result = self.CetmixTower.server_run_command(
server_reference="non_existent",
command_reference=command.reference,
)
self.assertEqual(result["exit_code"], NOT_FOUND)
self.assertEqual(result["message"], "Server not found")
# -- 2 -- Test with non-existent command
result = self.CetmixTower.server_run_command(
server_reference=self.server_test_1.reference,
command_reference="non_existent",
)
self.assertEqual(result["exit_code"], NOT_FOUND)
self.assertEqual(result["message"], "Command not found")
# -- 3 -- Test successful command execution
result = self.CetmixTower.server_run_command(
server_reference=self.server_test_1.reference,
command_reference=command.reference,
)
self.assertEqual(result["exit_code"], 0)
@mute_logger("odoo.addons.cetmix_tower_server.models.cetmix_tower")
def test_server_run_flight_plan(self):
"""Test running flight plan on server"""
# Create test flight plan
flight_plan = self.Plan.create(
{
"name": "Test Flight Plan",
"reference": "test_flight_plan",
}
)
# -- 1 -- Test with non-existent server
result = self.CetmixTower.server_run_flight_plan(
server_reference="non_existent",
flight_plan_reference=flight_plan.reference,
)
self.assertFalse(result, "Should return False for non-existent server")
# -- 2 -- Test with non-existent flight plan
result = self.CetmixTower.server_run_flight_plan(
server_reference=self.server_test_1.reference,
flight_plan_reference="non_existent",
)
self.assertFalse(result, "Should return False for non-existent flight plan")
# -- 3 -- Test successful flight plan execution
with patch.object(self.server_test_1.__class__, "run_flight_plan") as mock_run:
# Setup mock to return a plan log record
plan_log = self.PlanLog.create(
{
"name": "Test Log",
"server_id": self.server_test_1.id,
"plan_id": flight_plan.id,
}
)
mock_run.return_value = plan_log
# Run flight plan
result = self.CetmixTower.server_run_flight_plan(
server_reference=self.server_test_1.reference,
flight_plan_reference=flight_plan.reference,
)
# Verify result
self.assertEqual(result, plan_log, "Should return plan log record")
mock_run.assert_called_once_with(flight_plan)
@mute_logger("odoo.addons.cetmix_tower_server.models.cetmix_tower")
def test_server_run_command_with_variable_values(self):
"""Test running command with variable values"""
# Create test command
command = self.Command.create(
{
"name": "Test Command",
"reference": "test_command",
"code": "result = {'exit_code': 0, 'message': {{ test_version }}}",
"action": "python_code",
}
)
# Set variable value for the server
self.CetmixTower.server_set_variable_value(
server_reference=self.server_test_1.reference,
variable_reference=self.variable_version.reference,
value="prod",
)
# -- 1 --
# Run command without modifying variable values
result = self.CetmixTower.server_run_command(
server_reference=self.server_test_1.reference,
command_reference=command.reference,
)
self.assertEqual(result["exit_code"], 0)
self.assertEqual(result["message"], "prod")
# -- 2 --
# Run command with modified variable values
result = self.CetmixTower.server_run_command(
server_reference=self.server_test_1.reference,
command_reference=command.reference,
**{"test_version": "dev"},
)
self.assertEqual(result["exit_code"], 0)
self.assertEqual(result["message"], "dev")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,282 @@
# Copyright (C) 2025 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields
from odoo.exceptions import AccessError
from .common import TestTowerCommon
class TestTowerCommandLog(TestTowerCommon):
"""Test the cx.tower.command.log model access rights."""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create commands with different access levels
cls.command_level_1 = cls.Command.create(
{
"name": "Test Command L1",
"action": "ssh_command",
"access_level": "1",
}
)
cls.command_level_2 = cls.Command.create(
{
"name": "Test Command L2",
"action": "ssh_command",
"access_level": "2",
}
)
cls.command_level_3 = cls.Command.create(
{
"name": "Test Command L3",
"action": "ssh_command",
"access_level": "3",
}
)
# Create test command logs with specific users
cls.command_log_1 = (
cls.CommandLog.with_user(cls.user)
.sudo()
.create(
{
"server_id": cls.server_test_1.id,
"command_id": cls.command_level_1.id,
"start_date": fields.Datetime.now(),
}
)
)
cls.command_log_2 = (
cls.CommandLog.with_user(cls.manager)
.sudo()
.create(
{
"server_id": cls.server_test_1.id,
"command_id": cls.command_level_1.id,
"start_date": fields.Datetime.now(),
}
)
)
# Create additional server for testing
cls.server_2 = cls.Server.create(
{
"name": "Test Server 2",
"ip_v4_address": "localhost",
"ssh_username": "test2",
"ssh_password": "test2",
"ssh_port": 22,
"user_ids": [(6, 0, [])],
"manager_ids": [(6, 0, [])],
}
)
def test_user_read_access(self):
"""Test user read access to command logs"""
# Add user to server's user_ids to isolate creator check
self.server_test_1.write(
{
"user_ids": [(6, 0, [self.user.id])],
}
)
# Case 1: User should be able to read when:
# - access_level == "1"
# - created by user
# - user is in server's user_ids
recs = self.CommandLog.with_user(self.user).search(
[("id", "in", [self.command_log_1.id, self.command_log_2.id])]
)
self.assertEqual(
len(recs),
1,
"User should only be able to read their own logs",
)
self.assertIn(
self.command_log_1,
recs,
"User should be able to read own logs when conditions are met",
)
self.assertNotIn(
self.command_log_2,
recs,
"User should not be able to read logs created by others",
)
# Case 2: User should not be able to read when not in server's user_ids
self.server_test_1.write(
{
"user_ids": [(5, 0, 0)], # Remove all users
}
)
recs = self.CommandLog.with_user(self.user).search(
[("id", "=", self.command_log_1.id)]
)
self.assertNotIn(
self.command_log_1,
recs,
"User should not be able to read when not in server's user_ids",
)
# Case 3: User should not be able to read when access_level > "1"
self.server_test_1.write(
{
"user_ids": [(6, 0, [self.user.id])],
}
)
high_access_log = (
self.CommandLog.with_user(self.user)
.sudo()
.create(
{
"server_id": self.server_test_1.id,
"command_id": self.command_level_2.id, # Using command with access_level "2" # noqa: E501
"start_date": fields.Datetime.now(),
}
)
)
recs = self.CommandLog.with_user(self.user).search(
[("id", "=", high_access_log.id)]
)
self.assertNotIn(
high_access_log,
recs,
"User should not be able to read logs with access_level > '1'"
" even if created by them",
)
def test_manager_read_access(self):
"""Test manager read access to command logs"""
# Case 1: Manager should be able to read when:
# - access_level <= "2"
# - manager is in server's manager_ids
self.server_test_1.write(
{
"manager_ids": [(6, 0, [self.manager.id])],
}
)
recs = self.CommandLog.with_user(self.manager).search(
[("id", "in", [self.command_log_1.id, self.command_log_2.id])]
)
self.assertEqual(
len(recs),
2,
"Manager should be able to read all logs when in server's manager_ids",
)
# Case 2: Manager should be able to read when in server's user_ids
self.server_test_1.write(
{
"manager_ids": [(5, 0, 0)], # Remove all managers
"user_ids": [(6, 0, [self.manager.id])],
}
)
recs = self.CommandLog.with_user(self.manager).search(
[("id", "in", [self.command_log_1.id, self.command_log_2.id])]
)
self.assertEqual(
len(recs),
2,
"Manager should be able to read all logs when in server's user_ids",
)
# Case 3: Manager should not be able to read when access_level > "2"
high_access_log = (
self.CommandLog.with_user(self.manager)
.sudo()
.create(
{
"server_id": self.server_test_1.id,
"command_id": self.command_level_3.id, # Using command with access_level "3" # noqa: E501
"start_date": fields.Datetime.now(),
}
)
)
recs = self.CommandLog.with_user(self.manager).search(
[("id", "=", high_access_log.id)]
)
self.assertNotIn(
high_access_log,
recs,
"Manager should not be able to read logs with access_level > '2'",
)
# Case 4: Manager should not be able to read when he is not
# in users_ids or manager_ids
self.server_test_1.write(
{
"user_ids": [(5, 0, 0)],
"manager_ids": [(5, 0, 0)],
}
)
recs = self.CommandLog.with_user(self.manager).search(
[("id", "in", [self.command_log_1.id, self.command_log_2.id])]
)
self.assertNotIn(
self.command_log_1,
recs,
"Manager should not be able to read logs when he is not"
" in users_ids or manager_ids",
)
def test_root_read_only_access(self):
"""Root can read all command logs, but cannot create/modify/delete"""
# Create test logs with sudo()
test_logs = self.CommandLog.sudo().create(
[
{
"server_id": self.server_2.id,
"command_id": command.id,
"start_date": fields.Datetime.now(),
}
for command in [
self.command_level_1,
self.command_level_2,
self.command_level_3,
]
]
)
# Root cannot create logs
with self.assertRaises(AccessError):
self.CommandLog.with_user(self.root).create(
{
"server_id": self.server_2.id,
"command_id": self.command_level_1.id,
"start_date": fields.Datetime.now(),
}
)
# Root cannot modify logs
with self.assertRaises(AccessError):
test_logs.with_user(self.root).write({"start_date": fields.Datetime.now()})
# Root cannot delete logs
with self.assertRaises(AccessError):
test_logs.with_user(self.root).unlink()
# Root should be able to read all logs regardless of:
# - access_level
# - server relationships
# - who created them
recs = self.CommandLog.with_user(self.root).search(
[("id", "in", test_logs.ids)]
)
self.assertEqual(
len(recs),
3,
"Root should have unrestricted read access to all logs",
)
# Test read on all records
all_recs = self.CommandLog.with_user(self.root).search([])
self.assertGreater(
len(all_recs),
0,
"Root should be able to read all command logs",
)

View File

@@ -0,0 +1,572 @@
# Copyright (C) 2022 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.exceptions import AccessError, ValidationError
from .common import TestTowerCommon
class TestTowerCommandWizard(TestTowerCommon):
"""Test Tower Command Run Wizard"""
def test_user_access_rules(self):
"""Test user access rules"""
# Add Bob to `root` group in order to create a wizard
self.add_to_group(self.user_bob, "cetmix_tower_server.group_root")
# Create new wizard
test_wizard = (
self.env["cx.tower.command.run.wizard"]
.with_user(self.user_bob)
.create(
{
"server_ids": [self.server_test_1.id],
"command_id": self.command_create_dir.id,
}
)
).with_user(self.user_bob)
# Force rendered code computation
test_wizard._compute_rendered_code()
# Remove bob from all cxtower_server groups
self.remove_from_group(
self.user_bob,
[
"cetmix_tower_server.group_user",
"cetmix_tower_server.group_manager",
"cetmix_tower_server.group_root",
],
)
# Ensure that regular user cannot execute command in wizard
with self.assertRaises(AccessError):
test_wizard.run_command_in_wizard()
# Add bob back to `user` group and try again
self.add_to_group(self.user_bob, "cetmix_tower_server.group_user")
with self.assertRaises(AccessError):
test_wizard.run_command_in_wizard()
# Now promote bob to `manager` group and try again
self.add_to_group(self.user_bob, "cetmix_tower_server.group_manager")
test_wizard.run_command_in_wizard()
def test_execute_code_without_a_command(self):
"""Run command code without a command selected"""
# Add Bob to `root` group in order to create a wizard
self.add_to_group(self.user_bob, "cetmix_tower_server.group_root")
# Create new wizard
test_wizard = (
self.env["cx.tower.command.run.wizard"]
.with_user(self.user_bob)
.create(
{
"server_ids": [self.server_test_1.id],
}
)
).with_user(self.user_bob)
# Should not allow to run command on server if no command is selected
with self.assertRaises(ValidationError):
test_wizard.run_command_on_server()
def test_run_command_on_server_access_rights(self):
"""Test access rights for executing command on server"""
# Add Bob to `root` group
self.add_to_group(self.user_bob, "cetmix_tower_server.group_root")
# Create new wizard with Bob as a root user
test_wizard = (
self.env["cx.tower.command.run.wizard"]
.with_user(self.user_bob)
.create(
{
"server_ids": [self.server_test_1.id],
"command_id": self.command_create_dir.id,
}
)
).with_user(self.user_bob)
# Ensure command can be executed by root
test_wizard.run_command_on_server()
# Remove Bob from all tower server groups
self.remove_from_group(
self.user_bob,
[
"cetmix_tower_server.group_user",
"cetmix_tower_server.group_manager",
"cetmix_tower_server.group_root",
],
)
# Ensure that regular user cannot execute command on server
with self.assertRaises(AccessError):
test_wizard.run_command_on_server()
# Add Bob to `user` group and ensure he can execute commands
self.add_to_group(self.user_bob, "cetmix_tower_server.group_user")
test_wizard.run_command_on_server()
# Ensure that Bob has access to path field but can't read its value
allowed_path = (
self.user_bob.has_group("cetmix_tower_server.group_manager")
and test_wizard.path
)
self.assertEqual(allowed_path, False)
# Ensure that Bob can write to the path field as a member of `group_user`
# the result will be None
test_wizard.write({"path": "/new/invalid/path"})
allowed_path = (
test_wizard.path
if self.user_bob.has_group("cetmix_tower_server.group_manager")
and test_wizard.path
else None
)
self.assertEqual(allowed_path, None)
# Add Bob to `manager` group and ensure access to execute commands
self.add_to_group(self.user_bob, "cetmix_tower_server.group_manager")
test_wizard.run_command_on_server()
# Check that path access is valid for the manager
test_wizard.read(["path"])
def test_run_command_with_sensitive_vars_on_server_access_rights(self):
"""Test access rights for executing command on server"""
# create new command
command = self.Command.create(
{
"name": "Create new command",
"action": "python_code",
"code": """
properties = {
"Server Name": {{ tower.server.name }},
"Server Reference": {{ tower.server.reference }},
"SSH Username": {{ tower.server.username }},
"IPv4 Address": {{ tower.server.ipv4 }},
"IPv6 Address": {{ tower.server.ipv6 }},
"Partner Name": {{ tower.server.partner_name }}
}
result = {"exit_code": 0, "message": properties}
""",
"access_level": "1",
}
)
# Add Bob to `root` group in order to create a wizard
self.add_to_group(self.user_bob, "cetmix_tower_server.group_root")
server = self.Server.with_user(self.user_bob).create(
{
"name": "Test 2",
"ip_v4_address": "localhost",
"ssh_username": "root",
"ssh_password": "password",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
}
)
self.remove_from_group(
self.user_bob,
[
"cetmix_tower_server.group_user",
"cetmix_tower_server.group_manager",
"cetmix_tower_server.group_root",
],
)
# Add user bob to group user
self.add_to_group(self.user_bob, "cetmix_tower_server.group_user")
# Create new wizard with Bob
test_wizard = (
self.env["cx.tower.command.run.wizard"]
.with_user(self.user_bob)
.create(
{
"server_ids": [server.id],
"command_id": command.id,
}
)
).with_user(self.user_bob)
# Add Bob as a user to the command
command.write({"user_ids": [(4, self.user_bob.id)]})
# Ensure command can be executed by user
test_wizard.run_command_on_server()
def test_run_command_in_wizard_multiple_servers(self):
"""
Test that raises an error when multiple servers are selected
"""
# Add Bob to `root` group in order to create a wizard
server_test_2 = self.Server.create(
{
"name": "Test 2",
"ip_v4_address": "localhost",
"ssh_username": "root",
"ssh_password": "password",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
}
)
self.add_to_group(self.user_bob, "cetmix_tower_server.group_root")
# Create new wizard with multiple servers selected
test_wizard = (
self.env["cx.tower.command.run.wizard"]
.with_user(self.user_bob)
.create(
{
"server_ids": [self.server_test_1.id, server_test_2.id],
"command_id": self.command_create_dir.id,
}
)
).with_user(self.user_bob)
# Force rendered code computation
test_wizard._compute_rendered_code()
# Ensure that executing command with multiple servers
# selected raises a ValidationError
with self.assertRaises(
ValidationError,
msg="You cannot run custom code on multiple servers at once.",
):
test_wizard.run_command_in_wizard()
# Now, test with a single server selected
test_wizard.server_ids = [self.server_test_1.id]
# Ensure that executing command works with a single server selected
test_wizard.run_command_in_wizard()
self.assertTrue(
test_wizard.result,
msg="Command execution should succeed with a single server selected",
)
def test_custom_variable_value_ids_creation(self):
"""
Test that custom variable values are created properly
when command has variables
"""
# Add manager as server user
self.server_test_1.write({"user_ids": [(4, self.manager.id)]})
# Create variables that will be used in command
variable = self.Variable.create(
{
"name": "Test Variable",
"reference": "test_var",
"variable_type": "s", # string type
}
)
option_variable = self.Variable.create(
{
"name": "Option Variable",
"reference": "opt_var",
"variable_type": "o", # option type
}
)
option = self.VariableOption.create(
{
"name": "Test Option",
"value_char": "option_value",
"variable_id": option_variable.id,
}
)
# Add variable values to server
self.VariableValue.create(
[
{
"variable_id": variable.id,
"server_id": self.server_test_1.id,
"value_char": "server value",
},
{
"variable_id": option_variable.id,
"server_id": self.server_test_1.id,
"value_char": "option_value",
},
]
)
# Create command that uses these variables in its code
command = self.Command.create(
{
"name": "Test Command with Variables",
"action": "ssh_command",
"code": "echo {{ test_var }} && echo {{ opt_var }}",
}
)
# Create wizard
wizard = (
self.env["cx.tower.command.run.wizard"]
.with_user(self.manager)
.create(
{
"server_ids": [self.server_test_1.id],
"command_id": command.id,
"action": "ssh_command",
}
)
)
# Trigger onchange to generate custom_variable_value_ids
wizard._onchange_command_variable_ids()
# Check that custom variable values were created
self.assertEqual(len(wizard.custom_variable_value_ids), 2)
# Check char variable value
char_value = wizard.custom_variable_value_ids.filtered(
lambda v: v.variable_id == variable
)
self.assertTrue(char_value)
self.assertEqual(char_value.value_char, "server value")
# Check option variable value
option_value = wizard.custom_variable_value_ids.filtered(
lambda v: v.variable_id == option_variable
)
self.assertTrue(option_value)
self.assertEqual(option_value.value_char, "option_value")
self.assertEqual(option_value.option_id, option)
# Try to change variable value when user doesn't have write access
char_value.value_char = "custom value"
# Run command
wizard.run_command_on_server()
# Get latest command log
command_log = self.env["cx.tower.command.log"].search(
[
("server_id", "=", self.server_test_1.id),
("command_id", "=", command.id),
],
order="create_date desc",
limit=1,
)
# Verify that original server values were used
self.assertEqual(command_log.code, "echo server value && echo option_value")
def test_custom_variable_value_ids_with_manager_access(self):
"""
Test that custom variable values are applied
when manager has write access
"""
# Add manager as server manager
self.server_test_1.write({"manager_ids": [(4, self.manager.id)]})
# Create variables that will be used in command
variable = self.Variable.create(
{
"name": "Test Variable",
"reference": "test_var",
"variable_type": "s", # string type
}
)
# Add variable value to server
self.VariableValue.create(
{
"variable_id": variable.id,
"server_id": self.server_test_1.id,
"value_char": "server value",
}
)
# Create command that uses the variable
command = self.Command.create(
{
"name": "Test Command with Variables",
"action": "ssh_command",
"code": "echo {{ test_var }}",
}
)
# Create wizard
wizard = (
self.env["cx.tower.command.run.wizard"]
.with_user(self.manager)
.create(
{
"server_ids": [self.server_test_1.id],
"command_id": command.id,
"action": "ssh_command",
}
)
)
# Trigger onchange to generate custom_variable_value_ids
wizard._onchange_command_variable_ids()
# Modify variable value
wizard.custom_variable_value_ids.filtered(
lambda v: v.variable_id == variable
).value_char = "manager value"
# Run command
wizard.run_command_on_server()
# Get latest command log
command_log = self.env["cx.tower.command.log"].search(
[
("server_id", "=", self.server_test_1.id),
("command_id", "=", command.id),
],
order="create_date desc",
limit=1,
)
# Verify that custom value was used
self.assertEqual(command_log.code, "echo manager value")
def test_default_applicability_for_regular_and_manager(self):
"""sets applicability='this' for regular users, keeps default for managers."""
# Regular user (no special groups)
default_usr = (
self.env["cx.tower.command.run.wizard"]
.with_user(self.user_bob)
.default_get(["applicability"])
)
self.assertEqual(default_usr.get("applicability"), "this")
# Manager user should receive the original default ("shared")
self.add_to_group(self.user_bob, "cetmix_tower_server.group_manager")
default_mgr = (
self.env["cx.tower.command.run.wizard"]
.with_user(self.user_bob)
.default_get(["applicability"])
)
self.assertEqual(default_mgr.get("applicability"), "shared")
def test_compute_show_servers_behavior(self):
"""Should enforce 'this' for regular users but preserve manager choice."""
# Grant Bob the basic 'user' group so he can read servers and create the wizard
self.add_to_group(self.user_bob, "cetmix_tower_server.group_user")
# Ensure Bob has read access to the first server
self.server_test_1.write({"user_ids": [(4, self.user_bob.id)]})
# Create a second server and grant Bob read access to it
srv2 = self.Server.create(
{
"name": "Server 2",
"ip_v4_address": "127.0.0.2",
"ssh_username": "root",
"ssh_password": "pwd",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
}
)
srv2.write({"user_ids": [(4, self.user_bob.id)]})
# --- Regular user scenario ---
wiz_usr = (
self.env["cx.tower.command.run.wizard"]
.with_user(self.user_bob)
.create({"server_ids": [self.server_test_1.id, srv2.id]})
)
# Compute show_servers under Bob; he should see both servers
wiz_usr._compute_show_servers()
self.assertTrue(wiz_usr.show_servers)
# Enforcement should set applicability to 'this'
self.assertEqual(wiz_usr.applicability, "this")
# --- Manager user scenario ---
self.add_to_group(self.user_bob, "cetmix_tower_server.group_manager")
# Grant Bob manager access to both servers
self.server_test_1.write({"manager_ids": [(4, self.user_bob.id)]})
srv2.write({"manager_ids": [(4, self.user_bob.id)]})
wiz_mgr = (
self.env["cx.tower.command.run.wizard"]
.with_user(self.user_bob)
.create({"server_ids": [self.server_test_1.id, srv2.id]})
)
# Compute show_servers under Bob as manager
wiz_mgr._compute_show_servers()
# Manager should also see both servers
self.assertTrue(wiz_mgr.show_servers)
# Enforcement should not override manager's choice of 'shared'
self.assertEqual(wiz_mgr.applicability, "shared")
def test_required_variable_validation(self):
"""
Wizard must block execution when a required variable is empty
and allow it after the value is provided.
"""
# Create a required variable
var = self.Variable.create(
{
"name": "Req Var",
"reference": "req_var",
"variable_type": "s",
}
)
self.VariableValue.create(
{
"variable_id": var.id,
"server_id": self.server_test_1.id,
"required": True,
"value_char": "",
}
)
# Create command that uses this variable
cmd = self.Command.create(
{
"name": "Echo Req Var",
"action": "ssh_command",
"code": "echo {{ req_var }}",
"variable_ids": [(4, var.id)],
}
)
self.server_test_1.write({"user_ids": [(4, self.manager.id)]})
# Create wizard as manager user
wiz = (
self.env["cx.tower.command.run.wizard"]
.with_user(self.manager)
.create(
{
"server_ids": [self.server_test_1.id],
"command_id": cmd.id,
}
)
)
# Create lines of configuration
wiz._onchange_command_variable_ids()
wiz._compute_has_missing_required_values()
# Test blocking behavior
self.assertTrue(wiz.has_missing_required_values)
with self.assertRaises(ValidationError):
wiz.run_command_on_server()
# Fill the value directly in the wizard line
wiz.custom_variable_value_ids.filtered(
lambda line: line.variable_id == var
).value_char = "filled"
# Recompute the flag
wiz._compute_has_missing_required_values()
self.assertFalse(wiz.has_missing_required_values)
# Now the execution should pass
wiz.run_command_on_server()

View File

@@ -0,0 +1,482 @@
from odoo import exceptions
from odoo.exceptions import AccessError
from .common import TestTowerCommon
class TestTowerFile(TestTowerCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.file_template = cls.FileTemplate.create(
{
"name": "Test",
"file_name": "test.txt",
"server_dir": "/var/tmp",
"code": "Hello, world!",
}
)
cls.file = cls.File.create(
{
"name": "tower_demo_1.txt",
"source": "tower",
"template_id": cls.file_template.id,
"server_id": cls.server_test_1.id,
}
)
cls.file_2 = cls.File.create(
{
"name": "test.txt",
"source": "server",
"server_id": cls.server_test_1.id,
"server_dir": "/var/tmp",
}
)
# Create a dummy Server record that will be referenced by file records.
cls.server = cls.Server.create(
{
"name": "Test Server",
"manager_ids": [(6, 0, [cls.manager.id])],
"user_ids": [(6, 0, [cls.user.id])],
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"skip_host_key": True,
"os_id": cls.os_debian_10.id,
"ip_v4_address": "localhost",
}
)
def test_user_read_access(self):
"""
Test that a user in the custom User group can read a file record
when their ID is in the related server's user_ids.
"""
file_record = self.File.create(
{
"name": "Test File",
"server_dir": "/tmp",
"file_type": "text",
"source": "tower",
"server_id": self.server.id,
}
)
# As the user, the file record should be visible.
files_for_user = self.File.with_user(self.user).search(
[("id", "=", file_record.id)]
)
self.assertTrue(
files_for_user,
"User should be able to read the file record "
"because they are in server.user_ids.",
)
# Remove user from server.user_ids.
self.server.write({"user_ids": [(3, self.user.id)]})
files_for_user = self.File.with_user(self.user).search(
[("id", "=", file_record.id)]
)
self.assertFalse(
files_for_user,
"User should not be able to read the file record "
"because he is not in server.user_ids.",
)
def test_manager_write_create_access(self):
"""
Test that a manager in the custom Manager group can create and write
file records when his ID is in the related server's manager_ids.
"""
# Test creation: the manager is in server.manager_ids.
file_record = self.File.with_user(self.manager).create(
{
"name": "Manager Created File",
"server_dir": "/tmp",
"file_type": "text",
"source": "tower",
"server_id": self.server.id,
}
)
self.assertTrue(
file_record,
"Manager should be able to create a file record "
"because they are in server.manager_ids.",
)
# Test updating (write access).
try:
file_record.with_user(self.manager).write({"name": "Manager Updated File"})
except AccessError:
self.fail(
"Manager should be able to update the file record "
"because he is in server.manager_ids."
)
self.assertEqual(
file_record.with_user(self.manager).name,
"Manager Updated File",
"File record name should be updated by the manager.",
)
# Test that a manager who is not in the server's manager_ids
# cannot write or create.
# Remove manager from server.manager_ids.
self.server.write({"manager_ids": [(3, self.manager.id)]})
# Create a file record on this server.
file_record2 = self.File.create(
{
"name": "File on Server Without Manager",
"server_dir": "/tmp",
"file_type": "text",
"source": "tower",
"server_id": self.server.id,
}
)
with self.assertRaises(AccessError):
file_record2.with_user(self.manager).write({"name": "Should Not Update"})
# Test create access for a manager not in manager_ids.
with self.assertRaises(AccessError):
self.File.with_user(self.manager).create(
{
"name": "Invalid File",
"server_dir": "/tmp",
"file_type": "text",
"source": "tower",
"server_id": self.server.id,
}
)
def test_manager_unlink_access(self):
"""
Test that a manager in the custom Manager group can unlink (delete) a file
record only if he is in the related server's manager_ids
and they are the record's creator.
"""
# Scenario 1: Record created by the manager.
file_record = self.File.with_user(self.manager).create(
{
"name": "File to Delete",
"server_dir": "/tmp",
"file_type": "text",
"source": "tower",
"server_id": self.server.id,
}
)
try:
file_record.with_user(self.manager).unlink()
except AccessError:
self.fail(
"Manager should be able to delete their own file"
" record when in server.manager_ids."
)
# Scenario 2: Record created by someone else (e.g., the admin).
file_record2 = self.File.create(
{
"name": "File Not Deletable by Manager",
"server_dir": "/tmp",
"file_type": "text",
"source": "tower",
"server_id": self.server.id,
}
)
with self.assertRaises(AccessError):
file_record2.with_user(self.manager).unlink()
def test_upload_file(self):
"""
Upload file from tower to server
"""
self.file.action_push_to_server()
self.assertEqual(self.file.server_response, "ok")
def test_delete_file(self):
"""
Delete file remotely from server
"""
result = self.file.action_delete_from_server()
self.assertTrue(isinstance(result, dict))
self.assertEqual(result["params"]["message"], "File deleted!")
def test_delete_file_access(self):
"""
Test delete file access
"""
with self.assertRaises(exceptions.AccessError):
self.file.with_user(self.user_bob).delete(raise_error=True)
def test_download_file(self):
"""
Download file from server to tower
"""
self.file_2.action_pull_from_server()
self.assertEqual(self.file_2.code, "ok")
self.file_2.name = "binary.zip"
res = self.file_2.action_pull_from_server()
self.assertTrue(
isinstance(res, dict) and res["tag"] == "display_notification",
msg=(
"If file type is 'binary', then the result must be a dict "
"representing the display_notification action."
),
)
def test_get_current_server_code(self):
"""
Download file from server to tower
"""
self.file.action_push_to_server()
self.assertEqual(self.file.server_response, "ok")
self.file.action_get_current_server_code()
self.assertEqual(self.file.code_on_server, "ok")
def test_modify_template_code(self):
"""Test how template code modification affects related files"""
code = "Pepe frog is happy as always"
self.file_template.code = code
# Check file code before modifications
self.assertTrue(
self.file.code == code,
msg="File code must be the same "
"as template code before any modifications",
)
# Check file rendered code before modifications
self.assertTrue(
self.file.rendered_code == code,
msg="File rendered code must be the same"
" as template code before any modifications",
)
# Make possible to modify file code
self.file.action_unlink_from_template()
# Check if template was removed from file
self.assertFalse(
self.file.template_id,
msg="File template should be removed after modifying code.",
)
# Check if file code remains the same
self.assertTrue(
self.file.code == code, msg="File code should be the same as template."
)
def test_modify_template_related_files(self):
"""
Check that after change file template
all related files will update
"""
self.assertEqual(self.file_template.file_name, "test.txt")
# related files
self.assertTrue(
all(file.name == "test.txt" for file in self.file_template.file_ids)
)
# update file template name
self.file_template.file_name = "new_test.txt"
# Related files must updated
self.assertTrue(
all(file.name == "new_test.txt" for file in self.file_template.file_ids)
)
self.assertEqual(self.file_template.code, "Hello, world!")
# update file template code
self.file_template.code = "New code"
# Related files must updated
self.assertTrue(
all(file.code == "New code" for file in self.file_template.file_ids)
)
def test_create_file_with_template(self):
"""
Test if file is created with template code
"""
file_template = self.env["cx.tower.file.template"].create(
{
"name": "Test",
"file_name": "test.txt",
"server_dir": "/var/tmp",
"code": "Hello, world!",
}
)
file = file_template.create_file(
server=self.server_test_1,
server_dir=file_template.server_dir,
if_file_exists="overwrite",
)
self.assertEqual(file.code, self.file_template.code)
self.assertEqual(file.template_id, file_template)
self.assertEqual(file.server_id, self.server_test_1)
self.assertEqual(file.source, "tower")
self.assertEqual(file.server_dir, self.file_template.server_dir)
with self.assertRaises(exceptions.ValidationError):
file_template.create_file(
server=self.server_test_1,
server_dir=file_template.server_dir,
if_file_exists="raise",
)
another_file = file_template.create_file(
server=self.server_test_1,
server_dir=file_template.server_dir,
if_file_exists="skip",
)
self.assertEqual(another_file, file)
def test_create_file_with_template_custom_server_dir(self):
"""
Test if file is created with template code and custom server dir
"""
file_template = self.env["cx.tower.file.template"].create(
{
"name": "Test",
"file_name": "test.txt",
"server_dir": "/var/tmp",
"code": "Hello, world!",
}
)
file = file_template.create_file(
server=self.server_test_1, server_dir="/var/tmp/custom"
)
self.assertEqual(file.code, self.file_template.code)
self.assertEqual(file.template_id, file_template)
self.assertEqual(file.server_id, self.server_test_1)
self.assertEqual(file.source, "tower")
self.assertEqual(file.server_dir, "/var/tmp/custom")
with self.assertRaises(exceptions.ValidationError):
file_template.create_file(
server=self.server_test_1,
server_dir="/var/tmp/custom",
if_file_exists="raise",
)
another_file = file_template.create_file(
server=self.server_test_1,
server_dir="/var/tmp/custom",
if_file_exists="skip",
)
self.assertEqual(another_file, file)
def test_file_with_secret_key(self):
"""
Test case to verify that when a file includes a secret reference,
the secret key is automatically linked with the file.
"""
# Create a secret key
secret_python_key = self.Key.create(
{
"name": "python",
"reference": "PYTHON",
"secret_value": "secretPythonCode",
"key_type": "s",
}
)
# Create a file template with a reference to the secret key
file_template = self.env["cx.tower.file.template"].create(
{
"name": "Test",
"file_name": "test.txt",
"server_dir": "/var/tmp",
"code": "Please use this secret #!cxtower.secret.PYTHON!#",
}
)
# Create a file from the file template
file = file_template.create_file(
server=self.server_test_1, server_dir="/var/tmp/custom"
)
# Assert that the file's code matches the file template's code
self.assertEqual(
file.code,
file_template.code,
msg="The file's code does not match the file template's code.",
)
# Assert that the secret key is associated with the file
self.assertIn(
secret_python_key,
file.secret_ids,
msg="The secret key is not associated with the file.",
)
# Update the file's code to remove the secret reference
file.code = "Only text"
self.assertFalse(
file.secret_ids,
msg=(
"The secret_ids field should be empty after "
"removing the secret reference from file."
),
)
def test_file_with_sensitive_variable(self):
"""
Test case to verify that user has access to use file with sensitive variables.
"""
# Create file with sensitive variable
file = self.File.create(
{
"source": "tower",
"name": "test.txt",
"server_id": self.server_test_1.id,
"code": "'IPv4 Address': {{ tower.server.ipv4 }}",
}
)
# Remove user_bob from all cx_tower_server groups
self.remove_from_group(
self.user_bob,
[
"cetmix_tower_server.group_user",
"cetmix_tower_server.group_manager",
"cetmix_tower_server.group_root",
],
)
# Add bob to user group
self.add_to_group(self.user_bob, "cetmix_tower_server.group_user")
# Add bob as subscriber of the server to allow upload file
self.server_test_1.write({"user_ids": [(4, self.user_bob.id)]})
# Upload file to server
self.assertTrue(file.server_response != "ok")
file.with_user(self.user_bob).action_push_to_server()
self.assertEqual(file.server_response, "ok")
def test_sanitize_values(self):
"""
Test case to verify that the sanitize_values method works correctly.
"""
# 1. Root directory
values = self.File._sanitize_values({"server_dir": "/"})
self.assertEqual(values["server_dir"], "/")
# 2. Trailing slash
values = self.File._sanitize_values({"server_dir": "/var/tmp/"})
self.assertEqual(values["server_dir"], "/var/tmp")
# 3. Trailing whitespace
values = self.File._sanitize_values({"server_dir": "/var/tmp/ "})
self.assertEqual(values["server_dir"], "/var/tmp")
# 4. Leading whitespace
values = self.File._sanitize_values({"server_dir": " /var/tmp/"})
self.assertEqual(values["server_dir"], "/var/tmp")
# 5. Leading and trailing whitespace
values = self.File._sanitize_values({"server_dir": " /var/tmp/ "})
self.assertEqual(values["server_dir"], "/var/tmp")
# 6. Leading and trailing whitespace just one slash
values = self.File._sanitize_values({"server_dir": " / "})
self.assertEqual(values["server_dir"], "/")

View File

@@ -0,0 +1,234 @@
from odoo.exceptions import AccessError
from .common import TestTowerCommon
class TestCxTowerFileTemplateAccessRules(TestTowerCommon):
def test_user_no_access(self):
"""
Verify that a user in the User group has no access
to any file template records.
"""
# Create a file template record as admin.
record = self.FileTemplate.create(
{
"name": "Template 1",
"file_name": "template1.txt",
"code": "Sample code",
"server_dir": "/templates",
"file_type": "text",
"source": "tower",
}
)
# As the user, search for the record expect no records.
with self.assertRaises(AccessError):
self.FileTemplate.with_user(self.user).search([("id", "=", record.id)])
# Attempting to create a record as a user should raise an AccessError.
with self.assertRaises(AccessError):
self.FileTemplate.with_user(self.user).create(
{
"name": "Template 2",
"file_name": "user_template.txt",
"code": "User code",
"server_dir": "/templates",
"file_type": "text",
"source": "tower",
}
)
def test_manager_read_access(self):
"""
Verify that a manager can read file template records
if he is not in user_ids or manager_ids.
"""
# Create a record with the manager in manager_ids.
rec1 = self.FileTemplate.create(
{
"name": "Template 1",
"file_name": "template_manager.txt",
"code": "Manager code",
"server_dir": "/templates",
"file_type": "text",
"source": "tower",
"manager_ids": [(6, 0, [self.manager.id])],
}
)
# Create a record with the manager in user_ids.
rec2 = self.FileTemplate.create(
{
"name": "Template 2",
"file_name": "template_user.txt",
"code": "User code",
"server_dir": "/templates",
"file_type": "text",
"source": "tower",
"user_ids": [(6, 0, [self.manager.id])],
}
)
# Create a record that does not include the manager.
rec3 = self.FileTemplate.create(
{
"name": "Template 3",
"file_name": "template_none.txt",
"code": "None code",
"server_dir": "/templates",
"file_type": "text",
"source": "tower",
}
)
recs = self.FileTemplate.with_user(self.manager).search([])
self.assertIn(rec1, recs, "Manager should read records if in manager_ids.")
self.assertIn(rec2, recs, "Manager should read records if in user_ids.")
self.assertNotIn(
rec3,
recs,
"Manager should not see records if not in user_ids or manager_ids.",
)
def test_manager_write_create_access(self):
"""
Verify that a manager can write and create file template records
only if he is in manager_ids.
"""
# Create a record with manager_ids including the manager.
rec = self.FileTemplate.create(
{
"name": "Template 1",
"file_name": "template_for_update.txt",
"code": "Initial code",
"server_dir": "/templates",
"file_type": "text",
"source": "tower",
"manager_ids": [(6, 0, [self.manager.id])],
}
)
# Manager should be able to update the record.
try:
rec.with_user(self.manager).write({"file_name": "template_updated.txt"})
except AccessError:
self.fail(
"Manager should be able to update the record when in manager_ids."
)
self.assertEqual(rec.with_user(self.manager).file_name, "template_updated.txt")
# Manager should be able to create a record if included in manager_ids.
rec2 = self.FileTemplate.with_user(self.manager).create(
{
"name": "Template 2",
"file_name": "manager_created_template.txt",
"code": "Manager created",
"server_dir": "/templates",
"file_type": "text",
"source": "tower",
"manager_ids": [(6, 0, [self.manager.id])],
}
)
self.assertTrue(
rec2,
"Manager should be able to create a record when included in manager_ids.",
)
# Creating a record without including the manager should raise an AccessError.
with self.assertRaises(AccessError):
self.FileTemplate.with_user(self.manager).create(
{
"name": "Template 3",
"file_name": "invalid_template.txt",
"code": "Invalid",
"server_dir": "/templates",
"file_type": "text",
"source": "tower",
"manager_ids": [(5, 0, 0)],
}
)
def test_manager_unlink_access(self):
"""
Verify that a manager can delete a file template record only if
he is in manager_ids and is the creator.
"""
# Scenario 1: Record created by the manager.
rec = self.FileTemplate.with_user(self.manager).create(
{
"name": "Template 1",
"file_name": "template_to_delete.txt",
"code": "Code to delete",
"server_dir": "/templates",
"file_type": "text",
"source": "tower",
"manager_ids": [(6, 0, [self.manager.id])],
}
)
try:
rec.with_user(self.manager).unlink()
except AccessError:
self.fail(
"Manager should be able to delete a record "
"he created when in manager_ids."
)
# Scenario 2: Record created by admin (or another user)
# even though manager_ids includes the manager.
rec2 = self.FileTemplate.create(
{
"name": "Template 2",
"file_name": "template_not_deletable.txt",
"code": "Admin created code",
"server_dir": "/templates",
"file_type": "text",
"source": "tower",
"manager_ids": [(6, 0, [self.manager.id])],
}
)
with self.assertRaises(AccessError):
rec2.with_user(self.manager).unlink()
def test_root_unrestricted_access(self):
"""
Verify that a user in the Root group has unlimited access
to all file template records.
"""
# Create a file template record (with no particular restrictions).
rec = self.FileTemplate.create(
{
"name": "Template 1",
"file_name": "template_for_root.txt",
"code": "Root code",
"server_dir": "/templates",
"file_type": "text",
"source": "tower",
}
)
# As the root user, the record should be visible.
recs = self.FileTemplate.with_user(self.root).search([("id", "=", rec.id)])
self.assertTrue(recs, "Root should see the record regardless of restrictions.")
# Root should be able to update the record.
try:
rec.with_user(self.root).write({"file_name": "root_updated_template.txt"})
except AccessError:
self.fail("Root should be able to update the record without restrictions.")
self.assertEqual(
rec.with_user(self.root).file_name, "root_updated_template.txt"
)
# Root should be able to create a record.
rec2 = self.FileTemplate.with_user(self.root).create(
{
"name": "Template 2",
"file_name": "root_created_template.txt",
"code": "Created by root",
"server_dir": "/templates",
"file_type": "text",
"source": "tower",
}
)
self.assertTrue(
rec2, "Root should be able to create a record without restrictions."
)
# Root should be able to delete a record.
rec2.with_user(self.root).unlink()
recs_after = self.FileTemplate.with_user(self.root).search(
[("id", "=", rec2.id)]
)
self.assertFalse(
recs_after, "Root should be able to delete the record without restrictions."
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,442 @@
# Copyright (C) 2025 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.exceptions import AccessError
from .common_jets import TestTowerJetsCommon
class TestTowerJetAccess(TestTowerJetsCommon):
"""
Test access rules for Jet model
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create additional manager for multi-manager tests
cls.manager2 = cls.Users.create(
{
"name": "Test Manager 2",
"login": "test_manager_2",
"email": "test_manager_2@example.com",
"groups_id": [(6, 0, [cls.group_manager.id])],
}
)
# Create additional server for testing
cls.server_test_2 = cls.Server.create(
{
"name": "Test Server 2",
"ip_v4_address": "127.0.0.3",
"ssh_username": "test",
"ssh_password": "test",
"user_ids": [(5, 0, 0)],
"manager_ids": [(5, 0, 0)],
}
)
# ======================
# User Read Access Tests
# ======================
def test_user_read_access_jet_user_server_user(self):
"""Test User: Read when user in jet user_ids AND server user_ids"""
jet = self._create_jet(
"User Jet",
"user_jet",
user_ids=[(4, self.user.id)],
server_user_ids=[(4, self.user.id)],
)
records = self.Jet.with_user(self.user).search([("id", "=", jet.id)])
self.assertIn(
jet,
records,
"User should read when in jet user_ids AND server user_ids",
)
def test_user_read_no_access_jet_user_only(self):
"""Test User: No read when user in jet user_ids but NOT in server user_ids"""
jet = self._create_jet(
"User Jet No Server",
"user_jet_no_server",
user_ids=[(4, self.user.id)],
server_user_ids=[(5, 0, 0)],
)
records = self.Jet.with_user(self.user).search([("id", "=", jet.id)])
self.assertEqual(
len(records),
0,
"User should not read when not in server user_ids",
)
def test_user_read_no_access_server_user_only(self):
"""Test User: No read when user in server user_ids but NOT in jet user_ids"""
jet = self._create_jet(
"Server User No Jet",
"server_user_no_jet",
user_ids=[(5, 0, 0)],
server_user_ids=[(4, self.user.id)],
)
records = self.Jet.with_user(self.user).search([("id", "=", jet.id)])
self.assertEqual(
len(records),
0,
"User should not read when not in jet user_ids",
)
def test_user_write_forbidden(self):
"""Test User: Cannot write/create/delete records"""
jet = self._create_jet(
"User Jet",
"user_jet",
user_ids=[(4, self.user.id)],
server_user_ids=[(4, self.user.id)],
)
# User should not be able to write
with self.assertRaises(AccessError):
jet.with_user(self.user).write({"name": "Updated Name"})
# User should not be able to create
with self.assertRaises(AccessError):
self.Jet.with_user(self.user).create(
{
"name": "New Jet",
"reference": "new_jet",
"jet_template_id": self.jet_template_test.id,
"server_id": self.server_test_1.id,
}
)
# User should not be able to delete
# Jet is deletable by default, so this tests access control
with self.assertRaises(AccessError):
jet.with_user(self.user).unlink()
# ======================
# Manager Read Access Tests
# ======================
def test_manager_read_access_jet_user_server_user(self):
"""Test Manager: Read when in jet user_ids AND server user_ids"""
jet = self._create_jet(
"Manager Jet User",
"manager_jet_user",
user_ids=[(4, self.manager.id)],
server_user_ids=[(4, self.manager.id)],
)
records = self.Jet.with_user(self.manager).search([("id", "=", jet.id)])
self.assertIn(
jet,
records,
"Manager should read when in jet user_ids AND server user_ids",
)
def test_manager_read_access_jet_manager_server_manager(self):
"""Test Manager: Read when in jet manager_ids AND server manager_ids"""
jet = self._create_jet(
"Manager Jet Manager",
"manager_jet_manager",
manager_ids=[(4, self.manager.id)],
server_manager_ids=[(4, self.manager.id)],
)
records = self.Jet.with_user(self.manager).search([("id", "=", jet.id)])
self.assertIn(
jet,
records,
"Manager should read when in jet manager_ids AND server manager_ids",
)
def test_manager_read_access_jet_user_server_manager(self):
"""Test Manager: Read when in jet user_ids AND server manager_ids"""
jet = self._create_jet(
"Manager Jet User Server Manager",
"manager_jet_user_server_manager",
user_ids=[(4, self.manager.id)],
server_manager_ids=[(4, self.manager.id)],
)
records = self.Jet.with_user(self.manager).search([("id", "=", jet.id)])
self.assertIn(
jet,
records,
"Manager should read when in jet user_ids AND server manager_ids",
)
def test_manager_read_access_jet_manager_server_user(self):
"""Test Manager: Read when in jet manager_ids AND server user_ids"""
jet = self._create_jet(
"Manager Jet Manager Server User",
"manager_jet_manager_server_user",
manager_ids=[(4, self.manager.id)],
server_user_ids=[(4, self.manager.id)],
)
records = self.Jet.with_user(self.manager).search([("id", "=", jet.id)])
self.assertIn(
jet,
records,
"Manager should read when in jet manager_ids AND server user_ids",
)
def test_manager_read_no_access_jet_only(self):
"""Test Manager: No read when in jet but NOT in server"""
jet = self._create_jet(
"Manager Jet No Server",
"manager_jet_no_server",
user_ids=[(4, self.manager.id)],
server_user_ids=[(5, 0, 0)],
server_manager_ids=[(5, 0, 0)],
)
records = self.Jet.with_user(self.manager).search([("id", "=", jet.id)])
self.assertEqual(
len(records),
0,
"Manager should not read when not in server user_ids or manager_ids",
)
def test_manager_read_no_access_server_only(self):
"""Test Manager: No read when in server but NOT in jet"""
jet = self._create_jet(
"Manager Server No Jet",
"manager_server_no_jet",
user_ids=[(5, 0, 0)],
manager_ids=[(5, 0, 0)],
server_user_ids=[(4, self.manager.id)],
)
records = self.Jet.with_user(self.manager).search([("id", "=", jet.id)])
self.assertEqual(
len(records),
0,
"Manager should not read when not in jet user_ids or manager_ids",
)
# ======================
# Manager Write/Create Access Tests
# ======================
def test_manager_write_access_jet_manager_server_user(self):
"""Test Manager: Write when in jet manager_ids AND server user_ids"""
jet = self._create_jet(
"Manager Write Jet",
"manager_write_jet",
manager_ids=[(4, self.manager.id)],
server_user_ids=[(4, self.manager.id)],
)
try:
jet.with_user(self.manager).write({"name": "Updated Name"})
jet.invalidate_recordset()
self.assertEqual(
jet.name, "Updated Name", "Manager should be able to update"
)
except AccessError:
self.fail(
"Manager should be able to update when in jet"
" manager_ids AND server user_ids.",
)
def test_manager_write_access_jet_manager_server_manager(self):
"""Test Manager: Write when in jet manager_ids AND server manager_ids"""
jet = self._create_jet(
"Manager Write Jet Manager",
"manager_write_jet_manager",
manager_ids=[(4, self.manager.id)],
server_manager_ids=[(4, self.manager.id)],
)
try:
jet.with_user(self.manager).write({"name": "Updated"})
except AccessError:
self.fail(
"Manager should be able to write when in jet"
" manager_ids AND server manager_ids.",
)
def test_manager_write_forbidden_not_in_jet_manager_ids(self):
"""Test Manager: No write when NOT in jet manager_ids"""
jet = self._create_jet(
"Manager No Write Jet",
"manager_no_write_jet",
user_ids=[(4, self.manager.id)], # Only in user_ids, not manager_ids
server_user_ids=[(4, self.manager.id)],
)
with self.assertRaises(AccessError):
jet.with_user(self.manager).write({"name": "Should Fail"})
def test_manager_write_forbidden_not_in_server(self):
"""Test Manager: No write when in jet manager_ids but NOT in server"""
jet = self._create_jet(
"Manager No Write Server",
"manager_no_write_server",
manager_ids=[(4, self.manager.id)],
server_user_ids=[(5, 0, 0)],
server_manager_ids=[(5, 0, 0)],
)
with self.assertRaises(AccessError):
jet.with_user(self.manager).write({"name": "Should Fail"})
def test_manager_create_access(self):
"""
Test Manager:
Create when in jet manager_ids AND server user_ids or manager_ids.
"""
# Create with manager in jet manager_ids and server user_ids - should succeed
try:
jet = self._create_jet(
"Create Success",
"create_success",
user_ids=[(5, 0, 0)],
manager_ids=[(4, self.manager.id)],
server_user_ids=[(4, self.manager.id)],
with_user=self.manager,
)
records = self.Jet.search([("id", "=", jet.id)])
self.assertIn(jet, records, "Manager should be able to create")
except AccessError:
self.fail("Manager should be able to create when in jet manager_ids")
def test_manager_create_forbidden_not_in_manager_ids(self):
"""Test Manager: Cannot create when not in jet manager_ids"""
# Configure server access first (required, but jet manager_ids check will fail)
self.server_test_1.write({"user_ids": [(4, self.manager.id)]})
with self.assertRaises(AccessError):
self.Jet.with_user(self.manager).create(
{
"name": "Create Fail",
"reference": "create_fail",
"jet_template_id": self.jet_template_test.id,
"server_id": self.server_test_1.id,
"user_ids": [
(4, self.manager.id)
], # Only user_ids, not manager_ids
"manager_ids": [(5, 0, 0)],
}
)
# ======================
# Manager Delete Access Tests
# ======================
def test_manager_delete_own_record(self):
"""Test Manager: Delete own record when in jet manager_ids AND server"""
# Create as manager to ensure create_uid is set correctly
jet = self._create_jet(
"My Jet",
"my_jet",
manager_ids=[(4, self.manager.id)],
server_user_ids=[(4, self.manager.id)],
with_user=self.manager,
)
# Jet is deletable by default, so manager can delete it
try:
jet.with_user(self.manager).unlink()
records = self.Jet.search([("id", "=", jet.id)])
self.assertEqual(
len(records), 0, "Manager should be able to delete own record"
)
except AccessError:
self.fail("Manager should be able to delete own record")
def test_manager_delete_not_creator(self):
"""Test Manager: Cannot delete record created by another user"""
jet = self._create_jet(
"Other's Jet",
"others_jet",
manager_ids=[(4, self.manager.id), (4, self.manager2.id)],
server_user_ids=[(4, self.manager.id), (4, self.manager2.id)],
with_user=self.manager2,
)
# Manager1 cannot delete Manager2's record
# Jet is deletable by default, so this tests access control
with self.assertRaises(AccessError):
jet.with_user(self.manager).unlink()
def test_manager_delete_not_in_manager_ids(self):
"""Test Manager: Cannot delete when not in jet manager_ids"""
jet = self._create_jet(
"Removed Manager",
"removed_manager",
manager_ids=[(4, self.manager.id)],
server_user_ids=[(4, self.manager.id)],
with_user=self.manager,
)
# Remove from manager_ids
jet.write({"manager_ids": [(5, 0, 0)]})
# Cannot delete anymore
# Jet is deletable by default, so this tests access control
with self.assertRaises(AccessError):
jet.with_user(self.manager).unlink()
def test_manager_delete_not_in_server(self):
"""Test Manager: Cannot delete when in jet manager_ids but NOT in server"""
jet = self._create_jet(
"Manager Jet",
"manager_jet",
manager_ids=[(4, self.manager.id)],
server_user_ids=[(4, self.manager.id)],
with_user=self.manager,
)
# Remove server access
self.server_test_1.write({"user_ids": [(5, 0, 0)], "manager_ids": [(5, 0, 0)]})
# Cannot delete anymore
# Jet is deletable by default, so this tests access control
with self.assertRaises(AccessError):
jet.with_user(self.manager).unlink()
# ======================
# Root Access Tests
# ======================
def test_root_full_access(self):
"""Test Root: Full CRUD access regardless of access restrictions"""
# Test Root can create
jet = self.Jet.create(
{
"name": "Root Jet",
"reference": "root_jet",
"jet_template_id": self.jet_template_test.id,
"server_id": self.server_test_1.id,
"user_ids": [(5, 0, 0)],
"manager_ids": [(5, 0, 0)],
}
)
# Root can read
records = self.Jet.search([("id", "=", jet.id)])
self.assertIn(jet, records, "Root should be able to read")
# Root can write
jet.write({"name": "Root Updated Jet"})
jet.invalidate_recordset()
self.assertEqual(jet.name, "Root Updated Jet", "Root should be able to update")
# Test Root can delete records created by other users
manager_jet = self._create_jet(
"Manager's Jet",
"managers_jet",
manager_ids=[(4, self.manager.id)],
server_user_ids=[(4, self.manager.id)],
with_user=self.manager,
)
# Jet is deletable by default, so root can delete it
manager_jet.unlink()
records = self.Jet.search([("id", "=", manager_jet.id)])
self.assertEqual(
len(records), 0, "Root should be able to delete records from any creator"
)

View File

@@ -0,0 +1,647 @@
# Copyright (C) 2025 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.exceptions import AccessError
from .common_jets import TestTowerJetsCommon
class TestTowerJetActionAccess(TestTowerJetsCommon):
"""
Test access rules for Jet Action model (cx.tower.jet.action)
"""
# ======================
# User Read Access
# ======================
def test_user_read_access_level_user_and_template_user(self):
"""
User: can read when action access_level is User
(1) AND template access_level is User (1)
"""
template = self.JetTemplate.create(
{
"name": "User Level Template",
"reference": "user_level_template",
"access_level": "1", # User level
"user_ids": False,
"manager_ids": False,
}
)
action = self.JetAction.create(
{
"name": "Action U",
"reference": "action_u",
"access_level": "1", # User level
"jet_template_id": template.id,
"state_from_id": self.state_running.id,
"state_to_id": self.state_stopped.id,
"state_transit_id": self.state_stopping.id,
}
)
records = self.JetAction.with_user(self.user).search([("id", "=", action.id)])
self.assertEqual(
len(records),
1,
"User should read when action and template access_level are User",
)
def test_user_read_when_in_template_users(self):
"""
User: can read when action access_level is User (1)
AND user is added to template Users
"""
template = self.JetTemplate.create(
{
"name": "Manager Level Template (user granted)",
"reference": "manager_level_template_user",
"access_level": "2", # Manager level
"user_ids": [(4, self.user.id)],
"manager_ids": False,
}
)
action = self.JetAction.create(
{
"name": "Action TU",
"reference": "action_tu",
"access_level": "1", # User level
"jet_template_id": template.id,
"state_from_id": self.state_running.id,
"state_to_id": self.state_stopped.id,
"state_transit_id": self.state_stopping.id,
}
)
records = self.JetAction.with_user(self.user).search([("id", "=", action.id)])
self.assertEqual(
len(records),
1,
"User should read when action access_level is"
" User and user in template Users",
)
def test_user_read_when_in_jet_users(self):
"""
User: can read when action access_level is
User (1) AND user is added to Jet Users
"""
template = self.JetTemplate.create(
{
"name": "Manager Level Template",
"reference": "manager_level_template_jet",
"access_level": "2", # Manager level
"user_ids": False,
"manager_ids": False,
}
)
# Add server to template's server_ids for jet creation
template.write({"server_ids": [(4, self.server_test_1.id)]})
self._create_jet(
name="Test Jet from Template",
reference="test_jet_from_template",
template=template,
server=self.server_test_1,
user_ids=[(4, self.user.id)], # Add user to Jet's user_ids
server_user_ids=[(4, self.user.id)], # Also add to server for jet access
)
action = self.JetAction.create(
{
"name": "Action JU",
"reference": "action_ju",
"access_level": "1", # User level
"jet_template_id": template.id,
"state_from_id": self.state_running.id,
"state_to_id": self.state_stopped.id,
"state_transit_id": self.state_stopping.id,
}
)
records = self.JetAction.with_user(self.user).search([("id", "=", action.id)])
self.assertEqual(
len(records),
1,
"User should read when action access_level is User and user in Jet Users",
)
def test_user_read_no_access_action_not_user_level(self):
"""User: cannot read when action access_level is NOT User (1)"""
template = self.JetTemplate.create(
{
"name": "User Level Template",
"reference": "user_level_template_no_access",
"access_level": "1", # User level
"user_ids": False,
"manager_ids": False,
}
)
action = self.JetAction.create(
{
"name": "Action M",
"reference": "action_m",
"access_level": "2", # Manager level
"jet_template_id": template.id,
"state_from_id": self.state_running.id,
"state_to_id": self.state_stopped.id,
"state_transit_id": self.state_stopping.id,
}
)
records = self.JetAction.with_user(self.user).search([("id", "=", action.id)])
self.assertEqual(
len(records),
0,
"User should not read when action access_level is not User",
)
def test_user_read_no_access_template_conditions_not_met(self):
"""
User: cannot read when action access_level is User (1)
and template conditions not met
"""
template = self.JetTemplate.create(
{
"name": "Manager Level Template",
"reference": "manager_level_template_no_access",
"access_level": "2", # Manager level
"user_ids": False, # User not in template Users
"manager_ids": False,
}
)
# Don't create any jets with user in user_ids
action = self.JetAction.create(
{
"name": "Action NA",
"reference": "action_na",
"access_level": "1", # User level
"jet_template_id": template.id,
"state_from_id": self.state_running.id,
"state_to_id": self.state_stopped.id,
"state_transit_id": self.state_stopping.id,
}
)
records = self.JetAction.with_user(self.user).search([("id", "=", action.id)])
self.assertEqual(
len(records),
0,
"User should not read when action is User level"
" and template conditions not met",
)
def test_user_write_forbidden(self):
"""User: cannot write/create/delete records"""
template = self.JetTemplate.create(
{
"name": "User Level Template",
"reference": "user_level_template_write",
"access_level": "1",
"user_ids": [(4, self.user.id)],
}
)
action = self.JetAction.create(
{
"name": "Action W",
"reference": "action_w_user",
"access_level": "1",
"jet_template_id": template.id,
"state_from_id": self.state_running.id,
"state_to_id": self.state_stopped.id,
"state_transit_id": self.state_stopping.id,
}
)
# Write forbidden
with self.assertRaises(AccessError):
self.JetAction.with_user(self.user).browse(action.id).write({"priority": 5})
# Create forbidden
with self.assertRaises(AccessError):
self.JetAction.with_user(self.user).create(
{
"name": "Action Created",
"reference": "action_created_user",
"access_level": "1",
"jet_template_id": template.id,
"state_from_id": self.state_stopped.id,
"state_to_id": self.state_running.id,
"state_transit_id": self.state_starting.id,
}
)
# Delete forbidden
with self.assertRaises(AccessError):
self.JetAction.with_user(self.user).browse(action.id).unlink()
# ======================
# Manager Read Access
# ======================
def test_manager_read_access_level_manager_or_less(self):
"""
Manager: can read when action access_level <= Manager (2)
AND template access_level <= Manager (2)
"""
template = self.JetTemplate.create(
{
"name": "Manager Level Template",
"reference": "manager_level_template",
"access_level": "2",
}
)
action = self.JetAction.create(
{
"name": "Action R",
"reference": "action_r",
"access_level": "2", # Manager level
"jet_template_id": template.id,
"state_from_id": self.state_running.id,
"state_to_id": self.state_stopped.id,
"state_transit_id": self.state_stopping.id,
}
)
records = self.JetAction.with_user(self.manager).search(
[("id", "=", action.id)]
)
self.assertEqual(
len(records),
1,
"Manager should read when action and template level <= Manager",
)
def test_manager_read_when_in_template_users(self):
"""
Manager: can read when action access_level <= Manager (2)
AND user is added to template Users
even if template access_level is Root (3)
"""
template = self.JetTemplate.create(
{
"name": "Root Level Template (user granted)",
"reference": "root_level_template_user",
"access_level": "3",
"user_ids": [(4, self.manager.id)],
}
)
action = self.JetAction.create(
{
"name": "Action RU",
"reference": "action_ru",
"access_level": "2", # Manager level
"jet_template_id": template.id,
"state_from_id": self.state_running.id,
"state_to_id": self.state_stopped.id,
"state_transit_id": self.state_stopping.id,
}
)
records = self.JetAction.with_user(self.manager).search(
[("id", "=", action.id)]
)
self.assertEqual(
len(records),
1,
"Manager should read when action level <= Manager and in template Users",
)
def test_manager_read_when_in_template_managers(self):
"""
Manager: can read when action access_level <= Manager (2)
AND user is added to template Managers
even if template access_level is Root (3)
"""
template = self.JetTemplate.create(
{
"name": "Root Level Template (manager)",
"reference": "root_level_template_manager",
"access_level": "3",
"manager_ids": [(4, self.manager.id)],
}
)
action = self.JetAction.create(
{
"name": "Action RM",
"reference": "action_rm",
"access_level": "2", # Manager level
"jet_template_id": template.id,
"state_from_id": self.state_running.id,
"state_to_id": self.state_stopped.id,
"state_transit_id": self.state_stopping.id,
}
)
records = self.JetAction.with_user(self.manager).search(
[("id", "=", action.id)]
)
self.assertEqual(
len(records),
1,
"Manager should read when action level <= Manager and in template Managers",
)
def test_manager_read_no_access_action_root_level(self):
"""
Manager: cannot read when action access_level is Root (3)
even if template conditions are met
"""
template = self.JetTemplate.create(
{
"name": "Manager Level Template",
"reference": "manager_level_template_no_access",
"access_level": "2",
"manager_ids": [(4, self.manager.id)],
}
)
action = self.JetAction.create(
{
"name": "Action Root",
"reference": "action_root",
"access_level": "3", # Root level
"jet_template_id": template.id,
"state_from_id": self.state_running.id,
"state_to_id": self.state_stopped.id,
"state_transit_id": self.state_stopping.id,
}
)
records = self.JetAction.with_user(self.manager).search(
[("id", "=", action.id)]
)
self.assertEqual(
len(records),
0,
"Manager should not read when action access_level is Root",
)
# ======================
# Manager Write/Create/Delete
# ======================
def test_manager_write_when_in_template_managers(self):
"""
Manager: can write when action access_level <= Manager (2)
AND user is in template Managers
"""
template = self.JetTemplate.create(
{
"name": "Template For Write",
"reference": "template_for_write",
"manager_ids": [(4, self.manager.id)],
}
)
action = self.JetAction.create(
{
"name": "Action W",
"reference": "action_w",
"access_level": "2", # Manager level
"jet_template_id": template.id,
"state_from_id": self.state_running.id,
"state_to_id": self.state_stopped.id,
"state_transit_id": self.state_stopping.id,
}
)
# Write
self.JetAction.with_user(self.manager).browse(action.id).write({"priority": 99})
action.invalidate_recordset()
self.assertEqual(
action.priority,
99,
"Manager should be able to write when action level"
" <= Manager and in Managers",
)
# Create
created = self.JetAction.with_user(self.manager).create(
{
"name": "Action W Created",
"reference": "action_w_created",
"access_level": "2", # Manager level
"jet_template_id": template.id,
"state_from_id": self.state_stopped.id,
"state_to_id": self.state_running.id,
"state_transit_id": self.state_starting.id,
}
)
self.assertTrue(
created,
"Manager should be able to create when action level "
"<= Manager and in Managers",
)
# Delete
self.JetAction.with_user(self.manager).browse(created.id).unlink()
after = self.JetAction.search([("id", "=", created.id)])
self.assertEqual(
len(after),
0,
"Manager should be able to delete when action level "
"<= Manager and in Managers",
)
def test_manager_write_forbidden_when_not_in_template_managers(self):
"""
Manager: cannot write/create/delete if NOT in template Managers
even if action access_level <= Manager (2)
"""
template = self.JetTemplate.create(
{
"name": "Template No Write",
"reference": "template_no_write",
}
)
action = self.JetAction.create(
{
"name": "Action NW",
"reference": "action_nw",
"access_level": "2", # Manager level
"jet_template_id": template.id,
"state_from_id": self.state_running.id,
"state_to_id": self.state_stopped.id,
"state_transit_id": self.state_stopping.id,
}
)
# Write forbidden
with self.assertRaises(AccessError):
self.JetAction.with_user(self.manager).browse(action.id).write(
{"priority": 5}
)
# Create forbidden
with self.assertRaises(AccessError):
self.JetAction.with_user(self.manager).create(
{
"name": "Action NW Created",
"reference": "action_nw_created",
"access_level": "2", # Manager level
"jet_template_id": template.id,
"state_from_id": self.state_stopped.id,
"state_to_id": self.state_running.id,
"state_transit_id": self.state_starting.id,
}
)
# Delete forbidden
with self.assertRaises(AccessError):
self.JetAction.with_user(self.manager).browse(action.id).unlink()
def test_manager_write_forbidden_when_action_root_level(self):
"""
Manager: cannot write/create/delete when action access_level is Root (3)
even if user is in template Managers
"""
template = self.JetTemplate.create(
{
"name": "Template For Write",
"reference": "template_for_write_root",
"manager_ids": [(4, self.manager.id)],
}
)
action = self.JetAction.create(
{
"name": "Action Root W",
"reference": "action_root_w",
"access_level": "3", # Root level
"jet_template_id": template.id,
"state_from_id": self.state_running.id,
"state_to_id": self.state_stopped.id,
"state_transit_id": self.state_stopping.id,
}
)
# Write forbidden
with self.assertRaises(AccessError):
self.JetAction.with_user(self.manager).browse(action.id).write(
{"priority": 5}
)
# Create forbidden
with self.assertRaises(AccessError):
self.JetAction.with_user(self.manager).create(
{
"name": "Action Root Created",
"reference": "action_root_created",
"access_level": "3", # Root level
"jet_template_id": template.id,
"state_from_id": self.state_stopped.id,
"state_to_id": self.state_running.id,
"state_transit_id": self.state_starting.id,
}
)
# Delete forbidden
with self.assertRaises(AccessError):
self.JetAction.with_user(self.manager).browse(action.id).unlink()
def test_manager_write_on_root_level_template_when_in_managers(self):
"""
Manager: can write/create/delete on Root-level template
when action access_level <= Manager (2) AND user is in Managers
"""
template = self.JetTemplate.create(
{
"name": "Root Level Template For Write",
"reference": "root_level_template_for_write",
"access_level": "3",
"manager_ids": [(4, self.manager.id)],
}
)
action = self.JetAction.create(
{
"name": "Action RW",
"reference": "action_rw",
"access_level": "2", # Manager level
"jet_template_id": template.id,
"state_from_id": self.state_running.id,
"state_to_id": self.state_stopped.id,
"state_transit_id": self.state_stopping.id,
}
)
# Write
self.JetAction.with_user(self.manager).browse(action.id).write({"priority": 42})
action.invalidate_recordset()
self.assertEqual(
action.priority,
42,
"Manager should write on Root-level template when action level "
"<= Manager and in Managers",
)
# Create
created = self.JetAction.with_user(self.manager).create(
{
"name": "Action RW Created",
"reference": "action_rw_created",
"access_level": "2", # Manager level
"jet_template_id": template.id,
"state_from_id": self.state_stopped.id,
"state_to_id": self.state_running.id,
"state_transit_id": self.state_starting.id,
}
)
self.assertTrue(
created,
"Manager should create on Root-level template when action level "
"<= Manager and in Managers",
)
# Delete
self.JetAction.with_user(self.manager).browse(created.id).unlink()
after = self.JetAction.search([("id", "=", created.id)])
self.assertEqual(
len(after),
0,
"Manager should delete on Root-level template when action level "
"<= Manager and in Managers",
)
# ======================
# Root Access
# ======================
def test_root_full_access(self):
"""Root: full CRUD access for any record"""
template = self.JetTemplate.with_user(self.root).create(
{
"name": "Root Template",
"reference": "root_template",
"access_level": "3",
}
)
# Create
action = self.JetAction.with_user(self.root).create(
{
"name": "Root Action",
"reference": "root_action",
"jet_template_id": template.id,
"state_from_id": self.state_initial.id,
"state_to_id": self.state_running.id,
"state_transit_id": self.state_starting.id,
}
)
# Read
records = self.JetAction.with_user(self.root).search([("id", "=", action.id)])
self.assertEqual(len(records), 1, "Root should read any record")
# Write
action.with_user(self.root).write({"priority": 7})
action.invalidate_recordset()
self.assertEqual(action.priority, 7, "Root should update any record")
# Delete
action.with_user(self.root).unlink()
self.assertEqual(
len(
self.JetAction.with_user(self.root).search(
[("reference", "=", "root_action")]
)
),
0,
"Root should delete any record",
)

View File

@@ -0,0 +1,81 @@
# Copyright (C) 2025 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from .common_jets import TestTowerJetsCommon
class TestJetCreateWizard(TestTowerJetsCommon):
"""Tests for `cx.tower.jet.create.wizard`"""
def test_action_confirm_creates_jet(self):
"""
Ensure that the wizard creates a new jet using the selected template.
"""
wizard_model = self.env["cx.tower.jet.create.wizard"]
wizard = wizard_model.create(
{
"name_type": "m",
"name": "Wizard Jet",
"jet_template_id": self.jet_template_test.id,
"server_id": self.server_test_1.id,
}
)
action = wizard.action_confirm()
jet = self.Jet.browse(action["res_id"])
self.assertTrue(jet.exists(), "Wizard action should return the created jet")
self.assertEqual(jet.name, "Wizard Jet")
self.assertEqual(jet.server_id, self.server_test_1)
self.assertEqual(jet.jet_template_id, self.jet_template_test)
def test_action_confirm_sets_custom_variables(self):
"""
Ensure custom variable values from the wizard are stored on the created jet.
"""
wizard_model = self.env["cx.tower.jet.create.wizard"]
custom_variable = self.Variable.create(
{
"name": "Wizard Custom Variable",
}
)
custom_value = "custom value"
wizard = wizard_model.create(
{
"name_type": "m",
"name": "Wizard Jet With Variables",
"jet_template_id": self.jet_template_test.id,
"server_id": self.server_test_1.id,
"use_custom_variables": "y",
"line_ids": [
(
0,
0,
{
"variable_id": custom_variable.id,
"value_char": custom_value,
},
)
],
}
)
action = wizard.action_confirm()
jet = self.Jet.browse(action["res_id"])
custom_lines = jet.variable_value_ids.filtered(
lambda line: line.variable_id == custom_variable
)
self.assertEqual(len(custom_lines), 1, "Custom variable should be stored once")
self.assertEqual(
custom_lines.variable_id,
custom_variable,
"Custom variable record should be linked to the expected variable",
)
self.assertEqual(
custom_lines.value_char,
custom_value,
"Created jet should keep custom variable values from the wizard",
)

View File

@@ -0,0 +1,420 @@
# Copyright (C) 2025 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.exceptions import AccessError
from .common_jets import TestTowerJetsCommon
class TestTowerJetDependencyAccess(TestTowerJetsCommon):
"""
Test access rules for Jet Dependency model
"""
# ======================
# Manager Read Access Tests
# ======================
def test_manager_read_access_both_user_ids(self):
"""Test Manager: Read when in user_ids of both jets"""
_, _, dependency = self._create_jet_dependency(
"Jet 1",
"jet_1",
"Jet 2",
"jet_2",
jet_user_ids=[(4, self.manager.id)],
depends_on_user_ids=[(4, self.manager.id)],
jet_server_user_ids=[(4, self.manager.id)],
depends_on_server_user_ids=[(4, self.manager.id)],
)
records = self.JetDependency.with_user(self.manager).search(
[("id", "=", dependency.id)]
)
self.assertEqual(
len(records),
1,
"Manager should read when in user_ids of both jets",
)
self.assertIn(
dependency,
records,
"Manager should get exactly the dependency record we searched for",
)
def test_manager_read_access_both_manager_ids(self):
"""Test Manager: Read when in manager_ids of both jets"""
_, _, dependency = self._create_jet_dependency(
"Jet Manager 1",
"jet_manager_1",
"Jet Manager 2",
"jet_manager_2",
jet_manager_ids=[(4, self.manager.id)],
depends_on_manager_ids=[(4, self.manager.id)],
jet_server_user_ids=[(4, self.manager.id)],
depends_on_server_user_ids=[(4, self.manager.id)],
)
records = self.JetDependency.with_user(self.manager).search(
[("id", "=", dependency.id)]
)
self.assertEqual(
len(records),
1,
"Manager should read when in manager_ids of both jets",
)
self.assertIn(
dependency,
records,
"Manager should get exactly the dependency record we searched for",
)
def test_manager_read_access_jet_user_depends_manager(self):
"""Test Manager: Read when in user_ids of jet and manager_ids of depends"""
_, _, dependency = self._create_jet_dependency(
"Jet User",
"jet_user",
"Depends Manager",
"depends_manager",
jet_user_ids=[(4, self.manager.id)],
depends_on_manager_ids=[(4, self.manager.id)],
jet_server_user_ids=[(4, self.manager.id)],
depends_on_server_user_ids=[(4, self.manager.id)],
)
records = self.JetDependency.with_user(self.manager).search(
[("id", "=", dependency.id)]
)
self.assertEqual(
len(records),
1,
"Manager should read when in user_ids of jet and manager_ids of depends",
)
self.assertIn(
dependency,
records,
"Manager should get exactly the dependency record we searched for",
)
def test_manager_read_access_jet_manager_depends_user(self):
"""Test Manager: Read when in manager_ids of jet and user_ids of depends"""
_, _, dependency = self._create_jet_dependency(
"Jet Manager",
"jet_manager",
"Depends User",
"depends_user",
jet_manager_ids=[(4, self.manager.id)],
depends_on_user_ids=[(4, self.manager.id)],
jet_server_user_ids=[(4, self.manager.id)],
depends_on_server_user_ids=[(4, self.manager.id)],
)
records = self.JetDependency.with_user(self.manager).search(
[("id", "=", dependency.id)]
)
self.assertEqual(
len(records),
1,
"Manager should read when in manager_ids of jet and user_ids of depends",
)
self.assertIn(
dependency,
records,
"Manager should get exactly the dependency record we searched for",
)
def test_manager_read_no_access_jet_only(self):
"""Test Manager: No read when in jet but NOT in depends on jet"""
_, _, dependency = self._create_jet_dependency(
"Jet Has Access",
"jet_has_access",
"Depends No Access",
"depends_no_access",
jet_user_ids=[(4, self.manager.id)],
depends_on_user_ids=[(5, 0, 0)],
depends_on_manager_ids=[(5, 0, 0)],
jet_server_user_ids=[(4, self.manager.id)],
depends_on_server_user_ids=[(4, self.manager.id)],
)
records = self.JetDependency.with_user(self.manager).search(
[("id", "=", dependency.id)]
)
self.assertEqual(
len(records),
0,
"Manager should not read when not in depends_on"
" jet user_ids or manager_ids",
)
def test_manager_read_no_access_depends_only(self):
"""Test Manager: No read when in depends on jet but NOT in jet"""
_, _, dependency = self._create_jet_dependency(
"Jet No Access",
"jet_no_access",
"Depends Has Access",
"depends_has_access",
jet_user_ids=[(5, 0, 0)],
jet_manager_ids=[(5, 0, 0)],
depends_on_user_ids=[(4, self.manager.id)],
jet_server_user_ids=[(4, self.manager.id)],
depends_on_server_user_ids=[(4, self.manager.id)],
)
records = self.JetDependency.with_user(self.manager).search(
[("id", "=", dependency.id)]
)
self.assertEqual(
len(records),
0,
"Manager should not read when not in jet user_ids or manager_ids",
)
# ======================
# Manager CRUD Access Tests
# ======================
def test_manager_write_access(self):
"""
Test Manager:
Write access when in manager_ids of jet AND user_ids
or manager_ids of depends.
"""
# Test with depends_on user_ids (same conditions as create test,
# but tests write access on existing record)
_, _, dependency1 = self._create_jet_dependency(
"Write Jet Manager",
"write_jet_manager",
"Depends User",
"depends_user",
jet_manager_ids=[(4, self.manager.id)],
depends_on_user_ids=[(4, self.manager.id)],
jet_server_user_ids=[(4, self.manager.id)],
depends_on_server_user_ids=[(4, self.manager.id)],
)
# Verify manager can access the dependency (write permissions allow read access)
try:
dependency1.invalidate_recordset()
dependency1.with_user(self.manager).read(["jet_id", "jet_depends_on_id"])
# Perform an actual write: switch to an alternative valid depends_on jet
depends_on_jet_alt = self._create_jet(
"Depends User Alt",
"depends_user_alt",
template=self.jet_template_tower_core,
user_ids=[(4, self.manager.id)],
server_user_ids=[(4, self.manager.id)],
)
dependency1.with_user(self.manager).write(
{"jet_depends_on_id": depends_on_jet_alt.id}
)
except AccessError:
self.fail(
"Manager should be able to write when in jet manager_ids "
"AND depends_on user_ids"
)
# Test with depends_on manager_ids - use different templates
# to avoid duplicate template dependency
_, _, dependency2 = self._create_jet_dependency(
"Write Jet Manager 2",
"write_jet_manager_2",
"Depends Manager",
"depends_manager",
jet_manager_ids=[(4, self.manager.id)],
depends_on_manager_ids=[(4, self.manager.id)],
jet_server_user_ids=[(4, self.manager.id)],
depends_on_server_user_ids=[(4, self.manager.id)],
jet_template=self.jet_template_nginx,
# Use different template to avoid duplicate
depends_on_template=self.jet_template_docker,
)
try:
dependency2.invalidate_recordset()
dependency2.with_user(self.manager).read(["jet_id", "jet_depends_on_id"])
# Perform an actual write: switch to an alternative valid depends_on jet
depends_on_jet_alt2 = self._create_jet(
"Depends Manager Alt",
"depends_manager_alt",
template=self.jet_template_docker,
manager_ids=[(4, self.manager.id)],
server_user_ids=[(4, self.manager.id)],
)
dependency2.with_user(self.manager).write(
{"jet_depends_on_id": depends_on_jet_alt2.id}
)
except AccessError:
self.fail(
"Manager should be able to write when in jet manager_ids"
" AND depends_on manager_ids"
)
def test_manager_create_access(self):
"""
Test Manager: Create when in manager_ids of jet AND user_ids
or manager_ids of depends.
"""
# Try to create dependency as manager
# (helper ensures proper template dependency)
try:
_, _, dependency = self._create_jet_dependency(
"Create Jet",
"create_jet",
"Create Depends",
"create_depends",
jet_manager_ids=[(4, self.manager.id)],
depends_on_user_ids=[(4, self.manager.id)],
jet_server_user_ids=[(4, self.manager.id)],
depends_on_server_user_ids=[(4, self.manager.id)],
with_user=self.manager,
jet_template=self.jet_template_test,
depends_on_template=self.jet_template_tower_core,
)
records = self.JetDependency.search([("id", "=", dependency.id)])
self.assertIn(
dependency,
records,
"Manager should be able to create dependency",
)
except AccessError:
self.fail("Manager should be able to create when in jet manager_ids")
def test_manager_create_forbidden_not_in_jet_manager_ids(self):
"""Test Manager: Cannot create when not in jet manager_ids"""
# Should not be able to create (manager not in jet manager_ids)
self.assertRaises(
AccessError,
lambda: self._create_jet_dependency(
"No Create Jet",
"no_create_jet",
"No Create Depends",
"no_create_depends",
jet_user_ids=[(4, self.manager.id)],
depends_on_user_ids=[(4, self.manager.id)],
jet_server_user_ids=[(4, self.manager.id)],
depends_on_server_user_ids=[(4, self.manager.id)],
with_user=self.manager,
jet_template=self.jet_template_test,
depends_on_template=self.jet_template_tower_core,
),
)
def test_manager_create_forbidden_not_in_depends(self):
"""
Test Manager: Cannot create when in jet manager_ids but NOT in depends.
"""
# Should not be able to create (manager has no access to depends)
self.assertRaises(
AccessError,
lambda: self._create_jet_dependency(
"Create Jet",
"create_jet",
"No Depends Access",
"no_depends_access",
jet_manager_ids=[(4, self.manager.id)],
depends_on_user_ids=[(5, 0, 0)],
depends_on_manager_ids=[(5, 0, 0)],
jet_server_user_ids=[(4, self.manager.id)],
depends_on_server_user_ids=[(4, self.manager.id)],
with_user=self.manager,
jet_template=self.jet_template_test,
depends_on_template=self.jet_template_tower_core,
),
)
def test_manager_unlink_access(self):
"""
Test Manager: Delete when in manager_ids of jet AND user_ids
or manager_ids of depends.
"""
_, _, dependency = self._create_jet_dependency(
"Delete Jet",
"delete_jet",
"Delete Depends",
"delete_depends",
jet_manager_ids=[(4, self.manager.id)],
depends_on_user_ids=[(4, self.manager.id)],
jet_server_user_ids=[(4, self.manager.id)],
depends_on_server_user_ids=[(4, self.manager.id)],
with_user=self.manager,
)
# Refresh dependency in manager context to ensure access
dependency.invalidate_recordset()
dependency = dependency.with_user(self.manager)
try:
dependency.unlink()
records = self.JetDependency.search([("id", "=", dependency.id)])
self.assertEqual(
len(records),
0,
"Manager should be able to delete dependency",
)
except AccessError:
self.fail("Manager should be able to delete dependency")
def test_manager_unlink_forbidden_not_in_jet_manager_ids(self):
"""Test Manager: Cannot delete when not in jet manager_ids"""
_, _, dependency = self._create_jet_dependency(
"No Delete Jet",
"no_delete_jet",
"No Delete Depends",
"no_delete_depends",
jet_user_ids=[(4, self.manager.id)],
depends_on_user_ids=[(4, self.manager.id)],
jet_server_user_ids=[(4, self.manager.id)],
depends_on_server_user_ids=[(4, self.manager.id)],
)
self.assertRaises(AccessError, dependency.with_user(self.manager).unlink)
# ======================
# Root Access Tests
# ======================
def test_root_full_access(self):
"""Test Root: Full CRUD access regardless of access restrictions"""
# Root can create dependency via helper regardless of access
_, _, dependency = self._create_jet_dependency(
"Root Jet",
"root_jet",
"Root Depends",
"root_depends",
jet_user_ids=[(5, 0, 0)],
jet_manager_ids=[(5, 0, 0)],
depends_on_user_ids=[(5, 0, 0)],
depends_on_manager_ids=[(5, 0, 0)],
with_user=self.root,
jet_template=self.jet_template_test,
depends_on_template=self.jet_template_tower_core,
)
# Root can read
records = self.JetDependency.with_user(self.root).search(
[("id", "=", dependency.id)]
)
self.assertIn(dependency, records, "Root should be able to read")
# Root can write: switch depends_on to another valid jet
depends_on_jet_alt = self._create_jet(
"Root Depends Alt",
"root_depends_alt",
template=self.jet_template_tower_core,
)
dependency.invalidate_recordset()
dependency.with_user(self.root).write(
{"jet_depends_on_id": depends_on_jet_alt.id}
)
# Root can delete
dependency.with_user(self.root).unlink()
records = self.JetDependency.with_user(self.root).search(
[("id", "=", dependency.id)]
)
self.assertEqual(
len(records),
0,
"Root should be able to delete dependency",
)

View File

@@ -0,0 +1,522 @@
# Copyright (C) 2024 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.exceptions import AccessError, ValidationError
from .common_jets import TestTowerJetsCommon
class TestTowerJetState(TestTowerJetsCommon):
"""
Test the Jet State model functionality
"""
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# set_state Tests
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
def test_set_state_success_user_level(self):
"""
Test set_state succeeds when user has sufficient access level.
User (level 1) can set state with level 1.
"""
# Use existing state and set it to User access level (1)
self.state_running.access_level = "1"
self.state_running.invalidate_recordset(["access_level"])
# Ensure user has access to the jet and server
self.jet_test.write({"user_ids": [(4, self.user.id)]})
self.server_test_1.write({"user_ids": [(4, self.user.id)]})
# Set jet to initial state
self.jet_test.state_id = self.state_initial
# User should be able to set state
self.state_running.with_user(self.user).with_context(
cetmix_tower_no_commit=True
).set_state(self.jet_test)
self.assertEqual(
self.jet_test.state_id,
self.state_running,
"Jet should be set to user-level state by user",
)
def test_set_state_success_manager_level(self):
"""
Test set_state succeeds when manager has sufficient access level.
Manager (level 2) can set state with level 2.
"""
# Use existing state and set it to Manager access level (2)
self.state_stopped.access_level = "2"
self.state_stopped.invalidate_recordset(["access_level"])
# Ensure manager has access to the jet and server
self.jet_test.write({"manager_ids": [(4, self.manager.id)]})
self.server_test_1.write({"manager_ids": [(4, self.manager.id)]})
# Set jet to running state (which has action to stopped)
self.jet_test.state_id = self.state_running
# Manager should be able to set state
self.state_stopped.with_user(self.manager).with_context(
cetmix_tower_no_commit=True
).set_state(self.jet_test)
self.assertEqual(
self.jet_test.state_id,
self.state_stopped,
"Jet should be set to manager-level state by manager",
)
def test_set_state_success_root_level(self):
"""
Test set_state succeeds when root has sufficient access level.
Root (level 3) can set state with level 3.
"""
# Use existing state and set it to Root access level (3)
self.state_error.access_level = "3"
self.state_error.invalidate_recordset(["access_level"])
# Set jet to running state (which has action to error)
self.jet_test.state_id = self.state_running
# Root should be able to set state
self.state_error.with_user(self.root).with_context(
cetmix_tower_no_commit=True
).set_state(self.jet_test)
self.assertEqual(
self.jet_test.state_id,
self.state_error,
"Jet should be set to root-level state by root",
)
def test_set_state_access_error_user_to_manager(self):
"""
Test set_state raises AccessError when user (level 1)
tries to set manager-level state (level 2).
"""
# Use existing state and set it to Manager access level (2)
self.state_stopped.access_level = "2"
self.state_stopped.invalidate_recordset(["access_level"])
# Ensure user has access to the jet and server (for the access check to work)
self.jet_test.write({"user_ids": [(4, self.user.id)]})
self.server_test_1.write({"user_ids": [(4, self.user.id)]})
# Set jet to running state (which has action to stopped)
self.jet_test.state_id = self.state_running
# User should not be able to set manager-level state
with self.assertRaises(AccessError) as context:
self.state_stopped.with_user(self.user).with_context(
cetmix_tower_no_commit=True
).set_state(self.jet_test)
self.assertIn(
"You are not allowed to set the",
str(context.exception),
"Should raise AccessError with appropriate message",
)
self.assertIn(
self.state_stopped.name,
str(context.exception),
"Error message should include state name",
)
def test_set_state_access_error_user_to_root(self):
"""
Test set_state raises AccessError when user (level 1)
tries to set root-level state (level 3).
"""
# Use existing state and set it to Root access level (3)
self.state_error.access_level = "3"
self.state_error.invalidate_recordset(["access_level"])
# Ensure user has access to the jet and server (for the access check to work)
self.jet_test.write({"user_ids": [(4, self.user.id)]})
self.server_test_1.write({"user_ids": [(4, self.user.id)]})
# Set jet to running state (which has action to error)
self.jet_test.state_id = self.state_running
# User should not be able to set root-level state
with self.assertRaises(AccessError) as context:
self.state_error.with_user(self.user).with_context(
cetmix_tower_no_commit=True
).set_state(self.jet_test)
self.assertIn(
"You are not allowed to set the",
str(context.exception),
"Should raise AccessError with appropriate message",
)
self.assertIn(
self.state_error.name,
str(context.exception),
"Error message should include state name",
)
def test_set_state_access_error_manager_to_root(self):
"""
Test set_state raises AccessError when manager (level 2)
tries to set root-level state (level 3).
"""
# Use existing state and set it to Root access level (3)
self.state_error.access_level = "3"
self.state_error.invalidate_recordset(["access_level"])
# Ensure manager has access to the jet and server (for the access check to work)
self.jet_test.write({"manager_ids": [(4, self.manager.id)]})
self.server_test_1.write({"manager_ids": [(4, self.manager.id)]})
# Set jet to running state (which has action to error)
self.jet_test.state_id = self.state_running
# Manager should not be able to set root-level state
with self.assertRaises(AccessError) as context:
self.state_error.with_user(self.manager).with_context(
cetmix_tower_no_commit=True
).set_state(self.jet_test)
self.assertIn(
"You are not allowed to set the",
str(context.exception),
"Should raise AccessError with appropriate message",
)
self.assertIn(
self.state_error.name,
str(context.exception),
"Error message should include state name",
)
def test_set_state_manager_can_access_user_level(self):
"""
Test set_state succeeds when manager (level 2) who IS in manager_ids
accesses user-level state (level 1).
Higher access levels can access lower level states.
"""
# Use existing state and set it to User access level (1)
self.state_running.access_level = "1"
self.state_running.invalidate_recordset(["access_level"])
# Ensure manager has access to the jet and server
# Manager IS in manager_ids, so they keep their manager access level (2)
self.jet_test.write({"manager_ids": [(4, self.manager.id)]})
self.server_test_1.write({"manager_ids": [(4, self.manager.id)]})
# Set jet to initial state
self.jet_test.state_id = self.state_initial
# Manager should be able to set user-level state
self.state_running.with_user(self.manager).with_context(
cetmix_tower_no_commit=True
).set_state(self.jet_test)
self.assertEqual(
self.jet_test.state_id,
self.state_running,
"Manager should be able to set user-level state",
)
def test_set_state_manager_not_in_manager_ids_treated_as_user(self):
"""
Test set_state treats manager (level 2) who is NOT in manager_ids
as user (level 1).
Manager should be able to set user-level state but not manager-level state.
"""
# Use existing state and set it to User access level (1)
self.state_running.access_level = "1"
self.state_running.invalidate_recordset(["access_level"])
# Ensure manager has access to the jet and server via user_ids
# but NOT via manager_ids
self.jet_test.write({"user_ids": [(4, self.manager.id)]})
self.server_test_1.write({"user_ids": [(4, self.manager.id)]})
# Explicitly ensure manager is NOT in manager_ids
self.jet_test.write({"manager_ids": [(5, 0, 0)]})
# Set jet to initial state
self.jet_test.state_id = self.state_initial
# Manager (treated as user) should be able to set user-level state
self.state_running.with_user(self.manager).with_context(
cetmix_tower_no_commit=True
).set_state(self.jet_test)
self.assertEqual(
self.jet_test.state_id,
self.state_running,
"Manager not in manager_ids should be able to set user-level state",
)
def test_set_state_manager_not_in_manager_ids_cannot_access_manager_level(self):
"""
Test set_state raises AccessError when manager (level 2) who is NOT
in manager_ids tries to set manager-level state (level 2).
Manager should be treated as user (level 1) and cannot access level 2.
"""
# Use existing state and set it to Manager access level (2)
self.state_stopped.access_level = "2"
self.state_stopped.invalidate_recordset(["access_level"])
# Ensure manager has access to the jet and server via user_ids
# but NOT via manager_ids
self.jet_test.write({"user_ids": [(4, self.manager.id)]})
self.server_test_1.write({"user_ids": [(4, self.manager.id)]})
# Explicitly ensure manager is NOT in manager_ids
self.jet_test.write({"manager_ids": [(5, 0, 0)]})
# Set jet to running state (which has action to stopped)
self.jet_test.state_id = self.state_running
# Manager (treated as user) should not be able to set manager-level state
with self.assertRaises(AccessError) as context:
self.state_stopped.with_user(self.manager).with_context(
cetmix_tower_no_commit=True
).set_state(self.jet_test)
self.assertIn(
"You are not allowed to set the",
str(context.exception),
"Should raise AccessError with appropriate message",
)
self.assertIn(
self.state_stopped.name,
str(context.exception),
"Error message should include state name",
)
def test_set_state_root_can_access_manager_level(self):
"""
Test set_state succeeds when root (level 3)
accesses manager-level state (level 2).
Higher access levels can access lower level states.
"""
# Use existing state and set it to Manager access level (2)
self.state_stopped.access_level = "2"
self.state_stopped.invalidate_recordset(["access_level"])
# Set jet to running state (which has action to stopped)
self.jet_test.state_id = self.state_running
# Root should be able to set manager-level state
self.state_stopped.with_user(self.root).with_context(
cetmix_tower_no_commit=True
).set_state(self.jet_test)
self.assertEqual(
self.jet_test.state_id,
self.state_stopped,
"Root should be able to set manager-level state",
)
def test_set_state_with_context_jet_id(self):
"""
Test set_state retrieves jet from context when jet parameter is None.
"""
# Use existing state and set it to User access level (1)
self.state_running.access_level = "1"
self.state_running.invalidate_recordset(["access_level"])
# Ensure user has access to the jet and server
self.jet_test.write({"user_ids": [(4, self.user.id)]})
self.server_test_1.write({"user_ids": [(4, self.user.id)]})
# Set jet to initial state
self.jet_test.state_id = self.state_initial
# Set state using context instead of direct parameter
self.state_running.with_user(self.user).with_context(
jet_id=self.jet_test.id,
cetmix_tower_no_commit=True,
).set_state()
self.assertEqual(
self.jet_test.state_id,
self.state_running,
"Jet should be set to state using context jet_id",
)
def test_set_state_no_jet_in_context_returns_silently(self):
"""
Test set_state returns silently when no jet_id in context
and jet parameter is None.
"""
# Use existing state
self.state_running.access_level = "1"
self.state_running.invalidate_recordset(["access_level"])
# Call set_state without jet parameter and without context
# Should return silently without raising exception
result = (
self.state_running.with_user(self.user)
.with_context(cetmix_tower_no_commit=True)
.set_state()
)
self.assertIsNone(result, "Should return None when no jet in context")
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# unlink Tests
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
def test_unlink_success_when_not_used_in_action(self):
"""
Test unlink succeeds when state is not used in any action.
"""
# Create a state that is not used in any action
unused_state = self.JetState.create(
{
"name": "Unused State",
"reference": "unused_state",
"sequence": 100,
}
)
state_id = unused_state.id
# Unlink should succeed
unused_state.unlink()
# Verify state is deleted
self.assertFalse(
self.JetState.search([("id", "=", state_id)]),
"State should be deleted when not used in any action",
)
def test_unlink_fails_when_used_as_state_from(self):
"""
Test unlink raises ValidationError when state is used as state_from_id
in an action.
"""
# state_running is used as state_from_id in action_running_to_stopped
with self.assertRaises(ValidationError) as context:
self.state_running.unlink()
error_message = str(context.exception)
self.assertIn(
"Some states are still used in the following actions",
error_message,
"Should raise ValidationError with appropriate message",
)
self.assertIn(
self.action_running_to_stopped.name,
error_message,
"Error message should include action name",
)
self.assertIn(
self.jet_template_test.name,
error_message,
"Error message should include template name",
)
def test_unlink_fails_when_used_as_state_to(self):
"""
Test unlink raises ValidationError when state is used as state_to_id
in an action.
"""
# state_stopped is used as state_to_id in action_running_to_stopped
with self.assertRaises(ValidationError) as context:
self.state_stopped.unlink()
error_message = str(context.exception)
self.assertIn(
"Some states are still used in the following actions",
error_message,
"Should raise ValidationError with appropriate message",
)
self.assertIn(
self.action_running_to_stopped.name,
error_message,
"Error message should include action name",
)
self.assertIn(
self.jet_template_test.name,
error_message,
"Error message should include template name",
)
def test_unlink_fails_when_used_as_state_transit(self):
"""
Test unlink raises ValidationError when state is used as state_transit_id
in an action.
"""
# state_stopping is used as state_transit_id in action_running_to_stopped
with self.assertRaises(ValidationError) as context:
self.state_stopping.unlink()
error_message = str(context.exception)
self.assertIn(
"Some states are still used in the following actions",
error_message,
"Should raise ValidationError with appropriate message",
)
self.assertIn(
self.action_running_to_stopped.name,
error_message,
"Error message should include action name",
)
self.assertIn(
self.jet_template_test.name,
error_message,
"Error message should include template name",
)
def test_unlink_fails_with_multiple_actions(self):
"""
Test unlink raises ValidationError with multiple actions when state
is used in multiple actions.
"""
# state_running is used in multiple actions:
# - action_running_to_stopped (state_from_id)
# - action_stopped_to_running (state_to_id)
# - action_running_to_error (state_from_id)
# - action_initial_to_running (state_to_id)
with self.assertRaises(ValidationError) as context:
self.state_running.unlink()
error_message = str(context.exception)
self.assertIn(
"Some states are still used in the following actions",
error_message,
"Should raise ValidationError with appropriate message",
)
# Verify multiple actions are mentioned
self.assertIn(
self.action_running_to_stopped.name,
error_message,
"Error message should include first action name",
)
self.assertIn(
self.jet_template_test.name,
error_message,
"Error message should include template name",
)
def test_unlink_fails_with_multiple_states(self):
"""
Test unlink raises ValidationError when trying to unlink multiple states
where at least one is used in an action.
"""
# Create an unused state
unused_state = self.JetState.create(
{
"name": "Another Unused State",
"reference": "another_unused_state",
"sequence": 101,
}
)
# Try to unlink both unused_state and state_running (which is used)
states_to_unlink = unused_state | self.state_running
with self.assertRaises(ValidationError) as context:
states_to_unlink.unlink()
error_message = str(context.exception)
self.assertIn(
"Some states are still used in the following actions",
error_message,
"Should raise ValidationError with appropriate message",
)
# Verify that neither state was deleted
self.assertTrue(
unused_state.exists(),
"Unused state should not be deleted when another state fails",
)
self.assertTrue(
self.state_running.exists(),
"Used state should not be deleted",
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,551 @@
# Copyright (C) 2025 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.exceptions import AccessError
from .common_jets import TestTowerJetsCommon
class TestTowerJetTemplateAccess(TestTowerJetsCommon):
"""
Test access rules for Jet Template model
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Use existing users from common.py (cls.user, cls.manager, cls.root)
# Create additional manager for multi-manager tests
cls.manager2 = cls.Users.create(
{
"name": "Test Manager 2",
"login": "test_manager_2",
"email": "test_manager_2@example.com",
"groups_id": [(6, 0, [cls.group_manager.id])],
}
)
# ======================
# User Access Tests
# ======================
def test_user_read_access_level_user(self):
"""Test User: Read access when access_level is "User" (1)"""
record = self.JetTemplate.create(
{
"name": "User Level Template",
"reference": "user_level_template",
"access_level": "1", # User level
"user_ids": False, # No users initially
"manager_ids": False, # No managers initially
}
)
# User should be able to read when access_level is "User"
records = self.JetTemplate.with_user(self.user).search([("id", "=", record.id)])
self.assertEqual(
len(records),
1,
"User should be able to read record when access_level is 'User'",
)
def test_user_read_access_user_ids(self):
"""Test User: Read access when user is added in user_ids"""
record = self.JetTemplate.create(
{
"name": "User Added Template",
"reference": "user_added_template",
"access_level": "2", # Manager level - normally not accessible
"user_ids": [(4, self.user.id)], # User added
"manager_ids": False,
}
)
# User should be able to read when added to user_ids
records = self.JetTemplate.with_user(self.user).search([("id", "=", record.id)])
self.assertEqual(
len(records),
1,
"User should be able to read record when added to user_ids",
)
def test_user_read_access_jet_user_ids(self):
"""
Test User: Read access when user is added in "Users" of any Jets
created from the template
"""
# Create template with Manager level - normally not accessible
# and user NOT in template's user_ids
template = self.JetTemplate.create(
{
"name": "Template with Jet Users",
"reference": "template_with_jet_users",
"access_level": "2", # Manager level - normally not accessible
"user_ids": False, # No users in template
"manager_ids": False,
}
)
# User should NOT be able to read initially
records = self.JetTemplate.with_user(self.user).search(
[("id", "=", template.id)]
)
self.assertEqual(
len(records),
0,
"User should not be able to read template without access",
)
# Create a Jet from this template
# Need to add server to template's server_ids for jet creation
template.write({"server_ids": [(4, self.server_test_1.id)]})
self._create_jet(
name="Test Jet from Template",
reference="test_jet_from_template",
template=template,
server=self.server_test_1,
user_ids=[(4, self.user.id)], # Add user to Jet's user_ids
)
# User should now be able to read the template
records = self.JetTemplate.with_user(self.user).search(
[("id", "=", template.id)]
)
self.assertEqual(
len(records),
1,
"User should be able to read template when added to Jet's user_ids",
)
def test_user_read_no_access(self):
"""
Test User: No read access when access_level is higher,
user not in template's user_ids, and user not in any Jet's user_ids
"""
record = self.JetTemplate.create(
{
"name": "Manager Level Template",
"reference": "manager_level_template",
"access_level": "2", # Manager level
"user_ids": False, # No users
"manager_ids": False,
}
)
# User should not be able to read
# (no access via access_level, template user_ids, or jet user_ids)
records = self.JetTemplate.with_user(self.user).search([("id", "=", record.id)])
self.assertEqual(
len(records),
0,
"User should not see record with Manager level "
"when not in user_ids or jet user_ids",
)
def test_user_write_forbidden(self):
"""Test User: Cannot write/create/delete records"""
record = self.JetTemplate.create(
{
"name": "User Template",
"reference": "user_template",
"access_level": "1",
"user_ids": [(4, self.user.id)],
}
)
# User should not be able to write
with self.assertRaises(AccessError):
record.with_user(self.user).write({"name": "Updated Name"})
# User should not be able to create
with self.assertRaises(AccessError):
self.JetTemplate.with_user(self.user).create(
{"name": "New Template", "reference": "new_template"}
)
# User should not be able to delete
with self.assertRaises(AccessError):
record.with_user(self.user).unlink()
# ======================
# Manager Read Access Tests
# ======================
def test_manager_read_access_level_user(self):
"""Test Manager: Read when access_level is "User" (1)"""
record = self.JetTemplate.create(
{
"name": "User Level for Manager",
"reference": "user_level_manager",
"access_level": "1",
"user_ids": False,
"manager_ids": False,
}
)
records = self.JetTemplate.with_user(self.manager).search(
[("id", "=", record.id)]
)
self.assertEqual(len(records), 1, "Manager should read access_level='1'")
def test_manager_read_access_level_manager(self):
"""Test Manager: Read when access_level is "Manager" (2)"""
record = self.JetTemplate.create(
{
"name": "Manager Level",
"reference": "manager_level",
"access_level": "2",
"user_ids": False,
"manager_ids": False,
}
)
records = self.JetTemplate.with_user(self.manager).search(
[("id", "=", record.id)]
)
self.assertEqual(len(records), 1, "Manager should read access_level='2'")
def test_manager_read_access_user_ids(self):
"""Test Manager: Read when added to user_ids regardless of access_level"""
record = self.JetTemplate.create(
{
"name": "Manager in Users",
"reference": "manager_in_users",
"access_level": "3", # Root level - normally not accessible
"user_ids": [(4, self.manager.id)], # Manager added as user
"manager_ids": False,
}
)
records = self.JetTemplate.with_user(self.manager).search(
[("id", "=", record.id)]
)
self.assertEqual(len(records), 1, "Manager should read when in user_ids")
def test_manager_read_no_access_root_level(self):
"""Test Manager: No read access for Root level (3) without user_ids"""
record = self.JetTemplate.create(
{
"name": "Root Level",
"reference": "root_level",
"access_level": "3",
"user_ids": False,
"manager_ids": False,
}
)
records = self.JetTemplate.with_user(self.manager).search(
[("id", "=", record.id)]
)
self.assertEqual(len(records), 0, "Manager should not read access_level='3'")
# ======================
# Manager Write/Create Access Tests
# ======================
def test_manager_write_access_level_and_manager_ids(self):
"""Test Manager: Write when access_level <= 2 AND in manager_ids"""
record = self.JetTemplate.create(
{
"name": "Manager Can Write",
"reference": "manager_can_write",
"access_level": "2",
"user_ids": False,
"manager_ids": [(4, self.manager.id)], # Manager added
}
)
# Manager should be able to write
try:
record.with_user(self.manager).write({"name": "Updated Name"})
record.invalidate_recordset()
self.assertEqual(
record.name, "Updated Name", "Manager should be able to update"
)
except AccessError:
self.fail("Manager should be able to update when in manager_ids")
def test_manager_write_access_level_user(self):
"""Test Manager: Write when access_level = 1 and in manager_ids"""
record = self.JetTemplate.create(
{
"name": "User Level Manager Write",
"reference": "user_level_manager_write",
"access_level": "1",
"user_ids": False,
"manager_ids": [(4, self.manager.id)],
}
)
try:
record.with_user(self.manager).write({"name": "Updated"})
except AccessError:
self.fail("Manager should be able to write access_level='1'")
def test_manager_write_forbidden_not_in_manager_ids(self):
"""Test Manager: No write when not in manager_ids"""
record = self.JetTemplate.create(
{
"name": "No Write Access",
"reference": "no_write_access",
"access_level": "2",
"user_ids": [(4, self.manager.id)], # Only in user_ids, not manager_ids
"manager_ids": False,
}
)
with self.assertRaises(AccessError):
record.with_user(self.manager).write({"name": "Should Fail"})
def test_manager_write_forbidden_root_level(self):
"""Test Manager: No write when access_level is Root (3)"""
record = self.JetTemplate.create(
{
"name": "Root Level No Write",
"reference": "root_level_no_write",
"access_level": "3",
"user_ids": [(4, self.manager.id)],
"manager_ids": [(4, self.manager.id)], # In manager_ids
}
)
with self.assertRaises(AccessError):
record.with_user(self.manager).write({"name": "Should Fail"})
def test_manager_create_access(self):
"""Test Manager: Create when access_level <= 2 AND in manager_ids"""
# Try to create without adding to manager_ids - should fail
with self.assertRaises(AccessError):
self.JetTemplate.with_user(self.manager).create(
{
"name": "Create Fail",
"reference": "create_fail",
"access_level": "2",
"manager_ids": False, # Not in manager_ids
}
)
# Create with manager added - should succeed
try:
record = self.JetTemplate.with_user(self.manager).create(
{
"name": "Create Success",
"reference": "create_success",
"access_level": "2",
"manager_ids": [(4, self.manager.id)], # In manager_ids
}
)
records = self.JetTemplate.search([("id", "=", record.id)])
self.assertEqual(len(records), 1, "Manager should be able to create")
except AccessError:
self.fail("Manager should be able to create when in manager_ids")
# ======================
# Manager Delete Access Tests
# ======================
def test_manager_delete_own_record(self):
"""Test Manager: Delete own record when in manager_ids"""
record = self.JetTemplate.with_user(self.manager).create(
{
"name": "My Record",
"reference": "my_record",
"access_level": "2",
"manager_ids": [(4, self.manager.id)],
}
)
try:
record.with_user(self.manager).unlink()
records = self.JetTemplate.search([("id", "=", record.id)])
self.assertEqual(
len(records), 0, "Manager should be able to delete own record"
)
except AccessError:
self.fail("Manager should be able to delete own record")
def test_manager_delete_not_creator(self):
"""Test Manager: Cannot delete record created by another user"""
record = self.JetTemplate.with_user(self.manager2).create(
{
"name": "Other's Record",
"reference": "others_record",
"access_level": "2",
"manager_ids": [(4, self.manager.id), (4, self.manager2.id)],
}
)
# Manager1 cannot delete Manager2's record
with self.assertRaises(AccessError):
record.with_user(self.manager).unlink()
def test_manager_delete_not_in_manager_ids(self):
"""Test Manager: Cannot delete when not in manager_ids"""
record = self.JetTemplate.with_user(self.manager).create(
{
"name": "Removed Manager",
"reference": "removed_manager",
"access_level": "2",
"manager_ids": [(4, self.manager.id)],
}
)
# Remove from manager_ids
record.write({"manager_ids": False})
# Cannot delete anymore
with self.assertRaises(AccessError):
record.with_user(self.manager).unlink()
def test_manager_delete_root_level(self):
"""Test Manager: Cannot delete Root level record"""
# Create record with Root level as root (default user)
record = self.JetTemplate.create(
{
"name": "Root Level Delete",
"reference": "root_level_delete",
"access_level": "3", # Root level
"manager_ids": [(4, self.manager.id)],
}
)
with self.assertRaises(AccessError):
record.with_user(self.manager).unlink()
# ======================
# Root Access Tests
# ======================
def test_root_full_access(self):
"""
Test Root: Full CRUD access regardless of access_level or creator.
Root has unrestricted access to all records via security rule
[(1, '=', 1)], so we test:
- Create records with all access levels
- Read records with all access levels
- Write to records with all access levels
- Delete records regardless of creator
"""
# Test CRUD operations for all access levels
for access_level in ["1", "2", "3"]:
# Root can create any level
record = self.JetTemplate.with_user(self.root).create(
{
"name": f"Root Level {access_level}",
"reference": f"root_level_{access_level}",
"access_level": access_level,
"user_ids": False,
"manager_ids": False,
}
)
# Root can read any level
records = self.JetTemplate.with_user(self.root).search(
[("id", "=", record.id)]
)
self.assertEqual(
len(records),
1,
f"Root should be able to read access_level={access_level}",
)
# Root can write any level
record.with_user(self.root).write(
{"name": f"Root Updated Level {access_level}"}
)
record.invalidate_recordset()
self.assertEqual(
record.name,
f"Root Updated Level {access_level}",
f"Root should be able to update access_level={access_level}",
)
# Test Root can delete records created by other users
manager_record = self.JetTemplate.with_user(self.manager).create(
{
"name": "Manager's Record",
"reference": "managers_record",
"access_level": "2",
"manager_ids": [(4, self.manager.id)],
}
)
manager_record.with_user(self.root).unlink()
records = self.JetTemplate.with_user(self.root).search(
[("id", "=", manager_record.id)]
)
self.assertEqual(
len(records), 0, "Root should be able to delete records from any creator"
)
# ======================
# Edge Cases
# ======================
def test_access_level_changes_visibility(self):
"""Test that changing access_level affects visibility"""
# Create with User level
record = self.JetTemplate.create(
{
"name": "Changing Level",
"reference": "changing_level",
"access_level": "1",
"user_ids": False,
"manager_ids": False,
}
)
# User can read
records = self.JetTemplate.with_user(self.user).search([("id", "=", record.id)])
self.assertEqual(len(records), 1, "User should read level 1")
# Change to Root level
record.write({"access_level": "3"})
# User cannot read anymore
records = self.JetTemplate.with_user(self.user).search([("id", "=", record.id)])
self.assertEqual(len(records), 0, "User should not read level 3")
def test_multiple_managers_access(self):
"""Test multiple managers accessing the same record"""
record = self.JetTemplate.with_user(self.manager).create(
{
"name": "Multi Manager",
"reference": "multi_manager",
"access_level": "2",
"manager_ids": [(4, self.manager.id), (4, self.manager2.id)],
}
)
# Both managers should be able to read
records1 = self.JetTemplate.with_user(self.manager).search(
[("id", "=", record.id)]
)
records2 = self.JetTemplate.with_user(self.manager2).search(
[("id", "=", record.id)]
)
self.assertEqual(len(records1), 1, "Manager1 should read")
self.assertEqual(len(records2), 1, "Manager2 should read")
# Both can write
record.with_user(self.manager).write({"name": "Manager1 Update"})
record.with_user(self.manager2).write({"name": "Manager2 Update"})
# Only creator can delete
with self.assertRaises(AccessError):
record.with_user(self.manager2).unlink()
# Creator can delete
record = self.JetTemplate.with_user(self.manager).create(
{
"name": "Creator Delete",
"reference": "creator_delete",
"access_level": "2",
"manager_ids": [(4, self.manager.id), (4, self.manager2.id)],
}
)
try:
record.with_user(self.manager).unlink()
except AccessError:
self.fail("Creator should be able to delete")

View File

@@ -0,0 +1,195 @@
# Copyright (C) 2025 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.exceptions import AccessError
from .common_jets import TestTowerJetsCommon
class TestTowerJetTemplateDependencyAccess(TestTowerJetsCommon):
"""
Test access rules for Jet Template Dependency model
"""
# ======================
# Manager Read Access Tests
# ======================
def test_manager_read_access_level_manager(self):
"""Test Manager: Read when template access_level is 'Manager' (2)"""
_, _, dependency = self._create_jet_template_dependency(
"Manager Level Template", "manager_level_template", access_level="2"
)
records = self.JetTemplateDependency.with_user(self.manager).search(
[("id", "=", dependency.id)]
)
self.assertEqual(len(records), 1, "Manager should read when access_level='2'")
def test_manager_read_access_user_ids(self):
"""Test Manager: Read when added to template user_ids"""
_, _, dependency = self._create_jet_template_dependency(
"Manager in Users",
"manager_in_users",
access_level="3",
user_ids=[(4, self.manager.id)],
)
records = self.JetTemplateDependency.with_user(self.manager).search(
[("id", "=", dependency.id)]
)
self.assertEqual(len(records), 1, "Manager should read when in user_ids")
def test_manager_read_access_manager_ids(self):
"""Test Manager: Read when added to template manager_ids"""
_, _, dependency = self._create_jet_template_dependency(
"Manager in Managers",
"manager_in_managers",
access_level="3",
manager_ids=[(4, self.manager.id)],
)
records = self.JetTemplateDependency.with_user(self.manager).search(
[("id", "=", dependency.id)]
)
self.assertEqual(len(records), 1, "Manager should read when in manager_ids")
def test_manager_read_no_access_root_level(self):
"""Test Manager: No read access for Root level (3) without user_ids"""
_, _, dependency = self._create_jet_template_dependency(
"Root Level Template", "root_level_template", access_level="3"
)
records = self.JetTemplateDependency.with_user(self.manager).search(
[("id", "=", dependency.id)]
)
self.assertEqual(len(records), 0, "Manager should not read access_level='3'")
# ======================
# Manager CRUD Access Tests
# ======================
def test_manager_create_access(self):
"""
Test Manager: Create when template access_level <= '2'
AND manager is in template.manager_ids
"""
# Create a template dependency with manager access using helper
try:
_, _, dependency = self._create_jet_template_dependency(
template_name="Create Manager Template",
template_reference="create_manager_template",
access_level="2",
manager_ids=[(4, self.manager.id)],
template_required=self.jet_template_tower_core,
state_required_id=self.state_running.id,
with_user=self.manager,
)
# Ensure dependency was created
records = self.JetTemplateDependency.search([("id", "=", dependency.id)])
self.assertIn(
dependency, records, "Manager should be able to create dependency"
)
except AccessError:
self.fail("Manager should be able to create template dependency")
def test_manager_create_forbidden_not_in_manager_ids(self):
"""Test Manager: Cannot create when not in template.manager_ids"""
self.assertRaises(
AccessError,
lambda: self.JetTemplateDependency.with_user(self.manager).create(
{
"template_id": self.jet_template_test.id,
"template_required_id": self.jet_template_tower_core.id,
"state_required_id": self.state_running.id,
}
),
)
def test_manager_write_access(self):
"""
Test Manager: Can write when template access_level <= '2'
AND manager is in template.manager_ids. Toggle state_required_id.
"""
# Create dependency with proper access
_, _, dependency = self._create_jet_template_dependency(
template_name="Write Manager Template",
template_reference="write_manager_template",
access_level="2",
manager_ids=[(4, self.manager.id)],
template_required=self.jet_template_tower_core,
state_required_id=self.state_running.id,
with_user=self.manager,
)
# Perform an actual write: change state_required_id
try:
dependency.invalidate_recordset()
dependency.with_user(self.manager).write(
{"state_required_id": self.state_stopped.id}
)
except AccessError:
self.fail("Manager should be able to write state_required_id")
def test_manager_unlink_access(self):
"""
Test Manager: Can unlink when template access_level <= '2'
AND manager is in template.manager_ids.
"""
# Create dependency with proper access
_, _, dependency = self._create_jet_template_dependency(
template_name="Unlink Manager Template",
template_reference="unlink_manager_template",
access_level="2",
manager_ids=[(4, self.manager.id)],
template_required=self.jet_template_tower_core,
state_required_id=self.state_running.id,
with_user=self.manager,
)
dependency.invalidate_recordset()
dependency = dependency.with_user(self.manager)
try:
dependency.unlink()
records = self.JetTemplateDependency.search([("id", "=", dependency.id)])
self.assertEqual(
len(records), 0, "Manager should be able to unlink dependency"
)
except AccessError:
self.fail("Manager should be able to unlink dependency")
# ======================
# Root Access Tests
# ======================
def test_root_full_access(self):
"""Root: Full CRUD access regardless of access restrictions"""
# Root can create
_, _, dependency = self._create_jet_template_dependency(
template_name="Root Template",
template_reference="root_template",
access_level="3",
template_required=self.jet_template_tower_core,
state_required_id=self.state_running.id,
with_user=self.root,
)
# Root can read
records = self.JetTemplateDependency.with_user(self.root).search(
[("id", "=", dependency.id)]
)
self.assertIn(dependency, records, "Root should be able to read")
# Root can write allowed field
dependency.invalidate_recordset()
dependency.with_user(self.root).write(
{"state_required_id": self.state_running.id}
)
# Root can delete
dependency.with_user(self.root).unlink()
records = self.JetTemplateDependency.with_user(self.root).search(
[("id", "=", dependency.id)]
)
self.assertEqual(len(records), 0, "Root should be able to delete dependency")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,387 @@
# Copyright (C) 2025 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.exceptions import AccessError
from .common_jets import TestTowerJetsCommon
class TestTowerJetTemplateInstallAccess(TestTowerJetsCommon):
"""
Test access rules for Jet Template Install model
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create additional server for testing
cls.server_test_2 = cls.Server.create(
{
"name": "Test Server 2",
"reference": "test_server_2",
"ip_v4_address": "192.168.1.102",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"os_id": cls.os_debian_10.id,
}
)
def _create_install_record(
self,
template=None,
server=None,
template_access_level="2",
template_user_ids=None,
template_manager_ids=None,
server_user_ids=None,
server_manager_ids=None,
):
"""Helper method to create a jet template install record"""
if not template:
template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"access_level": template_access_level,
"user_ids": template_user_ids
if template_user_ids is not None
else [(5, 0, 0)],
"manager_ids": template_manager_ids
if template_manager_ids is not None
else [(5, 0, 0)],
}
)
if not server:
server = self.server_test_1
# Update server access if needed
if server_user_ids is not None:
server.write({"user_ids": server_user_ids})
if server_manager_ids is not None:
server.write({"manager_ids": server_manager_ids})
# Create install record
install_record = self.JetTemplateInstall.create(
{
"jet_template_id": template.id,
"server_id": server.id,
}
)
return template, server, install_record
# ======================
# Manager Read Access Tests
# ======================
def test_manager_read_server_user_ids_template_access_level_manager(self):
"""Test Manager: Read when in server user_ids and template access_level <= 2"""
_, _, install_record = self._create_install_record(
template_access_level="2",
server_user_ids=[(4, self.manager.id)],
)
records = self.JetTemplateInstall.with_user(self.manager).search(
[("id", "=", install_record.id)]
)
self.assertEqual(
len(records),
1,
"Manager should read when in server user_ids"
" and template access_level <= 2",
)
def test_manager_read_server_manager_ids_template_access_level_manager(self):
"""
Test Manager: Read when in server manager_ids
and template access_level <= 2.
"""
_, _, install_record = self._create_install_record(
template_access_level="2",
server_manager_ids=[(4, self.manager.id)],
)
records = self.JetTemplateInstall.with_user(self.manager).search(
[("id", "=", install_record.id)]
)
self.assertEqual(
len(records),
1,
"Manager should read when in server manager_ids"
" and template access_level <= 2",
)
def test_manager_read_template_user_ids_override(self):
"""
Test Manager: Read when in template user_ids overrides access_level
(server user_ids or manager_ids).
"""
# Test with server user_ids
_, _, install_record1 = self._create_install_record(
template_access_level="3", # Root level - normally not accessible
template_user_ids=[(4, self.manager.id)],
server_user_ids=[(4, self.manager.id)],
)
records = self.JetTemplateInstall.with_user(self.manager).search(
[("id", "=", install_record1.id)]
)
self.assertEqual(
len(records),
1,
"Manager should read when in template user_ids" " and server user_ids",
)
# Test with server manager_ids
_, _, install_record2 = self._create_install_record(
template_access_level="3", # Root level - normally not accessible
template_user_ids=[(4, self.manager.id)],
server_manager_ids=[(4, self.manager.id)],
)
records = self.JetTemplateInstall.with_user(self.manager).search(
[("id", "=", install_record2.id)]
)
self.assertEqual(
len(records),
1,
"Manager should read when in template user_ids" " and server manager_ids",
)
def test_manager_read_no_access_no_server_access(self):
"""
Test Manager: No read access when not in
server user_ids or manager_ids.
"""
_, _, install_record = self._create_install_record(
template_access_level="1",
server_user_ids=[(5, 0, 0)],
server_manager_ids=[(5, 0, 0)],
)
records = self.JetTemplateInstall.with_user(self.manager).search(
[("id", "=", install_record.id)]
)
self.assertEqual(
len(records),
0,
"Manager should not read when not in server user_ids or manager_ids",
)
def test_manager_read_no_access_template_root_level(self):
"""
Test Manager: No read access when template access_level
is Root and not in template user_ids.
"""
_, _, install_record = self._create_install_record(
template_access_level="3", # Root level
template_user_ids=[(5, 0, 0)],
server_user_ids=[(4, self.manager.id)],
)
records = self.JetTemplateInstall.with_user(self.manager).search(
[("id", "=", install_record.id)]
)
self.assertEqual(
len(records),
0,
"Manager should not read when template access_level"
" is Root and not in template user_ids",
)
def test_manager_read_no_access_template_manager_level_no_server_access(self):
"""
Test Manager: No read access when template access_level
is Manager but not in server.
"""
_, _, install_record = self._create_install_record(
template_access_level="2",
server_user_ids=[(5, 0, 0)],
server_manager_ids=[(5, 0, 0)],
)
records = self.JetTemplateInstall.with_user(self.manager).search(
[("id", "=", install_record.id)]
)
self.assertEqual(
len(records),
0,
"Manager should not read when not in server"
" even if template access_level is Manager",
)
def test_manager_write_forbidden(self):
"""Test Manager: Cannot write/create/delete records"""
_, _, install_record = self._create_install_record(
template_access_level="2",
server_user_ids=[(4, self.manager.id)],
)
# Manager should not be able to write
with self.assertRaises(AccessError):
install_record.with_user(self.manager).write({"state": "done"})
# Manager should not be able to create
template = self.JetTemplate.create(
{
"name": "New Template",
"reference": "new_template",
"access_level": "2",
}
)
server = self.server_test_1
server.write({"user_ids": [(4, self.manager.id)]})
with self.assertRaises(AccessError):
self.JetTemplateInstall.with_user(self.manager).create(
{
"jet_template_id": template.id,
"server_id": server.id,
}
)
# Manager should not be able to delete
with self.assertRaises(AccessError):
install_record.with_user(self.manager).unlink()
# ======================
# Root Access Tests
# ======================
def test_root_write_access(self):
"""Test Root: Can write any record"""
_, _, install_record = self._create_install_record()
# Root should be able to write
try:
install_record.with_user(self.root).write({"state": "done"})
install_record.invalidate_recordset()
self.assertEqual(
install_record.state, "done", "Root should be able to update"
)
except AccessError:
self.fail("Root should be able to update any record")
def test_root_create_access(self):
"""Test Root: Can create any record"""
template = self.JetTemplate.with_user(self.root).create(
{
"name": "Root Template",
"reference": "root_template",
"access_level": "3",
}
)
server = self.server_test_1
# Root should be able to create
try:
install_record = self.JetTemplateInstall.with_user(self.root).create(
{
"jet_template_id": template.id,
"server_id": server.id,
}
)
records = self.JetTemplateInstall.with_user(self.root).search(
[("id", "=", install_record.id)]
)
self.assertEqual(len(records), 1, "Root should be able to create")
except AccessError:
self.fail("Root should be able to create any record")
def test_root_delete_access(self):
"""Test Root: Can delete any record"""
_, _, install_record = self._create_install_record()
# Root should be able to delete
try:
install_record.with_user(self.root).unlink()
records = self.JetTemplateInstall.with_user(self.root).search(
[("id", "=", install_record.id)]
)
self.assertEqual(len(records), 0, "Root should be able to delete")
except AccessError:
self.fail("Root should be able to delete any record")
def test_root_access_all_scenarios(self):
"""Test Root can access records in all scenarios"""
# Test various combinations
scenarios = [
{
"template_access_level": "1",
"server_user_ids": [(5, 0, 0)],
"server_manager_ids": [(5, 0, 0)],
},
{
"template_access_level": "2",
"server_user_ids": [(5, 0, 0)],
"server_manager_ids": [(5, 0, 0)],
},
{
"template_access_level": "3",
"server_user_ids": [(5, 0, 0)],
"server_manager_ids": [(5, 0, 0)],
},
]
for scenario in scenarios:
_, _, install_record = self._create_install_record(**scenario)
records = self.JetTemplateInstall.with_user(self.root).search(
[("id", "=", install_record.id)]
)
self.assertEqual(
len(records),
1,
f"Root should be able to read record with scenario: {scenario}",
)
# ======================
# Edge Cases
# ======================
def test_manager_read_multiple_servers(self):
"""Test Manager access across multiple servers"""
# Manager in server 1, template accessible
template1, _, install1 = self._create_install_record(
template_access_level="2",
server_user_ids=[(4, self.manager.id)],
)
# Manager not in server 2, same template
_, _, install2 = self._create_install_record(
template=template1,
server=self.server_test_2,
template_access_level="2",
server_user_ids=[(5, 0, 0)],
server_manager_ids=[(5, 0, 0)],
)
# Manager should only see install1
records = self.JetTemplateInstall.with_user(self.manager).search(
[("id", "in", [install1.id, install2.id])]
)
self.assertEqual(len(records), 1, "Manager should only see accessible install")
self.assertEqual(records[0].id, install1.id, "Manager should see install1")
def test_manager_read_multiple_templates(self):
"""Test Manager access with multiple templates"""
# Template 1: Manager level, Manager in server
_, _, install1 = self._create_install_record(
template_access_level="2",
server_user_ids=[(4, self.manager.id)],
)
# Template 2: Root level, Manager in server but template user_ids
_, _, install2 = self._create_install_record(
template_access_level="3",
template_user_ids=[(4, self.manager.id)],
server_user_ids=[(4, self.manager.id)],
)
# Manager should see both
records = self.JetTemplateInstall.with_user(self.manager).search(
[("id", "in", [install1.id, install2.id])]
)
self.assertEqual(len(records), 2, "Manager should see both installs")

View File

@@ -0,0 +1,492 @@
# Copyright (C) 2025 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.exceptions import AccessError
from .common_jets import TestTowerJetsCommon
class TestTowerJetTemplateInstallLineAccess(TestTowerJetsCommon):
"""
Test access rules for Jet Template Install Line model
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create additional server for testing
cls.server_test_2 = cls.Server.create(
{
"name": "Test Server 2",
"reference": "test_server_2",
"ip_v4_address": "192.168.1.102",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"os_id": cls.os_debian_10.id,
}
)
def _create_install_line_record(
self,
template=None,
line_template=None,
server=None,
line_template_access_level="2",
line_template_user_ids=None,
server_user_ids=None,
server_manager_ids=None,
):
"""
Helper method to create a jet template install line record
Note: Install Line access rules only check server_id and line template
(jet_template_id), not the parent install template. So we only need
to vary these parameters for testing.
Args:
template: Template for the install record (parent)
- defaults to simple template.
line_template: Template for the install line
server: Server for the install record
line_template_access_level: Access level for line template
line_template_user_ids: User IDs for line template
server_user_ids: User IDs for server
server_manager_ids: Manager IDs for server
"""
if not template:
template = self.JetTemplate.create(
{
"name": "Test Template",
"access_level": "2", # Default, doesn't affect Install Line access
}
)
if not line_template:
line_template = self.JetTemplate.create(
{
"name": "Test Line Template",
"reference": "test_line_template",
"access_level": line_template_access_level,
"user_ids": line_template_user_ids
if line_template_user_ids is not None
else [(5, 0, 0)],
"manager_ids": [(5, 0, 0)],
}
)
if not server:
server = self.server_test_1
# Update server access if needed
if server_user_ids is not None:
server.write({"user_ids": server_user_ids})
if server_manager_ids is not None:
server.write({"manager_ids": server_manager_ids})
# Create install record
install_record = self.JetTemplateInstall.create(
{
"jet_template_id": template.id,
"server_id": server.id,
}
)
# Create install line record
install_line_record = self.JetTemplateInstallLine.create(
{
"jet_template_install_id": install_record.id,
"jet_template_id": line_template.id,
"order": 10,
}
)
return template, line_template, server, install_record, install_line_record
# ======================
# Manager Read Access Tests
# ======================
def test_manager_read_server_user_ids_line_template_access_level_manager(self):
"""
Test Manager: Read when in server user_ids
and line template access_level <= 2.
"""
_, _, _, _, install_line_record = self._create_install_line_record(
line_template_access_level="2",
server_user_ids=[(4, self.manager.id)],
)
records = self.JetTemplateInstallLine.with_user(self.manager).search(
[("id", "=", install_line_record.id)]
)
self.assertEqual(
len(records),
1,
"Manager should read when in server user_ids "
"and line template access_level <= 2.",
)
def test_manager_read_server_manager_ids_line_template_access_level_manager(self):
"""
Test Manager: Read when in server manager_ids
and line template access_level <= 2.
"""
_, _, _, _, install_line_record = self._create_install_line_record(
line_template_access_level="2",
server_manager_ids=[(4, self.manager.id)],
)
records = self.JetTemplateInstallLine.with_user(self.manager).search(
[("id", "=", install_line_record.id)]
)
self.assertEqual(
len(records),
1,
"Manager should read when in server manager_ids"
" and line template access_level <= 2",
)
def test_manager_read_line_template_user_ids_override(self):
"""
Test Manager: Read when in line template user_ids overrides access_level
(server user_ids or manager_ids).
"""
# Test with server user_ids
_, _, _, _, install_line_record1 = self._create_install_line_record(
line_template_access_level="3", # Root level - normally not accessible
line_template_user_ids=[(4, self.manager.id)],
server_user_ids=[(4, self.manager.id)],
)
records = self.JetTemplateInstallLine.with_user(self.manager).search(
[("id", "=", install_line_record1.id)]
)
self.assertEqual(
len(records),
1,
"Manager should read when in line template user_ids" " and server user_ids",
)
# Test with server manager_ids
_, _, _, _, install_line_record2 = self._create_install_line_record(
line_template_access_level="3", # Root level - normally not accessible
line_template_user_ids=[(4, self.manager.id)],
server_manager_ids=[(4, self.manager.id)],
)
records = self.JetTemplateInstallLine.with_user(self.manager).search(
[("id", "=", install_line_record2.id)]
)
self.assertEqual(
len(records),
1,
"Manager should read when in line template user_ids"
" and server manager_ids",
)
def test_manager_read_no_access_no_server_access(self):
"""
Test Manager: No read access when not in server
user_ids and manager_ids.
"""
_, _, _, _, install_line_record = self._create_install_line_record(
line_template_access_level="1",
server_user_ids=[(5, 0, 0)],
server_manager_ids=[(5, 0, 0)],
)
records = self.JetTemplateInstallLine.with_user(self.manager).search(
[("id", "=", install_line_record.id)]
)
self.assertEqual(
len(records),
0,
"Manager should not read when not in server user_ids or manager_ids",
)
def test_manager_read_no_access_line_template_root_level(self):
"""
Test Manager: No read access when line template
access_level is Root and not in line template user_ids.
"""
_, _, _, _, install_line_record = self._create_install_line_record(
line_template_access_level="3", # Root level
line_template_user_ids=[(5, 0, 0)],
server_user_ids=[(4, self.manager.id)],
)
records = self.JetTemplateInstallLine.with_user(self.manager).search(
[("id", "=", install_line_record.id)]
)
self.assertEqual(
len(records),
0,
"Manager should not read when line template access_level"
" is Root and not in line template user_ids",
)
def test_manager_read_no_access_line_template_manager_level_no_server_access(self):
"""
Test Manager: No read access when line template access_level
is Manager but not in server.
"""
_, _, _, _, install_line_record = self._create_install_line_record(
line_template_access_level="2",
server_user_ids=[(5, 0, 0)],
server_manager_ids=[(5, 0, 0)],
)
records = self.JetTemplateInstallLine.with_user(self.manager).search(
[("id", "=", install_line_record.id)]
)
self.assertEqual(
len(records),
0,
"Manager should not read when not in server"
" even if line template access_level is Manager",
)
def test_manager_write_forbidden(self):
"""Test Manager: Cannot write/create/delete records"""
_, _, _, _, install_line_record = self._create_install_line_record(
line_template_access_level="2",
server_user_ids=[(4, self.manager.id)],
)
# Manager should not be able to write
with self.assertRaises(AccessError):
install_line_record.with_user(self.manager).write({"state": "done"})
# Manager should not be able to create
template = self.JetTemplate.create(
{
"name": "New Template",
"reference": "new_template",
"access_level": "2",
}
)
line_template = self.JetTemplate.create(
{
"name": "New Line Template",
"reference": "new_line_template",
"access_level": "2",
}
)
server = self.server_test_1
server.write({"user_ids": [(4, self.manager.id)]})
install_record = self.JetTemplateInstall.create(
{
"jet_template_id": template.id,
"server_id": server.id,
}
)
with self.assertRaises(AccessError):
self.JetTemplateInstallLine.with_user(self.manager).create(
{
"jet_template_install_id": install_record.id,
"jet_template_id": line_template.id,
"order": 10,
}
)
# Manager should not be able to delete
with self.assertRaises(AccessError):
install_line_record.with_user(self.manager).unlink()
# ======================
# Root Access Tests
# ======================
def test_root_write_access(self):
"""Test Root: Can write any record"""
_, _, _, _, install_line_record = self._create_install_line_record()
# Root should be able to write
try:
install_line_record.with_user(self.root).write({"state": "done"})
install_line_record.invalidate_recordset()
self.assertEqual(
install_line_record.state, "done", "Root should be able to update"
)
except AccessError:
self.fail("Root should be able to update any record")
def test_root_create_access(self):
"""Test Root: Can create any record"""
template = self.JetTemplate.with_user(self.root).create(
{
"name": "Root Template",
"reference": "root_template",
"access_level": "3",
}
)
line_template = self.JetTemplate.with_user(self.root).create(
{
"name": "Root Line Template",
"reference": "root_line_template",
"access_level": "3",
}
)
server = self.server_test_1
install_record = self.JetTemplateInstall.create(
{
"jet_template_id": template.id,
"server_id": server.id,
}
)
# Root should be able to create
try:
install_line_record = self.JetTemplateInstallLine.with_user(
self.root
).create(
{
"jet_template_install_id": install_record.id,
"jet_template_id": line_template.id,
"order": 10,
}
)
records = self.JetTemplateInstallLine.with_user(self.root).search(
[("id", "=", install_line_record.id)]
)
self.assertEqual(len(records), 1, "Root should be able to create")
except AccessError:
self.fail("Root should be able to create any record")
def test_root_delete_access(self):
"""Test Root: Can delete any record"""
_, _, _, _, install_line_record = self._create_install_line_record()
# Root should be able to delete
try:
install_line_record.with_user(self.root).unlink()
records = self.JetTemplateInstallLine.with_user(self.root).search(
[("id", "=", install_line_record.id)]
)
self.assertEqual(len(records), 0, "Root should be able to delete")
except AccessError:
self.fail("Root should be able to delete any record")
def test_root_access_all_scenarios(self):
"""Test Root can access records in all scenarios"""
# Test various combinations
scenarios = [
{
"line_template_access_level": "1",
"server_user_ids": [(5, 0, 0)],
"server_manager_ids": [(5, 0, 0)],
},
{
"line_template_access_level": "2",
"server_user_ids": [(5, 0, 0)],
"server_manager_ids": [(5, 0, 0)],
},
{
"line_template_access_level": "3",
"server_user_ids": [(5, 0, 0)],
"server_manager_ids": [(5, 0, 0)],
},
]
for scenario in scenarios:
_, _, _, _, install_line_record = self._create_install_line_record(
**scenario
)
records = self.JetTemplateInstallLine.with_user(self.root).search(
[("id", "=", install_line_record.id)]
)
self.assertEqual(
len(records),
1,
f"Root should be able to read record with scenario: {scenario}",
)
# ======================
# Edge Cases
# ======================
def test_manager_read_multiple_servers(self):
"""Test Manager access across multiple servers"""
# Manager in server 1, line template accessible
_, line_template1, _, _, install_line1 = self._create_install_line_record(
line_template_access_level="2",
server_user_ids=[(4, self.manager.id)],
)
# Manager not in server 2, same line template
_, _, _, _, install_line2 = self._create_install_line_record(
line_template=line_template1,
server=self.server_test_2,
line_template_access_level="2",
server_user_ids=[(5, 0, 0)],
server_manager_ids=[(5, 0, 0)],
)
# Manager should only see install_line1
records = self.JetTemplateInstallLine.with_user(self.manager).search(
[("id", "in", [install_line1.id, install_line2.id])]
)
self.assertEqual(
len(records), 1, "Manager should only see accessible install line"
)
self.assertEqual(
records[0].id, install_line1.id, "Manager should see install_line1"
)
def test_manager_read_multiple_line_templates(self):
"""Test Manager access with multiple line templates"""
# Line Template 1: Manager level, Manager in server
_, _, _, _, install_line1 = self._create_install_line_record(
line_template_access_level="2",
server_user_ids=[(4, self.manager.id)],
)
# Line Template 2: Root level, Manager in server but line template user_ids
_, _, _, _, install_line2 = self._create_install_line_record(
line_template_access_level="3",
line_template_user_ids=[(4, self.manager.id)],
server_user_ids=[(4, self.manager.id)],
)
# Manager should see both
records = self.JetTemplateInstallLine.with_user(self.manager).search(
[("id", "in", [install_line1.id, install_line2.id])]
)
self.assertEqual(len(records), 2, "Manager should see both install lines")
def test_manager_read_parent_template_does_not_affect_access(self):
"""
Test Manager: Parent install template access level
does not affect Install Line access.
"""
# Verify that Install Line access only depends on server_id and line template,
# not the parent install template.
# Create a line with Root-level parent template,
# but accessible line template - should still be accessible.
_, _, _, _, install_line_record = self._create_install_line_record(
template=self.JetTemplate.create(
{
"name": "Root Parent Template",
"reference": "root_parent_template",
"access_level": "3",
}
),
line_template_access_level="2", # Manager level - accessible
server_user_ids=[(4, self.manager.id)],
)
records = self.JetTemplateInstallLine.with_user(self.manager).search(
[("id", "=", install_line_record.id)]
)
self.assertEqual(
len(records),
1,
"Manager should read Install Line when line template "
"and server are accessible, "
"regardless of parent install template access level",
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,970 @@
# Copyright (C) 2025 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.exceptions import AccessError
from .common_jets import TestTowerJetsCommon
class TestTowerJetWaypointAccess(TestTowerJetsCommon):
"""
Test access rules for Jet Waypoint model
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Use existing users from common.py (cls.user, cls.manager, cls.root)
# Create additional manager for multi-manager tests
cls.manager2 = cls.Users.create(
{
"name": "Test Manager 2",
"login": "test_manager_2",
"email": "test_manager_2@example.com",
"groups_id": [(6, 0, [cls.group_manager.id])],
}
)
# ======================
# Manager Read Access Tests
# ======================
def test_manager_read_access_jet_user_ids(self):
"""Test Manager: Read when user is added in jet's user_ids"""
# Use existing jet and add manager to user_ids
self.jet_test.write({"user_ids": [(4, self.manager.id)]})
jet = self.jet_test
record = self.JetWaypoint.create(
{
"name": "Waypoint with User Access",
"reference": "waypoint_user_access",
"jet_id": jet.id,
"waypoint_template_id": self.waypoint_template.id,
}
)
records = self.JetWaypoint.with_user(self.manager).search(
[("id", "=", record.id)]
)
self.assertEqual(
len(records),
1,
"Manager should be able to read when added to jet's user_ids",
)
def test_manager_read_access_jet_manager_ids(self):
"""Test Manager: Read when user is added in jet's manager_ids"""
# Use existing jet and add manager to manager_ids
self.jet_test.write({"manager_ids": [(4, self.manager.id)]})
jet = self.jet_test
record = self.JetWaypoint.create(
{
"name": "Waypoint with Manager Access",
"reference": "waypoint_manager_access",
"jet_id": jet.id,
"waypoint_template_id": self.waypoint_template.id,
}
)
records = self.JetWaypoint.with_user(self.manager).search(
[("id", "=", record.id)]
)
self.assertEqual(
len(records),
1,
"Manager should be able to read when added to jet's manager_ids",
)
def test_manager_read_no_access_root_level(self):
"""Test Manager: No read access for Root level (3) even with jet access"""
# Use existing jet and add manager to manager_ids (has jet access)
self.jet_test.write({"manager_ids": [(4, self.manager.id)]})
jet = self.jet_test
# Create waypoint template with Root level
waypoint_template_root = self.JetWaypointTemplate.create(
{
"name": "Root Level Template",
"reference": "root_level_template",
"jet_template_id": self.jet_template_test.id,
"access_level": "3", # Root level
}
)
record = self.JetWaypoint.create(
{
"name": "Root Level Waypoint",
"reference": "root_level_waypoint",
"jet_id": jet.id,
"waypoint_template_id": waypoint_template_root.id,
"access_level": "3", # Explicitly set Root level
}
)
records = self.JetWaypoint.with_user(self.manager).search(
[("id", "=", record.id)]
)
self.assertEqual(
len(records),
0,
"Manager should not read access_level='3' "
"even when in jet's manager_ids (Root level blocks access)",
)
def test_manager_read_no_access_not_in_jet(self):
"""Test Manager: No read access when not in jet's Users or Managers"""
# Use existing jet (manager not in user_ids/manager_ids)
jet = self.jet_test
record = self.JetWaypoint.create(
{
"name": "No Access Waypoint",
"reference": "no_access_waypoint",
"jet_id": jet.id,
"waypoint_template_id": self.waypoint_template.id,
}
)
records = self.JetWaypoint.with_user(self.manager).search(
[("id", "=", record.id)]
)
self.assertEqual(
len(records),
0,
"Manager should not read when not in jet's user_ids or manager_ids",
)
# ======================
# Manager Write/Create Access Tests
# ======================
def test_manager_write_access_level_and_template_manager_ids(self):
"""Test Manager: Write when access_level <= 2 AND in template's manager_ids"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
# Create jet from this template with unique name
jet = self._create_jet(
name="Write Access Jet",
reference="write_access_jet",
template=jet_template,
server=self.server_test_1,
)
# Create waypoint template
waypoint_template = self.JetWaypointTemplate.create(
{
"name": "Test Waypoint Template",
"reference": "test_waypoint_template",
"jet_template_id": jet_template.id,
}
)
record = self.JetWaypoint.create(
{
"name": "Manager Can Write",
"reference": "manager_can_write",
"jet_id": jet.id,
"waypoint_template_id": waypoint_template.id,
}
)
# Manager should be able to write
try:
record.with_user(self.manager).write({"name": "Updated Name"})
record.invalidate_recordset()
self.assertEqual(
record.name, "Updated Name", "Manager should be able to update"
)
except AccessError:
self.fail("Manager should be able to update when in template's manager_ids")
def test_manager_write_forbidden_not_in_template_manager_ids(self):
"""Test Manager: No write when not in template's manager_ids"""
# Create jet template without manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": False,
}
)
# Create jet with manager in manager_ids (for read access)
jet = self._create_jet(
name="No Write Jet",
reference="no_write_jet",
template=jet_template,
server=self.server_test_1,
manager_ids=[(4, self.manager.id)],
)
# Create waypoint template
waypoint_template = self.JetWaypointTemplate.create(
{
"name": "Test Waypoint Template",
"reference": "test_waypoint_template",
"jet_template_id": jet_template.id,
}
)
record = self.JetWaypoint.create(
{
"name": "No Write Access",
"reference": "no_write_access",
"jet_id": jet.id,
"waypoint_template_id": waypoint_template.id,
}
)
with self.assertRaises(AccessError):
record.with_user(self.manager).write({"name": "Should Fail"})
def test_manager_write_forbidden_root_level(self):
"""Test Manager: No write when access_level is Root (3)"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
# Create jet from this template with unique name
jet = self._create_jet(
name="Write Access Jet",
reference="write_access_jet",
template=jet_template,
server=self.server_test_1,
)
# Create waypoint template with Root level
waypoint_template_root = self.JetWaypointTemplate.create(
{
"name": "Root Level Template",
"reference": "root_level_template",
"jet_template_id": jet_template.id,
"access_level": "3", # Root level
}
)
record = self.JetWaypoint.create(
{
"name": "Root Level No Write",
"reference": "root_level_no_write",
"jet_id": jet.id,
"waypoint_template_id": waypoint_template_root.id,
"access_level": "3", # Explicitly set Root level
}
)
with self.assertRaises(AccessError):
record.with_user(self.manager).write({"name": "Should Fail"})
def test_manager_create_access(self):
"""Test Manager: Create when access_level <= 2 AND in template's manager_ids"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
# Create jet from this template with unique name
jet = self._create_jet(
name="Write Access Jet",
reference="write_access_jet",
template=jet_template,
server=self.server_test_1,
)
# Create waypoint template
waypoint_template = self.JetWaypointTemplate.create(
{
"name": "Test Waypoint Template",
"reference": "test_waypoint_template",
"jet_template_id": jet_template.id,
}
)
# Try to create without being in template's manager_ids - should fail
jet_template_no_access = self.JetTemplate.create(
{
"name": "No Access Template",
"reference": "no_access_template",
"manager_ids": False,
}
)
jet_no_access = self._create_jet(
name="No Access Jet",
reference="no_access_jet",
template=jet_template_no_access,
server=self.server_test_1,
manager_ids=[(4, self.manager.id)], # Manager in jet but not template
)
waypoint_template_no_access = self.JetWaypointTemplate.create(
{
"name": "No Access Waypoint Template",
"reference": "no_access_waypoint_template",
"jet_template_id": jet_template_no_access.id,
}
)
with self.assertRaises(AccessError):
self.JetWaypoint.with_user(self.manager).create(
{
"name": "Create Fail",
"reference": "create_fail",
"jet_id": jet_no_access.id,
"waypoint_template_id": waypoint_template_no_access.id,
}
)
# Create with manager in template's manager_ids - should succeed
try:
record = self.JetWaypoint.with_user(self.manager).create(
{
"name": "Create Success",
"reference": "create_success",
"jet_id": jet.id,
"waypoint_template_id": waypoint_template.id,
}
)
records = self.JetWaypoint.search([("id", "=", record.id)])
self.assertEqual(len(records), 1, "Manager should be able to create")
except AccessError:
self.fail("Manager should be able to create when in template's manager_ids")
# ======================
# Manager Delete Access Tests
# ======================
def test_manager_delete_own_record(self):
"""Test Manager: Delete own record when in template's manager_ids"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
# Create jet from this template with unique name
jet = self._create_jet(
name="Write Access Jet",
reference="write_access_jet",
template=jet_template,
server=self.server_test_1,
)
# Create waypoint template
waypoint_template = self.JetWaypointTemplate.create(
{
"name": "Test Waypoint Template",
"reference": "test_waypoint_template",
"jet_template_id": jet_template.id,
}
)
record = self.JetWaypoint.with_user(self.manager).create(
{
"name": "My Record",
"reference": "my_record",
"jet_id": jet.id,
"waypoint_template_id": waypoint_template.id,
}
)
try:
record.with_user(self.manager).unlink()
records = self.JetWaypoint.search([("id", "=", record.id)])
self.assertEqual(
len(records), 0, "Manager should be able to delete own record"
)
except AccessError:
self.fail("Manager should be able to delete own record")
def test_manager_delete_not_creator(self):
"""Test Manager: Cannot delete record created by another user"""
# Create jet template with both managers in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id), (4, self.manager2.id)],
}
)
# Create jet from this template with unique name
jet = self._create_jet(
name="Write Access Jet",
reference="write_access_jet",
template=jet_template,
server=self.server_test_1,
)
# Create waypoint template
waypoint_template = self.JetWaypointTemplate.create(
{
"name": "Test Waypoint Template",
"reference": "test_waypoint_template",
"jet_template_id": jet_template.id,
}
)
record = self.JetWaypoint.with_user(self.manager2).create(
{
"name": "Other's Record",
"reference": "others_record",
"jet_id": jet.id,
"waypoint_template_id": waypoint_template.id,
}
)
# Manager1 cannot delete Manager2's record
with self.assertRaises(AccessError):
record.with_user(self.manager).unlink()
def test_manager_delete_not_in_template_manager_ids(self):
"""Test Manager: Cannot delete when not in template's manager_ids"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
# Create jet from this template with unique name
jet = self._create_jet(
name="Delete Not In Template Jet",
reference="delete_not_in_template_jet",
template=jet_template,
server=self.server_test_1,
)
# Create waypoint template
waypoint_template = self.JetWaypointTemplate.create(
{
"name": "Test Waypoint Template",
"reference": "test_waypoint_template",
"jet_template_id": jet_template.id,
}
)
record = self.JetWaypoint.with_user(self.manager).create(
{
"name": "Removed Manager",
"reference": "removed_manager",
"jet_id": jet.id,
"waypoint_template_id": waypoint_template.id,
}
)
# Remove from template's manager_ids
jet_template.write({"manager_ids": False})
# Cannot delete anymore
with self.assertRaises(AccessError):
record.with_user(self.manager).unlink()
def test_manager_delete_root_level(self):
"""Test Manager: Cannot delete Root level record"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
# Create jet from this template with unique name
jet = self._create_jet(
name="Write Access Jet",
reference="write_access_jet",
template=jet_template,
server=self.server_test_1,
)
# Create waypoint template with Root level
waypoint_template_root = self.JetWaypointTemplate.create(
{
"name": "Root Level Template",
"reference": "root_level_template",
"jet_template_id": jet_template.id,
"access_level": "3", # Root level
}
)
# Create record with Root level as root (default user)
record = self.JetWaypoint.create(
{
"name": "Root Level Delete",
"reference": "root_level_delete",
"jet_id": jet.id,
"waypoint_template_id": waypoint_template_root.id,
}
)
with self.assertRaises(AccessError):
record.with_user(self.manager).unlink()
# ======================
# Root Access Tests
# ======================
def test_root_full_access(self):
"""
Test Root: Full CRUD access regardless of access_level or creator.
Root has unrestricted access to all records via security rule
[(1, '=', 1)], so we test:
- Create records with all access levels
- Read records with all access levels
- Write to records with all access levels
- Delete records regardless of creator
"""
# Create jet template for testing
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
}
)
# Create jet from this template with unique name
jet = self._create_jet(
name="Write Access Jet",
reference="write_access_jet",
template=jet_template,
server=self.server_test_1,
)
# Test CRUD operations for all access levels (only Manager and Root exist)
for access_level in ["2", "3"]:
# Create waypoint template with specific access level
waypoint_template = self.JetWaypointTemplate.create(
{
"name": f"Template Level {access_level}",
"reference": f"template_level_{access_level}",
"jet_template_id": jet_template.id,
"access_level": access_level,
}
)
# Root can create any level
record = self.JetWaypoint.with_user(self.root).create(
{
"name": f"Root Level {access_level}",
"reference": f"root_level_{access_level}",
"jet_id": jet.id,
"waypoint_template_id": waypoint_template.id,
}
)
# Root can read any level
records = self.JetWaypoint.with_user(self.root).search(
[("id", "=", record.id)]
)
self.assertEqual(
len(records),
1,
f"Root should be able to read access_level={access_level}",
)
# Root can write any level
record.with_user(self.root).write(
{"name": f"Root Updated Level {access_level}"}
)
record.invalidate_recordset()
self.assertEqual(
record.name,
f"Root Updated Level {access_level}",
f"Root should be able to update access_level={access_level}",
)
# Test Root can delete records created by other users
# Add manager to template's manager_ids so they can create the record
jet_template.write({"manager_ids": [(4, self.manager.id)]})
waypoint_template = self.JetWaypointTemplate.create(
{
"name": "Manager Template",
"reference": "manager_template",
"jet_template_id": jet_template.id,
}
)
manager_record = self.JetWaypoint.with_user(self.manager).create(
{
"name": "Manager's Record",
"reference": "managers_record",
"jet_id": jet.id,
"waypoint_template_id": waypoint_template.id,
}
)
manager_record.with_user(self.root).unlink()
records = self.JetWaypoint.with_user(self.root).search(
[("id", "=", manager_record.id)]
)
self.assertEqual(
len(records),
0,
"Root should be able to delete records from any creator",
)
# ======================
# Edge Cases
# ======================
def test_access_level_changes_visibility(self):
"""Test that changing access_level affects visibility"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
# Create jet with manager in manager_ids with unique name
jet = self._create_jet(
name="Access Level Changes Jet",
reference="access_level_changes_jet",
template=jet_template,
server=self.server_test_1,
manager_ids=[(4, self.manager.id)],
)
# Create waypoint template with Manager level
waypoint_template = self.JetWaypointTemplate.create(
{
"name": "Test Waypoint Template",
"reference": "test_waypoint_template",
"jet_template_id": jet_template.id,
"access_level": "2",
}
)
record = self.JetWaypoint.create(
{
"name": "Changing Level",
"reference": "changing_level",
"jet_id": jet.id,
"waypoint_template_id": waypoint_template.id,
}
)
# Manager can read
records = self.JetWaypoint.with_user(self.manager).search(
[("id", "=", record.id)]
)
self.assertEqual(len(records), 1, "Manager should read level 2")
# Change template to Root level
waypoint_template.write({"access_level": "3"})
# Update waypoint's access_level since it's stored and doesn't auto-update
record.write({"access_level": "3"})
record.invalidate_recordset()
# Manager cannot read anymore
records = self.JetWaypoint.with_user(self.manager).search(
[("id", "=", record.id)]
)
self.assertEqual(len(records), 0, "Manager should not read level 3")
def test_manager_prepare_forbidden_no_write_access(self):
"""Test Manager: Cannot prepare waypoint without write access"""
# Create jet template without manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": False,
}
)
# Create jet with manager in manager_ids (for read access)
jet = self._create_jet(
name="Prepare Forbidden Jet",
reference="prepare_forbidden_jet",
template=jet_template,
server=self.server_test_1,
manager_ids=[(4, self.manager.id)],
)
# Create waypoint template
waypoint_template = self.JetWaypointTemplate.create(
{
"name": "Test Waypoint Template",
"reference": "test_waypoint_template",
"jet_template_id": jet_template.id,
}
)
record = self.JetWaypoint.create(
{
"name": "Prepare Forbidden",
"reference": "prepare_forbidden",
"jet_id": jet.id,
"waypoint_template_id": waypoint_template.id,
"state": "draft",
}
)
# Manager should not be able to prepare without write access
with self.assertRaises(AccessError):
record.with_user(self.manager).prepare()
def test_manager_prepare_forbidden_root_level(self):
"""Test Manager: Cannot prepare waypoint with Root level"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
# Create jet from this template
jet = self._create_jet(
name="Prepare Root Level Jet",
reference="prepare_root_level_jet",
template=jet_template,
server=self.server_test_1,
)
# Create waypoint template with Root level
waypoint_template_root = self.JetWaypointTemplate.create(
{
"name": "Root Level Template",
"reference": "root_level_template",
"jet_template_id": jet_template.id,
"access_level": "3", # Root level
}
)
record = self.JetWaypoint.create(
{
"name": "Root Level Prepare",
"reference": "root_level_prepare",
"jet_id": jet.id,
"waypoint_template_id": waypoint_template_root.id,
"access_level": "3", # Explicitly set Root level
"state": "draft",
}
)
# Manager should not be able to prepare Root level waypoint
with self.assertRaises(AccessError):
record.with_user(self.manager).prepare()
def test_manager_fly_to_forbidden_no_write_access(self):
"""Test Manager: Cannot fly_to waypoint without write access"""
# Create jet template without manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": False,
}
)
# Create jet with manager in manager_ids (for read access)
jet = self._create_jet(
name="Fly To Forbidden Jet",
reference="fly_to_forbidden_jet",
template=jet_template,
server=self.server_test_1,
manager_ids=[(4, self.manager.id)],
)
# Create waypoint template
waypoint_template = self.JetWaypointTemplate.create(
{
"name": "Test Waypoint Template",
"reference": "test_waypoint_template",
"jet_template_id": jet_template.id,
}
)
record = self.JetWaypoint.create(
{
"name": "Fly To Forbidden",
"reference": "fly_to_forbidden",
"jet_id": jet.id,
"waypoint_template_id": waypoint_template.id,
"state": "ready",
}
)
# Manager should not be able to fly_to without write access
with self.assertRaises(AccessError):
record.with_user(self.manager).fly_to()
def test_manager_fly_to_forbidden_root_level(self):
"""Test Manager: Cannot fly_to waypoint with Root level"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
# Create jet from this template
jet = self._create_jet(
name="Fly To Root Level Jet",
reference="fly_to_root_level_jet",
template=jet_template,
server=self.server_test_1,
)
# Create waypoint template with Root level
waypoint_template_root = self.JetWaypointTemplate.create(
{
"name": "Root Level Template",
"reference": "root_level_template",
"jet_template_id": jet_template.id,
"access_level": "3", # Root level
}
)
record = self.JetWaypoint.create(
{
"name": "Root Level Fly To",
"reference": "root_level_fly_to",
"jet_id": jet.id,
"waypoint_template_id": waypoint_template_root.id,
"access_level": "3", # Explicitly set Root level
"state": "ready",
}
)
# Manager should not be able to fly_to Root level waypoint
with self.assertRaises(AccessError):
record.with_user(self.manager).fly_to()
def test_manager_prepare_success_with_write_access(self):
"""Test Manager: Can prepare waypoint with write access"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
# Ensure manager has server access
self.server_test_1.write({"user_ids": [(4, self.manager.id)]})
# Create jet from this template with manager in manager_ids
jet = self._create_jet(
name="Prepare Success Jet",
reference="prepare_success_jet",
template=jet_template,
server=self.server_test_1,
manager_ids=[(4, self.manager.id)],
)
# Create waypoint template
waypoint_template = self.JetWaypointTemplate.create(
{
"name": "Test Waypoint Template",
"reference": "test_waypoint_template",
"jet_template_id": jet_template.id,
}
)
record = self.JetWaypoint.create(
{
"name": "Prepare Success",
"reference": "prepare_success",
"jet_id": jet.id,
"waypoint_template_id": waypoint_template.id,
"state": "draft",
}
)
# Manager should be able to prepare with write access
try:
result = record.with_user(self.manager).prepare()
self.assertTrue(result, "Manager should be able to prepare")
record.invalidate_recordset()
# State should be ready (no plan_create_id)
self.assertEqual(record.state, "ready", "State should be ready")
except AccessError:
self.fail(
"Manager should be able to prepare when in template's manager_ids"
)
def test_manager_fly_to_success_with_write_access(self):
"""Test Manager: Can fly_to waypoint with write access"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
# Ensure manager has server access
self.server_test_1.write({"user_ids": [(4, self.manager.id)]})
# Create jet from this template with manager in manager_ids
jet = self._create_jet(
name="Fly To Success Jet",
reference="fly_to_success_jet",
template=jet_template,
server=self.server_test_1,
manager_ids=[(4, self.manager.id)],
)
# Create waypoint template
waypoint_template = self.JetWaypointTemplate.create(
{
"name": "Test Waypoint Template",
"reference": "test_waypoint_template",
"jet_template_id": jet_template.id,
}
)
record = self.JetWaypoint.create(
{
"name": "Fly To Success",
"reference": "fly_to_success",
"jet_id": jet.id,
"waypoint_template_id": waypoint_template.id,
"state": "ready",
}
)
# Manager should be able to fly_to with write access
try:
result = record.with_user(self.manager).fly_to()
self.assertTrue(result, "Manager should be able to fly_to")
record.invalidate_recordset()
# State should be current (no previous waypoint, no plan_arrive_id)
self.assertEqual(record.state, "current", "State should be current")
except AccessError:
self.fail("Manager should be able to fly_to when in template's manager_ids")

View File

@@ -0,0 +1,504 @@
# Copyright (C) 2025 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.exceptions import AccessError
from .common_jets import TestTowerJetsCommon
class TestTowerJetWaypointTemplateAccess(TestTowerJetsCommon):
"""
Test access rules for Jet Waypoint Template model
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Use existing users from common.py (cls.user, cls.manager, cls.root)
# Create additional manager for multi-manager tests
cls.manager2 = cls.Users.create(
{
"name": "Test Manager 2",
"login": "test_manager_2",
"email": "test_manager_2@example.com",
"groups_id": [(6, 0, [cls.group_manager.id])],
}
)
# ======================
# Manager Read Access Tests
# ======================
def test_manager_read_access_user_ids(self):
"""Test Manager: Read when user is added in template's user_ids"""
# Create jet template with manager in user_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"user_ids": [(4, self.manager.id)],
}
)
record = self.JetWaypointTemplate.create(
{
"name": "Waypoint with User Access",
"reference": "waypoint_user_access",
"jet_template_id": jet_template.id,
"access_level": "2", # Manager level
}
)
records = self.JetWaypointTemplate.with_user(self.manager).search(
[("id", "=", record.id)]
)
self.assertEqual(
len(records),
1,
"Manager should be able to read when added to template's user_ids",
)
def test_manager_read_access_manager_ids(self):
"""Test Manager: Read when user is added in template's manager_ids"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
record = self.JetWaypointTemplate.create(
{
"name": "Waypoint with Manager Access",
"reference": "waypoint_manager_access",
"jet_template_id": jet_template.id,
"access_level": "2", # Manager level
}
)
records = self.JetWaypointTemplate.with_user(self.manager).search(
[("id", "=", record.id)]
)
self.assertEqual(
len(records),
1,
"Manager should be able to read when added to template's manager_ids",
)
def test_manager_read_no_access_root_level(self):
"""
Test Manager: No read access for Root level (3)
without user_ids/manager_ids
"""
# Create jet template without manager access
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"user_ids": False,
"manager_ids": False,
}
)
record = self.JetWaypointTemplate.create(
{
"name": "Root Level Waypoint",
"reference": "root_level_waypoint",
"jet_template_id": jet_template.id,
"access_level": "3", # Root level
}
)
records = self.JetWaypointTemplate.with_user(self.manager).search(
[("id", "=", record.id)]
)
self.assertEqual(
len(records),
0,
"Manager should not read access_level='3' "
"when not in template's user_ids or manager_ids",
)
def test_manager_read_no_access_not_in_template(self):
"""Test Manager: No read access when not in template's Users or Managers"""
# Create jet template without manager access
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"user_ids": False,
"manager_ids": False,
}
)
record = self.JetWaypointTemplate.create(
{
"name": "No Access Waypoint",
"reference": "no_access_waypoint",
"jet_template_id": jet_template.id,
"access_level": "2", # Manager level
}
)
records = self.JetWaypointTemplate.with_user(self.manager).search(
[("id", "=", record.id)]
)
self.assertEqual(
len(records),
0,
"Manager should not read when not in template's user_ids or manager_ids",
)
# ======================
# Manager Write/Create Access Tests
# ======================
def test_manager_write_access_level_and_manager_ids(self):
"""Test Manager: Write when access_level <= 2 AND in template's manager_ids"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
record = self.JetWaypointTemplate.create(
{
"name": "Manager Can Write",
"reference": "manager_can_write",
"jet_template_id": jet_template.id,
"access_level": "2",
}
)
# Manager should be able to write
try:
record.with_user(self.manager).write({"name": "Updated Name"})
record.invalidate_recordset()
self.assertEqual(
record.name, "Updated Name", "Manager should be able to update"
)
except AccessError:
self.fail("Manager should be able to update when in template's manager_ids")
def test_manager_write_forbidden_not_in_manager_ids(self):
"""Test Manager: No write when not in template's manager_ids"""
# Create jet template with manager only in user_ids, not manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"user_ids": [(4, self.manager.id)], # Only in user_ids
"manager_ids": False,
}
)
record = self.JetWaypointTemplate.create(
{
"name": "No Write Access",
"reference": "no_write_access",
"jet_template_id": jet_template.id,
"access_level": "2",
}
)
with self.assertRaises(AccessError):
record.with_user(self.manager).write({"name": "Should Fail"})
def test_manager_write_forbidden_root_level(self):
"""Test Manager: No write when access_level is Root (3)"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
record = self.JetWaypointTemplate.create(
{
"name": "Root Level No Write",
"reference": "root_level_no_write",
"jet_template_id": jet_template.id,
"access_level": "3", # Root level
}
)
with self.assertRaises(AccessError):
record.with_user(self.manager).write({"name": "Should Fail"})
def test_manager_create_access(self):
"""Test Manager: Create when access_level <= 2 AND in template's manager_ids"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
# Try to create without being in manager_ids - should fail
jet_template_no_access = self.JetTemplate.create(
{
"name": "No Access Template",
"reference": "no_access_template",
"manager_ids": False,
}
)
with self.assertRaises(AccessError):
self.JetWaypointTemplate.with_user(self.manager).create(
{
"name": "Create Fail",
"reference": "create_fail",
"jet_template_id": jet_template_no_access.id,
"access_level": "2",
}
)
# Create with manager in template's manager_ids - should succeed
try:
record = self.JetWaypointTemplate.with_user(self.manager).create(
{
"name": "Create Success",
"reference": "create_success",
"jet_template_id": jet_template.id,
"access_level": "2",
}
)
records = self.JetWaypointTemplate.search([("id", "=", record.id)])
self.assertEqual(len(records), 1, "Manager should be able to create")
except AccessError:
self.fail("Manager should be able to create when in template's manager_ids")
# ======================
# Manager Delete Access Tests
# ======================
def test_manager_delete_own_record(self):
"""Test Manager: Delete own record when in template's manager_ids"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
record = self.JetWaypointTemplate.with_user(self.manager).create(
{
"name": "My Record",
"reference": "my_record",
"jet_template_id": jet_template.id,
"access_level": "2",
}
)
try:
record.with_user(self.manager).unlink()
records = self.JetWaypointTemplate.search([("id", "=", record.id)])
self.assertEqual(
len(records), 0, "Manager should be able to delete own record"
)
except AccessError:
self.fail("Manager should be able to delete own record")
def test_manager_delete_not_creator(self):
"""Test Manager: Cannot delete record created by another user"""
# Create jet template with both managers in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id), (4, self.manager2.id)],
}
)
record = self.JetWaypointTemplate.with_user(self.manager2).create(
{
"name": "Other's Record",
"reference": "others_record",
"jet_template_id": jet_template.id,
"access_level": "2",
}
)
# Manager1 cannot delete Manager2's record
with self.assertRaises(AccessError):
record.with_user(self.manager).unlink()
def test_manager_delete_not_in_manager_ids(self):
"""Test Manager: Cannot delete when not in template's manager_ids"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
record = self.JetWaypointTemplate.with_user(self.manager).create(
{
"name": "Removed Manager",
"reference": "removed_manager",
"jet_template_id": jet_template.id,
"access_level": "2",
}
)
# Remove from manager_ids
jet_template.write({"manager_ids": False})
# Cannot delete anymore
with self.assertRaises(AccessError):
record.with_user(self.manager).unlink()
def test_manager_delete_root_level(self):
"""Test Manager: Cannot delete Root level record"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
# Create record with Root level as root (default user)
record = self.JetWaypointTemplate.create(
{
"name": "Root Level Delete",
"reference": "root_level_delete",
"jet_template_id": jet_template.id,
"access_level": "3", # Root level
}
)
with self.assertRaises(AccessError):
record.with_user(self.manager).unlink()
# ======================
# Root Access Tests
# ======================
def test_root_full_access(self):
"""
Test Root: Full CRUD access regardless of access_level or creator.
Root has unrestricted access to all records via security rule
[(1, '=', 1)], so we test:
- Create records with all access levels
- Read records with all access levels
- Write to records with all access levels
- Delete records regardless of creator
"""
# Create jet template for testing
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
}
)
# Test CRUD operations for all access levels (only Manager and Root exist)
for access_level in ["2", "3"]:
# Root can create any level
record = self.JetWaypointTemplate.with_user(self.root).create(
{
"name": f"Root Level {access_level}",
"reference": f"root_level_{access_level}",
"jet_template_id": jet_template.id,
"access_level": access_level,
}
)
# Root can read any level
records = self.JetWaypointTemplate.with_user(self.root).search(
[("id", "=", record.id)]
)
self.assertEqual(
len(records),
1,
f"Root should be able to read access_level={access_level}",
)
# Root can write any level
record.with_user(self.root).write(
{"name": f"Root Updated Level {access_level}"}
)
record.invalidate_recordset()
self.assertEqual(
record.name,
f"Root Updated Level {access_level}",
f"Root should be able to update access_level={access_level}",
)
# Test Root can delete records created by other users
# Add manager to template's manager_ids so they can create the record
jet_template.write({"manager_ids": [(4, self.manager.id)]})
manager_record = self.JetWaypointTemplate.with_user(self.manager).create(
{
"name": "Manager's Record",
"reference": "managers_record",
"jet_template_id": jet_template.id,
"access_level": "2",
}
)
manager_record.with_user(self.root).unlink()
records = self.JetWaypointTemplate.with_user(self.root).search(
[("id", "=", manager_record.id)]
)
self.assertEqual(
len(records),
0,
"Root should be able to delete records from any creator",
)
# ======================
# Edge Cases
# ======================
def test_access_level_changes_visibility(self):
"""Test that changing access_level affects visibility"""
# Create jet template with manager in manager_ids
jet_template = self.JetTemplate.create(
{
"name": "Test Template",
"reference": "test_template",
"manager_ids": [(4, self.manager.id)],
}
)
# Create with Manager level
record = self.JetWaypointTemplate.create(
{
"name": "Changing Level",
"reference": "changing_level",
"jet_template_id": jet_template.id,
"access_level": "2",
}
)
# Manager can read
records = self.JetWaypointTemplate.with_user(self.manager).search(
[("id", "=", record.id)]
)
self.assertEqual(len(records), 1, "Manager should read level 2")
# Change to Root level
record.write({"access_level": "3"})
# Manager cannot read anymore
records = self.JetWaypointTemplate.with_user(self.manager).search(
[("id", "=", record.id)]
)
self.assertEqual(len(records), 0, "Manager should not read level 3")

View File

@@ -0,0 +1,919 @@
from odoo.exceptions import AccessError, ValidationError
from .common import TestTowerCommon
class TestTowerKey(TestTowerCommon):
"""Test class for tower key."""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create another manager for testing
cls.manager_2 = cls.Users.create(
{
"name": "Second Manager",
"login": "manager2",
"email": "manager2@test.com",
"groups_id": [(4, cls.env.ref("cetmix_tower_server.group_manager").id)],
}
)
# Create test servers
cls.server_1 = cls.Server.create(
{
"name": "Test Server 1",
"ip_v4_address": "192.168.1.1",
"ssh_port": 22,
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
}
)
cls.server_2 = cls.Server.create(
{
"name": "Test Server 2",
"ip_v4_address": "192.168.1.2",
"ssh_port": 22,
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
}
)
cls.test_key = cls.Key.create(
{"name": "Test Key", "key_type": "s", "secret_value": "test value"}
)
def test_key_creation(self):
"""
Test key creation.
We override create method so need to check if reference is generated properly
"""
# -- 1--
# Check new key values
key_one = self.Key.create(
{"name": " test key meme ", "secret_value": "test value", "key_type": "s"}
)
self.assertEqual(
key_one.reference, "test_key_meme", "Reference must be 'test_key_meme'"
)
self.assertEqual(
key_one.name,
"test key meme",
"Trailing and leading whitespaces must be removed from name",
)
def test_extract_key_strings(self):
"""Check if key strings are extracted properly"""
code = (
"Hey #!cxtower.secret.MEME_KEY!# & Doge #!cxtower.secret.DOGE_KEY !# so "
"like #!cxtower.secret.MEME_KEY!#!\n"
"They make #!memes together."
"And this is another string for the same #!cxtower.secret.MEME_KEY !#"
)
key_strings = self.Key._extract_key_strings(code)
self.assertEqual(len(key_strings), 3, "Must be 3 key stings")
self.assertIn(
"#!cxtower.secret.MEME_KEY!#",
key_strings,
"Key string must be in key strings",
)
self.assertIn(
"#!cxtower.secret.DOGE_KEY !#",
key_strings,
"Key string must be in key strings",
)
self.assertIn(
"#!cxtower.secret.MEME_KEY !#",
key_strings,
"Key string must be in key strings",
)
def test_parse_key_string(self):
"""Check if key string is parsed correctly"""
# Test global key
doge_key = self.Key.create(
{
"name": "doge key",
"reference": "DOGE_KEY",
"secret_value": "Doge dog",
"key_type": "s",
}
)
key_string = "#!cxtower.secret.DOGE_KEY!#"
key_value = self.Key._parse_key_string(key_string)
self.assertEqual(key_value, "Doge dog", "Key value doesn't match")
# Test the same key string but with some spaces before the key terminator
key_string = "#!cxtower.secret.DOGE_KEY !#"
key_value = self.Key._parse_key_string(key_string)
self.assertEqual(key_value, "Doge dog", "Key value doesn't match")
# Test partner specific key
self.KeyValue.create(
{
"key_id": doge_key.id,
"secret_value": "Doge partner",
"partner_id": self.user_bob.partner_id.id,
}
)
# compose kwargs
kwargs = {
"partner_id": self.user_bob.partner_id.id,
"server_id": self.server_test_1.id,
}
key_value = self.Key._parse_key_string(key_string, **kwargs)
self.assertEqual(key_value, "Doge partner", "Key value doesn't match")
# Test server specific key
self.KeyValue.create(
{
"key_id": doge_key.id,
"secret_value": "Doge server",
"server_id": self.server_test_1.id,
}
)
key_value = self.Key._parse_key_string(key_string, **kwargs)
# Test server and partner specific key
self.KeyValue.create(
{
"key_id": doge_key.id,
"secret_value": "Doge server and partner",
"server_id": self.server_test_1.id,
"partner_id": self.user_bob.partner_id.id,
}
)
key_value = self.Key._parse_key_string(key_string, **kwargs)
self.assertEqual(
key_value, "Doge server and partner", "Key value doesn't match"
)
# Test missing key
key_string = "#!cxtower.secret.ANOTHER_KEY!#"
key_value = self.Key._parse_key_string(key_string)
self.assertIsNone(key_value, "Key value must be 'None'")
# Test missformatted key
key_string = "#!cxtower.ANOTHER_KEY!#"
key_value = self.Key._parse_key_string(key_string)
self.assertIsNone(key_value, "Key value must be 'None'")
# Test another missformatted key
key_string = "#!cxtower.notasecret.DOGE_KEY!#"
key_value = self.Key._parse_key_string(key_string)
self.assertIsNone(key_value, "Key value must be 'None'")
def test_resolve_key(self):
"""Check generic key resolver"""
self.Key.create(
{
"name": "doge key",
"reference": "DOGE_KEY",
"secret_value": "Doge dog",
"key_type": "s",
}
)
# Existing key
key_value = self.Key._resolve_key("secret", "DOGE_KEY")
self.assertEqual(key_value, "Doge dog", "Key value doesn't match")
# Non existing key
key_value = self.Key._resolve_key("server", "PEPE_KEY")
self.assertIsNone(key_value, "Key value must be 'None'")
def test_resolve_key_type_secret(self):
"""Check 'secret' type key resolver"""
doge_key = self.Key.create(
{
"name": "doge key",
"reference": "DOGE_KEY",
"key_type": "s",
}
)
# 1. Test server and partner specific key
server_partner_value = self.KeyValue.create(
{
"key_id": doge_key.id,
"secret_value": "Doge server and partner",
"server_id": self.server_test_1.id,
"partner_id": self.user_bob.partner_id.id,
}
)
kwargs = {
"partner_id": self.user_bob.partner_id.id,
"server_id": self.server_test_1.id,
}
key_value = self.Key._resolve_key_type_secret("DOGE_KEY", **kwargs)
self.assertEqual(
key_value, "Doge server and partner", "Key value doesn't match"
)
# 2. Global key
doge_key.write({"secret_value": "Doge dog"})
key_value = self.Key._resolve_key_type_secret("DOGE_KEY")
self.assertEqual(key_value, "Doge dog", "Key value doesn't match")
# 3. Non existing key
key_value = self.Key._resolve_key_type_secret("PEPE_KEY")
self.assertIsNone(key_value, "Key value must be 'None'")
# 4. Partner specific key
self.KeyValue.create(
{
"key_id": doge_key.id,
"secret_value": "Doge partner",
"partner_id": self.user_bob.partner_id.id,
}
)
kwargs = {
"partner_id": self.user_bob.partner_id.id,
}
key_value = self.Key._resolve_key_type_secret("DOGE_KEY", **kwargs)
self.assertEqual(key_value, "Doge partner", "Key value doesn't match")
# 5. Test server specific key
self.KeyValue.create(
{
"key_id": doge_key.id,
"secret_value": "Doge server",
"server_id": self.server_test_1.id,
}
)
kwargs = {
"server_id": self.server_test_1.id,
}
key_value = self.Key._resolve_key_type_secret("DOGE_KEY", **kwargs)
self.assertEqual(key_value, "Doge server", "Key value doesn't match")
# 6. Test with non matching partner. Should return server specific value
kwargs = {
"partner_id": self.user.partner_id.id,
"server_id": self.server_test_1.id,
}
key_value = self.Key._resolve_key_type_secret("DOGE_KEY", **kwargs)
self.assertEqual(key_value, "Doge server", "Key value doesn't match")
# 7. Change partner in the server-partner specific value.
# Should return server specific value
server_partner_value.write({"partner_id": self.manager.partner_id.id})
kwargs = {
"server_id": self.server_test_1.id,
}
key_value = self.Key._resolve_key_type_secret("DOGE_KEY", **kwargs)
self.assertEqual(key_value, "Doge server", "Key value doesn't match")
# 8. Test with the global key again
key_value = self.Key._resolve_key_type_secret("DOGE_KEY")
self.assertEqual(key_value, "Doge dog", "Key value doesn't match")
def test_parse_code(self):
"""Test code parsing"""
def check_parsed_code(
code, code_parsed_expected, expected_key_values=None, **kwargs
):
"""Helper function for code parse testing
Args:
code (Text): code to parse
code_parsed_expected (Text): expected parsed code
expected_key_values (list, optional): key values that are expected
to be returned. Defaults to None.
"""
code_parsed = self.Key._parse_code(code, **kwargs)
self.assertEqual(
code_parsed,
code_parsed_expected,
msg="Parsed code doesn't match expected one",
)
if expected_key_values:
result = self.Key._parse_code_and_return_key_values(code, **kwargs)
code_parsed = result["code"]
key_values = result["key_values"]
self.assertEqual(
code_parsed,
code_parsed_expected,
msg="Parsed code doesn't match expected one",
)
self.assertEqual(
len(key_values),
len(expected_key_values),
"Number of key values doesn't match number of expected ones",
)
for expected_value in expected_key_values:
self.assertIn(
expected_value,
key_values,
f"Value {expected_value} must be in the returned key values",
)
# Create new key
self.Key.create(
{
"name": "Meme key",
"reference": "MEME_KEY",
"secret_value": "Pepe Frog",
"key_type": "s",
}
)
# Check key parser
# 1 - single line
code = "The key to understand this meme is #!cxtower.secret.MEME_KEY!#"
code_parsed_expected = "The key to understand this meme is Pepe Frog"
expected_key_values = ["Pepe Frog"]
check_parsed_code(code, code_parsed_expected, expected_key_values)
# 2 - multi line
code = "Welcome #!cxtower.secret.MEME_KEY!#\nNew hero of this city!"
code_parsed_expected = "Welcome Pepe Frog\nNew hero of this city!"
expected_key_values = ["Pepe Frog"]
check_parsed_code(code, code_parsed_expected, expected_key_values)
# 3 - Key not found
code = "Don't mess with #!cxtower.secret.DOGE_LIKE!# He will make you cry"
code_parsed_expected = "Don't mess with None He will make you cry"
expected_key_values = []
check_parsed_code(code, code_parsed_expected, expected_key_values)
check_parsed_code(code, code_parsed_expected)
# 4 - Multi keys
# Create new key
doge_key = self.Key.create(
{
"name": "doge key",
"reference": "DOGE_KEY",
"secret_value": "Doge dog",
"key_type": "s",
}
)
code = (
"Hey #!cxtower.secret.MEME_KEY!# & Doge #!cxtower.secret.DOGE_KEY !# so "
"like #!cxtower.secret.MEME_KEY!#!\n"
"They make #!memes together. Check #!cxtower.secret.MEME_KEY&#!"
"cxtower.secret.DOGE_KEY"
)
code_parsed_expected = (
"Hey Pepe Frog & Doge Doge dog so "
"like Pepe Frog!\n"
"They make #!memes together. Check #!cxtower.secret.MEME_KEY&#!"
"cxtower.secret.DOGE_KEY"
)
expected_key_values = ["Pepe Frog", "Doge dog"]
check_parsed_code(code, code_parsed_expected, expected_key_values)
# 5 - Partner specific key
# Create new key for partner Bob
self.KeyValue.create(
{
"key_id": doge_key.id,
"secret_value": "Doge wow",
"partner_id": self.user_bob.partner_id.id,
}
)
# compose kwargs
kwargs = {"partner_id": self.user_bob.partner_id.id}
code_parsed_expected = (
"Hey Pepe Frog & Doge Doge wow so "
"like Pepe Frog!\n"
"They make #!memes together. Check #!cxtower.secret.MEME_KEY&#!"
"cxtower.secret.DOGE_KEY"
)
expected_key_values = ["Pepe Frog", "Doge wow"]
check_parsed_code(code, code_parsed_expected, expected_key_values, **kwargs)
# 6 - Server specific key
# Create new key for server Test 1
self.KeyValue.create(
{
"key_id": doge_key.id,
"secret_value": "Doge much",
"server_id": self.server_test_1.id,
}
)
# compose kwargs
kwargs = {
"partner_id": self.user_bob.partner_id.id, # not needed but may keep it
"server_id": self.server_test_1.id,
}
code_parsed_expected = (
"Hey Pepe Frog & Doge Doge much so "
"like Pepe Frog!\n"
"They make #!memes together. Check #!cxtower.secret.MEME_KEY&#!"
"cxtower.secret.DOGE_KEY"
)
expected_key_values = ["Pepe Frog", "Doge much"]
check_parsed_code(code, code_parsed_expected, expected_key_values, **kwargs)
def test_replace_with_spoiler(self):
"""Check if secrets are replaced with spoiler correctly"""
code = (
"Hey Pepe Frog & Doge Doge much so "
"like Pepe Frog!\n"
"They make #!memes together. Check #!cxtower.secret.MEME_KEY&#!"
"cxtower.secret.DOGE_KEY"
)
placeholder = self.Key.SECRET_VALUE_PLACEHOLDER
expected_code = (
f"Hey {placeholder} & Doge {placeholder} so "
f"like {placeholder}!\n"
"They make #!memes together. Check #!cxtower.secret.MEME_KEY&#!"
"cxtower.secret.DOGE_KEY"
)
key_values = ["Pepe Frog", "Doge much"]
result = self.Key._replace_with_spoiler(code, key_values)
self.assertEqual(result, expected_code, "Result doesn't match expected code")
# --------------------------------------
# Check with some random key values now
# Original code should rename unchanged
# --------------------------------------
key_values = ["Wow much", "No like"]
result = self.Key._replace_with_spoiler(code, key_values)
self.assertEqual(result, code, "Result doesn't match expected code")
def test_user_access(self):
"""Test that regular users have no access to keys"""
user_key = self.Key.with_user(self.user)
# Create test key
key = self.Key.create(
{"name": "Test Key", "secret_value": "test value", "key_type": "s"}
)
# Test CRUD operations
with self.assertRaises(AccessError):
user_key.create(
{"name": "New Key", "secret_value": "secret", "key_type": "s"}
)
with self.assertRaises(AccessError):
user_key.browse(key.id).read(["name"])
with self.assertRaises(AccessError):
user_key.browse(key.id).write({"name": "Updated Name"})
with self.assertRaises(AccessError):
user_key.browse(key.id).unlink()
def test_manager_read_access(self):
"""Test manager read access rules"""
manager_key = self.Key.with_user(self.manager)
# Create test keys
key_secret = self.Key.create(
{"name": "Secret Key", "secret_value": "secret value", "key_type": "s"}
)
key_ssh = self.Key.create(
{"name": "SSH Key", "secret_value": "ssh key", "key_type": "k"}
)
# Test read access for secret key - should read (all managers can read secrets)
self.assertTrue(manager_key.search([("id", "=", key_secret.id)]))
# Test read access for SSH key without server access - should not find
self.assertFalse(manager_key.search([("id", "=", key_ssh.id)]))
# Add manager to server users and set SSH key - should find SSH key
self.write_and_invalidate(
self.server_1,
**{"user_ids": [(4, self.manager.id)], "ssh_key_id": key_ssh.id},
)
self.assertTrue(manager_key.search([("id", "=", key_ssh.id)]))
# Remove key from server - should not find again
self.server_1.write({"ssh_key_id": False})
self.assertFalse(manager_key.search([("id", "=", key_ssh.id)]))
# Add as key user - should find both
key_secret.write({"user_ids": [(4, self.manager.id)]})
key_ssh.write({"user_ids": [(4, self.manager.id)]})
self.assertTrue(manager_key.search([("id", "=", key_secret.id)]))
self.assertTrue(manager_key.search([("id", "=", key_ssh.id)]))
def test_manager_write_access(self):
"""Test manager write/create access rules"""
manager_key = self.Key.with_user(self.manager)
# Create test keys as root and ensure manager is not in manager_ids
key_secret = self.Key.create(
{
"name": "Secret Key",
"secret_value": "secret value",
"key_type": "s",
"manager_ids": [(5, 0)], # Clear manager_ids
}
)
key_ssh = self.Key.create(
{
"name": "SSH Key",
"secret_value": "ssh key",
"key_type": "k",
"manager_ids": [(5, 0)], # Clear manager_ids
}
)
# Try write without being manager - should fail
with self.assertRaises(AccessError):
manager_key.browse(key_secret.id).write({"name": "Updated Secret"})
with self.assertRaises(AccessError):
manager_key.browse(key_ssh.id).write({"name": "Updated SSH"})
# Add as key manager - should write to secret
key_secret.write({"manager_ids": [(4, self.manager.id)]})
manager_key.browse(key_secret.id).write({"name": "Updated Secret"})
self.assertEqual(key_secret.name, "Updated Secret")
# Add as server manager and set SSH key - should write to SSH key
self.server_1.write(
{"manager_ids": [(4, self.manager.id)], "ssh_key_id": key_ssh.id}
)
manager_key.browse(key_ssh.id).write({"name": "Updated SSH"})
self.assertEqual(key_ssh.name, "Updated SSH")
def test_manager_create_access(self):
"""Test manager create access rules"""
manager_key = self.Key.with_user(self.manager)
manager_2_key = self.Key.with_user(self.manager_2)
# Try create secret key when not a manager - should fail
with self.assertRaises(AccessError):
manager_2_key.create(
{
"name": "New Secret",
"secret_value": "secret",
"key_type": "s",
"manager_ids": [(5, 0)], # Prevent automatic manager addition
}
)
# Try create SSH key when not a server manager - should fail
with self.assertRaises(AccessError):
manager_2_key.create(
{
"name": "New SSH",
"secret_value": "ssh key",
"key_type": "k",
"manager_ids": [(5, 0)], # Prevent automatic manager addition
}
)
# Add as server manager - should create SSH key
self.server_1.write({"manager_ids": [(4, self.manager.id)]})
new_ssh_key = manager_key.create(
{"name": "New SSH", "secret_value": "ssh key", "key_type": "k"}
)
# Link key to server
self.server_1.write({"ssh_key_id": new_ssh_key.id})
self.assertTrue(new_ssh_key.exists())
def test_manager_unlink_access(self):
"""Test manager unlink access rules"""
manager_key = self.Key.with_user(self.manager)
# Create keys as root
key_secret = self.Key.create(
{"name": "Secret Key", "secret_value": "secret value", "key_type": "s"}
)
key_ssh = self.Key.create(
{"name": "SSH Key", "secret_value": "ssh key", "key_type": "k"}
)
# Link SSH key to server
self.server_1.write({"ssh_key_id": key_ssh.id})
# Try delete without being manager and creator - should fail
with self.assertRaises(AccessError):
manager_key.browse(key_secret.id).unlink()
with self.assertRaises(AccessError):
manager_key.browse(key_ssh.id).unlink()
# Add as manager but not creator - should still fail
key_secret.write({"manager_ids": [(4, self.manager.id)]})
self.server_1.write({"manager_ids": [(4, self.manager.id)]})
with self.assertRaises(AccessError):
manager_key.browse(key_secret.id).unlink()
with self.assertRaises(AccessError):
manager_key.browse(key_ssh.id).unlink()
# Create own keys - should delete
own_secret = manager_key.create(
{
"name": "Own Secret",
"secret_value": "secret",
"key_type": "s",
"manager_ids": [(4, self.manager.id)],
}
)
own_ssh = manager_key.create(
{"name": "Own SSH", "secret_value": "ssh key", "key_type": "k"}
)
# Link own SSH key to server
self.server_1.write({"ssh_key_id": own_ssh.id})
own_secret.unlink()
own_ssh.unlink()
self.assertFalse(own_secret.exists())
self.assertFalse(own_ssh.exists())
def test_root_access(self):
"""Test root access rules"""
root_key = self.Key.with_user(self.root)
# Create
key = root_key.create(
{"name": "Root Key", "secret_value": "root secret", "key_type": "s"}
)
self.assertTrue(key.exists())
# Read
self.assertEqual(root_key.browse(key.id).name, "Root Key")
# Write
root_key.browse(key.id).write({"name": "Updated Root Key"})
self.assertEqual(key.name, "Updated Root Key")
# Delete
key.unlink()
self.assertFalse(key.exists())
def test_key_value_user_access(self):
"""Test that regular users have no access to key values"""
user_key_value = self.KeyValue.with_user(self.user)
# Create test key and key value
key = self.Key.create({"name": "Test Key", "key_type": "s"})
key_value = self.KeyValue.create(
{"key_id": key.id, "secret_value": "test value"}
)
# Test CRUD operations
with self.assertRaises(AccessError):
user_key_value.create({"key_id": key.id, "secret_value": "new value"})
with self.assertRaises(AccessError):
user_key_value.browse(key_value.id).read(["secret_value"])
with self.assertRaises(AccessError):
user_key_value.browse(key_value.id).write({"secret_value": "updated value"})
with self.assertRaises(AccessError):
user_key_value.browse(key_value.id).unlink()
def test_key_value_manager_read_access(self):
"""Test manager read access rules for key values"""
manager_key_value = self.KeyValue.with_user(self.manager)
# Create test key and key values
key = self.Key.create({"name": "Test Key", "key_type": "s"})
global_value = self.KeyValue.create(
{"key_id": key.id, "secret_value": "global value"}
)
server_value = self.KeyValue.create(
{
"key_id": key.id,
"secret_value": "server value",
"server_id": self.server_1.id,
}
)
# Test read access - should not find without proper access
self.assertTrue(manager_key_value.search([("id", "=", global_value.id)]))
self.assertFalse(manager_key_value.search([("id", "=", server_value.id)]))
# Add as key user - should find global value and server value for that key
key.write({"user_ids": [(4, self.manager.id)]})
self.assertTrue(manager_key_value.search([("id", "=", global_value.id)]))
self.assertTrue(manager_key_value.search([("id", "=", server_value.id)]))
# Remove from key users
key.write({"user_ids": [(3, self.manager.id)]})
self.assertTrue(manager_key_value.search([("id", "=", global_value.id)]))
self.assertFalse(manager_key_value.search([("id", "=", server_value.id)]))
# Add as server user - should find server value
self.server_1.write({"user_ids": [(4, self.manager.id)]})
self.assertTrue(manager_key_value.search([("id", "=", global_value.id)]))
self.assertTrue(manager_key_value.search([("id", "=", server_value.id)]))
def test_key_value_manager_write_access(self):
"""Test manager write/create access rules for key values"""
manager_key_value = self.KeyValue.with_user(self.manager)
# Create test key and key values
key = self.Key.create({"name": "Test Key", "key_type": "s"})
global_value = self.KeyValue.create(
{"key_id": key.id, "secret_value": "global value"}
)
server_value = self.KeyValue.create(
{
"key_id": key.id,
"secret_value": "server value",
"server_id": self.server_1.id,
}
)
# Try write without proper access - should fail
with self.assertRaises(AccessError):
manager_key_value.browse(global_value.id).write(
{"secret_value": "new value"}
)
with self.assertRaises(AccessError):
manager_key_value.browse(server_value.id).write(
{"secret_value": "new value"}
)
# Add as key manager - should write to global value
key.write({"manager_ids": [(4, self.manager.id)]})
manager_key_value.browse(global_value.id).write(
{"secret_value": "updated global"}
)
self.assertEqual(
global_value._get_secret_value("secret_value"), "updated global"
)
# Add as server manager - should write to server value
self.server_1.write({"manager_ids": [(4, self.manager.id)]})
manager_key_value.browse(server_value.id).write(
{"secret_value": "updated server"}
)
self.assertEqual(
server_value._get_secret_value("secret_value"), "updated server"
)
# Test create access
for_bob = manager_key_value.create(
{
"key_id": key.id,
"secret_value": "for bob",
"partner_id": self.user_bob.partner_id.id,
}
)
self.assertTrue(for_bob.exists())
def test_key_value_manager_unlink_access(self):
"""Test manager unlink access rules for key values"""
manager_key_value = self.KeyValue.with_user(self.manager)
# Create test key and key values
key = self.Key.create({"name": "Test Key", "key_type": "s"})
# Create values as root
global_value = self.KeyValue.create(
{"key_id": key.id, "secret_value": "global value"}
)
server_value = self.KeyValue.create(
{
"key_id": key.id,
"secret_value": "server value",
"server_id": self.server_1.id,
}
)
# Try delete without proper access - should fail
with self.assertRaises(AccessError):
manager_key_value.browse(global_value.id).unlink()
with self.assertRaises(AccessError):
manager_key_value.browse(server_value.id).unlink()
# Add as manager but not creator - should still fail
key.write({"manager_ids": [(4, self.manager.id)]})
self.server_1.write({"manager_ids": [(4, self.manager.id)]})
with self.assertRaises(AccessError):
manager_key_value.browse(global_value.id).unlink()
with self.assertRaises(AccessError):
manager_key_value.browse(server_value.id).unlink()
# Create own values - should delete
own_partner_value = manager_key_value.create(
{
"key_id": key.id,
"secret_value": "own partner",
"partner_id": self.user_bob.partner_id.id,
}
)
# Unlink server value first to avoid constraint error
server_value.unlink()
# Create server value
own_server_value = manager_key_value.create(
{
"key_id": key.id,
"secret_value": "own server",
"server_id": self.server_1.id,
}
)
own_partner_value.unlink()
own_server_value.unlink()
self.assertFalse(own_partner_value.exists())
self.assertFalse(own_server_value.exists())
def test_key_value_root_access(self):
"""Test root access rules for key values"""
root_key_value = self.KeyValue.with_user(self.root)
# Create test key
key = self.Key.create({"name": "Test Key", "key_type": "s"})
# Create
value = root_key_value.create({"key_id": key.id, "secret_value": "root value"})
self.assertTrue(value.exists())
# Read
self.assertEqual(
root_key_value.browse(value.id)._get_secret_value("secret_value"),
"root value",
)
# Write
root_key_value.browse(value.id).write({"secret_value": "updated value"})
self.assertEqual(value._get_secret_value("secret_value"), "updated value")
# Delete
value.unlink()
self.assertFalse(value.exists())
def test_key_value_global_unique(self):
"""Test global value uniqueness"""
# Try to create a value for the same key
with self.assertRaises(ValidationError):
another_global_value = self.KeyValue.create(
{"key_id": self.test_key.id, "secret_value": "another test value"}
)
#
another_global_value.unlink()
def test_key_value_server_unique(self):
"""Test server value uniqueness"""
# Create server tight value
self.KeyValue.create(
{
"key_id": self.test_key.id,
"secret_value": "server related",
"server_id": self.server_1.id,
}
)
# Try create another value for the same server
with self.assertRaises(ValidationError):
self.KeyValue.create(
{
"key_id": self.test_key.id,
"secret_value": "another server related",
"server_id": self.server_1.id,
}
)
def test_key_value_partner_unique(self):
"""Test partner value uniqueness"""
# Create partner tight value
self.KeyValue.create(
{
"key_id": self.test_key.id,
"secret_value": "partner related",
"partner_id": self.user_bob.partner_id.id,
}
)
# Try create another value for the same partner
with self.assertRaises(ValidationError):
self.KeyValue.create(
{
"key_id": self.test_key.id,
"secret_value": "another partner related",
"partner_id": self.user_bob.partner_id.id,
}
)
def test_key_value_server_partner_unique(self):
"""Test server and partner value uniqueness"""
# Create server and partner tight value
self.KeyValue.create(
{
"key_id": self.test_key.id,
"secret_value": "server related",
"server_id": self.server_1.id,
"partner_id": self.user_bob.partner_id.id,
}
)
# Try create another value for the same server and partner
with self.assertRaises(ValidationError):
self.KeyValue.create(
{
"key_id": self.test_key.id,
"secret_value": "another server related",
"server_id": self.server_1.id,
"partner_id": self.user_bob.partner_id.id,
}
)

View File

@@ -0,0 +1,58 @@
# Copyright (C) 2022 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.tests.common import tagged
from .common import TestTowerCommon
@tagged("partner_servers_btn")
class TestPartnerServers(TestTowerCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.partner_a = cls.env["res.partner"].create({"name": "Partner A"})
cls.partner_b = cls.env["res.partner"].create({"name": "Partner B"})
cls.partner_b_child = cls.env["res.partner"].create(
{
"name": "Partner B Child",
"parent_id": cls.partner_b.id,
}
)
cls.server_defaults = {
"name": "Test Server",
"ssh_username": "root",
"ssh_port": 22,
"ssh_password": "Test-P@ssw0rd-123",
"ip_v4_address": "127.0.0.1",
"skip_host_key": True,
}
cls.Server.create({"partner_id": cls.partner_b.id, **cls.server_defaults})
cls.Server.create({"partner_id": cls.partner_b.id, **cls.server_defaults})
cls.Server.create({"partner_id": cls.partner_b_child.id, **cls.server_defaults})
key = cls.Key.create({"name": "SSH Token", "key_type": "s"})
cls.KeyValue.create(
{
"key_id": key.id,
"partner_id": cls.partner_b.id,
"secret_value": "TOPSECRET",
}
)
def test_server_count_compute(self):
"""Server count: direct + onelevel child + zero if none."""
self.assertEqual(self.partner_b.server_count, 3)
self.assertEqual(self.partner_b_child.server_count, 1)
self.assertEqual(self.partner_a.server_count, 0)
def test_parent_with_only_child_servers(self):
"""Parent without servers directs and with child_of."""
parent = self.env["res.partner"].create({"name": "Parent Only"})
child = self.env["res.partner"].create(
{"name": "Child with Server", "parent_id": parent.id}
)
self.Server.create({"partner_id": child.id, **self.server_defaults})
self.assertEqual(parent.server_count, 1)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,540 @@
# Copyright (C) 2025 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.exceptions import AccessError
from .common import TestTowerCommon
class TestTowerPlanLine(TestTowerCommon):
"""Test the cx.tower.plan.line model access rights."""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create a test plan with access level 1 for user tests
cls.test_plan = cls.Plan.create(
{
"name": "Test Access Plan",
"access_level": "1",
"user_ids": [(6, 0, [cls.user.id])],
"manager_ids": [(6, 0, [cls.manager.id])],
}
)
# Create a test plan line
cls.test_line = cls.plan_line.create(
{
"plan_id": cls.test_plan.id,
"command_id": cls.command_create_dir.id,
"sequence": 10,
}
)
# Create additional servers for testing server-based access
cls.server_2 = cls.Server.create(
{
"name": "Test Server 2",
"ip_v4_address": "localhost",
"ssh_username": "test2",
"ssh_password": "test2",
"ssh_port": 22,
"user_ids": [(6, 0, [])],
"manager_ids": [(6, 0, [])],
}
)
cls.server_3 = cls.Server.create(
{
"name": "Test Server 3",
"ip_v4_address": "localhost",
"ssh_username": "test3",
"ssh_password": "test3",
"ssh_port": 22,
"user_ids": [(6, 0, [])],
"manager_ids": [(6, 0, [])],
}
)
def test_user_read_access(self):
"""Test user read access to plan lines"""
# Case 1: User should be able to read line when:
# - access_level == "1"
# - user is in plan's user_ids OR server's user_ids
recs = self.plan_line.with_user(self.user).search(
[("id", "=", self.test_line.id)]
)
self.assertIn(
self.test_line,
recs,
"User should be able to read line when conditions are met",
)
# Case 2: User should not be able to read when access_level > "1"
self.test_plan.write(
{
"access_level": "2",
}
)
recs = self.plan_line.with_user(self.user).search(
[("id", "=", self.test_line.id)]
)
self.assertNotIn(
self.test_line,
recs,
"User should not be able to read line when access_level > '1'",
)
# Case 3: User should be able to read when in server's user_ids
self.test_plan.write(
{
"access_level": "1",
"server_ids": [(6, 0, [self.server_test_1.id])],
}
)
self.server_test_1.write(
{
"user_ids": [(6, 0, [self.user.id])],
}
)
recs = self.plan_line.with_user(self.user).search(
[("id", "=", self.test_line.id)]
)
self.assertIn(
self.test_line,
recs,
"User should be able to read line when in server's user_ids",
)
def test_user_write_create_unlink_access(self):
"""Test user write/create/unlink access restrictions"""
# Users should not be able to create lines
with self.assertRaises(AccessError):
self.plan_line.with_user(self.user).create(
{
"plan_id": self.test_plan.id,
"command_id": self.command_create_dir.id,
"sequence": 20,
}
)
# Users should not be able to write lines
with self.assertRaises(AccessError):
self.test_line.with_user(self.user).write({"sequence": 30})
# Users should not be able to unlink lines
with self.assertRaises(AccessError):
self.test_line.with_user(self.user).unlink()
def test_manager_read_access(self):
"""Test manager read access to plan lines"""
# Case 1: Manager should be able to read when:
# - access_level <= "2"
# - manager is in plan's manager_ids OR user_ids
recs = self.plan_line.with_user(self.manager).search(
[("id", "=", self.test_line.id)]
)
self.assertIn(
self.test_line,
recs,
"Manager should be able to read line when conditions are met",
)
# Case 2: Manager should not be able to read when access_level > "2"
self.test_plan.write(
{
"access_level": "3",
"manager_ids": [(5, 0, 0)], # Remove all managers
}
)
recs = self.plan_line.with_user(self.manager).search(
[("id", "=", self.test_line.id)]
)
self.assertNotIn(
self.test_line,
recs,
"Manager should not be able to read line when access_level > '2'",
)
# Case 2.5: Manager not not be able to read when not in plan managers
self.test_plan.write(
{
"access_level": "2",
"manager_ids": [(5, 0, 0)], # Remove all managers
"server_ids": [(6, 0, [self.server_test_1.id])],
}
)
self.server_test_1.write(
{
"user_ids": [(5, 0, 0)], # Remove all users
"manager_ids": [(5, 0, 0)], # Remove all managers
}
)
recs = self.plan_line.with_user(self.manager).search(
[("id", "=", self.test_line.id)]
)
self.assertNotIn(
self.test_line,
recs,
"Manager should not be able to read line when access_level > '2'",
)
# Case 3: Manager should be able to read when in server's manager_ids
self.test_plan.write(
{
"access_level": "2",
"server_ids": [(6, 0, [self.server_test_1.id])],
}
)
self.server_test_1.write(
{
"manager_ids": [(6, 0, [self.manager.id])],
}
)
recs = self.plan_line.with_user(self.manager).search(
[("id", "=", self.test_line.id)]
)
self.assertIn(
self.test_line,
recs,
"Manager should be able to read line when in server's manager_ids",
)
def test_manager_write_create_access(self):
"""Test manager write/create access to plan lines"""
# Case 1: Manager should be able to create/write when:
# - access_level <= "2"
# - manager is in plan's manager_ids
try:
# Test create
self.plan_line.with_user(self.manager).create(
{
"plan_id": self.test_plan.id,
"command_id": self.command_create_dir.id,
"sequence": 20,
}
)
# Test write
self.test_line.with_user(self.manager).write({"sequence": 30})
except AccessError:
self.fail("Manager should be able to create/write when conditions are met")
# Case 2: Manager should not be able to create/write when access_level > "2"
self.test_plan.write(
{
"access_level": "3",
}
)
with self.assertRaises(AccessError):
self.plan_line.with_user(self.manager).create(
{
"plan_id": self.test_plan.id,
"command_id": self.command_create_dir.id,
"sequence": 40,
}
)
with self.assertRaises(AccessError):
self.test_line.with_user(self.manager).write({"sequence": 50})
def test_manager_unlink_access(self):
"""Test manager unlink access to plan lines"""
# Create line as manager to test unlink rights
line = self.plan_line.with_user(self.manager).create(
{
"plan_id": self.test_plan.id,
"command_id": self.command_create_dir.id,
"sequence": 20,
}
)
# Case 1: Manager should be able to unlink when:
# - access_level <= "2"
# - manager is the creator
# - manager is in plan's manager_ids
try:
line.unlink()
except AccessError:
self.fail("Manager should be able to unlink when conditions are met")
# Case 2: Manager should not be able to unlink lines created by others
line = self.test_line # Created by admin in setUp
with self.assertRaises(AccessError):
line.with_user(self.manager).unlink()
def test_root_unrestricted_read_access(self):
"""Test root user unrestricted read access"""
# Set most restrictive conditions
self.test_plan.write(
{
"access_level": "3",
"user_ids": [(5, 0, 0)],
"manager_ids": [(5, 0, 0)],
"server_ids": [(6, 0, [self.server_2.id, self.server_3.id])],
}
)
# Root should still be able to read
recs = self.plan_line.with_user(self.root).search(
[("id", "=", self.test_line.id)]
)
self.assertIn(
self.test_line,
recs,
"Root should be able to read regardless of access restrictions",
)
# Root should be able to read all records
all_recs = self.plan_line.with_user(self.root).search([])
self.assertIn(
self.test_line,
all_recs,
"Root should be able to read all records",
)
def test_root_unrestricted_write_access(self):
"""Test root user unrestricted write access"""
# Set most restrictive conditions
self.test_plan.write(
{
"access_level": "3",
"user_ids": [(5, 0, 0)],
"manager_ids": [(5, 0, 0)],
"server_ids": [(6, 0, [self.server_2.id, self.server_3.id])],
}
)
try:
# Test single field update
self.test_line.with_user(self.root).write({"sequence": 100})
# Test multiple field update
self.test_line.with_user(self.root).write(
{
"sequence": 200,
"path": "/test/path",
"use_sudo": True,
}
)
except AccessError:
self.fail("Root should be able to write regardless of access restrictions")
def test_root_unrestricted_create_access(self):
"""Test root user unrestricted create access"""
# Set most restrictive conditions
self.test_plan.write(
{
"access_level": "3",
"user_ids": [(5, 0, 0)],
"manager_ids": [(5, 0, 0)],
"server_ids": [(6, 0, [self.server_2.id, self.server_3.id])],
}
)
try:
# Test create with minimal values
new_line_1 = self.plan_line.with_user(self.root).create(
{
"plan_id": self.test_plan.id,
"command_id": self.command_create_dir.id,
}
)
# Test create with all values
new_line_2 = self.plan_line.with_user(self.root).create(
{
"plan_id": self.test_plan.id,
"command_id": self.command_create_dir.id,
"sequence": 300,
"path": "/another/test/path",
"use_sudo": True,
"condition": "{{ test_condition }}",
}
)
# Verify created records are readable
recs = self.plan_line.with_user(self.root).search(
[("id", "in", [new_line_1.id, new_line_2.id])]
)
self.assertEqual(
len(recs),
2,
"Root should be able to read newly created records",
)
except AccessError:
self.fail("Root should be able to create regardless of access restrictions")
def test_root_unrestricted_unlink_access(self):
"""Test root user unrestricted unlink access"""
# Set most restrictive conditions
self.test_plan.write(
{
"access_level": "3",
"user_ids": [(5, 0, 0)],
"manager_ids": [(5, 0, 0)],
"server_ids": [(6, 0, [self.server_2.id, self.server_3.id])],
}
)
# Create test records
test_lines = self.plan_line.with_user(self.root).create(
[
{
"plan_id": self.test_plan.id,
"command_id": self.command_create_dir.id,
"sequence": seq,
}
for seq in range(400, 403)
]
)
try:
# Test single record unlink
test_lines[0].with_user(self.root).unlink()
# Test multiple record unlink
test_lines[1:].with_user(self.root).unlink()
# Verify records are deleted
recs = self.plan_line.with_user(self.root).search(
[("id", "in", test_lines.ids)]
)
self.assertEqual(
len(recs),
0,
"Root should be able to delete records completely",
)
except AccessError:
self.fail("Root should be able to unlink regardless of access restrictions")
def test_manager_server_based_read_access(self):
"""Test manager read access based on server relationships"""
# Remove direct manager access from plan
self.test_plan.write(
{
"manager_ids": [(5, 0, 0)], # Clear manager_ids
"access_level": "2",
}
)
# Case 1: No servers linked - should have access
recs = self.plan_line.with_user(self.manager).search(
[("id", "=", self.test_line.id)]
)
self.assertIn(
self.test_line,
recs,
"Manager should be able to read when no servers are linked",
)
# Case 2: Server linked but manager not in server's users/managers
self.test_plan.write(
{
"server_ids": [(6, 0, [self.server_2.id])],
}
)
recs = self.plan_line.with_user(self.manager).search(
[("id", "=", self.test_line.id)]
)
self.assertNotIn(
self.test_line,
recs,
"Manager should not be able to read when not in server's users/managers",
)
# Case 3: Manager in server's user_ids
self.server_2.write(
{
"user_ids": [(6, 0, [self.manager.id])],
}
)
recs = self.plan_line.with_user(self.manager).search(
[("id", "=", self.test_line.id)]
)
self.assertIn(
self.test_line,
recs,
"Manager should be able to read when in server's user_ids",
)
# Case 4: Manager in server's manager_ids
self.server_2.write(
{
"user_ids": [(5, 0, 0)],
"manager_ids": [(6, 0, [self.manager.id])],
}
)
recs = self.plan_line.with_user(self.manager).search(
[("id", "=", self.test_line.id)]
)
self.assertIn(
self.test_line,
recs,
"Manager should be able to read when in server's manager_ids",
)
# Case 5: Multiple servers - access through one server
self.test_plan.write(
{
"server_ids": [(6, 0, [self.server_2.id, self.server_3.id])],
}
)
recs = self.plan_line.with_user(self.manager).search(
[("id", "=", self.test_line.id)]
)
self.assertIn(
self.test_line,
recs,
"Manager should be able to read when in at least one server's manager_ids",
)
# Case 6: Multiple servers - no access
self.server_2.write(
{
"manager_ids": [(5, 0, 0)],
}
)
recs = self.plan_line.with_user(self.manager).search(
[("id", "=", self.test_line.id)]
)
self.assertNotIn(
self.test_line,
recs,
"Manager should not be able to read when not "
"in any server's users/managers",
)
def test_manager_server_based_write_access(self):
"""Test manager write access based on server relationships"""
# Remove direct manager access from plan
self.test_plan.write(
{
"manager_ids": [(5, 0, 0)], # Clear manager_ids
"access_level": "2",
"server_ids": [(6, 0, [self.server_2.id])],
}
)
# Case 1: No server access - should not be able to write
with self.assertRaises(AccessError):
self.test_line.with_user(self.manager).write({"sequence": 40})
# Case 2: Manager in server's manager_ids - still should not be able to write
self.server_2.write(
{
"manager_ids": [(6, 0, [self.manager.id])],
}
)
with self.assertRaises(AccessError):
self.test_line.with_user(self.manager).write({"sequence": 50})
# Case 3: Manager in plan's manager_ids - should be able to write
self.test_plan.write(
{
"manager_ids": [(6, 0, [self.manager.id])],
}
)
try:
self.test_line.with_user(self.manager).write({"sequence": 60})
except AccessError:
self.fail("Manager should be able to write when in plan's manager_ids")

View File

@@ -0,0 +1,255 @@
# Copyright (C) 2025 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.exceptions import AccessError
from .common import TestTowerCommon
class TestTowerPlanLineAction(TestTowerCommon):
"""Test the cx.tower.plan.line.action model access rights."""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create a test server
cls.server = cls.Server.create(
{
"name": "Test Server",
"ip_v4_address": "localhost",
"ssh_username": "test",
"ssh_password": "test",
"ssh_port": 22,
"user_ids": [(6, 0, [cls.user.id])],
"manager_ids": [(6, 0, [cls.manager.id])],
}
)
# Create a test plan with access level 1 for user tests
cls.test_plan = cls.Plan.create(
{
"name": "Test Access Plan",
"access_level": "1",
"user_ids": [(6, 0, [cls.user.id])],
"manager_ids": [(6, 0, [cls.manager.id])],
}
)
# Create a test plan line
cls.test_plan_line = cls.plan_line.create(
{
"plan_id": cls.test_plan.id,
"command_id": cls.command_create_dir.id,
"sequence": 10,
}
)
# Create a test action
cls.test_action = cls.plan_line_action.create(
{
"line_id": cls.test_plan_line.id,
"condition": "==",
"value_char": "0",
"action": "n",
}
)
def test_user_read_access(self):
"""Test user read access to plan line actions"""
# Case 1: User should be able to read action when:
# - access_level == "1"
# - user is in plan's user_ids OR server's user_ids
recs = self.plan_line_action.with_user(self.user).search(
[("id", "=", self.test_action.id)]
)
self.assertIn(
self.test_action,
recs,
"User should be able to read action when conditions are met",
)
# Case 2: User should not be able to read when access_level > "1"
self.test_plan.access_level = "2"
recs = self.plan_line_action.with_user(self.user).search(
[("id", "=", self.test_action.id)]
)
self.assertNotIn(
self.test_action,
recs,
"User should not be able to read action when access_level > '1'",
)
# Case 3: User should not be able to read when not in user_ids
self.test_plan.access_level = "1"
self.test_plan.user_ids = [(5, 0, 0)] # Remove all users
recs = self.plan_line_action.with_user(self.user).search(
[("id", "=", self.test_action.id)]
)
self.assertNotIn(
self.test_action,
recs,
"User should not be able to read action when not in user_ids",
)
# Case 4: User should be able to read when in server's user_ids
self.test_plan.server_ids = [(6, 0, [self.server.id])]
recs = self.plan_line_action.with_user(self.user).search(
[("id", "=", self.test_action.id)]
)
self.assertIn(
self.test_action,
recs,
"User should be able to read action when in server's user_ids",
)
def test_user_write_create_unlink_access(self):
"""Test user write/create/unlink access restrictions"""
# Users should not be able to create actions
with self.assertRaises(AccessError):
self.plan_line_action.with_user(self.user).create(
{
"line_id": self.test_plan_line.id,
"condition": "==",
"value_char": "0",
"action": "n",
}
)
# Users should not be able to write actions
with self.assertRaises(AccessError):
self.test_action.with_user(self.user).write({"value_char": "1"})
# Users should not be able to unlink actions
with self.assertRaises(AccessError):
self.test_action.with_user(self.user).unlink()
def test_manager_read_access(self):
"""Test manager read access to plan line actions"""
# Case 1: Manager should be able to read when:
# - access_level <= "2"
# - manager is in plan's manager_ids
recs = self.plan_line_action.with_user(self.manager).search(
[("id", "=", self.test_action.id)]
)
self.assertIn(
self.test_action,
recs,
"Manager should be able to read action when conditions are met",
)
# Case 2: Manager should not be able to read when access_level > "2"
self.test_plan.access_level = "3"
recs = self.plan_line_action.with_user(self.manager).search(
[("id", "=", self.test_action.id)]
)
self.assertNotIn(
self.test_action,
recs,
"Manager should not be able to read action when access_level > '2'",
)
# Case 3: Manager should be able to read when in server's manager_ids
self.test_plan.access_level = "2"
self.test_plan.manager_ids = [(5, 0, 0)] # Remove all managers
self.test_plan.server_ids = [(6, 0, [self.server.id])]
recs = self.plan_line_action.with_user(self.manager).search(
[("id", "=", self.test_action.id)]
)
self.assertIn(
self.test_action,
recs,
"Manager should be able to read when in server's manager_ids",
)
def test_manager_write_create_access(self):
"""Test manager write/create access to plan line actions"""
# Case 1: Manager should be able to create/write when:
# - access_level <= "2"
# - manager is in plan's manager_ids
try:
# Test create
self.plan_line_action.with_user(self.manager).create(
{
"line_id": self.test_plan_line.id,
"condition": "==",
"value_char": "1",
"action": "n",
}
)
# Test write
self.test_action.with_user(self.manager).write({"value_char": "2"})
except AccessError:
self.fail("Manager should be able to create/write when conditions are met")
# Case 2: Manager should not be able to create/write when access_level > "2"
self.test_plan.access_level = "3"
with self.assertRaises(AccessError):
self.plan_line_action.with_user(self.manager).create(
{
"line_id": self.test_plan_line.id,
"condition": "==",
"value_char": "1",
"action": "n",
}
)
with self.assertRaises(AccessError):
self.test_action.with_user(self.manager).write({"value_char": "3"})
def test_manager_unlink_access(self):
"""Test manager unlink access to plan line actions"""
# Create action as manager to test unlink rights
action = self.plan_line_action.with_user(self.manager).create(
{
"line_id": self.test_plan_line.id,
"condition": "==",
"value_char": "0",
"action": "n",
}
)
# Case 1: Manager should be able to unlink when:
# - access_level <= "2"
# - manager is the creator
# - manager is in plan's manager_ids
try:
action.unlink()
except AccessError:
self.fail("Manager should be able to unlink when conditions are met")
# Case 2: Manager should not be able to unlink actions created by others
action = self.test_action # Created by admin in setUp
with self.assertRaises(AccessError):
action.with_user(self.manager).unlink()
def test_root_unrestricted_access(self):
"""Test root user unrestricted access"""
# Root should have full access regardless of conditions
try:
# Test read
recs = self.plan_line_action.with_user(self.root).search(
[("id", "=", self.test_action.id)]
)
self.assertIn(
self.test_action,
recs,
"Root should be able to read action without restrictions",
)
# Test create
new_action = self.plan_line_action.with_user(self.root).create(
{
"line_id": self.test_plan_line.id,
"condition": "==",
"value_char": "1",
"action": "n",
}
)
# Test write
self.test_action.with_user(self.root).write({"value_char": "2"})
# Test unlink
new_action.unlink()
except AccessError:
self.fail("Root user should have unrestricted access")

View File

@@ -0,0 +1,274 @@
# Copyright (C) 2025 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields
from odoo.exceptions import AccessError
from .common import TestTowerCommon
class TestTowerPlanLog(TestTowerCommon):
"""Test the cx.tower.plan.log model access rights."""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create plans with different access levels
cls.plan_level_1 = cls.Plan.create(
{
"name": "Test Plan L1",
"access_level": "1",
}
)
cls.plan_level_2 = cls.Plan.create(
{
"name": "Test Plan L2",
"access_level": "2",
}
)
cls.plan_level_3 = cls.Plan.create(
{
"name": "Test Plan L3",
"access_level": "3",
}
)
# Create test plan logs with specific users
cls.plan_log_1 = (
cls.PlanLog.with_user(cls.user)
.sudo()
.create(
{
"server_id": cls.server_test_1.id,
"plan_id": cls.plan_level_1.id,
"start_date": fields.Datetime.now(),
}
)
)
cls.plan_log_2 = (
cls.PlanLog.with_user(cls.manager)
.sudo()
.create(
{
"server_id": cls.server_test_1.id,
"plan_id": cls.plan_level_1.id,
"start_date": fields.Datetime.now(),
}
)
)
# Create additional server for testing
cls.server_2 = cls.Server.create(
{
"name": "Test Server 2",
"ip_v4_address": "localhost",
"ssh_username": "test2",
"ssh_password": "test2",
"ssh_port": 22,
"user_ids": [(6, 0, [])],
"manager_ids": [(6, 0, [])],
}
)
def test_user_read_access(self):
"""Test user read access to plan logs"""
# Add user to server's user_ids to isolate creator check
self.server_test_1.write(
{
"user_ids": [(6, 0, [self.user.id])],
}
)
# Case 1: User should be able to read when:
# - access_level == "1"
# - created by user
# - user is in server's user_ids
recs = self.PlanLog.with_user(self.user).search(
[("id", "in", [self.plan_log_1.id, self.plan_log_2.id])]
)
self.assertEqual(
len(recs),
1,
"User should only be able to read their own logs",
)
self.assertIn(
self.plan_log_1,
recs,
"User should be able to read own logs when conditions are met",
)
self.assertNotIn(
self.plan_log_2,
recs,
"User should not be able to read logs created by others",
)
# Case 2: User should not be able to read when not in server's user_ids
self.server_test_1.write(
{
"user_ids": [(5, 0, 0)], # Remove all users
}
)
recs = self.PlanLog.with_user(self.user).search(
[("id", "=", self.plan_log_1.id)]
)
self.assertNotIn(
self.plan_log_1,
recs,
"User should not be able to read when not in server's user_ids",
)
# Case 3: User should not be able to read when access_level > "1"
self.server_test_1.write(
{
"user_ids": [(6, 0, [self.user.id])],
}
)
high_access_log = (
self.PlanLog.with_user(self.user)
.sudo()
.create(
{
"server_id": self.server_test_1.id,
"plan_id": self.plan_level_2.id,
"start_date": fields.Datetime.now(),
}
)
)
recs = self.PlanLog.with_user(self.user).search(
[("id", "=", high_access_log.id)]
)
self.assertNotIn(
high_access_log,
recs,
"User should not be able to read logs with access_level > '1'"
" even if created by them",
)
def test_manager_read_access(self):
"""Test manager read access to plan logs"""
# Case 1: Manager should be able to read when:
# - access_level <= "2"
# - manager is in server's manager_ids
self.server_test_1.write(
{
"manager_ids": [(6, 0, [self.manager.id])],
}
)
recs = self.PlanLog.with_user(self.manager).search(
[("id", "in", [self.plan_log_1.id, self.plan_log_2.id])]
)
self.assertEqual(
len(recs),
2,
"Manager should be able to read all logs when in server's manager_ids",
)
# Case 2: Manager should be able to read when in server's user_ids
self.server_test_1.write(
{
"manager_ids": [(5, 0, 0)], # Remove all managers
"user_ids": [(6, 0, [self.manager.id])],
}
)
recs = self.PlanLog.with_user(self.manager).search(
[("id", "in", [self.plan_log_1.id, self.plan_log_2.id])]
)
self.assertEqual(
len(recs),
2,
"Manager should be able to read all logs when in server's user_ids",
)
# Case 3: Manager should not be able to read when access_level > "2"
high_access_log = (
self.PlanLog.with_user(self.manager)
.sudo()
.create(
{
"server_id": self.server_test_1.id,
"plan_id": self.plan_level_3.id,
"start_date": fields.Datetime.now(),
}
)
)
recs = self.PlanLog.with_user(self.manager).search(
[("id", "=", high_access_log.id)]
)
self.assertNotIn(
high_access_log,
recs,
"Manager should not be able to read logs with access_level > '2'",
)
# Case 4: Manager should not be able to read when he is not
# in users_ids or manager_ids
self.server_test_1.write(
{
"user_ids": [(5, 0, 0)],
"manager_ids": [(5, 0, 0)],
}
)
recs = self.PlanLog.with_user(self.manager).search(
[("id", "in", [self.plan_log_1.id, self.plan_log_2.id])]
)
self.assertNotIn(
self.plan_log_1,
recs,
"Manager should not be able to read logs when he is not"
" in users_ids or manager_ids",
)
def test_root_read_only_access(self):
"""Root can read all plan logs, but cannot create/modify/delete"""
# Create test logs with sudo()
test_logs = self.PlanLog.sudo().create(
[
{
"server_id": self.server_2.id,
"plan_id": plan.id,
"start_date": fields.Datetime.now(),
}
for plan in [self.plan_level_1, self.plan_level_2, self.plan_level_3]
]
)
# Root should be able to read all logs regardless of:
# - access_level
# - server relationships
# - who created them
recs = self.PlanLog.with_user(self.root).search([("id", "in", test_logs.ids)])
self.assertEqual(
len(recs),
3,
"Root should have unrestricted read access to all logs",
)
# Root can't create logs
with self.assertRaises(AccessError):
self.PlanLog.with_user(self.root).create(
{
"server_id": self.server_2.id,
"plan_id": self.plan_level_1.id,
"start_date": fields.Datetime.now(),
}
)
# Root cannot modify logs
with self.assertRaises(AccessError):
test_logs.with_user(self.root).write({"start_date": fields.Datetime.now()})
# Root cannot delete logs
with self.assertRaises(AccessError):
test_logs.with_user(self.root).unlink()
# Test read on all records
all_recs = self.PlanLog.with_user(self.root).search([])
self.assertGreater(
len(all_recs),
0,
"Root should be able to read all plan logs",
)

View File

@@ -0,0 +1,310 @@
import re
from .common import TestTowerCommon
class TestTowerReference(TestTowerCommon):
"""Test reference generation.
We are using ServerTemplate for that.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.plan_test_mixin = cls.Plan.create(
{"name": "Test Plan reference mixin", "note": "Test Note reference mixin"}
)
cls.plan_line_reference_mixin = cls.plan_line.create(
{
"plan_id": cls.plan_test_mixin.id,
"sequence": 1,
"command_id": cls.command_list_dir.id,
}
)
def test_reference_generation(self):
"""Test reference generation"""
# --- 1 ---
# Check if auto generated reference matches the pattern
reference_pattern = self.ServerTemplate._get_reference_pattern()
self.assertTrue(
re.match(rf"{reference_pattern}", self.server_template_sample.reference),
"Reference doesn't match template",
)
# --- 2 ---
# Create a new server template with custom reference
# and ensure that it's fixed according to the pattern
new_template = self.ServerTemplate.create(
{"name": "Such Much Template", "reference": " Some reference x*((*)) "}
)
self.assertEqual(new_template.reference, "some_reference_x")
# --- 3 ---
# Try to create another server template with the same reference and ensure
# that its reference is corrected automatically
yet_another_template = self.ServerTemplate.create(
{"name": "Yet another template", "reference": "some_reference_x"}
)
self.assertEqual(yet_another_template.reference, "some_reference_x_2")
# -- 4 ---
# Duplicate the server template and ensure that its name and reference
# are generated properly
yet_another_template_copy = yet_another_template.copy()
self.assertEqual(yet_another_template_copy.name, "Yet another template (copy)")
self.assertEqual(
yet_another_template_copy.reference, "yet_another_template_copy"
)
# -- 5 ---
# Update reference and ensure that updated value is correct
yet_another_template_copy.write({"reference": " Some reference x*((*)) "})
self.assertEqual(yet_another_template_copy.reference, "some_reference_x_3")
# -- 6 ---
# Update template with a new name and remove reference simultaneously
yet_another_template_copy.write({"name": "Doge so like", "reference": False})
self.assertEqual(yet_another_template_copy.reference, "doge_so_like")
# -- 7 ---
# Rename the template and ensure reference is not affected
yet_another_template_copy.write({"name": "Chad"})
self.assertEqual(yet_another_template_copy.reference, "doge_so_like")
# -- 8 ---
# Remove the reference and ensure it's regenerated from the name
yet_another_template_copy.write({"reference": False})
self.assertEqual(yet_another_template_copy.reference, "chad")
# -- 9 --
# Update record with the same reference name and ensure it remains the same
yet_another_template_copy.write({"reference": "chad"})
self.assertEqual(yet_another_template_copy.reference, "chad")
# -- 10 --
# Create new template with reference set to False
expected_reference = self.ServerTemplate._generate_or_fix_reference(
"Such Much False Template"
)
new_template_with_false = self.ServerTemplate.create(
{"name": "Such Much False Template", "reference": False}
)
self.assertEqual(
new_template_with_false.reference,
expected_reference,
"Reference doesn't match expected one",
)
# -- 11 --
# Create new template with reference and name set to a non valid symbol
# Generic model reference should be used as a reference
expected_reference = self.ServerTemplate._get_model_generic_reference()
new_template_with_non_valid_reference = self.ServerTemplate.create(
{"name": "/", "reference": "/"}
)
self.assertEqual(
new_template_with_non_valid_reference.reference,
expected_reference,
"Reference doesn't match expected one",
)
def test_search_by_reference(self):
"""Search record by its reference"""
# Create a new server template with custom reference
server_template = self.ServerTemplate.create(
{"name": "Such Much Template", "reference": "such_much_template"}
)
# Search using correct template reference
search_result = self.ServerTemplate.get_by_reference("such_much_template")
self.assertEqual(server_template, search_result, "Template must be found")
# Search using malformed (case sensitive)
search_result = self.ServerTemplate.get_by_reference("not_much_template")
self.assertEqual(len(search_result), 0, "Result should be empty")
def test_prepare_references_valid_input(self):
"""
Ensure references are correctly prepared for valid input.
"""
vals_list = [{"plan_id": self.plan_test_mixin.id}]
result = self.plan_line._prepare_references(
"cx.tower.plan", "plan_id", vals_list
)
# Verify the result contains the expected reference
self.assertIn(
self.plan_test_mixin.id,
result,
"The reference ID should be in the result.",
)
self.assertEqual(
result[self.plan_test_mixin.id],
self.plan_test_mixin.reference,
"The reference should match the expected value.",
)
def test_prepare_references_invalid_model_name(self):
"""
Check that an error is raised for an invalid model name.
"""
vals_list = [{"plan_id": self.plan_test_mixin.id}]
with self.assertRaises(ValueError) as cm:
self.plan_line._prepare_references("invalid.model", "plan_id", vals_list)
# Confirm the exception message is as expected
self.assertEqual(
str(cm.exception),
"Model 'invalid.model' does not exist. Please provide a valid model name.",
"The error message should indicate an invalid model name.",
)
def test_prepare_references_empty_vals_list(self):
"""
Verify that an empty vals_list returns an empty dictionary.
"""
result = self.plan_line._prepare_references("cx.tower.plan", "plan_id", [])
self.assertEqual(
result,
{},
"The result should be an empty dictionary when vals_list is empty.",
)
def test_populate_references_with_valid_input(self):
"""
Ensure references are populated correctly in the provided values list.
"""
vals_list = [{"plan_id": self.plan_test_mixin.id}]
updated_vals = self.plan_line._pre_populate_references(
"cx.tower.plan", "plan_id", vals_list
)
# Check the updated values contain the expected reference format
self.assertEqual(
updated_vals[0]["reference"],
f"{self.plan_test_mixin.reference}_plan_line_1",
"The reference should be correctly populated with the suffix.",
)
def test_populate_references_missing_field(self):
"""
Confirm that entries missing the required field are handled properly.
"""
vals_list_with_missing_field = [{"another_key": 123}]
updated_vals_with_missing = self.plan_line._pre_populate_references(
"cx.tower.plan", "plan_id", vals_list_with_missing_field
)
self.assertEqual(
updated_vals_with_missing[0]["reference"],
"no_plan_line_1",
"Entries missing the required field should have a default reference.",
)
def test_populate_references_duplicate_ids(self):
"""
Ensure that duplicate IDs in the input list are correctly
handled and referenced.
"""
vals_list = [
{"plan_id": self.plan_test_mixin.id},
{"plan_id": self.plan_test_mixin.id},
]
updated_vals = self.plan_line._pre_populate_references(
"cx.tower.plan", "plan_id", vals_list
)
# Verify that each duplicate entry has a unique suffix
self.assertEqual(
updated_vals[0]["reference"],
f"{self.plan_test_mixin.reference}_plan_line_1",
"The first duplicate reference should have the correct suffix.",
)
self.assertEqual(
updated_vals[1]["reference"],
f"{self.plan_test_mixin.reference}_plan_line_2",
"The second duplicate reference should have the correct suffix.",
)
def test_populate_references_empty_vals_list(self):
"""
Check that an empty input list returns an empty result
when populating references.
"""
updated_vals = self.plan_line._pre_populate_references(
"cx.tower.plan", "plan_id", []
)
self.assertEqual(
updated_vals,
[],
"The result should be an empty list when vals_list is empty.",
)
def test_populate_references_reference_present(self):
"""
Check that reference is preserver when present in vals
"""
vals_list = [
{"reference": "my_custom_line_1"},
{"reference": "my_custom_line_2"},
]
updated_vals = self.plan_line._pre_populate_references(
"cx.tower.plan", "plan_id", vals_list
)
self.assertEqual(
updated_vals[0]["reference"],
"my_custom_line_1",
"Original reference must be preserved",
)
self.assertEqual(
updated_vals[1]["reference"],
"my_custom_line_2",
"Original reference must be preserved",
)
def test_populate_references_mixed_scenarios(self):
"""Test mixed scenarios with existing and missing references"""
vals_list = [
{"reference": "my_custom_line_1"},
{"plan_id": self.plan_test_mixin.id}, # No reference
{"reference": " "}, # Whitespace reference
{"reference": ""}, # Empty reference
{"reference": "\n_"}, # Some irrelevant symbols
]
updated_vals = self.plan_line._pre_populate_references(
"cx.tower.plan", "plan_id", vals_list
)
self.assertEqual(
updated_vals[0]["reference"],
"my_custom_line_1",
"Original reference must be preserved",
)
self.assertEqual(
updated_vals[1]["reference"],
f"{self.plan_test_mixin.reference}_plan_line_1",
"Missing reference should be generated",
)
self.assertEqual(
updated_vals[2]["reference"],
"no_plan_line_1",
"Missing reference should be generated",
)
self.assertEqual(
updated_vals[3]["reference"],
"no_plan_line_2",
"Missing reference should be generated",
)
self.assertEqual(
updated_vals[4]["reference"],
"no_plan_line_3",
"Missing reference should be generated",
)

View File

@@ -0,0 +1,893 @@
# Copyright (C) 2025 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import datetime
from odoo import fields
from odoo.exceptions import AccessError, ValidationError
from .common import TestTowerCommon
class TestTowerScheduledTask(TestTowerCommon):
"""Test the cx.tower.scheduled.task model."""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create an additional server for multi-server command test
cls.server_test_2 = cls.Server.create(
{
"name": "Test 2",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"host_key": "test_key",
"os_id": cls.os_debian_10.id,
}
)
# Scheduled task: command (multi-server)
cls.command_scheduled_task = cls.ScheduledTask.create(
{
"name": "Test Command Scheduled Task",
"action": "command",
"command_id": cls.command_list_dir.id,
"interval_number": 1,
"interval_type": "days",
"next_call": fields.Datetime.now(),
"server_ids": [(6, 0, [cls.server_test_1.id, cls.server_test_2.id])],
}
)
# Scheduled task: plan (single server)
cls.plan_scheduled_task = cls.ScheduledTask.create(
{
"name": "Test Plan Scheduled Task",
"action": "plan",
"plan_id": cls.plan_1.id,
"interval_number": 1,
"interval_type": "days",
"next_call": fields.Datetime.now(),
"server_ids": [(6, 0, [cls.server_test_1.id])],
}
)
# Custom variable for task (option type)
cls.variable_odoo_versions = cls.Variable.create(
{
"name": "odoo_versions",
"variable_type": "o",
}
)
cls.variable_option_16_0 = cls.VariableOption.create(
{
"name": "16.0",
"value_char": "16.0",
"variable_id": cls.variable_odoo_versions.id,
}
)
# Add custom variables to tasks
cls.scheduled_task_cv_os = cls.ScheduledTaskCv.create(
{
"scheduled_task_id": cls.command_scheduled_task.id,
"variable_id": cls.variable_os.id,
"value_char": "Windows 2k",
}
)
cls.scheduled_task_cv_version = cls.ScheduledTaskCv.create(
{
"scheduled_task_id": cls.command_scheduled_task.id,
"variable_id": cls.variable_odoo_versions.id,
"option_id": cls.variable_option_16_0.id,
}
)
cls.scheduled_task_cv_version_plan = cls.ScheduledTaskCv.create(
{
"scheduled_task_id": cls.plan_scheduled_task.id,
"variable_id": cls.variable_odoo_versions.id,
"option_id": cls.variable_option_16_0.id,
}
)
# Create additional Jet Template for access testing
cls.jet_template_test_access = cls.JetTemplate.create(
{
"name": "Test Jet Template for Access",
"server_ids": [(4, cls.server_test_1.id)],
}
)
# Create additional Jet for access testing
cls.jet_test_access = cls.Jet.create(
{
"name": "Test Jet for Access",
"jet_template_id": cls.jet_template_test_access.id,
"server_id": cls.server_test_1.id,
}
)
# Scheduled task with Jet and Jet Template for access testing
cls.jet_scheduled_task = cls.ScheduledTask.create(
{
"name": "Test Jet Scheduled Task",
"action": "command",
"command_id": cls.command_list_dir.id,
"interval_number": 1,
"interval_type": "days",
"next_call": fields.Datetime.now(),
"jet_ids": [(6, 0, [cls.jet_test_access.id])],
"jet_template_ids": [(6, 0, [cls.jet_template_test_access.id])],
}
)
def _assert_log_records(self, log_model, scheduled_task, expected_count):
"""Helper: Assert that log records exist for the task"""
logs = log_model.search([("scheduled_task_id", "=", scheduled_task.id)])
self.assertTrue(logs, f"{log_model._name} logs should be created after run.")
self.assertEqual(
len(logs),
expected_count,
f"Expected {expected_count} logs for {scheduled_task.display_name}, "
f"got {len(logs)}.",
)
def _assert_next_and_last_call_changed(
self, task, last_call_before, next_call_before
):
"""Helper: Assert next_call and last_call changed after run"""
task.invalidate_recordset()
self.assertNotEqual(
task.last_call, last_call_before, "last_call must be changed after run."
)
self.assertNotEqual(
task.next_call, next_call_before, "next_call must be changed after run."
)
def _clear_all_access(
self,
scheduled_task,
jet=None,
jet_template=None,
server=None,
server_template=None,
):
"""Helper: Clear all access paths for a scheduled task and related objects."""
scheduled_task.manager_ids = [(5, 0, 0)]
scheduled_task.user_ids = [(5, 0, 0)]
if jet:
jet.manager_ids = [(5, 0, 0)]
jet.user_ids = [(5, 0, 0)]
if jet_template:
jet_template.manager_ids = [(5, 0, 0)]
jet_template.user_ids = [(5, 0, 0)]
if server:
server.manager_ids = [(5, 0, 0)]
server.user_ids = [(5, 0, 0)]
if server_template:
server_template.manager_ids = [(5, 0, 0)]
server_template.user_ids = [(5, 0, 0)]
def test_reserve_tasks_atomic(self):
"""Scheduled Task: reserve_tasks must only lock available"""
tasks = self.command_scheduled_task + self.plan_scheduled_task
reserved = tasks._reserve_tasks()
self.assertEqual(
set(reserved.ids), set(tasks.ids), "Both tasks should be reserved"
)
# Repeated reservation should return empty (already running)
tasks.invalidate_recordset()
reserved_again = tasks._reserve_tasks()
self.assertFalse(
reserved_again, "Already reserved tasks must not be reserved again"
)
def test_run_task_command(self):
"""Running a scheduled command task creates logs per server."""
logs_before = self.CommandLog.search(
[("scheduled_task_id", "=", self.command_scheduled_task.id)]
)
self.assertFalse(logs_before, "No command logs should exist before run.")
last_call_before = self.command_scheduled_task.last_call
next_call_before = self.command_scheduled_task.next_call
self.command_scheduled_task._run()
self._assert_next_and_last_call_changed(
self.command_scheduled_task, last_call_before, next_call_before
)
self._assert_log_records(
self.CommandLog,
self.command_scheduled_task,
expected_count=len(self.command_scheduled_task.server_ids),
)
def test_run_task_plan(self):
"""Running a scheduled plan task creates one log per server."""
logs_before = self.PlanLog.search(
[("scheduled_task_id", "=", self.plan_scheduled_task.id)]
)
self.assertFalse(logs_before, "No plan logs should exist before run.")
last_call_before = self.plan_scheduled_task.last_call
next_call_before = self.plan_scheduled_task.next_call
self.plan_scheduled_task._run()
self._assert_next_and_last_call_changed(
self.plan_scheduled_task, last_call_before, next_call_before
)
self._assert_log_records(
self.PlanLog,
self.plan_scheduled_task,
expected_count=len(self.plan_scheduled_task.server_ids),
)
def test_user_write_create_unlink_access(self):
"""User: cannot create, write or unlink scheduled tasks."""
with self.assertRaises(AccessError):
self.ScheduledTask.with_user(self.user).create(
{
"name": "Test",
"action": "command",
"command_id": self.command_list_dir.id,
"server_ids": [(6, 0, [self.server_test_1.id])],
}
)
with self.assertRaises(AccessError):
self.command_scheduled_task.with_user(self.user).write({"sequence": 33})
with self.assertRaises(AccessError):
self.command_scheduled_task.with_user(self.user).unlink()
def test_manager_read_access(self):
"""Manager: can read scheduled task if in manager_ids or in server's
manager_ids/user_ids."""
self.command_scheduled_task.manager_ids = [(6, 0, [self.manager.id])]
tasks = self.ScheduledTask.with_user(self.manager).search(
[("id", "=", self.command_scheduled_task.id)]
)
self.assertIn(
self.command_scheduled_task,
tasks,
"Manager should be able to read their task.",
)
# Remove from manager_ids, but add to server manager_ids
self.command_scheduled_task.manager_ids = [(5, 0, 0)]
self.server_test_1.manager_ids = [(6, 0, [self.manager.id])]
tasks = self.ScheduledTask.with_user(self.manager).search(
[("id", "=", self.command_scheduled_task.id)]
)
self.assertIn(
self.command_scheduled_task,
tasks,
"Manager should be able to read task via server manager_ids.",
)
# Test server user_ids access
self.server_test_1.manager_ids = [(5, 0, 0)]
self.server_test_1.user_ids = [(6, 0, [self.manager.id])]
tasks = self.ScheduledTask.with_user(self.manager).search(
[("id", "=", self.command_scheduled_task.id)]
)
self.assertIn(
self.command_scheduled_task,
tasks,
"Manager should be able to read task via server user_ids.",
)
# Remove manager from everywhere
self._clear_all_access(self.command_scheduled_task, server=self.server_test_1)
tasks = self.ScheduledTask.with_user(self.manager).search(
[("id", "=", self.command_scheduled_task.id)]
)
self.assertNotIn(
self.command_scheduled_task,
tasks,
"Manager should NOT be able to read task without relation.",
)
def test_manager_read_access_via_jet(self):
"""Manager: can read scheduled task if in jet's user_ids/manager_ids."""
# Test access via jet manager_ids
self.jet_test_access.manager_ids = [(6, 0, [self.manager.id])]
tasks = self.ScheduledTask.with_user(self.manager).search(
[("id", "=", self.jet_scheduled_task.id)]
)
self.assertIn(
self.jet_scheduled_task,
tasks,
"Manager should be able to read task via jet manager_ids.",
)
# Test access via jet user_ids
self.jet_test_access.manager_ids = [(5, 0, 0)]
self.jet_test_access.user_ids = [(6, 0, [self.manager.id])]
tasks = self.ScheduledTask.with_user(self.manager).search(
[("id", "=", self.jet_scheduled_task.id)]
)
self.assertIn(
self.jet_scheduled_task,
tasks,
"Manager should be able to read task via jet user_ids.",
)
# Test access via jet_template manager_ids
self.jet_test_access.user_ids = [(5, 0, 0)]
self.jet_template_test_access.manager_ids = [(6, 0, [self.manager.id])]
tasks = self.ScheduledTask.with_user(self.manager).search(
[("id", "=", self.jet_scheduled_task.id)]
)
self.assertIn(
self.jet_scheduled_task,
tasks,
"Manager should be able to read task via jet_template manager_ids.",
)
# Test access via jet_template user_ids
self.jet_template_test_access.manager_ids = [(5, 0, 0)]
self.jet_template_test_access.user_ids = [(6, 0, [self.manager.id])]
tasks = self.ScheduledTask.with_user(self.manager).search(
[("id", "=", self.jet_scheduled_task.id)]
)
self.assertIn(
self.jet_scheduled_task,
tasks,
"Manager should be able to read task via jet_template user_ids.",
)
# Remove manager from everywhere
self._clear_all_access(
self.jet_scheduled_task,
jet=self.jet_test_access,
jet_template=self.jet_template_test_access,
server=self.server_test_1,
)
tasks = self.ScheduledTask.with_user(self.manager).search(
[("id", "=", self.jet_scheduled_task.id)]
)
self.assertNotIn(
self.jet_scheduled_task,
tasks,
"Manager should NOT be able to read task without relation.",
)
def test_manager_read_access_via_server_template(self):
"""Manager: can read scheduled task if in server_template's
user_ids/manager_ids."""
# Create scheduled task with server template
server_template_task = self.ScheduledTask.create(
{
"name": "Test Server Template Scheduled Task",
"action": "command",
"command_id": self.command_list_dir.id,
"interval_number": 1,
"interval_type": "days",
"next_call": fields.Datetime.now(),
"server_template_ids": [(6, 0, [self.server_template_sample.id])],
}
)
# Test access via server_template manager_ids
self.server_template_sample.manager_ids = [(6, 0, [self.manager.id])]
tasks = self.ScheduledTask.with_user(self.manager).search(
[("id", "=", server_template_task.id)]
)
self.assertIn(
server_template_task,
tasks,
"Manager should be able to read task via server_template manager_ids.",
)
# Test access via server_template user_ids
self.server_template_sample.manager_ids = [(5, 0, 0)]
self.server_template_sample.user_ids = [(6, 0, [self.manager.id])]
tasks = self.ScheduledTask.with_user(self.manager).search(
[("id", "=", server_template_task.id)]
)
self.assertIn(
server_template_task,
tasks,
"Manager should be able to read task via server_template user_ids.",
)
# Remove manager from everywhere
self._clear_all_access(
server_template_task,
server_template=self.server_template_sample,
server=self.server_test_1,
)
tasks = self.ScheduledTask.with_user(self.manager).search(
[("id", "=", server_template_task.id)]
)
self.assertNotIn(
server_template_task,
tasks,
"Manager should NOT be able to read task without relation.",
)
def test_manager_write_create_access(self):
"""Manager: can create/write if in manager_ids, else denied."""
# Create as manager
task = self.ScheduledTask.with_user(self.manager).create(
{
"name": "Test",
"action": "command",
"command_id": self.command_list_dir.id,
"manager_ids": [(6, 0, [self.manager.id])],
"server_ids": [(6, 0, [self.server_test_1.id])],
}
)
try:
task.with_user(self.manager).write({"sequence": 77})
except AccessError:
self.fail("Manager should be able to write their own scheduled tasks.")
# Should fail if not in manager_ids
self.command_scheduled_task.manager_ids = [(5, 0, 0)]
with self.assertRaises(AccessError):
self.command_scheduled_task.with_user(self.manager).write({"sequence": 11})
def test_manager_unlink_access(self):
"""Manager: can unlink only their own tasks (in manager_ids & creator)."""
# Create as manager
task = self.ScheduledTask.with_user(self.manager).create(
{
"name": "Test",
"action": "command",
"command_id": self.command_list_dir.id,
"manager_ids": [(6, 0, [self.manager.id])],
"server_ids": [(6, 0, [self.server_test_1.id])],
}
)
try:
task.with_user(self.manager).unlink()
except AccessError:
self.fail("Manager should be able to unlink their own task.")
# Not creator
with self.assertRaises(AccessError):
self.command_scheduled_task.with_user(self.manager).unlink()
def test_root_unrestricted_access(self):
"""Root: full unrestricted access to all scheduled tasks."""
# Read
tasks = self.ScheduledTask.with_user(self.root).search(
[("id", "=", self.command_scheduled_task.id)]
)
self.assertIn(
self.command_scheduled_task, tasks, "Root should be able to read any task."
)
# Create
task = self.ScheduledTask.with_user(self.root).create(
{
"name": "Test",
"action": "command",
"command_id": self.command_list_dir.id,
"server_ids": [(6, 0, [self.server_test_1.id])],
}
)
try:
task.with_user(self.root).write({"sequence": 123})
task.with_user(self.root).unlink()
except AccessError:
self.fail("Root should be able to write/unlink any scheduled task.")
def test_get_next_call_dow_wednesday(self):
"""Test _get_next_call_dow when today is Wednesday.
Task runs Monday, Wednesday, Friday -> should return Friday."""
# Create task with Monday, Wednesday, Friday selected
task = self.ScheduledTask.create(
{
"name": "Test DOW Task",
"action": "command",
"command_id": self.command_list_dir.id,
"interval_type": "dow",
"monday": True,
"wednesday": True,
"friday": True,
"server_ids": [(6, 0, [self.server_test_1.id])],
}
)
# Create a Wednesday datetime (2024-01-03 is a Wednesday)
# Set time to 10:30:45
wednesday_date = datetime(2024, 1, 3, 10, 30, 45)
# Calculate next call
next_call = task._get_next_call_dow(task, wednesday_date)
# Should be Friday (2 days ahead) at the same time
expected_friday = datetime(2024, 1, 5, 10, 30, 45)
self.assertEqual(
next_call,
expected_friday,
"Next call from Wednesday should be Friday at the same time.",
)
def test_get_next_call_dow_friday(self):
"""Test _get_next_call_dow when today is Friday.
Task runs Monday, Wednesday, Friday -> should return Monday (next week)."""
# Create task with Monday, Wednesday, Friday selected
task = self.ScheduledTask.create(
{
"name": "Test DOW Task",
"action": "command",
"command_id": self.command_list_dir.id,
"interval_type": "dow",
"monday": True,
"wednesday": True,
"friday": True,
"server_ids": [(6, 0, [self.server_test_1.id])],
}
)
# Create a Friday datetime (2024-01-05 is a Friday)
# Set time to 14:15:30
friday_date = datetime(2024, 1, 5, 14, 15, 30)
# Calculate next call
next_call = task._get_next_call_dow(task, friday_date)
# Should be Monday next week (3 days ahead) at the same time
expected_monday = datetime(2024, 1, 8, 14, 15, 30)
self.assertEqual(
next_call,
expected_monday,
"Next call from Friday should be Monday next week at the same time.",
)
def test_check_days_of_week_constraint(self):
"""
Test _check_days_of_week constraint:
no days selected should raise ValidationError.
"""
# Try to create a task with interval_type="dow" but no days selected
with self.assertRaises(ValidationError) as context:
self.ScheduledTask.create(
{
"name": "Test DOW Task No Days",
"action": "command",
"command_id": self.command_list_dir.id,
"interval_type": "dow",
"monday": False,
"tuesday": False,
"wednesday": False,
"thursday": False,
"friday": False,
"saturday": False,
"sunday": False,
"server_ids": [(6, 0, [self.server_test_1.id])],
}
)
self.assertIn(
"At least one day of week must be selected",
str(context.exception),
"ValidationError should mention that at " "least one day must be selected.",
)
# Try to update an existing task to have no days selected
task = self.ScheduledTask.create(
{
"name": "Test DOW Task",
"action": "command",
"command_id": self.command_list_dir.id,
"interval_type": "dow",
"monday": True,
"server_ids": [(6, 0, [self.server_test_1.id])],
}
)
with self.assertRaises(ValidationError):
task.write(
{
"monday": False,
"tuesday": False,
"wednesday": False,
"thursday": False,
"friday": False,
"saturday": False,
"sunday": False,
}
)
def test_get_next_call_dow_single_day_monday(self):
"""Test _get_next_call_dow edge case: only Monday selected,
current day is Monday.
Should wrap to next week's Monday."""
# Create task with only Monday selected
task = self.ScheduledTask.create(
{
"name": "Test DOW Task Single Day",
"action": "command",
"command_id": self.command_list_dir.id,
"interval_type": "dow",
"monday": True,
"server_ids": [(6, 0, [self.server_test_1.id])],
}
)
# Create a Monday datetime (2024-01-01 is a Monday)
# Set time to 09:00:00
monday_date = datetime(2024, 1, 1, 9, 0, 0)
# Calculate next call
next_call = task._get_next_call_dow(task, monday_date)
# Should be Monday next week (7 days ahead) at the same time
expected_next_monday = datetime(2024, 1, 8, 9, 0, 0)
self.assertEqual(
next_call,
expected_next_monday,
"Next call from Monday (only day selected) should be"
" next Monday at the same time.",
)
def test_scheduled_task_cv_manager_read_access(self):
"""Manager: can read scheduled task CV if in scheduled task's
manager_ids/user_ids or via server's manager_ids/user_ids."""
# Test access via scheduled task manager_ids
self.command_scheduled_task.manager_ids = [(6, 0, [self.manager.id])]
cvs = self.ScheduledTaskCv.with_user(self.manager).search(
[("id", "=", self.scheduled_task_cv_os.id)]
)
self.assertIn(
self.scheduled_task_cv_os,
cvs,
"Manager should be able to read CV via scheduled task manager_ids.",
)
# Test access via scheduled task user_ids
self.command_scheduled_task.manager_ids = [(5, 0, 0)]
self.command_scheduled_task.user_ids = [(6, 0, [self.manager.id])]
cvs = self.ScheduledTaskCv.with_user(self.manager).search(
[("id", "=", self.scheduled_task_cv_os.id)]
)
self.assertIn(
self.scheduled_task_cv_os,
cvs,
"Manager should be able to read CV via scheduled task user_ids.",
)
# Test access via server manager_ids
self.command_scheduled_task.user_ids = [(5, 0, 0)]
self.server_test_1.manager_ids = [(6, 0, [self.manager.id])]
cvs = self.ScheduledTaskCv.with_user(self.manager).search(
[("id", "=", self.scheduled_task_cv_os.id)]
)
self.assertIn(
self.scheduled_task_cv_os,
cvs,
"Manager should be able to read CV via server manager_ids.",
)
# Test access via server user_ids
self.server_test_1.manager_ids = [(5, 0, 0)]
self.server_test_1.user_ids = [(6, 0, [self.manager.id])]
cvs = self.ScheduledTaskCv.with_user(self.manager).search(
[("id", "=", self.scheduled_task_cv_os.id)]
)
self.assertIn(
self.scheduled_task_cv_os,
cvs,
"Manager should be able to read CV via server user_ids.",
)
# Remove manager from everywhere
self.server_test_1.user_ids = [(5, 0, 0)]
cvs = self.ScheduledTaskCv.with_user(self.manager).search(
[("id", "=", self.scheduled_task_cv_os.id)]
)
self.assertNotIn(
self.scheduled_task_cv_os,
cvs,
"Manager should NOT be able to read CV without relation.",
)
def test_scheduled_task_cv_manager_read_access_via_jet(self):
"""Manager: can read scheduled task CV if in jet's user_ids/manager_ids."""
# Create CV for jet scheduled task
jet_cv = self.ScheduledTaskCv.create(
{
"scheduled_task_id": self.jet_scheduled_task.id,
"variable_id": self.variable_os.id,
"value_char": "Linux",
}
)
# Test access via jet manager_ids
self.jet_test_access.manager_ids = [(6, 0, [self.manager.id])]
cvs = self.ScheduledTaskCv.with_user(self.manager).search(
[("id", "=", jet_cv.id)]
)
self.assertIn(
jet_cv,
cvs,
"Manager should be able to read CV via jet manager_ids.",
)
# Test access via jet user_ids
self.jet_test_access.manager_ids = [(5, 0, 0)]
self.jet_test_access.user_ids = [(6, 0, [self.manager.id])]
cvs = self.ScheduledTaskCv.with_user(self.manager).search(
[("id", "=", jet_cv.id)]
)
self.assertIn(
jet_cv,
cvs,
"Manager should be able to read CV via jet user_ids.",
)
# Test access via jet_template manager_ids
self.jet_test_access.user_ids = [(5, 0, 0)]
self.jet_template_test_access.manager_ids = [(6, 0, [self.manager.id])]
cvs = self.ScheduledTaskCv.with_user(self.manager).search(
[("id", "=", jet_cv.id)]
)
self.assertIn(
jet_cv,
cvs,
"Manager should be able to read CV via jet_template manager_ids.",
)
# Test access via jet_template user_ids
self.jet_template_test_access.manager_ids = [(5, 0, 0)]
self.jet_template_test_access.user_ids = [(6, 0, [self.manager.id])]
cvs = self.ScheduledTaskCv.with_user(self.manager).search(
[("id", "=", jet_cv.id)]
)
self.assertIn(
jet_cv,
cvs,
"Manager should be able to read CV via jet_template user_ids.",
)
# Remove manager from everywhere
self._clear_all_access(
self.jet_scheduled_task,
jet=self.jet_test_access,
jet_template=self.jet_template_test_access,
server=self.server_test_1,
)
cvs = self.ScheduledTaskCv.with_user(self.manager).search(
[("id", "=", jet_cv.id)]
)
self.assertNotIn(
jet_cv,
cvs,
"Manager should NOT be able to read CV without relation.",
)
def test_scheduled_task_cv_manager_read_access_via_server_template(self):
"""Manager: can read scheduled task CV if in server_template's
user_ids/manager_ids."""
# Create scheduled task with server template
server_template_task = self.ScheduledTask.create(
{
"name": "Test Server Template Scheduled Task for CV",
"action": "command",
"command_id": self.command_list_dir.id,
"interval_number": 1,
"interval_type": "days",
"next_call": fields.Datetime.now(),
"server_template_ids": [(6, 0, [self.server_template_sample.id])],
}
)
server_template_cv = self.ScheduledTaskCv.create(
{
"scheduled_task_id": server_template_task.id,
"variable_id": self.variable_os.id,
"value_char": "Debian",
}
)
# Test access via server_template manager_ids
self.server_template_sample.manager_ids = [(6, 0, [self.manager.id])]
cvs = self.ScheduledTaskCv.with_user(self.manager).search(
[("id", "=", server_template_cv.id)]
)
self.assertIn(
server_template_cv,
cvs,
"Manager should be able to read CV via server_template manager_ids.",
)
# Test access via server_template user_ids
self.server_template_sample.manager_ids = [(5, 0, 0)]
self.server_template_sample.user_ids = [(6, 0, [self.manager.id])]
cvs = self.ScheduledTaskCv.with_user(self.manager).search(
[("id", "=", server_template_cv.id)]
)
self.assertIn(
server_template_cv,
cvs,
"Manager should be able to read CV via server_template user_ids.",
)
# Remove manager from everywhere
self._clear_all_access(
server_template_task,
server_template=self.server_template_sample,
server=self.server_test_1,
)
cvs = self.ScheduledTaskCv.with_user(self.manager).search(
[("id", "=", server_template_cv.id)]
)
self.assertNotIn(
server_template_cv,
cvs,
"Manager should NOT be able to read CV without relation.",
)
def test_scheduled_task_cv_manager_write_create_access(self):
"""Manager: can create/write CV if in scheduled task's manager_ids."""
# Create CV as manager
self.command_scheduled_task.manager_ids = [(6, 0, [self.manager.id])]
cv = self.ScheduledTaskCv.with_user(self.manager).create(
{
"scheduled_task_id": self.command_scheduled_task.id,
"variable_id": self.variable_os.id,
"value_char": "Ubuntu",
}
)
try:
cv.with_user(self.manager).write({"value_char": "Fedora"})
except AccessError:
self.fail(
"Manager should be able to write CV if in scheduled task manager_ids."
)
# Should fail if not in manager_ids
self.command_scheduled_task.manager_ids = [(5, 0, 0)]
with self.assertRaises(AccessError):
self.scheduled_task_cv_os.with_user(self.manager).write(
{"value_char": "CentOS"}
)
def test_scheduled_task_cv_manager_unlink_access(self):
"""Manager: can unlink CV only if in scheduled task's manager_ids & creator."""
# Create CV as manager
self.command_scheduled_task.manager_ids = [(6, 0, [self.manager.id])]
cv = self.ScheduledTaskCv.with_user(self.manager).create(
{
"scheduled_task_id": self.command_scheduled_task.id,
"variable_id": self.variable_os.id,
"value_char": "Arch",
}
)
try:
cv.with_user(self.manager).unlink()
except AccessError:
self.fail("Manager should be able to unlink CV they created.")
# Not creator
self.command_scheduled_task.manager_ids = [(6, 0, [self.manager.id])]
with self.assertRaises(AccessError):
self.scheduled_task_cv_os.with_user(self.manager).unlink()
def test_scheduled_task_cv_root_unrestricted_access(self):
"""Root: full unrestricted access to all scheduled task CVs."""
# Read
cvs = self.ScheduledTaskCv.with_user(self.root).search(
[("id", "=", self.scheduled_task_cv_os.id)]
)
self.assertIn(
self.scheduled_task_cv_os,
cvs,
"Root should be able to read any CV.",
)
# Create
cv = self.ScheduledTaskCv.with_user(self.root).create(
{
"scheduled_task_id": self.command_scheduled_task.id,
"variable_id": self.variable_os.id,
"value_char": "SUSE",
}
)
try:
cv.with_user(self.root).write({"value_char": "OpenSUSE"})
cv.with_user(self.root).unlink()
except AccessError:
self.fail("Root should be able to write/unlink any scheduled task CV.")

View File

@@ -0,0 +1,890 @@
from odoo.exceptions import AccessError, ValidationError
from ..models.constants import COMMAND_NOT_COMPATIBLE_WITH_SERVER
from .common import TestTowerCommon
class TestTowerServer(TestTowerCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.os_ubuntu_20_04 = cls.env["cx.tower.os"].create({"name": "Ubuntu 20.04"})
# Define model variables to avoid unsubscriptable errors
Key = cls.env["cx.tower.key"]
Server = cls.env["cx.tower.server"]
secret_1 = Key.create(
{
"name": "Secret 1",
"secret_value": "secret_value_1",
"key_type": "s",
},
)
secret_2 = Key.create(
{
"name": "Secret 2",
"secret_value": "secret_value_2",
"key_type": "s",
},
)
cls.server_test_2 = Server.create(
{
"name": "Test Server #2",
"color": 2,
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "k",
"host_key": "test_key",
"use_sudo": "p",
"ssh_key_id": cls.key_1.id,
"os_id": cls.os_ubuntu_20_04.id,
"secret_ids": [
(
0,
0,
{
"key_id": secret_1.id,
"secret_value": "secret_value_1",
},
),
(
0,
0,
{
"key_id": secret_2.id,
"secret_value": "secret_value_2",
},
),
],
"tag_ids": [(6, 0, [cls.tag_test_production.id])],
}
)
# Files
File = cls.env["cx.tower.file"]
cls.server_test_2_file = File.create(
{
"name": "tower_demo_without_template_{{ branch }}.txt",
"source": "tower",
"server_id": cls.server_test_2.id,
"server_dir": "{{ test_path }}",
"code": "Please, check url: {{ url }}",
}
)
# Flight plan to delete the server
Command = cls.env["cx.tower.command"]
Plan = cls.env["cx.tower.plan"]
# Add a command to delete the server
cls.command_delete_server = Command.create(
{
"name": "Python command for deleting server",
"action": "python_code",
"code": """
partner = env["res.partner"].create({"name": "Partner 1", "ref": "delete_server"})
result = {
"exit_code": 0,
"message": partner.name,
}
""",
}
)
cls.plan_delete_server = Plan.create(
{
"name": "Delete server",
"line_ids": [
(0, 0, {"command_id": cls.command_delete_server.id, "sequence": 1}),
],
}
)
# Create two test users that belong only to the "User" group.
cls.user1 = cls.Users.create(
{
"name": "Test User 1",
"login": "test_user1",
"email": "test_user1@example.com",
"groups_id": [(6, 0, [cls.group_user.id])],
}
)
cls.user2 = cls.Users.create(
{
"name": "Test User 2",
"login": "test_user2",
"email": "test_user2@example.com",
"groups_id": [(6, 0, [cls.group_user.id])],
}
)
# 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_server_copy(self):
"""Test server copy"""
# Let's say we have auto sync enabled on one of the files in server 2
self.server_test_2_file.auto_sync = True
fields_to_check = [
"ip_v4_address",
"ip_v6_address",
"ssh_username",
"ssh_password",
"ssh_key_id",
]
# Crete a log from file of type 'server'
file_for_log = self.File.create(
{
"source": "server",
"name": "test.log",
"server_dir": "/tmp",
"server_id": self.server_test_2.id,
"code": "Some log record - server",
}
)
server_log_server = self.ServerLog.create(
{
"name": "Log from file",
"server_id": self.server_test_2.id,
"log_type": "file",
"file_id": file_for_log.id,
}
)
# Add variable values to server
self.env["cx.tower.variable.value"].create(
{
"server_id": self.server_test_2.id,
"variable_id": self.variable_dir.id,
"value_char": "test",
}
)
# Copy server 2
server_test_2_copy = self.server_test_2.copy()
# The name of copy should contain '~ (copy)' suffix
self.assertTrue(
server_test_2_copy.name == self.server_test_2.name + " (copy)",
msg="Server name should contain '~ (copy)' suffix!",
)
# Check server logs
# Check that the copied server has the same number of server logs
self.assertEqual(
len(server_test_2_copy.server_log_ids),
len(self.server_test_2.server_log_ids),
(
"Copied template should have the same "
"number of server logs as the original"
),
)
# Ensure the first server log in the copied server matches the original
copied_log = server_test_2_copy.server_log_ids
self.assertEqual(
copied_log.name,
server_log_server.name,
"Server log name should be the same in the copied server",
)
self.assertEqual(
copied_log.command_id.id,
server_log_server.command_id.id,
"Command ID should be the same in the copied server log",
)
self.assertEqual(
copied_log.command_id.code,
server_log_server.command_id.code,
"Command code should be the same in the copied server log",
)
# Check fields match list
for field_ in fields_to_check:
self.assertTrue(
getattr(server_test_2_copy, field_)
== getattr(self.server_test_2, field_),
msg=(
f"Field {field_} value on server copy "
"does not match with the source!"
),
)
# Check if auto sync is disabled on the all the files
# in the copied server
self.assertTrue(
all([not file.auto_sync for file in server_test_2_copy.file_ids]),
msg="Auto sync should be disabled on all the files in the copied server!",
)
# Check if 'keep_when_deleted' option is enabled on all the files
# in the copied server
self.assertTrue(
all([file.keep_when_deleted for file in server_test_2_copy.file_ids]),
msg=(
"keep_when_deleted option should be enabled on all the files "
"in the copied server!"
),
)
# Check if secret values of keys in the copied server are the same
# as in source server
self.assertTrue(
all(
[
key_copy.secret_value == key_src.secret_value
for key_src, key_copy in zip( # noqa: B905 we need to run on Python 3.10
self.server_test_2.secret_ids.sudo(),
server_test_2_copy.secret_ids.sudo(),
)
]
),
msg=(
"Secret values of keys in the copied server "
"should be the same as in source server!"
),
)
# Variable names and values in server copy should be the same
# as in source server
self.assertTrue(
all(
[
var_copy.variable_reference == var_src.variable_reference
and var_copy.value_char == var_src.value_char
for var_src, var_copy in zip( # noqa: B905 we need to run on Python 3.10
self.server_test_2.variable_value_ids,
server_test_2_copy.variable_value_ids,
)
]
),
msg=(
"Variable names and values in server copy "
"should be the same as in source server!"
),
)
# Copy copied server
server_test_2_new_copy = server_test_2_copy.copy()
# Variable names and values in server copy should be the same
# as in source server
self.assertTrue(
all(
[
var_copy.variable_reference == var_src.variable_reference
and var_copy.value_char == var_src.value_char
and var_copy.reference == f"{var_src.reference}_copy"
for var_src, var_copy in zip( # noqa: B905 we need to run on Python 3.10
server_test_2_copy.variable_value_ids,
server_test_2_new_copy.variable_value_ids,
)
]
),
msg=(
"Variable names and values in server copy "
"should be the same as in source server!"
),
)
def test_server_archive_unarchive(self):
"""Test Server archived/unarchived"""
server = self.server_test_1.copy()
self.assertTrue(server, msg="Server must be unarchived")
server.toggle_active()
server.toggle_active()
self.assertTrue(server, msg="Server must be unarchived")
def test_server_unlink(self):
"""
Test cascading deletion of server and its related records.
"""
secret_1 = self.Key.create(
{
"name": "Secret 1",
"secret_value": "secret_value_1",
"key_type": "s",
},
)
# Create a test server
server = self.Server.create(
{
"name": "Test Server #3",
"color": 3,
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "k",
"use_sudo": "p",
"ssh_key_id": self.key_1.id,
"host_key": "test_key",
"os_id": self.os_ubuntu_20_04.id,
"secret_ids": [
(
0,
0,
{
"key_id": secret_1.id,
"secret_value": "secret_value_1",
},
),
],
}
)
# Create related file
file = self.File.create(
{"name": "Test File", "server_id": server.id, "source": "server"}
)
# Related secret
secret = server.secret_ids[0]
variable_meme = self.Variable.create({"name": "meme"})
# Create related variable value
variable_value = self.env["cx.tower.variable.value"].create(
{
"variable_id": variable_meme.id, # Replace with valid reference
"value_char": "Test Value",
"server_id": server.id,
}
)
plan_1 = self.Plan.create(
{
"name": "Test plan",
"note": "Create directory and list its content",
}
)
# Create a related plan log
plan_log = self.PlanLog.create(
{
"server_id": server.id,
"plan_id": plan_1.id, # Replace with valid reference
}
)
# Check that all records are created
self.assertTrue(server, "Server should be created successfully")
self.assertTrue(file, "File should be created successfully")
self.assertTrue(secret, "Secret should be created successfully")
self.assertTrue(variable_value, "Variable Value should be created successfully")
self.assertTrue(plan_log, "Plan Log should be created successfully")
# Collect IDs for verification post-deletion
file_id = file.id
variable_value_id = variable_value.id
plan_log_id = plan_log.id
# Delete the server
server.unlink()
# Verify that the server is deleted
self.assertFalse(
self.Server.search([("id", "=", server.id)]),
msg="Server should be deleted",
)
# Verify that related records are deleted
self.assertFalse(
self.File.search([("id", "=", file_id)]),
msg="File should be deleted when server is deleted",
)
# Verify that unrelated records are not affected
self.assertTrue(
self.Plan.search([("id", "=", plan_1.id)]),
msg="Unrelated plan should not be deleted when server is deleted",
)
self.assertFalse(
self.KeyValue.search([("id", "=", secret.id)]),
msg="Secret should be deleted when server is deleted",
)
self.assertFalse(
self.VariableValue.search([("id", "=", variable_value_id)]),
msg="Variable Value should be deleted when server is deleted",
)
self.assertFalse(
self.PlanLog.search([("id", "=", plan_log_id)]),
msg="Plan Log should be deleted when server is deleted",
)
def test_server_delete_plan_success(self):
"""Test server delete plan"""
# Set plan to delete the server
self.server_test_2.plan_delete_id = self.plan_delete_server.id
# Delete the server
self.server_test_2.unlink()
# Check if the server has been deleted
self.assertFalse(
self.server_test_2.exists(),
msg="Server should be deleted",
)
# Check if the partner has been created
self.assertTrue(
self.env["res.partner"].search([("ref", "=", "delete_server")]),
msg="Partner should be created",
)
def test_server_delete_plan_error(self):
"""Test server delete plan error"""
# Modify the command to fail
self.command_delete_server.code = """
result = {
"exit_code": 4,
"message": 'Such much error',
}
"""
# Set plan to delete the server
self.server_test_2.plan_delete_id = self.plan_delete_server.id
# Delete the server
self.server_test_2.unlink()
# Check if the server has been deleted
self.assertTrue(
self.server_test_2.exists(),
msg="Server should not be deleted",
)
self.assertEqual(
self.server_test_2.status,
"delete_error",
msg="Server status should be delete_error",
)
# ------------------------------------------------------------
# ---- Access
# ------------------------------------------------------------
def test_user_record_not_visible_without_user_ids(self):
"""
Test that a user in the 'cetmix_tower_server.group_user' group cannot see
a Tower Server record if not added to user_ids.
"""
# Create a Tower Server record without any user_ids.
record = self.Server.create(
{
"name": "User Visibility Test",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
"user_ids": [(5, 0, 0)],
}
)
# As user1, search for the record. Since user1's partner is not subscribed,
# the record should not be returned.
records = self.Server.with_user(self.user1).search([("id", "=", record.id)])
self.assertFalse(
records,
"User1 should not see the record if not added to user_ids.",
)
def test_user_record_visible_after_added_to_user_ids(self):
"""
Test that a user sees a Tower Server record after being added to user_ids.
"""
record = self.Server.create(
{
"name": "User Visibility Test",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
"user_ids": [(4, self.user1.id)],
}
)
# Now, as user1 the record should be visible.
records = self.Server.with_user(self.user1).search([("id", "=", record.id)])
self.assertTrue(
records,
"User1 should see the record after being added to message_partner_ids.",
)
def test_only_added_user_can_see(self):
"""
Test that only the added user can see the Tower Server record.
"""
record = self.Server.create(
{
"name": "User Visibility Test",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
"user_ids": [(4, self.user1.id)],
}
)
# Subscribe only user1's partner.
records_user1 = self.Server.with_user(self.user1).search(
[("id", "=", record.id)]
)
records_user2 = self.Server.with_user(self.user2).search(
[("id", "=", record.id)]
)
self.assertTrue(
records_user1, "User1 should see the record after being added to user_ids."
)
self.assertFalse(
records_user2,
"User2 should not see the record if they are not added to user_ids.",
)
def test_manager_read_access_as_follower(self):
"""A manager should be able to read a record if his partner is a follower."""
# Create a record without any managers in manager_ids.
record = self.Server.create(
{
"name": "Test Server (Follower)",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
# Explicitly clear manager_ids
"manager_ids": [(6, 0, [])],
}
)
# Subscribe manager1 to the record so that his partner becomes a follower.
record.write({"user_ids": [(4, self.manager1.id)]})
# As manager1 (a follower) the record should be visible.
records = self.Server.with_user(self.manager1).search([("id", "=", record.id)])
self.assertTrue(records, "Manager1 (user) must be able to read the record.")
# As manager2 (not a follower and not in manager_ids)
# the record should not be visible.
records = self.Server.with_user(self.manager2).search([("id", "=", record.id)])
self.assertFalse(
records,
"Manager2 (not user_ids and not in manager_ids) must not see the record.",
)
def test_manager_read_access_as_manager_ids(self):
"""A manager should be able to read a record if he is added to manager_ids."""
# Create a record with manager2 added to manager_ids.
record = self.Server.create(
{
"name": "Test Server (Manager)",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
"manager_ids": [(6, 0, [self.manager2.id])],
}
)
# Without adding to user_ids, manager2 should be able to see the record.
records = self.Server.with_user(self.manager2).search([("id", "=", record.id)])
self.assertTrue(
records, "Manager2 (in manager_ids) must be able to read the record."
)
# Manager1 is not added to user_ids nor in manager_ids
# so should not see the record.
records = self.Server.with_user(self.manager1).search([("id", "=", record.id)])
self.assertFalse(
records,
"Manager1 (neither user_ids nor in manager_ids) must not see the record.",
)
# Add manager1 to user_ids
record.write({"user_ids": [(4, self.manager1.id)]})
records = self.Server.with_user(self.manager1).search([("id", "=", record.id)])
self.assertTrue(
records,
"Manager1 (added to user_ids) must be able to see the record.",
)
def test_manager_write_access(self):
"""A manager should be able to update a record only if he is in manager_ids."""
# Create a record with no managers.
record = self.Server.create(
{
"name": "Test Server (Write)",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
"manager_ids": [(6, 0, [])],
}
)
# Manager1 (not in manager_ids) tries to update: should raise an AccessError.
with self.assertRaises(AccessError):
record.with_user(self.manager1).write({"name": "Updated Name"})
# Update the record to include manager1 in manager_ids.
record.write({"manager_ids": [(4, self.manager1.id)]})
try:
record.with_user(self.manager1).write({"name": "Updated Name"})
except AccessError:
self.fail(
"Manager1 must be able to update the "
"record after being added to manager_ids."
)
def test_manager_create_access(self):
"""
A manager should be allowed to create a record only if he is added
in the "Managers".
"""
# Manager1 attempts to create a record without including himself in manager_ids.
with self.assertRaises(AccessError):
self.Server.with_user(self.manager1).create(
{
"name": "Test Server (Create Denied)",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
"manager_ids": [(6, 0, [])],
}
)
# Manager1 creates a record with himself added to manager_ids.
try:
record = self.Server.with_user(self.manager1).create(
{
"name": "Test Server (Create Allowed)",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
"manager_ids": [(6, 0, [self.manager1.id])],
}
)
self.assertTrue(
record,
"Manager1 must be able to create the record if he is in manager_ids.",
)
except AccessError:
self.fail(
"Manager1 should be allowed to create a "
"record when included in manager_ids."
)
def test_manager_delete_access(self):
"""
A manager should be allowed to delete a record only if:
- He is in the manager_ids field, and
- He is the creator of the record.
"""
# -- Scenario 1: Manager1 creates a record with himself in manager_ids.
record = self.Server.with_user(self.manager1).create(
{
"name": "Test Server (Delete Allowed)",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
"manager_ids": [(6, 0, [self.manager1.id])],
}
)
# Manager1 should be able to delete his own record.
try:
record.with_user(self.manager1).unlink()
except AccessError:
self.fail(
"Manager1 must be able to delete his own record if in manager_ids."
)
# -- Scenario 2: Manager2 creates a record (with himself in manager_ids).
record2 = self.Server.with_user(self.manager2).create(
{
"name": "Test Server (Delete Denied - Not Creator)",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
"manager_ids": [(6, 0, [self.manager2.id, self.manager1.id])],
}
)
# Manager1, should not be able to delete record2.
with self.assertRaises(AccessError):
record2.with_user(self.manager1).unlink()
# Remove manager2 from manager_ids.
record2.write({"manager_ids": [(6, 0, [])]})
# Manager2 should not be able to delete record2 now
# because he is not in manager_ids.
with self.assertRaises(AccessError):
record2.with_user(self.manager2).unlink()
def test_command_server_compatibility(self):
"""Test command compatibility with servers"""
# Create a command restricted to specific servers
command = self.Command.create(
{
"name": "Restricted Command",
"action": "ssh_command",
"code": "echo 'test'",
"server_ids": [(6, 0, [self.server_test_1.id])],
}
)
# Should work on allowed server
try:
self.server_test_1.run_command(command)
except Exception as e:
self.fail(f"Command should execute on allowed server but failed: {e}")
# Should fail on non-allowed server
command_result = self.server_test_2.with_context(
no_command_log=True
).run_command(command)
self.assertEqual(
command_result["status"],
COMMAND_NOT_COMPATIBLE_WITH_SERVER,
"Command should not execute on non-allowed server",
)
# Clear all existing command logs
self.CommandLog.search([]).unlink()
# Same test but with command log
self.server_test_2.run_command(command)
command_log = self.CommandLog.search([])
self.assertEqual(len(command_log), 1, "Must be a single log record")
self.assertEqual(
command_log.command_status,
COMMAND_NOT_COMPATIBLE_WITH_SERVER,
"Command should not execute on non-allowed server",
)
# Command without server restrictions should work on any server
unrestricted_command = self.Command.create(
{
"name": "Unrestricted Command",
"action": "ssh_command",
"code": "echo 'test'",
}
)
try:
self.server_test_1.run_command(unrestricted_command)
self.server_test_2.run_command(unrestricted_command)
except Exception as e:
self.fail(
f"Unrestricted command should execute on any server but failed: {e}"
)
def test_server_host_key_validation(self):
"""Test server host key validation"""
server = self.Server.create(
{
"name": "Test Server",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
"host_key": "test_key",
"skip_host_key": False,
}
)
# Test with host key
server.test_ssh_connection()
# Test without host key
server.host_key = None
with self.assertRaises(ValidationError):
server.test_ssh_connection()
# Test with skip_host_key
server.skip_host_key = True
server.test_ssh_connection()
def test_server_reference_update(self):
"""Test server reference update cascades to dependent models"""
# 1. Add a variable value to server_test_1
variable_value = self.VariableValue.create(
{
"variable_id": self.variable_os.id,
"value_char": "Ubuntu 20.04",
"server_id": self.server_test_1.id,
}
)
# 2. Add a file to server_test_1
server_file = self.File.create(
{
"name": "test_file.txt",
"server_id": self.server_test_1.id,
"source": "tower",
"code": "Test file content",
}
)
# Store original references for comparison
original_server_reference = self.server_test_1.reference
original_variable_value_reference = variable_value.reference
original_file_reference = server_file.reference
# 3. Change the reference for server_test_1 to "awesome_server"
self.server_test_1.write({"reference": "awesome_server"})
# 4. Verify that references are updated for dependent models
# Invalidate models to refresh all references
self.env["cx.tower.server"].invalidate_model(["reference"])
self.env["cx.tower.variable.value"].invalidate_model(["reference"])
self.env["cx.tower.file"].invalidate_model(["reference"])
# Check that server reference was updated
self.assertEqual(self.server_test_1.reference, "awesome_server")
self.assertNotEqual(self.server_test_1.reference, original_server_reference)
# Check that variable value reference was updated
# to include the new server reference
self.assertIn("awesome_server", variable_value.reference)
self.assertNotEqual(variable_value.reference, original_variable_value_reference)
# Check that file reference was updated to include the new server reference
self.assertIn("awesome_server", server_file.reference)
self.assertNotEqual(server_file.reference, original_file_reference)
# Verify the reference pattern for variable value follows the expected format:
# <variable_reference>_<model_generic_reference>_<linked_model_generic_reference>_<linked_record_reference> # noqa: E501
expected_variable_pattern = (
f"{self.variable_os.reference}_variable_value_server_"
f"{self.server_test_1.reference}"
)
self.assertEqual(variable_value.reference, expected_variable_pattern)
# Verify the reference pattern for file follows the expected format:
# <parent_reference>_<model_generic_reference>_<index>
expected_file_pattern = f"{self.server_test_1.reference}_file_1"
self.assertEqual(server_file.reference, expected_file_pattern)

View File

@@ -0,0 +1,231 @@
# Copyright (C) 2024 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from unittest.mock import patch
from odoo import _
from odoo.exceptions import ValidationError
from odoo.addons.cetmix_tower_server.models.constants import (
GENERAL_ERROR,
JET_NOT_FOUND,
JET_TEMPLATE_NOT_FOUND,
)
from .common_jets import TestTowerJetsCommon
class TestTowerServerJetActionCommand(TestTowerJetsCommon): # pylint: disable=protected-access
"""Tests for cx.tower.server._command_runner_jet_action."""
def _create_jet_action_command(self, jet_template, jet_action):
"""Create a command that triggers a jet action for the given template."""
return self.Command.create(
{
"name": "Test jet action command",
"action": "jet_action",
"jet_template_id": jet_template.id,
"jet_action_id": jet_action.id,
}
)
def _create_jet_action_log(self, jet, command):
"""Create a command log bound to a jet and command."""
return self.CommandLog.create(
{
"server_id": jet.server_id.id,
"command_id": command.id,
"jet_id": jet.id,
}
)
def test_command_runner_jet_action_requires_log_record(self):
"""Calling without a log record must raise ValidationError."""
with self.assertRaises(ValidationError):
self.server_test_1._command_runner_jet_action(False)
def test_command_runner_jet_action_missing_jet_action(self):
"""Missing command jet_action_id finishes with GENERAL_ERROR."""
command = self._create_jet_action_command(
self.jet_template_test,
self.action_stopped_to_running,
)
command.write({"jet_action_id": False})
log = self._create_jet_action_log(self.jet_test, command)
result = self.server_test_1._command_runner_jet_action(log)
self.assertEqual(result["status"], GENERAL_ERROR)
self.assertEqual(result["response"], None)
self.assertEqual(result["error"], _("Jet action is not found."))
log.invalidate_recordset()
self.assertEqual(log.command_status, GENERAL_ERROR)
def test_command_runner_jet_action_missing_jet(self):
"""Missing jet on the log finishes with JET_NOT_FOUND."""
command = self._create_jet_action_command(
self.jet_template_test,
self.action_stopped_to_running,
)
log = self.CommandLog.create(
{
"server_id": self.server_test_1.id,
"command_id": command.id,
"jet_id": False,
}
)
result = self.server_test_1._command_runner_jet_action(log)
self.assertEqual(result["status"], JET_NOT_FOUND)
self.assertIsNotNone(result["error"])
def test_command_runner_jet_action_missing_jet_template(self):
"""
Missing jet_template_id on the command finishes with
JET_TEMPLATE_NOT_FOUND.
"""
command = self._create_jet_action_command(
self.jet_template_test,
self.action_stopped_to_running,
)
command.write({"jet_template_id": False})
log = self._create_jet_action_log(self.jet_test, command)
result = self.server_test_1._command_runner_jet_action(log)
self.assertEqual(result["status"], JET_TEMPLATE_NOT_FOUND)
self.assertIsNotNone(result["error"])
@patch(
"odoo.addons.cetmix_tower_server.models.cx_tower_jet.CxTowerJet._trigger_action",
autospec=True,
)
def test_command_runner_jet_action_success_aggregates_response(self, mock_trigger):
mock_trigger.return_value = {"status": 0, "error": None}
command = self._create_jet_action_command(
self.jet_template_test,
self.action_stopped_to_running,
)
log = self._create_jet_action_log(self.jet_test, command)
result = self.server_test_1._command_runner_jet_action(log)
self.assertEqual(result["status"], 0)
self.assertIsNone(result["error"])
self.assertTrue(result["response"])
self.assertIn("Action triggered for", result["response"])
self.assertIn(self.jet_test.reference, result["response"])
mock_trigger.assert_called_once()
log.invalidate_recordset()
self.assertEqual(log.command_status, 0)
self.assertIn("Action triggered for", log.command_response)
self.assertFalse(log.command_error)
@patch(
"odoo.addons.cetmix_tower_server.models.cx_tower_jet.CxTowerJet._trigger_action",
autospec=True,
)
def test_command_runner_jet_action_failure_single_jet_error_message(
self, mock_trigger
):
mock_trigger.return_value = {"status": 1, "error": "No action found"}
command = self._create_jet_action_command(
self.jet_template_test,
self.action_stopped_to_running,
)
log = self._create_jet_action_log(self.jet_test, command)
result = self.server_test_1._command_runner_jet_action(log)
self.assertEqual(result["status"], GENERAL_ERROR)
self.assertIsNone(result["response"])
self.assertTrue(result["error"])
lines = result["error"].split("\n")
self.assertEqual(len(lines), 2)
self.assertIn("Action triggered for", lines[0])
self.assertIn(self.jet_test.reference, lines[1])
self.assertIn("No action found", lines[1])
@patch(
"odoo.addons.cetmix_tower_server.models.cx_tower_jet.CxTowerJet._trigger_action",
autospec=True,
)
def test_command_runner_jet_action_failure_status_without_error_text(
self, mock_trigger
):
mock_trigger.return_value = {"status": 99, "error": None}
command = self._create_jet_action_command(
self.jet_template_test,
self.action_stopped_to_running,
)
log = self._create_jet_action_log(self.jet_test, command)
result = self.server_test_1._command_runner_jet_action(log)
self.assertEqual(result["status"], GENERAL_ERROR)
self.assertIn(self.jet_test.reference, result["error"])
self.assertIn("99", result["error"])
@patch(
"odoo.addons.cetmix_tower_server.models.cx_tower_jet.CxTowerJet._trigger_action",
autospec=True,
)
def test_command_runner_jet_action_failure_multiple_jets(self, mock_trigger):
jet_b = self._create_jet(
name="Second Jet",
reference="jet_second",
template=self.jet_template_test,
server=self.server_test_1,
)
def side_effect(jet_self, *_args, **_kwargs):
jet_self.ensure_one()
if jet_self.id == self.jet_test.id:
return {"status": 1, "error": "No action found"}
return {"status": 2, "error": "Jet is busy"}
mock_trigger.side_effect = side_effect
command = self._create_jet_action_command(
self.jet_template_test,
self.action_stopped_to_running,
)
log = self._create_jet_action_log(self.jet_woocommerce, command)
with patch(
"odoo.addons.cetmix_tower_server.models.cx_tower_jet.CxTowerJet._get_dependent_jets_by_template",
autospec=True,
return_value=self.jet_test | jet_b,
):
result = self.server_test_1._command_runner_jet_action(log)
self.assertEqual(result["status"], GENERAL_ERROR)
lines = result["error"].split("\n")
self.assertEqual(len(lines), 2)
self.assertIn("Action triggered for", lines[0])
self.assertIn(self.jet_test.reference, lines[0])
self.assertIn(jet_b.reference, lines[0])
agg = lines[1]
self.assertIn(f"{self.jet_test.reference}: No action found", agg)
self.assertIn(f"{jet_b.reference}: Jet is busy", agg)
@patch(
"odoo.addons.cetmix_tower_server.models.cx_tower_jet.CxTowerJet._get_dependent_jets_by_template",
autospec=True,
)
def test_command_runner_jet_action_no_dependent_jets(self, mock_deps):
mock_deps.return_value = self.Jet.browse()
command = self._create_jet_action_command(
self.jet_template_test,
self.action_stopped_to_running,
)
log = self._create_jet_action_log(self.jet_woocommerce, command)
result = self.server_test_1._command_runner_jet_action(log)
self.assertEqual(result["status"], 0)
self.assertIsNone(result["error"])
self.assertTrue(result["response"])
self.assertIn(self.jet_woocommerce.name, result["response"])
self.assertIn(self.jet_template_test.name, result["response"])

View File

@@ -0,0 +1,657 @@
# Copyright (C) 2025 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.exceptions import AccessError
from .common_jets import TestTowerJetsCommon
class TestTowerServerLog(TestTowerJetsCommon):
"""Test the cx.tower.server.log model access rights."""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create test server logs
cls.server_log_1 = cls.ServerLog.create(
{
"name": "Test Log 1",
"server_id": cls.server_test_1.id,
"log_type": "file",
"access_level": "1",
}
)
cls.server_log_2 = cls.ServerLog.create(
{
"name": "Test Log 2",
"server_id": cls.server_test_1.id,
"log_type": "file",
"access_level": "1",
}
)
# Create additional server for testing
cls.server_2 = cls.Server.create(
{
"name": "Test Server 2",
"ip_v4_address": "localhost",
"ssh_username": "test2",
"ssh_password": "test2",
"ssh_port": 22,
"user_ids": [(6, 0, [])],
"manager_ids": [(6, 0, [])],
}
)
# Use pre-created jet_template_test and jet_test from TestTowerJetsCommon
# Ensure jet_template_test has server_test_1 in server_ids
cls.jet_template_test.write({"server_ids": [(4, cls.server_test_1.id)]})
# Create server logs linked to Jet
cls.server_log_jet_1 = cls.ServerLog.create(
{
"name": "Test Jet Log 1",
"server_id": cls.server_test_1.id,
"jet_id": cls.jet_test.id,
"log_type": "file",
"access_level": "1",
}
)
cls.server_log_jet_2 = cls.ServerLog.create(
{
"name": "Test Jet Log 2",
"server_id": cls.server_test_1.id,
"jet_id": cls.jet_test.id,
"log_type": "file",
"access_level": "2",
}
)
# Create server logs linked to Jet Template
cls.server_log_jet_template_1 = cls.ServerLog.create(
{
"name": "Test Jet Template Log 1",
"server_id": cls.server_test_1.id,
"jet_template_id": cls.jet_template_test.id,
"log_type": "file",
"access_level": "1",
}
)
cls.server_log_jet_template_2 = cls.ServerLog.create(
{
"name": "Test Jet Template Log 2",
"server_id": cls.server_test_1.id,
"jet_template_id": cls.jet_template_test.id,
"log_type": "file",
"access_level": "2",
}
)
def test_user_access(self):
"""Test user access to server logs"""
# Add user to server's user_ids
self.server_test_1.write(
{
"user_ids": [(6, 0, [self.user.id])],
}
)
# Case 1: User should be able to read when:
# - access_level == "1"
# - user is in server's user_ids
recs = self.ServerLog.with_user(self.user).search(
[("id", "in", [self.server_log_1.id, self.server_log_2.id])]
)
self.assertEqual(
len(recs),
2,
"User should be able to read all logs with access_level '1'"
" when in user_ids",
)
# Case 2: User should not be able to read when not in server's user_ids
self.server_test_1.write(
{
"user_ids": [(5, 0, 0)], # Remove all users
}
)
recs = self.ServerLog.with_user(self.user).search(
[("id", "=", self.server_log_1.id)]
)
self.assertEqual(
len(recs),
0,
"User should not be able to read when not in server's user_ids",
)
# Case 3: User should not be able to read when access_level > "1"
self.server_test_1.write(
{
"user_ids": [(6, 0, [self.user.id])],
}
)
high_access_log = (
self.ServerLog.with_user(self.user)
.sudo()
.create(
{
"name": "High Access Log",
"server_id": self.server_test_1.id,
"log_type": "file",
"access_level": "2",
}
)
)
recs = self.ServerLog.with_user(self.user).search(
[("id", "=", high_access_log.id)]
)
self.assertEqual(
len(recs),
0,
"User should not be able to read logs with access_level > '1'",
)
def test_manager_access(self):
"""Test manager access to server logs"""
# Add manager to server's manager_ids
self.server_test_1.write(
{
"manager_ids": [(6, 0, [self.manager.id])],
}
)
# Case 1: Manager should be able to read when:
# - access_level <= "2"
# - manager is in server's manager_ids
recs = self.ServerLog.with_user(self.manager).search(
[("id", "in", [self.server_log_1.id, self.server_log_2.id])]
)
self.assertEqual(
len(recs),
2,
"Manager should be able to read all logs when in manager_ids",
)
# Case 2: Manager should be able to create and write when:
# - access_level <= "2"
# - manager is in server's manager_ids
try:
new_log = self.ServerLog.with_user(self.manager).create(
{
"name": "Manager Test Log",
"server_id": self.server_test_1.id,
"log_type": "file",
"access_level": "2",
}
)
except AccessError:
self.fail(
"Manager should be able to create logs when in server's manager_ids"
)
try:
new_log.write({"name": "Updated Name"})
except AccessError:
self.fail(
"Manager should be able to write logs when in server's manager_ids"
)
self.assertEqual(new_log.name, "Updated Name")
# Case 3: Manager should be able to unlink when:
# - access_level <= "2"
# - created by manager
# - manager is in server's manager_ids
try:
new_log.unlink()
except AccessError:
self.fail(
"Manager should be able to unlink own logs when in server's manager_ids"
)
# Case 4: Manager should not be able to unlink logs created by others
with self.assertRaises(AccessError):
self.server_log_1.with_user(self.manager).unlink()
# Case 5: Manager should not be able to access logs with access_level > "2"
high_access_log = (
self.ServerLog.with_user(self.manager)
.sudo()
.create(
{
"name": "High Access Log",
"server_id": self.server_test_1.id,
"log_type": "file",
"access_level": "3",
}
)
)
recs = self.ServerLog.with_user(self.manager).search(
[("id", "=", high_access_log.id)]
)
self.assertEqual(
len(recs),
0,
"Manager should not be able to read logs with access_level > '2'",
)
def test_root_access(self):
"""Test root user unrestricted access"""
# Create test logs with various conditions
test_logs = self.ServerLog.with_user(self.root).create(
[
{
"name": f"Root Test Log {level}",
"server_id": self.server_test_1.id,
"log_type": "file",
"access_level": level,
}
for level in ["1", "2", "3"]
]
)
# Root should be able to read all logs regardless of conditions
recs = self.ServerLog.with_user(self.root).search([("id", "in", test_logs.ids)])
self.assertEqual(
len(recs),
3,
"Root should have unrestricted read access to all logs",
)
# Root should be able to write all logs
try:
for log in test_logs:
log.write({"name": "Updated by Root"})
except AccessError:
self.fail("Root should be able to write any logs")
# Root should be able to unlink all logs
try:
test_logs.unlink()
except AccessError:
self.fail("Root should be able to unlink any logs")
def test_log_text_access_restrictions(self):
"""Test log_text field access controls"""
test_log = self.ServerLog.create(
{
"name": "Access Test Log",
"server_id": self.server_test_1.id,
"log_type": "file",
"access_level": "1",
"log_text": "<p>Test content</p>",
}
)
# 1. Verify read access for all roles
for user in (self.root, self.manager, self.user):
content = test_log.with_user(user).log_text
self.assertEqual(
content, "<p>Test content</p>", f"{user.name} should read log_text"
)
# 2. Verify write prohibition for all roles
for user in (self.root, self.manager, self.user):
with self.assertRaises(
AccessError, msg=f"{user.name} shouldn't modify log_text"
):
test_log.with_user(user).write({"log_text": "<p>Modified</p>"})
def test_log_text_refresh_mechanism(self):
"""Test log_text can only be updated via refresh action"""
test_log = self.ServerLog.create(
{
"name": "Refresh Test Log",
"server_id": self.server_test_1.id,
"log_type": "file",
"access_level": "1",
"log_text": "<p>Initial</p>",
}
)
# 1. Direct write attempts should fail
with self.assertRaises(AccessError):
test_log.sudo().write({"log_text": "<p>Illegal Update</p>"})
# 2. Verify refresh action updates content
original_content = test_log.log_text
test_log.action_update_log()
self.assertNotEqual(
test_log.log_text,
original_content,
"action_update_log() should update log_text",
)
def test_log_text_copy(self):
"""Duplicating a log must NOT keep the log output"""
original = self.ServerLog.create(
{
"name": "Original Log",
"server_id": self.server_test_1.id,
"log_type": "file",
"access_level": "1",
"log_text": "<p>Original content</p>",
}
)
copied = original.copy()
# log_text must be cleared because copy=False
self.assertFalse(copied.log_text, "Copied log must not keep log_text")
self.assertNotEqual(copied.id, original.id)
self.assertTrue(bool(copied.name))
def test_jet_user_access(self):
"""Test user access to server logs via Jet"""
# Set user to jet's user_ids (replaces any existing users)
self.jet_test.write({"user_ids": [(6, 0, [self.user.id])]})
# Case 1: User should be able to read when:
# - access_level == "1"
# - user is in jet's user_ids
recs = self.ServerLog.with_user(self.user).search(
[("id", "in", [self.server_log_jet_1.id, self.server_log_jet_2.id])]
)
self.assertEqual(
len(recs),
1,
"User should be able to read logs with access_level '1'"
" when in jet's user_ids",
)
self.assertEqual(recs.id, self.server_log_jet_1.id)
# Case 2: User should not be able to read when not in jet's user_ids
self.jet_test.write({"user_ids": [(5, 0, 0)]}) # Remove all users
recs = self.ServerLog.with_user(self.user).search(
[("id", "=", self.server_log_jet_1.id)]
)
self.assertEqual(
len(recs),
0,
"User should not be able to read when not in jet's user_ids",
)
# Case 3: User should not be able to read when access_level > "1"
# Set user back to jet's user_ids
self.jet_test.write({"user_ids": [(6, 0, [self.user.id])]})
recs = self.ServerLog.with_user(self.user).search(
[("id", "=", self.server_log_jet_2.id)]
)
self.assertEqual(
len(recs),
0,
"User should not be able to read logs with access_level > '1'",
)
def test_jet_manager_access(self):
"""Test manager access to server logs via Jet"""
# Set manager to jet's manager_ids (replaces any existing managers)
self.jet_test.write({"manager_ids": [(6, 0, [self.manager.id])]})
# Case 1: Manager should be able to read when:
# - access_level <= "2"
# - manager is in jet's user_ids or manager_ids
recs = self.ServerLog.with_user(self.manager).search(
[("id", "in", [self.server_log_jet_1.id, self.server_log_jet_2.id])]
)
self.assertEqual(
len(recs),
2,
"Manager should be able to read all logs when in jet's manager_ids",
)
# Case 2: Manager should be able to create and write when:
# - access_level <= "2"
# - manager is in jet's manager_ids
try:
new_log = self.ServerLog.with_user(self.manager).create(
{
"name": "Manager Jet Test Log",
"server_id": self.server_test_1.id,
"jet_id": self.jet_test.id,
"log_type": "file",
"access_level": "2",
}
)
except AccessError:
self.fail("Manager should be able to create logs when in jet's manager_ids")
try:
new_log.write({"name": "Updated Jet Name"})
except AccessError:
self.fail("Manager should be able to write logs when in jet's manager_ids")
self.assertEqual(new_log.name, "Updated Jet Name")
# Case 3: Manager should be able to unlink when:
# - access_level <= "2"
# - created by manager
# - manager is in jet's manager_ids
try:
new_log.unlink()
except AccessError:
self.fail(
"Manager should be able to unlink own logs when in jet's manager_ids"
)
# Case 4: Manager should not be able to unlink logs created by others
with self.assertRaises(AccessError):
self.server_log_jet_1.with_user(self.manager).unlink()
# Case 5: Manager should not be able to access logs with access_level > "2"
high_access_log = (
self.ServerLog.with_user(self.manager)
.sudo()
.create(
{
"name": "High Access Jet Log",
"server_id": self.server_test_1.id,
"jet_id": self.jet_test.id,
"log_type": "file",
"access_level": "3",
}
)
)
recs = self.ServerLog.with_user(self.manager).search(
[("id", "=", high_access_log.id)]
)
self.assertEqual(
len(recs),
0,
"Manager should not be able to read logs with access_level > '2'",
)
# Case 6: Manager should be able to read when in jet's user_ids
# Remove managers and add manager to jet's user_ids
self.jet_test.write(
{
"manager_ids": [(5, 0, 0)], # Remove managers
"user_ids": [(6, 0, [self.manager.id])], # Set to users
}
)
recs = self.ServerLog.with_user(self.manager).search(
[("id", "in", [self.server_log_jet_1.id, self.server_log_jet_2.id])]
)
self.assertEqual(
len(recs),
2,
"Manager should be able to read when in jet's user_ids",
)
def test_jet_template_user_access(self):
"""Test user access to server logs via Jet Template"""
# Set user to jet template's user_ids (replaces any existing users)
self.jet_template_test.write({"user_ids": [(6, 0, [self.user.id])]})
# Case 1: User should be able to read when:
# - access_level == "1"
# - user is in jet template's user_ids
recs = self.ServerLog.with_user(self.user).search(
[
(
"id",
"in",
[
self.server_log_jet_template_1.id,
self.server_log_jet_template_2.id,
],
)
]
)
self.assertEqual(
len(recs),
1,
"User should be able to read logs with access_level '1'"
" when in jet template's user_ids",
)
self.assertEqual(recs.id, self.server_log_jet_template_1.id)
# Case 2: User should not be able to read when not in jet template's user_ids
self.jet_template_test.write({"user_ids": [(5, 0, 0)]}) # Remove all users
recs = self.ServerLog.with_user(self.user).search(
[("id", "=", self.server_log_jet_template_1.id)]
)
self.assertEqual(
len(recs),
0,
"User should not be able to read when not in jet template's user_ids",
)
# Case 3: User should not be able to read when access_level > "1"
# Set user back to jet template's user_ids
self.jet_template_test.write({"user_ids": [(6, 0, [self.user.id])]})
recs = self.ServerLog.with_user(self.user).search(
[("id", "=", self.server_log_jet_template_2.id)]
)
self.assertEqual(
len(recs),
0,
"User should not be able to read logs with access_level > '1'",
)
def test_jet_template_manager_access(self):
"""Test manager access to server logs via Jet Template"""
# Set manager to jet template's manager_ids (replaces any existing managers)
self.jet_template_test.write({"manager_ids": [(6, 0, [self.manager.id])]})
# Case 1: Manager should be able to read when:
# - access_level <= "2"
# - manager is in jet template's user_ids or manager_ids
recs = self.ServerLog.with_user(self.manager).search(
[
(
"id",
"in",
[
self.server_log_jet_template_1.id,
self.server_log_jet_template_2.id,
],
)
]
)
self.assertEqual(
len(recs),
2,
"Manager should be able to read all logs when"
" in jet template's manager_ids",
)
# Case 2: Manager should be able to create and write when:
# - access_level <= "2"
# - manager is in jet template's manager_ids
try:
new_log = self.ServerLog.with_user(self.manager).create(
{
"name": "Manager Jet Template Test Log",
"server_id": self.server_test_1.id,
"jet_template_id": self.jet_template_test.id,
"log_type": "file",
"access_level": "2",
}
)
except AccessError:
self.fail(
"Manager should be able to create logs when "
"in jet template's manager_ids"
)
try:
new_log.write({"name": "Updated Jet Template Name"})
except AccessError:
self.fail(
"Manager should be able to write logs when "
"in jet template's manager_ids"
)
self.assertEqual(new_log.name, "Updated Jet Template Name")
# Case 3: Manager should be able to unlink when:
# - access_level <= "2"
# - created by manager
# - manager is in jet template's manager_ids
try:
new_log.unlink()
except AccessError:
self.fail(
"Manager should be able to unlink own logs"
" when in jet template's manager_ids"
)
# Case 4: Manager should not be able to unlink logs created by others
with self.assertRaises(AccessError):
self.server_log_jet_template_1.with_user(self.manager).unlink()
# Case 5: Manager should not be able to access logs with access_level > "2"
high_access_log = (
self.ServerLog.with_user(self.manager)
.sudo()
.create(
{
"name": "High Access Jet Template Log",
"server_id": self.server_test_1.id,
"jet_template_id": self.jet_template_test.id,
"log_type": "file",
"access_level": "3",
}
)
)
recs = self.ServerLog.with_user(self.manager).search(
[("id", "=", high_access_log.id)]
)
self.assertEqual(
len(recs),
0,
"Manager should not be able to read logs with access_level > '2'",
)
# Case 6: Manager should be able to read when in jet template's user_ids
# Remove managers and add manager to jet template's user_ids
self.jet_template_test.write(
{
"manager_ids": [(5, 0, 0)], # Remove managers
"user_ids": [(6, 0, [self.manager.id])], # Set to users
}
)
recs = self.ServerLog.with_user(self.manager).search(
[
(
"id",
"in",
[
self.server_log_jet_template_1.id,
self.server_log_jet_template_2.id,
],
)
]
)
self.assertEqual(
len(recs),
2,
"Manager should be able to read when in jet template's user_ids",
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,244 @@
from .common import TestTowerCommon
class TestTowerShortcut(TestTowerCommon):
"""Test Tower Shortcut"""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Server
cls.server_test_1_pro = cls.Server.create(
{
"name": "Test 1 Pro",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"skip_host_key": True,
}
)
# Variable
cls.variable_path_pro = cls.Variable.create({"name": "test_path_pro"})
# Command
cls.command_list_dir_pro = cls.Command.create(
{
"name": "Test create directory",
"code": "ls -l {{ test_path_ }}",
}
)
# Flight plan
cls.plan_1_pro = cls.Plan.create(
{
"name": "Test plan 1 Pro",
"note": "List directory contents",
}
)
cls.plan_line_1_pro = cls.plan_line.create(
{
"sequence": 5,
"plan_id": cls.plan_1_pro.id,
"command_id": cls.command_list_dir_pro.id,
}
)
# Shortcuts
cls.shortcut_for_command = cls.Shortcut.create(
{
"name": "Shortcut for Command",
"action": "command",
"command_id": cls.command_list_dir_pro.id,
"server_ids": [(4, cls.server_test_1_pro.id)],
}
)
cls.shortcut_for_flight_plan = cls.Shortcut.create(
{
"name": "Shortcut for Flight Plan",
"action": "plan",
"plan_id": cls.plan_1_pro.id,
"server_ids": [(4, cls.server_test_1_pro.id)],
}
)
def test_shortcut_user_access_rules(self):
"""Test shortcut user access rules"""
# Create shortcuts with different access levels and server/template assignments
shortcut_level_1_server = self.Shortcut.create(
{
"name": "Level 1 Server Shortcut",
"action": "command",
"command_id": self.command_list_dir_pro.id,
"server_ids": [(4, self.server_test_1_pro.id)],
"access_level": "1",
}
)
shortcut_level_2_template = self.Shortcut.create(
{
"name": "Level 2 Template Shortcut",
"action": "command",
"command_id": self.command_list_dir_pro.id,
"server_template_ids": [(4, self.server_template_sample.id)],
"access_level": "2",
}
)
# Remove bob from all cxtower_server groups
self.remove_from_group(
self.user_bob,
[
"cetmix_tower_server.group_user",
"cetmix_tower_server.group_manager",
"cetmix_tower_server.group_root",
],
)
shortcut_server_as_bob = shortcut_level_1_server.with_user(self.user_bob)
shortcut_template_as_bob = shortcut_level_2_template.with_user(self.user_bob)
# Test: User access
self.add_to_group(self.user_bob, "cetmix_tower_server.group_user")
self.server_test_1_pro.write({"user_ids": [(4, self.user_bob.id)]})
# User should see level 1 shortcuts for their servers
res = shortcut_server_as_bob.read(["name"])
self.assertEqual(res[0]["name"], shortcut_level_1_server.name)
# User should NOT see level 2 shortcuts
search_result = shortcut_template_as_bob.search(
[("id", "=", shortcut_level_2_template.id)]
)
self.assertEqual(len(search_result), 0)
# Test: Manager access through server assignment
self.add_to_group(self.user_bob, "cetmix_tower_server.group_manager")
self.server_test_1_pro.write({"manager_ids": [(4, self.user_bob.id)]})
# Manager should see shortcuts for servers they manage
res = shortcut_server_as_bob.read(["name"])
self.assertEqual(res[0]["name"], shortcut_level_1_server.name)
# Manager should NOT see template shortcuts without template access
search_result = shortcut_template_as_bob.search(
[("id", "=", shortcut_level_2_template.id)]
)
self.assertEqual(len(search_result), 0)
# Test: Manager access through template assignment
self.server_template_sample.write({"manager_ids": [(4, self.user_bob.id)]})
# Manager should now see template shortcuts
res = shortcut_template_as_bob.read(["name"])
self.assertEqual(res[0]["name"], shortcut_level_2_template.name)
# Test: Manager access as template user
self.server_template_sample.write(
{
"manager_ids": [(3, self.user_bob.id)], # Remove from managers
"user_ids": [(4, self.user_bob.id)], # Add as user
}
)
# Manager should still see template shortcuts when they're a template user
res = shortcut_template_as_bob.read(["name"])
self.assertEqual(res[0]["name"], shortcut_level_2_template.name)
# Test: Root access to all shortcuts
shortcut_level_3 = self.Shortcut.create(
{
"name": "Level 3 Mixed Shortcut",
"action": "command",
"command_id": self.command_list_dir_pro.id,
"server_ids": [(4, self.server_test_1_pro.id)],
"server_template_ids": [(4, self.server_template_sample.id)],
"access_level": "3",
}
)
shortcut_level_3_as_bob = shortcut_level_3.with_user(self.user_bob)
# Manager should NOT see level 3 shortcuts
search_result = shortcut_level_3_as_bob.search(
[("id", "=", shortcut_level_3.id)]
)
self.assertEqual(len(search_result), 0)
# Root should see all shortcuts
self.add_to_group(self.user_bob, "cetmix_tower_server.group_root")
search_result = shortcut_level_3_as_bob.search(
[
(
"id",
"in",
[
shortcut_level_1_server.id,
shortcut_level_2_template.id,
shortcut_level_3.id,
],
)
]
)
self.assertEqual(len(search_result), 3)
def test_shortcut_run_type_command(self):
"""Test run shortcut of type 'command'"""
self.shortcut_for_command.run(self.server_test_1_pro)
# Check command log
shortcut_result = self.CommandLog.search(
[("command_id", "=", self.shortcut_for_command.command_id.id)]
)
self.assertEqual(len(shortcut_result), 1, "Must be single log record")
self.assertEqual(
shortcut_result.server_id,
self.server_test_1_pro,
"Server should match",
)
def test_shortcut_run_type_plan(self):
"""Test run shortcut of type 'plan'"""
self.shortcut_for_flight_plan.run(self.server_test_1_pro)
# Check shortcut log
shortcut_result = self.PlanLog.search(
[("plan_id", "=", self.shortcut_for_flight_plan.plan_id.id)]
)
self.assertEqual(len(shortcut_result), 1, "Must be single log record")
self.assertEqual(
shortcut_result.server_id,
self.server_test_1_pro,
"Server should match",
)
def test_shortcut_run_from_context(self):
"""Test running shortcut with server from context"""
# Create a test shortcut
shortcut = self.Shortcut.create(
{
"name": "Context Test Shortcut",
"action": "command",
"command_id": self.command_list_dir_pro.id,
"server_ids": [(4, self.server_test_1_pro.id)],
}
)
# Run with server_id in context
shortcut.with_context(server_id=self.server_test_1_pro.id).run()
# Check command log was created
log_entries = self.CommandLog.search(
[
("command_id", "=", shortcut.command_id.id),
("server_id", "=", self.server_test_1_pro.id),
]
)
self.assertEqual(len(log_entries), 1, "Should create a log entry")
self.assertEqual(
log_entries.server_id,
self.server_test_1_pro,
"Server should match",
)

View File

@@ -0,0 +1,91 @@
from odoo.exceptions import AccessError, ValidationError
from .common import TestTowerCommon
class TestTowerTag(TestTowerCommon):
"""Test for the 'cx.tower.tag' model"""
def test_01_unlink_as_user_with_used_tag(self):
"""Test that user cannot delete tag that is in use"""
# Create test tag
test_tag = self.Tag.create(
{
"name": "Test Tag User",
}
)
# Link tag to server
self.server_test_1.write({"tag_ids": [(4, test_tag.id)]})
with self.assertRaises(ValidationError):
test_tag.with_user(self.user).unlink()
def test_02_unlink_as_user_with_unused_tag(self):
"""Test that user cannot delete tag even if it's not in use"""
# Create new unused tag
unused_tag = self.Tag.create(
{
"name": "Unused Tag",
}
)
# Try to delete unused tag
with self.assertRaises(AccessError):
unused_tag.with_user(self.user).unlink()
def test_03_unlink_as_manager_with_used_tag(self):
"""Test that manager cannot delete tag that is in use"""
# Create test tag as manager
test_tag = self.Tag.with_user(self.manager).create(
{
"name": "Test Tag Manager",
}
)
# Link tag to server
test_tag.write({"server_ids": [(4, self.server_test_1.id)]})
# Access error because user doesn't have access to server
with self.assertRaises(AccessError):
test_tag.with_user(self.user).unlink()
# Add 'manager' to server
self.server_test_1.write({"user_ids": [(4, self.manager.id)]})
# Validation error
with self.assertRaises(ValidationError):
test_tag.with_user(self.manager).unlink()
def test_04_unlink_as_manager_with_own_tag(self):
"""Test that manager can delete their own unused tag"""
# Create new unused tag as manager
unused_tag = self.Tag.with_user(self.manager).create(
{
"name": "Manager's Tag",
}
)
# Manager should be able to delete their own unused tag
unused_tag.with_user(self.manager).unlink()
def test_05_unlink_as_manager_with_other_tag(self):
"""Test that manager cannot delete tag created by other user"""
# Create tag as root
other_tag = self.Tag.create(
{
"name": "Other's Tag",
}
)
# Manager should not be able to delete tag created by other user
with self.assertRaises(AccessError):
other_tag.with_user(self.manager).unlink()
def test_06_unlink_as_sudo(self):
"""Test that sudo can delete tag that is in use"""
# Create test tag
test_tag = self.Tag.create(
{
"name": "Test Tag Sudo",
}
)
# Link tag to server
self.server_test_1.write({"tag_ids": [(4, test_tag.id)]})
test_tag.with_user(self.user).sudo().unlink()

View File

@@ -0,0 +1,167 @@
from .common import TestTowerCommon
class TestTowerTagMixin(TestTowerCommon):
"""Test class for tower tag mixin."""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create 3 tags to test tag mixin
cls.tag_test_1 = cls.Tag.create(
{
"name": "Test Tag 1",
}
)
cls.tag_test_2 = cls.Tag.create(
{
"name": "Test Tag 2",
}
)
cls.tag_test_3 = cls.Tag.create(
{
"name": "Test Tag 3",
}
)
# Create 3 commands to test tag mixin
cls.command_test_1 = cls.Command.create(
{
"name": "Test Command 1",
}
)
cls.command_test_2 = cls.Command.create(
{
"name": "Test Command 2",
}
)
cls.command_test_3 = cls.Command.create(
{
"name": "Test Command 3",
}
)
cls.all_commands = cls.command_test_1 | cls.command_test_2 | cls.command_test_3
# Add tags to commands
# - Command 1: Test Tag 1, Test Tag 2
cls.command_test_1.add_tags(["Test Tag 1", "Test Tag 2", "Test Tag 3"])
# - Command 2: Test Tag 2, Test Tag 3
cls.command_test_2.add_tags(["Test Tag 2", "Test Tag 3"])
# - Command 3: Test Tag 3
cls.command_test_3.add_tags(["Test Tag 3"])
def test_01_add_tags(self):
"""Test that tags are added to the record"""
self.assertEqual(len(self.command_test_1.tag_ids), 3)
self.assertEqual(len(self.command_test_2.tag_ids), 2)
self.assertEqual(len(self.command_test_3.tag_ids), 1)
self.assertIn(self.tag_test_1, self.command_test_1.tag_ids)
self.assertIn(self.tag_test_2, self.command_test_1.tag_ids)
self.assertIn(self.tag_test_3, self.command_test_1.tag_ids)
self.assertIn(self.tag_test_2, self.command_test_2.tag_ids)
self.assertIn(self.tag_test_3, self.command_test_2.tag_ids)
self.assertIn(self.tag_test_3, self.command_test_3.tag_ids)
# Test adding duplicate tags (should be idempotent)
self.command_test_1.add_tags(["Test Tag 1"])
self.assertEqual(len(self.command_test_1.tag_ids), 3)
# Test adding single tag name
self.command_test_1.add_tags("Test Tag 1")
self.assertEqual(len(self.command_test_1.tag_ids), 3)
self.assertIn(self.tag_test_1, self.command_test_1.tag_ids)
self.assertIn(self.tag_test_2, self.command_test_1.tag_ids)
self.assertIn(self.tag_test_3, self.command_test_1.tag_ids)
# Test adding invalid type (should return True)
self.assertTrue(self.command_test_1.add_tags(123))
self.assertTrue(self.command_test_1.add_tags([]))
# Test adding invalid type (should return True)
# Empty list is a no-op
before = len(self.command_test_1.tag_ids)
self.assertTrue(self.command_test_1.add_tags([]))
self.assertEqual(len(self.command_test_1.tag_ids), before)
# Test adding non-existent tags (should be ignored)
initial_count = len(self.command_test_1.tag_ids)
self.command_test_1.add_tags(["Non Existent Tag"])
self.assertEqual(len(self.command_test_1.tag_ids), initial_count)
def test_02_remove_tags(self):
"""Test that tags are removed from the record"""
self.command_test_1.remove_tags(["Test Tag 1", "Test Tag 2"])
self.assertEqual(len(self.command_test_1.tag_ids), 1)
# Test removing single tag name
self.command_test_2.remove_tags("Test Tag 2")
self.assertEqual(len(self.command_test_2.tag_ids), 1)
self.assertIn(self.tag_test_3, self.command_test_2.tag_ids)
# Test removing invalid type (should return True)
self.assertTrue(self.command_test_1.remove_tags(123))
# Test removing no tags (should return True)
self.assertTrue(self.command_test_1.remove_tags([]))
def test_03_has_tags(self):
"""Test that the record has any of the given tags"""
# Search selected records
commands_with_any_tags = self.all_commands.has_tags(
["Test Tag 1", "Test Tag 2"]
)
self.assertEqual(len(commands_with_any_tags), 2)
self.assertIn(self.command_test_1, commands_with_any_tags)
self.assertIn(self.command_test_2, commands_with_any_tags)
# Search all records in the model
commands_with_any_tags = self.Command.has_tags(
["Test Tag 1", "Test Tag 2"], search_all=True
)
self.assertEqual(len(commands_with_any_tags), 2)
self.assertIn(self.command_test_1, commands_with_any_tags)
self.assertIn(self.command_test_2, commands_with_any_tags)
# Search with single tag name
commands_with_any_tags = self.all_commands.has_tags("Test Tag 2")
self.assertEqual(len(commands_with_any_tags), 2)
self.assertIn(self.command_test_1, commands_with_any_tags)
self.assertIn(self.command_test_2, commands_with_any_tags)
commands_with_any_tags = self.Command.has_tags("Test Tag 2", search_all=True)
self.assertEqual(len(commands_with_any_tags), 2)
self.assertIn(self.command_test_1, commands_with_any_tags)
self.assertIn(self.command_test_2, commands_with_any_tags)
# Search with invalid type (should return empty recordset)
commands_with_any_tags = self.Command.has_tags(123)
self.assertEqual(len(commands_with_any_tags), 0)
# Search with no tags (should return empty recordset)
commands_with_any_tags = self.Command.has_tags([])
self.assertEqual(len(commands_with_any_tags), 0)
def test_04_has_all_tags(self):
"""Test that the record has all of the given tags"""
# Search selected records
commands_with_all_tags = self.all_commands.has_all_tags(
["Test Tag 1", "Test Tag 2"]
)
self.assertEqual(len(commands_with_all_tags), 1)
self.assertIn(self.command_test_1, commands_with_all_tags)
# Search all records in the model
commands_with_all_tags = self.Command.has_all_tags(
["Test Tag 1", "Test Tag 2"], search_all=True
)
self.assertEqual(len(commands_with_all_tags), 1)
self.assertIn(self.command_test_1, commands_with_all_tags)
# Search with invalid type (should return empty recordset)
commands_with_all_tags = self.Command.has_all_tags(123)
self.assertEqual(len(commands_with_all_tags), 0)
# Search with no tags (should return empty recordset)
commands_with_all_tags = self.Command.has_all_tags([])
self.assertEqual(len(commands_with_all_tags), 0)

View File

@@ -0,0 +1,38 @@
from odoo.tests import common
from ..models.tools import CHARS, generate_random_id
class TestTools(common.TransactionCase):
"""Test class for tools module."""
def test_generate_random_id(self):
"""Test random id generation"""
# Test single section
result = generate_random_id()
self.assertEqual(len(result), 4) # Default length is 4
self.assertTrue(all(c in CHARS for c in result)) # All chars from CHARS
# Test multiple sections
result = generate_random_id(sections=2)
sections = result.split("-")
self.assertEqual(len(sections), 2)
self.assertTrue(all(len(s) == 4 for s in sections))
self.assertTrue(all(c in CHARS for s in sections for c in s))
# Test custom population
result = generate_random_id(population=6)
self.assertEqual(len(result), 6)
# Test custom separator
result = generate_random_id(sections=2, separator="_")
self.assertIn("_", result)
self.assertEqual(len(result.split("_")), 2)
# Test invalid inputs
self.assertIsNone(generate_random_id(sections=0))
self.assertIsNone(generate_random_id(population=-1))
# Test empty separator
result = generate_random_id(sections=3, separator="")
self.assertEqual(len(result), 12) # 3 sections of 4 chars with no separator

View File

@@ -0,0 +1,204 @@
from .common import TestTowerCommon
class TestUpdateRelatedVariableNames(TestTowerCommon):
"""Test Update Related Variable Names"""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create test variables
cls.var1 = cls.Variable.create({"name": "var1", "reference": "var1"})
cls.var2 = cls.Variable.create({"name": "var2", "reference": "var2"})
cls.var3 = cls.Variable.create({"name": "var3", "reference": "var3"})
cls.test_command = cls.Command.create(
{
"name": "Test Command",
"code": "{{ var1 }} and {{ var2 }}",
"path": "{{ var3 }}",
}
)
cls.server = cls.Server.create(
{
"name": "Test Server",
"color": 2,
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "k",
"ssh_key_id": cls.key_1.id,
}
)
cls.test_file = cls.File.create(
{
"server_id": cls.server.id,
"code": "{{ var1 }} is used",
"server_dir": "path/to/{{ var2 }}",
"name": "{{ var3 }}.txt",
}
)
cls.test_plan_line = cls.plan_line.create(
{
"command_id": cls.test_command.id,
"condition": "Condition with {{ var1 }} and {{ var2 }}",
}
)
cls.test_variable_value = cls.VariableValue.create(
{
"variable_id": cls.variable_os.id,
"value_char": "{{ var1 }} is here and {{ var2 }} too",
}
)
cls.test_file_template = cls.FileTemplate.create(
{
"name": "Test File Template",
"code": "{{ var1 }} in code",
"server_dir": "This path has {{ var2 }}",
"file_name": "file_name_with_{{ var1 }}",
}
)
def test_variables_command_computation(self):
"""
Test that the variable_ids field is correctly computed based on the 'code'
and 'path' fields of the command.
"""
# Verify that the correct variables are assigned to variable_ids
self.assertEqual(
set(self.test_command.variable_ids.ids),
{self.var1.id, self.var2.id, self.var3.id},
"The variable_ids should contain var1, var2, and var3.",
)
def test_variables_command_clearing(self):
"""
Test that the variable_ids field is cleared when
no variables are found in the code or path.
"""
# Update code and path to remove references
self.test_command.write(
{"code": "No variables here", "path": "No variables here either"}
)
# Verify that variable_ids is empty
self.assertFalse(
self.test_command.variable_ids,
"The variable_ids should be empty when no variables are found.",
)
def test_variables_file_computation(self):
"""
Test that the variable_ids field is correctly computed based on the 'code',
'server_dir', and 'name' fields of the file.
"""
# Verify that the correct variables are assigned to variable_ids
self.assertEqual(
set(self.test_file.variable_ids.ids),
{self.var1.id, self.var2.id, self.var3.id},
"The variable_ids should contain var1, var2, and var3.",
)
def test_variables_file_clearing(self):
"""
Test that the variable_ids field is cleared when
no variables are found in the code, server_dir, or name fields.
"""
# Update the file to remove references
self.test_file.write(
{
"code": "No variables here",
"server_dir": "No variables here either",
"name": "no_var.txt",
}
)
# Verify that variable_ids is empty
self.assertFalse(
self.test_file.variable_ids,
"The variable_ids should be empty when no variables are found.",
)
def test_variables_plan_line_computation(self):
"""
Test that the variable_ids field is correctly
computed based on the 'condition' field of the plan line.
"""
# Verify that the correct variables are assigned to variable_ids
self.assertEqual(
set(self.test_plan_line.variable_ids.ids),
{self.var1.id, self.var2.id},
"The variable_ids should contain var1 and var2.",
)
def test_variables_plan_line_clearing(self):
"""
Test that the variable_ids field is cleared when
no variables are found in the condition field.
"""
# Update the plan line to remove references
self.test_plan_line.write({"condition": "No variables in this condition"})
# Verify that variable_ids is empty
self.assertFalse(
self.test_plan_line.variable_ids,
"The variable_ids should be empty when no variables are found.",
)
def test_variables_variable_value_computation(self):
"""
Test that the variable_ids field is correctly
computed based on the 'value_char' field.
"""
# Verify that the correct variables are assigned to variable_ids
self.assertEqual(
set(self.test_variable_value.variable_ids.ids),
{self.var1.id, self.var2.id},
"The variable_ids should contain var1 and var2.",
)
def test_variables_variable_value_clearing(self):
"""
Test that the variable_ids field is cleared when
no variables are found in the value_char field.
"""
# Update the variable value to remove references
self.test_variable_value.write({"value_char": "No variables in this text"})
# Verify that variable_ids is empty
self.assertFalse(
self.test_variable_value.variable_ids,
"The variable_ids should be empty when no variables are found.",
)
def test_variables_file_template_computation(self):
"""
Test that the variable_ids field is correctly computed
based on 'code', 'server_dir', and 'file_name' fields.
"""
# Verify that the correct variables are assigned to variable_ids
self.assertEqual(
set(self.test_file_template.variable_ids.ids),
{self.var1.id, self.var2.id},
"The variable_ids should contain var1 and var2.",
)
def test_variable_file_template_clearing(self):
"""
Test that the variable_ids field is cleared when
no variables are found in code, server_dir, or file_name.
"""
# Update the file template to remove references
self.test_file_template.write(
{
"code": "No variables here",
"server_dir": "No variables here either",
"file_name": "no_var_in_file",
}
)
# Verify that variable_ids is empty
self.assertFalse(
self.test_file_template.variable_ids,
"The variable_ids should be empty when no variables are found.",
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,285 @@
from odoo.exceptions import AccessError, ValidationError
from .common import TestTowerCommon
class TestTowerVariableOption(TestTowerCommon):
"""Test case class to validate the behavior of
'cx.tower.variable.option' model.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.variable_odoo_versions = cls.Variable.create(
{
"name": "odoo_versions",
"variable_type": "o",
}
)
cls.variable_option_17_0 = cls.VariableOption.create(
{
"name": "17.0",
"value_char": "17.0",
"variable_id": cls.variable_odoo_versions.id,
}
)
cls.variable_option_18_0 = cls.VariableOption.create(
{
"name": "18.0",
"value_char": "18.0",
"variable_id": cls.variable_odoo_versions.id,
}
)
# Create additional test users
cls.manager2 = cls.Users.create(
{
"name": "Manager 2",
"login": "manager2@example.com",
"groups_id": [(4, cls.group_manager.id)],
}
)
# Create variables with different access levels
cls.variable_level_1 = cls.Variable.create(
{
"name": "Level 1 Variable",
"access_level": "1",
}
)
cls.variable_level_2 = cls.Variable.create(
{
"name": "Level 2 Variable",
"access_level": "2",
}
)
# Create options with different access levels (inherited from variables)
cls.option_level_1 = cls.VariableOption.create(
{
"name": "Option Level 1",
"value_char": "value1",
"variable_id": cls.variable_level_1.id,
}
)
cls.option_level_2 = cls.VariableOption.create(
{
"name": "Option Level 2",
"value_char": "value2",
"variable_id": cls.variable_level_2.id,
}
)
def test_variable_value_set_from_option(self):
"""Test that a variable value can be set from an option."""
variable_value = self.VariableValue.create(
{
"server_id": self.server_test_1.id,
"variable_id": self.variable_odoo_versions.id,
}
)
# -- 1 --
# Set value_char to an existing option
variable_value.value_char = "17.0"
self.assertEqual(
variable_value.option_id,
self.variable_option_17_0,
)
# -- 2 --
# Set value_char to a non-existing option
variable_meme_level = self.Variable.create(
{
"name": "meme_level",
"variable_type": "o",
}
)
option_meme_level_high = self.VariableOption.create(
{
"name": "high",
"value_char": "high",
"variable_id": variable_meme_level.id,
}
)
with self.assertRaises(ValidationError):
variable_value.option_id = option_meme_level_high
# -- 3 --
# Set value_char to a non-existing option
variable_value.value_char = "29.0"
self.assertFalse(variable_value.option_id)
def test_access_level_consistency(self):
"""Test that variable option access level cannot be lower
than variable access level."""
# Create a variable with access level "2"
variable_restricted = self.Variable.create(
{
"name": "restricted_variable",
"variable_type": "o",
"access_level": "2",
}
)
# Should succeed: option with same access level as variable
try:
self.VariableOption.create(
{
"name": "Option 1",
"value_char": "value1",
"variable_id": variable_restricted.id,
"access_level": "2",
}
)
except ValidationError:
self.fail("Should allow creating option with same access level as variable")
# Should succeed: option with higher access level than variable
try:
self.VariableOption.create(
{
"name": "Option 2",
"value_char": "value2",
"variable_id": variable_restricted.id,
"access_level": "3",
}
)
except ValidationError:
self.fail(
"Should allow creating option with higher access level than variable"
)
# Should fail: option with lower access level than variable
with self.assertRaises(
ValidationError,
msg="Should not allow creating option "
"with lower access level than variable",
):
self.VariableOption.create(
{
"name": "Option 3",
"value_char": "value3",
"variable_id": variable_restricted.id,
"access_level": "1",
}
)
# Test updating existing option's access level
option = self.VariableOption.create(
{
"name": "Option 4",
"value_char": "value4",
"variable_id": variable_restricted.id,
"access_level": "2",
}
)
# Should fail: updating to lower access level than variable
with self.assertRaises(
ValidationError,
msg="Should not allow updating option to lower access level than variable",
):
option.write({"access_level": "1"})
# Should succeed: updating to higher access level than variable
try:
option.write({"access_level": "3"})
except ValidationError:
self.fail(
"Should allow updating option to higher access level than variable"
)
def test_variable_option_access_rights(self):
"""
Test access rights for variable options
based on access levels and user roles.
"""
# Test User Access
# ---------------
# Should see level 1 options only
records = self.VariableOption.with_user(self.user).search(
[("id", "in", [self.option_level_1.id, self.option_level_2.id])]
)
self.assertEqual(len(records), 1, "User should only see level 1 options")
self.assertEqual(
records.id, self.option_level_1.id, "User should only see level 1 options"
)
# Test Manager Access
# -----------------
# Should see level 1 and 2 options
records = self.VariableOption.with_user(self.manager).search(
[("id", "in", [self.option_level_1.id, self.option_level_2.id])]
)
self.assertEqual(len(records), 2, "Manager should see level 1 and 2 options")
self.assertIn(
self.option_level_1.id, records.ids, "Manager should see level 1 options"
)
self.assertIn(
self.option_level_2.id, records.ids, "Manager should see level 2 options"
)
# Test Manager Write Access
# -----------------------
# Create an option as manager
manager_option = self.VariableOption.with_user(self.manager).create(
{
"name": "Manager Created Option",
"value_char": "manager_value",
"variable_id": self.variable_level_2.id,
}
)
# Manager should be able to modify their own option
try:
manager_option.with_user(self.manager).write({"name": "Updated Name"})
except AccessError:
self.fail("Manager should be able to modify their own options")
# Manager should not be able to modify another manager's option
manager2_option = self.VariableOption.with_user(self.manager2).create(
{
"name": "Other Manager Option",
"value_char": "other_value",
"variable_id": self.variable_level_2.id,
}
)
with self.assertRaises(AccessError):
manager2_option.with_user(self.manager).write({"name": "Try Update"})
# Test Root Access
# --------------
# Root should see all options
records = self.VariableOption.with_user(self.root).search(
[("id", "in", [self.option_level_1.id, self.option_level_2.id])]
)
self.assertEqual(len(records), 2, "Root should see all options")
# Root should be able to create any option
try:
self.VariableOption.with_user(self.root).create(
{
"name": "Root Created Option",
"value_char": "root_value",
"variable_id": self.variable_level_2.id,
}
)
except AccessError:
self.fail("Root should be able to create any option")
# Root should be able to modify any option
try:
self.option_level_2.with_user(self.root).write({"name": "Updated by Root"})
except AccessError:
self.fail("Root should be able to modify any option")

View File

@@ -0,0 +1,952 @@
from odoo.exceptions import AccessError
from . import common
class TestTowerVariableValue(common.TestTowerCommon):
"""Testing variable values."""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create additional test users
cls.user2 = cls.Users.create(
{
"name": "Test User 2",
"login": "test_user2",
"email": "test_user2@example.com",
"groups_id": [(6, 0, [cls.group_user.id])],
}
)
cls.manager2 = cls.Users.create(
{
"name": "Test Manager 2",
"login": "test_manager2",
"email": "test_manager2@example.com",
"groups_id": [(6, 0, [cls.group_manager.id])],
}
)
# Create variables with different access levels
cls.variable_level_1 = cls.Variable.create(
{
"name": "Level 1 Variable",
"access_level": "1",
}
)
cls.variable_level_2 = cls.Variable.create(
{
"name": "Level 2 Variable",
"access_level": "2",
}
)
# Create servers
cls.server_1 = cls.Server.create(
{
"name": "Test Server 1",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"os_id": cls.os_debian_10.id,
"user_ids": [(4, cls.user.id)],
"manager_ids": [(4, cls.manager.id)],
}
)
cls.server_2 = cls.Server.create(
{
"name": "Test Server 2",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"os_id": cls.os_debian_10.id,
"user_ids": [(4, cls.user2.id)],
"manager_ids": [(4, cls.manager2.id)],
}
)
# Create test command
cls.test_command = cls.Command.create(
{
"name": "Test Command",
"code": "echo 'test'",
}
)
# Create flight plan and its components
cls.test_plan = cls.Plan.create(
{
"name": "Test Plan",
"user_ids": [(4, cls.user.id)],
"manager_ids": [(4, cls.manager.id)],
}
)
cls.test_plan_line = cls.plan_line.create(
{
"name": "Test Line",
"plan_id": cls.test_plan.id,
"command_id": cls.test_command.id,
}
)
cls.test_plan_line_action = cls.plan_line_action.create(
{
"name": "Test Action",
"line_id": cls.test_plan_line.id,
"condition": "==",
"value_char": "0",
"action": "n",
}
)
# Create variable values
cls.global_value_1 = cls.VariableValue.create(
{
"variable_id": cls.variable_level_1.id,
"value_char": "global_value_1",
}
)
cls.global_value_2 = cls.VariableValue.create(
{
"variable_id": cls.variable_level_2.id,
"value_char": "global_value_2",
}
)
cls.server_value_1 = cls.VariableValue.create(
{
"variable_id": cls.variable_level_1.id,
"value_char": "server_value_1",
"server_id": cls.server_1.id,
}
)
cls.server_value_2 = cls.VariableValue.with_user(cls.manager).create(
{
"variable_id": cls.variable_level_2.id,
"value_char": "server_value_2",
"server_id": cls.server_1.id,
}
)
cls.plan_value_1 = cls.VariableValue.create(
{
"variable_id": cls.variable_level_1.id,
"value_char": "plan_value_1",
"plan_line_action_id": cls.test_plan_line_action.id,
}
)
cls.plan_value_2 = cls.VariableValue.create(
{
"variable_id": cls.variable_level_2.id,
"value_char": "plan_value_2",
"plan_line_action_id": cls.test_plan_line_action.id,
}
)
# Add server template setup
cls.server_template = cls.ServerTemplate.create(
{
"name": "Test Template",
"ssh_username": "admin",
"ssh_password": "password",
"os_id": cls.os_debian_10.id,
"manager_ids": [
(4, cls.manager.id)
], # Only managers should have access
}
)
# Add template variable values
cls.template_value_1 = cls.VariableValue.create(
{
"variable_id": cls.variable_level_1.id,
"value_char": "template_value_1",
"server_template_id": cls.server_template.id,
}
)
cls.template_value_2 = cls.VariableValue.with_user(cls.manager).create(
{
"variable_id": cls.variable_level_2.id,
"value_char": "template_value_2",
"server_template_id": cls.server_template.id,
}
)
# Add server to plan
cls.test_plan.write({"server_ids": [(4, cls.server_1.id)]})
# Create Jet Template
cls.jet_template = cls.JetTemplate.create(
{
"name": "Test Jet Template",
"server_ids": [(4, cls.server_1.id)],
"user_ids": [(4, cls.user.id)],
"manager_ids": [(4, cls.manager.id)],
}
)
# Create Jet Template variable values
cls.jet_template_value_1 = cls.VariableValue.create(
{
"variable_id": cls.variable_level_1.id,
"value_char": "jet_template_value_1",
"jet_template_id": cls.jet_template.id,
}
)
cls.jet_template_value_2 = cls.VariableValue.with_user(cls.manager).create(
{
"variable_id": cls.variable_level_2.id,
"value_char": "jet_template_value_2",
"jet_template_id": cls.jet_template.id,
}
)
# Create Jet
cls.jet = cls.Jet.create(
{
"name": "Test Jet",
"jet_template_id": cls.jet_template.id,
"server_id": cls.server_1.id,
"user_ids": [(4, cls.user.id)],
"manager_ids": [(4, cls.manager.id)],
}
)
# Create Jet variable values
cls.jet_value_1 = cls.VariableValue.create(
{
"variable_id": cls.variable_level_1.id,
"value_char": "jet_value_1",
"jet_id": cls.jet.id,
}
)
cls.jet_value_2 = cls.VariableValue.with_user(cls.manager).create(
{
"variable_id": cls.variable_level_2.id,
"value_char": "jet_value_2",
"jet_id": cls.jet.id,
}
)
def test_variable_value_access_rights(self):
"""
Test access rights for variable values
based on access levels and user roles.
"""
# Test User Access
# ---------------
user_values = self.VariableValue.with_user(self.user).search(
[
(
"id",
"in",
[
self.global_value_1.id,
self.global_value_2.id,
self.server_value_1.id,
self.server_value_2.id,
self.plan_value_1.id,
self.plan_value_2.id,
],
)
]
)
# User should see level 1 global values and level 1 values
# from their server/plan
self.assertEqual(len(user_values), 3)
self.assertIn(self.global_value_1.id, user_values.ids)
self.assertIn(self.server_value_1.id, user_values.ids)
self.assertIn(self.plan_value_1.id, user_values.ids)
# User should not be able to create/write/unlink values
with self.assertRaises(AccessError):
self.VariableValue.with_user(self.user).create(
{
"variable_id": self.variable_level_1.id,
"value_char": "test",
"server_id": self.server_1.id,
}
)
with self.assertRaises(AccessError):
self.server_value_1.with_user(self.user).write({"value_char": "new_value"})
with self.assertRaises(AccessError):
self.server_value_1.with_user(self.user).unlink()
# Test Manager Access
# ------------------
manager_values = self.VariableValue.with_user(self.manager).search(
[
(
"id",
"in",
[
self.global_value_1.id,
self.global_value_2.id,
self.server_value_1.id,
self.server_value_2.id,
self.plan_value_1.id,
self.plan_value_2.id,
],
)
]
)
# Manager should see all level 1 and 2 values from their server/plan
self.assertEqual(len(manager_values), 6)
# Manager should be able to create values for their server/plan
test_variable = self.Variable.create(
{
"name": "Test Variable",
"access_level": "2",
}
)
try:
new_value = self.VariableValue.with_user(self.manager).create(
{
"variable_id": test_variable.id,
"value_char": "manager_value",
"server_id": self.server_1.id,
}
)
except AccessError:
self.fail("Manager should be able to create values for their server")
# Manager should be able to modify values for their server/plan
try:
self.server_value_2.with_user(self.manager).write(
{"value_char": "updated_value"}
)
except AccessError:
self.fail("Manager should be able to modify values for their server")
# Manager should be able to delete their own values
try:
new_value.with_user(self.manager).unlink()
except AccessError:
self.fail("Manager should be able to delete their own values")
# Manager should not be able to modify other manager's values
with self.assertRaises(AccessError):
self.VariableValue.with_user(self.manager).create(
{
"variable_id": self.variable_level_1.id,
"value_char": "test",
"server_id": self.server_2.id,
}
)
# Test Root Access
# ---------------
root_values = self.VariableValue.with_user(self.root).search(
[
(
"id",
"in",
[
self.global_value_1.id,
self.global_value_2.id,
self.server_value_1.id,
self.server_value_2.id,
self.plan_value_1.id,
self.plan_value_2.id,
],
)
]
)
# Root should see all values
self.assertEqual(len(root_values), 6)
# Root should be able to create any value
try:
root_value = self.VariableValue.with_user(self.root).create(
{
"variable_id": self.variable_level_2.id,
"value_char": "root_value",
"server_id": self.server_2.id,
"access_level": "2",
}
)
except AccessError:
self.fail("Root should be able to create any value")
# Root should be able to modify any value
try:
self.server_value_2.with_user(self.root).write(
{"value_char": "root_updated"}
)
except AccessError:
self.fail("Root should be able to modify any value")
# Root should be able to delete any value
try:
root_value.with_user(self.root).unlink()
except AccessError:
self.fail("Root should be able to delete any value")
def test_server_template_access(self):
"""Test access rights for server template variable values"""
# Test user access to template values
# (should see none since they don't have template access)
user_template_values = self.VariableValue.with_user(self.user).search(
[("server_template_id", "=", self.server_template.id)]
)
self.assertEqual(
len(user_template_values), 0
) # Users can't see template values
# Test manager access to template values
manager_template_values = self.VariableValue.with_user(self.manager).search(
[("server_template_id", "=", self.server_template.id)]
)
self.assertEqual(len(manager_template_values), 2)
# Create a new variable for testing manager create rights
test_variable = self.Variable.create(
{
"name": "Test Template Manager Variable",
"access_level": "2",
}
)
# Test manager create rights
new_template_value = self.VariableValue.with_user(self.manager).create(
{
"variable_id": test_variable.id, # Use the new variable
"value_char": "new_template_value",
"server_template_id": self.server_template.id,
}
)
self.assertTrue(new_template_value.exists())
# Test manager write rights
self.template_value_2.with_user(self.manager).write(
{"value_char": "updated_template_value"}
)
self.assertEqual(self.template_value_2.value_char, "updated_template_value")
# Test manager unlink rights (only own records)
new_template_value.with_user(self.manager).unlink()
self.assertFalse(new_template_value.exists())
def test_server_template_manager_in_users_access(self):
"""Test access rights for server template when manager is in user_ids only"""
# Create new template with manager in user_ids only (not in manager_ids)
template_with_manager_user = self.ServerTemplate.create(
{
"name": "Template With Manager User",
"ssh_username": "admin",
"ssh_password": "password",
"os_id": self.os_debian_10.id,
"user_ids": [(4, self.manager.id)], # Add manager to user_ids only
}
)
# Create test values as root to set up the test
template_value_1 = self.VariableValue.create(
{
"variable_id": self.variable_level_1.id,
"value_char": "manager_user_value_1",
"server_template_id": template_with_manager_user.id,
}
)
template_value_2 = self.VariableValue.create(
{
"variable_id": self.variable_level_2.id,
"value_char": "manager_user_value_2",
"server_template_id": template_with_manager_user.id,
}
)
# Test manager can read both level 1 and level 2 values
# (Manager Read rule allows access_level <= '2' when manager is in user_ids)
manager_values = self.VariableValue.with_user(self.manager).search(
[("server_template_id", "=", template_with_manager_user.id)]
)
self.assertEqual(len(manager_values), 2)
self.assertIn(template_value_1.id, manager_values.ids)
self.assertIn(template_value_2.id, manager_values.ids)
# Create a new variable for testing create access
test_variable = self.Variable.create(
{
"name": "Test Template User Variable",
"access_level": "1",
}
)
# Test manager cannot create values
with self.assertRaises(AccessError):
self.VariableValue.with_user(self.manager).create(
{
"variable_id": test_variable.id, # Use the new variable
"value_char": "new_manager_user_value",
"server_template_id": template_with_manager_user.id,
}
)
# Test manager cannot write values
with self.assertRaises(AccessError):
template_value_1.with_user(self.manager).write(
{"value_char": "updated_manager_user_value"}
)
# Test manager cannot delete values
with self.assertRaises(AccessError):
template_value_1.with_user(self.manager).unlink()
def test_plan_server_access(self):
"""Test access rights for plan server variable values"""
# Create a new variable for testing
test_variable = self.Variable.create(
{
"name": "Test Plan Server Variable",
"access_level": "2",
}
)
# Create variable value for plan server (only assign to server)
plan_server_value = self.VariableValue.with_user(self.manager).create(
{
"variable_id": test_variable.id,
"value_char": "plan_server_value",
"server_id": self.server_1.id,
}
)
# Test user read access
user_plan_server_values = self.VariableValue.with_user(self.user).search(
[("server_id", "=", self.server_1.id), ("access_level", "=", "1")]
)
self.assertTrue(user_plan_server_values)
# Test manager read/write access
manager_plan_server_values = self.VariableValue.with_user(self.manager).search(
[("server_id", "=", self.server_1.id)]
)
self.assertTrue(manager_plan_server_values)
# Test manager write rights
plan_server_value.with_user(self.manager).write(
{"value_char": "updated_plan_server_value"}
)
self.assertEqual(plan_server_value.value_char, "updated_plan_server_value")
# Create another new variable for testing create rights
test_variable_2 = self.Variable.create(
{
"name": "Test Plan Server Variable 2",
"access_level": "2",
}
)
# Test manager create rights (only assign to server)
new_plan_server_value = self.VariableValue.with_user(self.manager).create(
{
"variable_id": test_variable_2.id,
"value_char": "new_plan_server_value",
"server_id": self.server_1.id,
}
)
self.assertTrue(new_plan_server_value.exists())
# Test manager unlink rights (only own records)
new_plan_server_value.with_user(self.manager).unlink()
self.assertFalse(new_plan_server_value.exists())
# Test plan-specific variable values
test_variable_3 = self.Variable.create(
{
"name": "Test Plan Action Variable",
"access_level": "2",
}
)
# Create variable value for plan action
plan_action_value = self.VariableValue.with_user(self.manager).create(
{
"variable_id": test_variable_3.id,
"value_char": "plan_action_value",
"plan_line_action_id": self.test_plan_line_action.id,
}
)
self.assertTrue(plan_action_value.exists())
# Test manager access to plan action values
manager_plan_values = self.VariableValue.with_user(self.manager).search(
[("plan_line_action_id", "=", self.test_plan_line_action.id)]
)
self.assertIn(plan_action_value.id, manager_plan_values.ids)
def test_jet_access(self):
"""Test access rights for Jet variable values"""
# Test user access to jet values
# User should see level 1 values from jets they're added to
user_jet_values = self.VariableValue.with_user(self.user).search(
[("jet_id", "=", self.jet.id)]
)
self.assertEqual(len(user_jet_values), 1)
self.assertIn(self.jet_value_1.id, user_jet_values.ids)
# User should not be able to create/write/unlink values
with self.assertRaises(AccessError):
self.VariableValue.with_user(self.user).create(
{
"variable_id": self.variable_level_1.id,
"value_char": "test",
"jet_id": self.jet.id,
}
)
with self.assertRaises(AccessError):
self.jet_value_1.with_user(self.user).write({"value_char": "new_value"})
with self.assertRaises(AccessError):
self.jet_value_1.with_user(self.user).unlink()
# Test manager access to jet values
# Manager should see all level 1 and 2 values from jets they're added to
manager_jet_values = self.VariableValue.with_user(self.manager).search(
[("jet_id", "=", self.jet.id)]
)
self.assertEqual(len(manager_jet_values), 2)
self.assertIn(self.jet_value_1.id, manager_jet_values.ids)
self.assertIn(self.jet_value_2.id, manager_jet_values.ids)
# Create a new variable for testing manager create rights
test_variable = self.Variable.create(
{
"name": "Test Jet Manager Variable",
"access_level": "2",
}
)
# Test manager create rights (only when manager in jet manager_ids)
new_jet_value = self.VariableValue.with_user(self.manager).create(
{
"variable_id": test_variable.id,
"value_char": "new_jet_value",
"jet_id": self.jet.id,
}
)
self.assertTrue(new_jet_value.exists())
# Test manager write rights
self.jet_value_2.with_user(self.manager).write(
{"value_char": "updated_jet_value"}
)
self.assertEqual(self.jet_value_2.value_char, "updated_jet_value")
# Test manager unlink rights (only own records)
new_jet_value.with_user(self.manager).unlink()
self.assertFalse(new_jet_value.exists())
# Test manager cannot create values for jets they're not managers of
jet_without_manager = self.Jet.create(
{
"name": "Jet Without Manager",
"jet_template_id": self.jet_template.id,
"server_id": self.server_1.id,
"user_ids": [(4, self.user2.id)],
"manager_ids": [(4, self.manager2.id)],
}
)
with self.assertRaises(AccessError):
self.VariableValue.with_user(self.manager).create(
{
"variable_id": self.variable_level_1.id,
"value_char": "test",
"jet_id": jet_without_manager.id,
}
)
def test_jet_manager_in_users_access(self):
"""Test access rights for Jet when manager is in user_ids only"""
# Create new jet with manager in user_ids only (not in manager_ids)
jet_with_manager_user = self.Jet.create(
{
"name": "Jet With Manager User",
"jet_template_id": self.jet_template.id,
"server_id": self.server_1.id,
"user_ids": [(4, self.manager.id)], # Add manager to user_ids only
"manager_ids": [(5, 0, 0)],
}
)
# Create test values as root to set up the test
jet_value_1 = self.VariableValue.create(
{
"variable_id": self.variable_level_1.id,
"value_char": "manager_user_value_1",
"jet_id": jet_with_manager_user.id,
}
)
jet_value_2 = self.VariableValue.create(
{
"variable_id": self.variable_level_2.id,
"value_char": "manager_user_value_2",
"jet_id": jet_with_manager_user.id,
}
)
# Test manager can read both level 1 and level 2 values
# (Manager Read rule allows access_level <= '2' when manager is in user_ids)
manager_values = self.VariableValue.with_user(self.manager).search(
[("jet_id", "=", jet_with_manager_user.id)]
)
self.assertEqual(len(manager_values), 2)
self.assertIn(jet_value_1.id, manager_values.ids)
self.assertIn(jet_value_2.id, manager_values.ids)
# Create a new variable for testing create access
test_variable = self.Variable.create(
{
"name": "Test Jet User Variable",
"access_level": "1",
}
)
# Test manager cannot create values
with self.assertRaises(AccessError):
self.VariableValue.with_user(self.manager).create(
{
"variable_id": test_variable.id,
"value_char": "new_manager_user_value",
"jet_id": jet_with_manager_user.id,
}
)
# Test manager cannot write values
with self.assertRaises(AccessError):
jet_value_1.with_user(self.manager).write(
{"value_char": "updated_manager_user_value"}
)
# Test manager cannot delete values
with self.assertRaises(AccessError):
jet_value_1.with_user(self.manager).unlink()
def test_jet_template_access(self):
"""Test access rights for Jet Template variable values"""
# Test user access to template values
# User should see level 1 values from jet templates they're added to
user_jet_template_values = self.VariableValue.with_user(self.user).search(
[("jet_template_id", "=", self.jet_template.id)]
)
self.assertEqual(len(user_jet_template_values), 1)
self.assertIn(self.jet_template_value_1.id, user_jet_template_values.ids)
# User should not be able to create/write/unlink values
with self.assertRaises(AccessError):
self.VariableValue.with_user(self.user).create(
{
"variable_id": self.variable_level_1.id,
"value_char": "test",
"jet_template_id": self.jet_template.id,
}
)
with self.assertRaises(AccessError):
self.jet_template_value_1.with_user(self.user).write(
{"value_char": "new_value"}
)
with self.assertRaises(AccessError):
self.jet_template_value_1.with_user(self.user).unlink()
# Test manager access to template values
# Manager should see all level 1 and 2 values from jet templates
# they're added to
manager_jet_template_values = self.VariableValue.with_user(self.manager).search(
[("jet_template_id", "=", self.jet_template.id)]
)
self.assertEqual(len(manager_jet_template_values), 2)
self.assertIn(self.jet_template_value_1.id, manager_jet_template_values.ids)
self.assertIn(self.jet_template_value_2.id, manager_jet_template_values.ids)
# Create a new variable for testing manager create rights
test_variable = self.Variable.create(
{
"name": "Test Jet Template Manager Variable",
"access_level": "2",
}
)
# Test manager create rights (only when manager in template manager_ids)
new_jet_template_value = self.VariableValue.with_user(self.manager).create(
{
"variable_id": test_variable.id,
"value_char": "new_jet_template_value",
"jet_template_id": self.jet_template.id,
}
)
self.assertTrue(new_jet_template_value.exists())
# Test manager write rights
self.jet_template_value_2.with_user(self.manager).write(
{"value_char": "updated_jet_template_value"}
)
self.assertEqual(
self.jet_template_value_2.value_char, "updated_jet_template_value"
)
# Test manager unlink rights (only own records)
new_jet_template_value.with_user(self.manager).unlink()
self.assertFalse(new_jet_template_value.exists())
# Test manager cannot create values for templates they're not managers of
jet_template_without_manager = self.JetTemplate.create(
{
"name": "Template Without Manager",
"server_ids": [(4, self.server_1.id)],
"user_ids": [(4, self.user2.id)],
"manager_ids": [(4, self.manager2.id)],
}
)
with self.assertRaises(AccessError):
self.VariableValue.with_user(self.manager).create(
{
"variable_id": self.variable_level_1.id,
"value_char": "test",
"jet_template_id": jet_template_without_manager.id,
}
)
def test_jet_template_manager_in_users_access(self):
"""Test access rights for Jet Template when manager is in user_ids only"""
# Create new template with manager in user_ids only (not in manager_ids)
template_with_manager_user = self.JetTemplate.create(
{
"name": "Template With Manager User",
"server_ids": [(4, self.server_1.id)],
"user_ids": [(4, self.manager.id)], # Add manager to user_ids only
"manager_ids": [(5, 0, 0)],
}
)
# Create test values as root to set up the test
template_value_1 = self.VariableValue.create(
{
"variable_id": self.variable_level_1.id,
"value_char": "manager_user_value_1",
"jet_template_id": template_with_manager_user.id,
}
)
template_value_2 = self.VariableValue.create(
{
"variable_id": self.variable_level_2.id,
"value_char": "manager_user_value_2",
"jet_template_id": template_with_manager_user.id,
}
)
# Test manager can read both level 1 and level 2 values
# (Manager Read rule allows access_level <= '2' when manager is in user_ids)
manager_values = self.VariableValue.with_user(self.manager).search(
[("jet_template_id", "=", template_with_manager_user.id)]
)
self.assertEqual(len(manager_values), 2)
self.assertIn(template_value_1.id, manager_values.ids)
self.assertIn(template_value_2.id, manager_values.ids)
# Create a new variable for testing create access
test_variable = self.Variable.create(
{
"name": "Test Template User Variable",
"access_level": "1",
}
)
# Test manager cannot create values
with self.assertRaises(AccessError):
self.VariableValue.with_user(self.manager).create(
{
"variable_id": test_variable.id,
"value_char": "new_manager_user_value",
"jet_template_id": template_with_manager_user.id,
}
)
# Test manager cannot write values
with self.assertRaises(AccessError):
template_value_1.with_user(self.manager).write(
{"value_char": "updated_manager_user_value"}
)
# Test manager cannot delete values
with self.assertRaises(AccessError):
template_value_1.with_user(self.manager).unlink()
def test_reference_pattern_global_server_template_action(self):
"""Ensure model-scoped references follow the required pattern."""
# Global
model_ref = self.VariableValue._get_model_generic_reference()
self.assertTrue(self.global_value_1.reference.endswith(f"_{model_ref}_global"))
# Server
srv_model_ref = self.Server._get_model_generic_reference()
self.assertTrue(
self.server_value_1.reference.startswith(
f"{self.variable_level_1.reference}_{model_ref}_{srv_model_ref}_"
)
)
# Server Template
tmpl_model_ref = self.ServerTemplate._get_model_generic_reference()
self.assertTrue(
self.template_value_1.reference.startswith(
f"{self.variable_level_1.reference}_{model_ref}_{tmpl_model_ref}_"
)
)
# Plan Line Action
action_model_ref = self.plan_line_action._get_model_generic_reference()
self.assertTrue(
self.plan_value_1.reference.startswith(
f"{self.variable_level_1.reference}_{model_ref}_{action_model_ref}_"
)
)
# Jet Template
jet_tmpl_model_ref = self.JetTemplate._get_model_generic_reference()
self.assertTrue(
self.jet_template_value_1.reference.startswith(
f"{self.variable_level_1.reference}_{model_ref}_{jet_tmpl_model_ref}_"
)
)
# Jet
jet_model_ref = self.Jet._get_model_generic_reference()
self.assertTrue(
self.jet_value_1.reference.startswith(
f"{self.variable_level_1.reference}_{model_ref}_{jet_model_ref}_"
)
)

View File

@@ -0,0 +1,534 @@
# Copyright (C) 2022 Cetmix OÜ
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from .common import TestTowerCommon
class TestVaultMixin(TestTowerCommon):
"""Test vault mixin functionality."""
def test_vault_mixin_secret_fields(self):
"""Test vault mixin functionality for secret fields
(host_key and ssh_password)"""
# Create a server with initial secret values
initial_password = "initial_password"
initial_host_key = "initial_host_key"
server = self.Server.create(
{
"name": "Vault Test Server",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": initial_password,
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
"host_key": initial_host_key,
"skip_host_key": False,
}
)
# Test 1: Verify initial values are stored in vault and accessible
# Read values using common way - should return placeholder
self.assertEqual(
server.ssh_password,
self.Server.SECRET_VALUE_PLACEHOLDER,
"ssh_password should return placeholder value when read normally",
)
self.assertEqual(
server.host_key,
self.Server.SECRET_VALUE_PLACEHOLDER,
"host_key should return placeholder value when read normally",
)
# Read using _get_secret_values() - should return actual initial values
secret_values = server._get_secret_values()
self.assertIsNotNone(secret_values, "secret_values should not be None")
self.assertIn(server.id, secret_values, "Server ID should be in secret values")
server_secrets = secret_values[server.id]
self.assertIn(
"ssh_password", server_secrets, "ssh_password should be in secret values"
)
self.assertIn("host_key", server_secrets, "host_key should be in secret values")
self.assertEqual(
server_secrets["ssh_password"],
initial_password,
"ssh_password should return initial value from vault",
)
self.assertEqual(
server_secrets["host_key"],
initial_host_key,
"host_key should return initial value from vault",
)
# Read individual fields using _get_secret_value()
# should return initial values
retrieved_password = server._get_secret_value("ssh_password")
retrieved_host_key = server._get_secret_value("host_key")
self.assertEqual(
retrieved_password,
initial_password,
"_get_secret_value should return correct initial ssh_password",
)
self.assertEqual(
retrieved_host_key,
initial_host_key,
"_get_secret_value should return correct initial host_key",
)
# Test 2: Save new values to secret fields
new_password = "new_secure_password_123"
new_host_key = "new_host_key_456"
server.write(
{
"ssh_password": new_password,
"host_key": new_host_key,
}
)
# Test 3: Read values using common way after update - should return placeholder
# Note: In Odoo, we need to re-read the record to see updated values
server = self.Server.browse(server.id)
self.assertEqual(
server.ssh_password,
self.Server.SECRET_VALUE_PLACEHOLDER,
"ssh_password should return placeholder value when read normally "
"after update",
)
self.assertEqual(
server.host_key,
self.Server.SECRET_VALUE_PLACEHOLDER,
"host_key should return placeholder value when read normally "
"after update",
)
# Test 4: Read using _get_secret_values() after update
# should return new values
secret_values = server._get_secret_values()
self.assertIsNotNone(
secret_values, "secret_values should not be None after update"
)
self.assertIn(
server.id,
secret_values,
"Server ID should be in secret values after update",
)
server_secrets = secret_values[server.id]
self.assertIn(
"ssh_password",
server_secrets,
"ssh_password should be in secret values after update",
)
self.assertIn(
"host_key",
server_secrets,
"host_key should be in secret values after update",
)
self.assertEqual(
server_secrets["ssh_password"],
new_password,
"ssh_password should return new value from vault after update",
)
self.assertEqual(
server_secrets["host_key"],
new_host_key,
"host_key should return new value from vault after update",
)
# Test 5: Read individual fields using _get_secret_value() after update
# Get both values in one call using _get_secret_values()
secret_values = server._get_secret_values()
self.assertIsNotNone(
secret_values, "secret_values should not be None for individual field test"
)
self.assertIn(
server.id,
secret_values,
"Server ID should be in secret values for individual field test",
)
server_secrets = secret_values[server.id]
retrieved_password = server_secrets["ssh_password"]
retrieved_host_key = server_secrets["host_key"]
self.assertEqual(
retrieved_password,
new_password,
"_get_secret_values should return correct new ssh_password after update",
)
self.assertEqual(
retrieved_host_key,
new_host_key,
"_get_secret_values should return correct new host_key after update",
)
# Test 6: Verify that non-secret fields are not affected
self.assertEqual(
server.name,
"Vault Test Server",
"Non-secret field should not be affected by vault mixin",
)
self.assertEqual(
server.ssh_username,
"admin",
"Non-secret field should not be affected by vault mixin",
)
def test_vault_mixin_create_with_secret_fields(self):
"""Test vault mixin functionality when creating records with secret fields"""
# Create a server with secret fields
server = self.Server.create(
{
"name": "Create Test Server",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "create_password",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
"host_key": "create_host_key",
"skip_host_key": False,
}
)
# Verify secret fields are stored in vault and not in main table
self.assertEqual(
server.ssh_password,
self.Server.SECRET_VALUE_PLACEHOLDER,
"ssh_password should return placeholder after creation",
)
self.assertEqual(
server.host_key,
self.Server.SECRET_VALUE_PLACEHOLDER,
"host_key should return placeholder after creation",
)
# Verify actual values are accessible via vault methods
secret_values = server._get_secret_values()
self.assertIn(
server.id,
secret_values,
"Server ID should be in secret values after creation",
)
server_secrets = secret_values[server.id]
self.assertEqual(
server_secrets["ssh_password"],
"create_password",
"ssh_password should be stored in vault after creation",
)
self.assertEqual(
server_secrets["host_key"],
"create_host_key",
"host_key should be stored in vault after creation",
)
def test_vault_mixin_delete_secret_fields(self):
"""Test vault mixin functionality when deleting secret field values"""
# Create a server with secret fields
server = self.Server.create(
{
"name": "Delete Test Server",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "delete_password",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
"host_key": "delete_host_key",
"skip_host_key": False,
}
)
# Verify initial values exist
secret_values = server._get_secret_values()
self.assertIn(
"ssh_password",
secret_values[server.id],
"ssh_password should exist initially",
)
self.assertIn(
"host_key", secret_values[server.id], "host_key should exist initially"
)
# Delete secret field values
server.write(
{
"ssh_password": False,
"host_key": False,
}
)
# Verify values are removed from vault
secret_values = server._get_secret_values()
server_secrets = secret_values.get(server.id, {})
self.assertNotIn(
"ssh_password", server_secrets, "ssh_password should be removed from vault"
)
self.assertNotIn(
"host_key", server_secrets, "host_key should be removed from vault"
)
# Verify normal field access still returns placeholders
server = self.Server.browse(server.id)
self.assertEqual(
server.ssh_password,
self.Server.SECRET_VALUE_PLACEHOLDER,
"ssh_password should return placeholder after deletion",
)
self.assertEqual(
server.host_key,
self.Server.SECRET_VALUE_PLACEHOLDER,
"host_key should return placeholder after deletion",
)
def test_vault_mixin_bulk_create_with_secret_fields(self):
"""Test vault mixin functionality when creating multiple servers with different
secret field configurations"""
placeholder = self.Server.SECRET_VALUE_PLACEHOLDER
# Create 3 servers with different secret field configurations
servers_data = [
{
"name": "Server 1 - Both Fields",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password1",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
"host_key": "host_key1",
"skip_host_key": False,
},
{
"name": "Server 2 - Host Key Only",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_auth_mode": "k",
"os_id": self.os_debian_10.id,
"host_key": "host_key2",
"skip_host_key": False,
"ssh_key_id": self.key_1.id,
},
{
"name": "Server 3 - SSH Password Only",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password3",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
"skip_host_key": True,
},
]
# Create all servers in one call
servers = self.Server.create(servers_data)
# Verify we have 3 servers
self.assertEqual(len(servers), 3, "Should have created 3 servers")
# Test 1: Get values for all 3 servers regular way - should return placeholders
for server in servers:
self.assertEqual(
server.ssh_password,
placeholder,
f"Server {server.name} ssh_password should return placeholder "
f"when read normally",
)
self.assertEqual(
server.host_key,
placeholder,
f"Server {server.name} host_key should return placeholder "
f"when read normally",
)
# Test 2: Get values for all 3 servers at once using _get_secret_values()
all_secret_values = servers._get_secret_values()
self.assertIsNotNone(all_secret_values, "all_secret_values should not be None")
# Verify Server 1 (both fields)
server1 = servers[0]
self.assertIn(
server1.id, all_secret_values, "Server 1 should be in secret values"
)
server1_secrets = all_secret_values[server1.id]
self.assertEqual(
server1_secrets.get("ssh_password"),
"password1",
"Server 1 ssh_password should be preserved correctly in vault",
)
self.assertEqual(
server1_secrets.get("host_key"),
"host_key1",
"Server 1 host_key should be preserved correctly in vault",
)
# Verify Server 2 (host key only)
server2 = servers[1]
self.assertIn(
server2.id, all_secret_values, "Server 2 should be in secret values"
)
server2_secrets = all_secret_values[server2.id]
self.assertIsNone(
server2_secrets.get("ssh_password"),
"Server 2 should not have ssh_password in vault",
)
self.assertEqual(
server2_secrets.get("host_key"),
"host_key2",
"Server 2 host_key should be preserved correctly in vault",
)
# Verify Server 3 (ssh password only)
server3 = servers[2]
self.assertIn(
server3.id, all_secret_values, "Server 3 should be in secret values"
)
server3_secrets = all_secret_values[server3.id]
self.assertEqual(
server3_secrets.get("ssh_password"),
"password3",
"Server 3 ssh_password should be preserved correctly in vault",
)
self.assertIsNone(
server3_secrets.get("host_key"),
"Server 3 should not have host_key in vault",
)
# Test 3: Verify that non-secret fields are not affected
for server in servers:
self.assertIsNotNone(
server.name,
f"Server {server.id} name should not be affected by vault mixin",
)
self.assertIsNotNone(
server.ssh_username,
f"Server {server.id} ssh_username should not be affected "
f"by vault mixin",
)
self.assertIsNotNone(
server.ip_v4_address,
f"Server {server.id} ip_v4_address should not be affected "
f"by vault mixin",
)
# Test 4: Modify secret fields and verify changes are handled correctly
# Change the ssh password and remove the host key from Server 1
server1 = servers.filtered(lambda s: s.name == "Server 1 - Both Fields")
server1.write(
{
"ssh_password": "updated_password1",
"host_key": False,
}
)
# Remove host key and add an ssh password in Server 2
server2 = servers.filtered(lambda s: s.name == "Server 2 - Host Key Only")
server2.write(
{
"host_key": False,
"ssh_password": "new_password2",
}
)
# Remove ssh password from Server 3
server3 = servers.filtered(lambda s: s.name == "Server 3 - SSH Password Only")
server3.write(
{
"ssh_password": False,
}
)
# Test 5: Get values for all 3 servers regular way after modifications
# Ensure that all values are replaced with placeholders
for server in servers:
self.assertEqual(
server.ssh_password,
placeholder,
f"Server {server.id} ssh_password should return placeholder "
f"after modifications",
)
self.assertEqual(
server.host_key,
placeholder,
f"Server {server.id} host_key should return placeholder "
f"after modifications",
)
# Test 6: Get values for all 3 servers at once using _get_secret_values()
# Ensure that all values are preserved correctly after modifications
all_secret_values = servers._get_secret_values()
self.assertIsNotNone(
all_secret_values,
"all_secret_values should not be None after modifications",
)
# Verify Server 1 (updated password, no host key)
server1 = servers[0]
server1_secrets = all_secret_values[server1.id]
self.assertEqual(
server1_secrets.get("ssh_password"),
"updated_password1",
"Server 1 ssh_password should be updated correctly in vault",
)
self.assertIsNone(
server1_secrets.get("host_key"),
"Server 1 host_key should be removed from vault",
)
# Verify Server 2 (new password, no host key)
server2_secrets = all_secret_values[server2.id]
self.assertEqual(
server2_secrets.get("ssh_password"),
"new_password2",
"Server 2 ssh_password should be added correctly in vault",
)
self.assertIsNone(
server2_secrets.get("host_key"),
"Server 2 host_key should be removed from vault",
)
# Verify Server 3 (no ssh password, no host key)
# Server 3 should not be in the result since it has no secret values
self.assertNotIn(
server3.id,
all_secret_values,
"Server 3 should not be in secret values since it has no secret fields",
)
def test_is_secret_value_set(self):
"""Test _is_secret_value_set returns True/False for host_key correctly."""
server = self.Server.create(
{
"name": "Is Secret Set Test Server",
"ip_v4_address": "localhost",
"ssh_username": "admin",
"ssh_password": "password",
"ssh_auth_mode": "p",
"os_id": self.os_debian_10.id,
"host_key": "test_host_key_value",
"skip_host_key": False,
}
)
self.assertTrue(
server._is_secret_value_set("host_key"),
"host_key should be considered set when value exists in vault",
)
server.write({"host_key": False})
server = self.Server.browse(server.id)
self.assertFalse(
server._is_secret_value_set("host_key"),
"host_key should be considered not set when cleared",
)