Public release from ruodoo-project: 19.0 - 2026-11-01 22:52:25 UTC

This commit is contained in:
CI Publish Bot
2026-11-01 22:54:30 +00:00
commit 26250aa580
2492 changed files with 664277 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
from . import test_directive_layer
from . import test_directive_security

View File

@@ -0,0 +1,120 @@
from odoo.tests.common import TransactionCase, tagged
# post_install (НЕ at_install): обе политики опираются на project.task с
# примешанным directive.mixin и на метод action_demo_directive_callback, которые
# определены в companion-модуле directive_common. at_install прогонял бы эти тесты
# в фазе установки directive_layer — ДО загрузки directive_common, когда project.task
# ещё не перехватывает write/callback. post_install гарантирует, что оба модуля уже
# загружены (design 7.2).
@tagged('standard', '-at_install', 'post_install', 'directive_logic')
class TestDirectivePolicy(TransactionCase):
def setUp(self):
"""
Метод запускается перед каждым тестом.
Гарантирует изоляцию данных: каждый тест работает с новой задачей и новой политикой.
"""
super(TestDirectivePolicy, self).setUp()
self.work_type = self.env['directive.work.type'].create({
'name': 'Development',
'code': 'dev'
})
self.stage_draft = self.env['directive.stage'].search([('code', '=', 'draft')], limit=1)
if not self.stage_draft:
self.stage_draft = self.env['directive.stage'].create({'name': 'Draft', 'code': 'draft'})
self.template = self.env['directive.template'].create({
'name': 'Шаблон авто-задачи',
'code': 'T001',
'work_type_id': self.work_type.id,
'planned_hours': 5.0,
})
self.template_group = self.env['directive.template.group'].create({
'name': 'Test Group',
'template_line_ids': [(0, 0, {'template_id': self.template.id})]
})
self.policy_callback = self.env['directive.policy'].create({
'name': 'On Callback Policy',
'origin_model': 'project.task',
'event': 'on_callback',
'trigger_field': 'action_demo_directive_callback',
'mode': 'create_once',
'use_record_template_group': False,
'template_group_id': self.template_group.id,
})
self.policy_write = self.env['directive.policy'].create({
'name': 'On Write Policy',
'origin_model': 'project.task',
'event': 'on_write',
'trigger_field': 'color',
'trigger_value_int': 5,
'mode': 'create_once',
'use_record_template_group': False,
'template_group_id': self.template_group.id,
})
self.task = self.env['project.task'].create({
'name': 'Source Task',
'color': 0,
})
def test_01_policy_on_write(self):
"""Тест генерации директивы при изменении поля (on_write)"""
self.task.write({'color': 5})
directives = self.env['directive.directive'].search([
('origin_id.res_model', '=', 'project.task'),
('origin_id.res_id', '=', self.task.id),
('policy_id', '=', self.policy_write.id)
])
self.assertEqual(len(directives), 1, "Директива не создалась через on_write!")
self.assertEqual(directives.name, 'Шаблон авто-задачи', "Имя перенеслось неверно")
self.assertEqual(directives.planned_hours, 5.0, "Трудоемкость перенеслась неверно")
# Проверка create_once
self.task.write({'color': 0})
self.task.write({'color': 5})
directives_count = self.env['directive.directive'].search_count([
('origin_id.res_model', '=', 'project.task'),
('origin_id.res_id', '=', self.task.id),
('policy_id', '=', self.policy_write.id)
])
self.assertEqual(directives_count, 1, "Сломался create_once! Создался дубль директивы.")
def test_02_policy_on_callback(self):
"""Тест генерации директивы при вызове метода (on_callback).
Механизм теперь ЯВНЫЙ: project.task.action_demo_directive_callback
(directive_common) сам вызывает self._directive_run_callback_policies(
"action_demo_directive_callback") после message_post (FEAT-003) — это НЕ
monkey-patch. Контекст-гард directive_skip_generation не используется.
Ассерты API не изменились относительно as-is.
"""
self.task.action_demo_directive_callback()
directives = self.env['directive.directive'].search([
('origin_id.res_model', '=', 'project.task'),
('origin_id.res_id', '=', self.task.id),
('policy_id', '=', self.policy_callback.id)
])
self.assertEqual(len(directives), 1, "Директива не создалась через on_callback!")
# Проверка create_once
self.task.action_demo_directive_callback()
directives_count = self.env['directive.directive'].search_count([
('origin_id.res_model', '=', 'project.task'),
('origin_id.res_id', '=', self.task.id),
('policy_id', '=', self.policy_callback.id)
])
self.assertEqual(directives_count, 1, "Сломался create_once для методов! Создался дубль.")

View File

