Tower: upload cetmix_tower_server 18.0.2.0.0 (was 18.0.2.0.0, via marketplace)
This commit is contained in:
42
addons/cetmix_tower_server/tests/__init__.py
Normal file
42
addons/cetmix_tower_server/tests/__init__.py
Normal 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
|
||||
515
addons/cetmix_tower_server/tests/common.py
Normal file
515
addons/cetmix_tower_server/tests/common.py
Normal 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())
|
||||
732
addons/cetmix_tower_server/tests/common_jets.py
Normal file
732
addons/cetmix_tower_server/tests/common_jets.py
Normal 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
|
||||
244
addons/cetmix_tower_server/tests/test_cetmix_tower.py
Normal file
244
addons/cetmix_tower_server/tests/test_cetmix_tower.py
Normal 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")
|
||||
1964
addons/cetmix_tower_server/tests/test_command.py
Normal file
1964
addons/cetmix_tower_server/tests/test_command.py
Normal file
File diff suppressed because it is too large
Load Diff
282
addons/cetmix_tower_server/tests/test_command_log.py
Normal file
282
addons/cetmix_tower_server/tests/test_command_log.py
Normal 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",
|
||||
)
|
||||
572
addons/cetmix_tower_server/tests/test_command_wizard.py
Normal file
572
addons/cetmix_tower_server/tests/test_command_wizard.py
Normal 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()
|
||||
482
addons/cetmix_tower_server/tests/test_file.py
Normal file
482
addons/cetmix_tower_server/tests/test_file.py
Normal 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"], "/")
|
||||
234
addons/cetmix_tower_server/tests/test_file_template.py
Normal file
234
addons/cetmix_tower_server/tests/test_file_template.py
Normal 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."
|
||||
)
|
||||
1750
addons/cetmix_tower_server/tests/test_jet.py
Normal file
1750
addons/cetmix_tower_server/tests/test_jet.py
Normal file
File diff suppressed because it is too large
Load Diff
442
addons/cetmix_tower_server/tests/test_jet_access.py
Normal file
442
addons/cetmix_tower_server/tests/test_jet_access.py
Normal 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"
|
||||
)
|
||||
647
addons/cetmix_tower_server/tests/test_jet_action_access.py
Normal file
647
addons/cetmix_tower_server/tests/test_jet_action_access.py
Normal 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",
|
||||
)
|
||||
81
addons/cetmix_tower_server/tests/test_jet_create_wizard.py
Normal file
81
addons/cetmix_tower_server/tests/test_jet_create_wizard.py
Normal 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",
|
||||
)
|
||||
420
addons/cetmix_tower_server/tests/test_jet_dependency_access.py
Normal file
420
addons/cetmix_tower_server/tests/test_jet_dependency_access.py
Normal 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",
|
||||
)
|
||||
522
addons/cetmix_tower_server/tests/test_jet_state.py
Normal file
522
addons/cetmix_tower_server/tests/test_jet_state.py
Normal 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",
|
||||
)
|
||||
3226
addons/cetmix_tower_server/tests/test_jet_template.py
Normal file
3226
addons/cetmix_tower_server/tests/test_jet_template.py
Normal file
File diff suppressed because it is too large
Load Diff
551
addons/cetmix_tower_server/tests/test_jet_template_access.py
Normal file
551
addons/cetmix_tower_server/tests/test_jet_template_access.py
Normal 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")
|
||||
@@ -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")
|
||||
1777
addons/cetmix_tower_server/tests/test_jet_template_install.py
Normal file
1777
addons/cetmix_tower_server/tests/test_jet_template_install.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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")
|
||||
@@ -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",
|
||||
)
|
||||
1995
addons/cetmix_tower_server/tests/test_jet_waypoint.py
Normal file
1995
addons/cetmix_tower_server/tests/test_jet_waypoint.py
Normal file
File diff suppressed because it is too large
Load Diff
970
addons/cetmix_tower_server/tests/test_jet_waypoint_access.py
Normal file
970
addons/cetmix_tower_server/tests/test_jet_waypoint_access.py
Normal 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")
|
||||
@@ -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")
|
||||
919
addons/cetmix_tower_server/tests/test_key.py
Normal file
919
addons/cetmix_tower_server/tests/test_key.py
Normal 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,
|
||||
}
|
||||
)
|
||||
58
addons/cetmix_tower_server/tests/test_partner_server_btn.py
Normal file
58
addons/cetmix_tower_server/tests/test_partner_server_btn.py
Normal 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 + one‑level 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)
|
||||
2899
addons/cetmix_tower_server/tests/test_plan.py
Normal file
2899
addons/cetmix_tower_server/tests/test_plan.py
Normal file
File diff suppressed because it is too large
Load Diff
540
addons/cetmix_tower_server/tests/test_plan_line.py
Normal file
540
addons/cetmix_tower_server/tests/test_plan_line.py
Normal 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")
|
||||
255
addons/cetmix_tower_server/tests/test_plan_line_action.py
Normal file
255
addons/cetmix_tower_server/tests/test_plan_line_action.py
Normal 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")
|
||||
274
addons/cetmix_tower_server/tests/test_plan_log.py
Normal file
274
addons/cetmix_tower_server/tests/test_plan_log.py
Normal 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",
|
||||
)
|
||||
310
addons/cetmix_tower_server/tests/test_reference_mixin.py
Normal file
310
addons/cetmix_tower_server/tests/test_reference_mixin.py
Normal 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",
|
||||
)
|
||||
893
addons/cetmix_tower_server/tests/test_scheduled_task.py
Normal file
893
addons/cetmix_tower_server/tests/test_scheduled_task.py
Normal 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.")
|
||||
890
addons/cetmix_tower_server/tests/test_server.py
Normal file
890
addons/cetmix_tower_server/tests/test_server.py
Normal 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)
|
||||
@@ -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"])
|
||||
657
addons/cetmix_tower_server/tests/test_server_log.py
Normal file
657
addons/cetmix_tower_server/tests/test_server_log.py
Normal 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",
|
||||
)
|
||||
1073
addons/cetmix_tower_server/tests/test_server_template.py
Normal file
1073
addons/cetmix_tower_server/tests/test_server_template.py
Normal file
File diff suppressed because it is too large
Load Diff
244
addons/cetmix_tower_server/tests/test_shortcut.py
Normal file
244
addons/cetmix_tower_server/tests/test_shortcut.py
Normal 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",
|
||||
)
|
||||
91
addons/cetmix_tower_server/tests/test_tag.py
Normal file
91
addons/cetmix_tower_server/tests/test_tag.py
Normal 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()
|
||||
167
addons/cetmix_tower_server/tests/test_tag_mixin.py
Normal file
167
addons/cetmix_tower_server/tests/test_tag_mixin.py
Normal 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)
|
||||
38
addons/cetmix_tower_server/tests/test_tools.py
Normal file
38
addons/cetmix_tower_server/tests/test_tools.py
Normal 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
|
||||
@@ -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.",
|
||||
)
|
||||
1189
addons/cetmix_tower_server/tests/test_variable.py
Normal file
1189
addons/cetmix_tower_server/tests/test_variable.py
Normal file
File diff suppressed because it is too large
Load Diff
285
addons/cetmix_tower_server/tests/test_variable_option.py
Normal file
285
addons/cetmix_tower_server/tests/test_variable_option.py
Normal 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")
|
||||
952
addons/cetmix_tower_server/tests/test_variable_value.py
Normal file
952
addons/cetmix_tower_server/tests/test_variable_value.py
Normal 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}_"
|
||||
)
|
||||
)
|
||||
534
addons/cetmix_tower_server/tests/test_vault_mixin.py
Normal file
534
addons/cetmix_tower_server/tests/test_vault_mixin.py
Normal 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",
|
||||
)
|
||||
Reference in New Issue
Block a user