@@ -0,0 +1,145 @@
from odoo.exceptions import AccessError
from odoo.tests.common import TransactionCase, tagged
@tagged('standard', 'at_install', 'directive_logic')
class TestDirectiveSecurity(TransactionCase):
"""Поведенческая проверка ролевой модели безопасности (design 5.1/5.2).
Назначаем бизнес-роль через role_line_ids и проверяем, что:
- пользователь попадает в техническую группу _res_groups (implied_ids);
- исполнитель читает/двигает свою директиву, но не может её удалить;
- исполнитель не видит чужие директивы (record rule rule_directive_own);
- диспетчер видит все директивы и может удалять.
"""
def setUp(self):
super().setUp()
self.role_executor = self.env.ref('directive_layer.role_directive_executor')
self.role_dispatcher = self.env.ref('directive_layer.role_directive_dispatcher')
self.grp_executor = self.env.ref('directive_layer.role_directive_executor_res_groups')
self.grp_dispatcher = self.env.ref('directive_layer.role_directive_dispatcher_res_groups')
self.work_type = self.env['directive.work.type'].create({
'name': 'Development',
'code': 'dev',
})
self.stage_draft = self.env['directive.stage'].search([('code', '=', 'draft')], limit=1)
if not self.stage_draft:
self.stage_draft = self.env['directive.stage'].create({'name': 'Draft', 'code': 'draft'})
self.executor_user = self._make_user('exec_user', self.role_executor)
self.dispatcher_user = self._make_user('disp_user', self.role_dispatcher)
# Рядовой internal user без единой роли директив (fail-closed).
self.roleless_user = self.env['res.users'].create({
'name': 'roleless_user',
'login': 'roleless_user',
'email': 'roleless_user@example.com',
'group_ids': [(4, self.env.ref('base.group_user').id)],
})
# Своя директива исполнителя + чужая директива.
self.own_directive = self._make_directive(
'Own directive', executor_user=self.executor_user)
self.other_directive = self._make_directive(
'Other directive', executor_user=self.dispatcher_user)
def _make_user(self, login, role):
# На create res.users вызывается set_groups_from_roles(): группы
# пользователя заменяются на role.group_id + implied_ids (т.е. _res_groups).
return self.env['res.users'].create({
'name': login,
'login': login,
'email': f'{login}@example.com',
'role_line_ids': [(0, 0, {'role_id': role.id})],
})
def _make_directive(self, name, executor_user):
origin = self.env['directive.origin'].create({
'res_model': 'res.partner',
'res_id': self.env.user.partner_id.id,
})
return self.env['directive.directive'].create({
'name': name,
'deadline_at': '2099-01-01 00:00:00',
'planned_hours': 1.0,
'work_type_id': self.work_type.id,
'stage_id': self.stage_draft.id,
'origin_id': origin.id,
'executor_user_id': executor_user.id,
})
def test_01_role_grants_technical_group(self):
"""Назначение бизнес-роли переводит пользователя в её _res_groups."""
self.assertTrue(
self.executor_user.has_group('directive_layer.role_directive_executor_res_groups'),
"Исполнитель не попал в role_directive_executor_res_groups после назначения роли.",
)
self.assertTrue(
self.dispatcher_user.has_group('directive_layer.role_directive_dispatcher_res_groups'),
"Диспетчер не попал в role_directive_dispatcher_res_groups после назначения роли.",
)
def test_02_executor_read_write_own(self):
"""Исполнитель читает и двигает свою директиву."""
directive = self.own_directive.with_user(self.executor_user)
self.assertEqual(directive.name, 'Own directive')
directive.write({'load_percent': 50})
self.assertEqual(directive.load_percent, 50)
def test_03_executor_cannot_unlink(self):
"""Исполнитель не может удалить директиву (нет perm_unlink)."""
directive = self.own_directive.with_user(self.executor_user)
with self.assertRaises(AccessError):
directive.unlink()
def test_04_executor_cannot_create(self):
"""Исполнитель не может создавать директивы напрямую (нет perm_create)."""
origin = self.env['directive.origin'].create({
'res_model': 'res.partner',
'res_id': self.env.user.partner_id.id,
})
with self.assertRaises(AccessError):
self.env['directive.directive'].with_user(self.executor_user).create({
'name': 'Illegal',
'deadline_at': '2099-01-01 00:00:00',
'planned_hours': 1.0,
'work_type_id': self.work_type.id,
'stage_id': self.stage_draft.id,
'origin_id': origin.id,
})
def test_05_executor_sees_only_own(self):
"""Record rule: исполнитель видит свою директиву, но не чужую."""
Directive = self.env['directive.directive'].with_user(self.executor_user)
visible_ids = Directive.search([]).ids
self.assertIn(self.own_directive.id, visible_ids,
"Исполнитель не видит собственную директиву.")
self.assertNotIn(self.other_directive.id, visible_ids,
"Исполнитель видит чужую директиву — record rule не работает.")
def test_06_dispatcher_sees_all_and_can_unlink(self):
"""Диспетчер видит все директивы и может удалять."""
Directive = self.env['directive.directive'].with_user(self.dispatcher_user)
visible_ids = Directive.search([]).ids
self.assertIn(self.own_directive.id, visible_ids)
self.assertIn(self.other_directive.id, visible_ids)
Directive.browse(self.own_directive.id).unlink()
self.assertFalse(self.own_directive.exists())
def test_07_roleless_user_sees_zero(self):
"""Fail-closed: internal user без ролей директив видит ноль директив.
base.group_user даёт только bounded read (чтобы related-поля на формах-
источниках грузились), а global record rule ограничивает выборку нулём
строк для пользователя без совпадений own/role. Эта проверка фиксирует
инвариант, чтобы будущий рефактор не ослабил fail-closed молча.
"""
Directive = self.env['directive.directive'].with_user(self.roleless_user)
self.assertFalse(self.roleless_user.has_group(
'directive_layer.role_directive_dispatcher_res_groups'))
self.assertEqual(
Directive.search([]).ids, [],
"Пользователь без ролей директив видит директивы — нарушен fail-closed.",
)