285 lines
14 KiB
Python
285 lines
14 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
import ast
|
||
|
|
||
|
from markupsafe import Markup
|
||
|
|
||
|
from odoo import Command
|
||
|
from odoo.tests.common import TransactionCase, BaseCase
|
||
|
from odoo.tools import mute_logger
|
||
|
from odoo.tools.safe_eval import safe_eval, const_eval, expr_eval
|
||
|
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
|
||
|
|
||
|
|
||
|
class TestSafeEval(BaseCase):
|
||
|
def test_const(self):
|
||
|
# NB: True and False are names in Python 2 not consts
|
||
|
expected = (1, {"a": {2.5}}, [None, u"foo"])
|
||
|
actual = const_eval('(1, {"a": {2.5}}, [None, u"foo"])')
|
||
|
self.assertEqual(actual, expected)
|
||
|
|
||
|
def test_expr(self):
|
||
|
# NB: True and False are names in Python 2 not consts
|
||
|
expected = 3 * 4
|
||
|
actual = expr_eval('3 * 4')
|
||
|
self.assertEqual(actual, expected)
|
||
|
|
||
|
def test_01_safe_eval(self):
|
||
|
""" Try a few common expressions to verify they work with safe_eval """
|
||
|
expected = (1, {"a": 9 * 2}, (True, False, None))
|
||
|
actual = safe_eval('(1, {"a": 9 * 2}, (True, False, None))')
|
||
|
self.assertEqual(actual, expected, "Simple python expressions are not working with safe_eval")
|
||
|
|
||
|
def test_02_literal_eval(self):
|
||
|
""" Try simple literal definition to verify it works with literal_eval """
|
||
|
expected = (1, {"a": 9}, (True, False, None))
|
||
|
actual = ast.literal_eval('(1, {"a": 9}, (True, False, None))')
|
||
|
self.assertEqual(actual, expected, "Simple python expressions are not working with literal_eval")
|
||
|
|
||
|
def test_03_literal_eval_arithmetic(self):
|
||
|
""" Try arithmetic expression in literal_eval to verify it does not work """
|
||
|
with self.assertRaises(ValueError):
|
||
|
ast.literal_eval('(1, {"a": 2*9}, (True, False, None))')
|
||
|
|
||
|
def test_04_literal_eval_forbidden(self):
|
||
|
""" Try forbidden expressions in literal_eval to verify they are not allowed """
|
||
|
with self.assertRaises(ValueError):
|
||
|
ast.literal_eval('{"a": True.__class__}')
|
||
|
|
||
|
@mute_logger('odoo.tools.safe_eval')
|
||
|
def test_05_safe_eval_forbiddon(self):
|
||
|
""" Try forbidden expressions in safe_eval to verify they are not allowed"""
|
||
|
# no forbidden builtin expression
|
||
|
with self.assertRaises(ValueError):
|
||
|
safe_eval('open("/etc/passwd","r")')
|
||
|
|
||
|
# no forbidden opcodes
|
||
|
with self.assertRaises(ValueError):
|
||
|
safe_eval("import odoo", mode="exec")
|
||
|
|
||
|
# no dunder
|
||
|
with self.assertRaises(NameError):
|
||
|
safe_eval("self.__name__", {'self': self}, mode="exec")
|
||
|
|
||
|
def test_06_safe_eval_format(self):
|
||
|
# string.format
|
||
|
self.assertEqual(safe_eval("'__{0}__'.format('Foo')"), '__Foo__')
|
||
|
self.assertEqual(safe_eval("'{0.__self__}'.format(abs)"), '{0.__self__}')
|
||
|
self.assertEqual(safe_eval("'{0.f_globals}'.format(abs)"), '{0.f_globals}')
|
||
|
|
||
|
# string.format_map
|
||
|
self.assertEqual(safe_eval("'__{foo}__'.format_map({'foo': 'Foo'})"), '__Foo__')
|
||
|
self.assertEqual(safe_eval("'{foo.__self__}'.format_map({'foo': abs})"), '{foo.__self__}')
|
||
|
self.assertEqual(safe_eval("'{foo.f_globals}'.format_map({'foo': abs})"), '{foo.f_globals}')
|
||
|
|
||
|
# Evaluation context for Markup asserts
|
||
|
c = {"Markup": Markup}
|
||
|
|
||
|
# Markup.format
|
||
|
self.assertEqual(safe_eval("Markup('__{0}__').format('Foo')", c), Markup('__Foo__'))
|
||
|
with self.assertRaisesRegex(ValueError, 'Access to forbidden name'):
|
||
|
safe_eval("Markup('{0.__self__}').format(abs)", c)
|
||
|
with self.assertRaisesRegex(ValueError, 'Access to forbidden name'):
|
||
|
safe_eval("Markup('{0.f_globals}').format(abs)", c)
|
||
|
|
||
|
# Markup.format_map
|
||
|
self.assertEqual(safe_eval("Markup('__{foo}__').format_map({'foo': 'Foo'})", c), Markup('__Foo__'))
|
||
|
self.assertEqual(safe_eval("Markup('{foo.__self__}').format_map({'foo': abs})", c), Markup('{foo.__self__}'))
|
||
|
self.assertEqual(safe_eval("Markup('{foo.f_globals}').format_map({'foo': abs})", c), Markup('{foo.f_globals}'))
|
||
|
|
||
|
def test_07_safe_eval_attribute_error_obj(self):
|
||
|
locals_dict = {}
|
||
|
try:
|
||
|
safe_eval("""
|
||
|
try:
|
||
|
dict.foo
|
||
|
except Exception as e:
|
||
|
action = {'args': e.args, 'obj': e.obj, 'name': e.name}
|
||
|
""", locals_dict=locals_dict, mode="exec", nocopy=True)
|
||
|
except ValueError as e:
|
||
|
# AttributeError.name, AttributeError.obj added in Python 3.10
|
||
|
# https://github.com/python/cpython/commit/37494b441aced0362d7edd2956ab3ea7801e60c8
|
||
|
self.assertIn("'AttributeError' object has no attribute 'obj'", e.args[0])
|
||
|
else:
|
||
|
exception = locals_dict.get('action')
|
||
|
self.assertEqual(exception['args'], ("type object 'dict' has no attribute 'foo'",))
|
||
|
self.assertIsNone(exception['name'])
|
||
|
self.assertIsNone(exception['obj'])
|
||
|
|
||
|
attribute_error = None
|
||
|
try:
|
||
|
raise AttributeError('Foo', name='Bar', obj=[])
|
||
|
except TypeError as e:
|
||
|
# AttributeError does not take keyword arguments before Python 3.10
|
||
|
# https://github.com/python/cpython/commit/37494b441aced0362d7edd2956ab3ea7801e60c8
|
||
|
# Error can be either, according to the Python version:
|
||
|
# - AttributeError does not take keyword arguments
|
||
|
# - AttributeError() takes no keyword arguments
|
||
|
self.assertIn("keyword arguments", e.args[0])
|
||
|
except AttributeError as e:
|
||
|
attribute_error = e
|
||
|
if attribute_error:
|
||
|
self.assertEqual(attribute_error.args, ('Foo',))
|
||
|
self.assertEqual(attribute_error.name, 'Bar')
|
||
|
self.assertIsNone(attribute_error.obj)
|
||
|
|
||
|
|
||
|
class TestParentStore(TransactionCase):
|
||
|
""" Verify that parent_store computation is done right """
|
||
|
|
||
|
def setUp(self):
|
||
|
super(TestParentStore, self).setUp()
|
||
|
|
||
|
# force res_partner_category.copy() to copy children
|
||
|
category = self.env['res.partner.category']
|
||
|
self.patch(category._fields['child_ids'], 'copy', True)
|
||
|
|
||
|
# setup categories
|
||
|
self.root = category.create({'name': 'Root category'})
|
||
|
self.cat0 = category.create({'name': 'Parent category', 'parent_id': self.root.id})
|
||
|
self.cat1 = category.create({'name': 'Child 1', 'parent_id': self.cat0.id})
|
||
|
self.cat2 = category.create({'name': 'Child 2', 'parent_id': self.cat0.id})
|
||
|
self.cat21 = category.create({'name': 'Child 2-1', 'parent_id': self.cat2.id})
|
||
|
|
||
|
def test_duplicate_parent(self):
|
||
|
""" Duplicate the parent category and verify that the children have been duplicated too """
|
||
|
new_cat0 = self.cat0.copy()
|
||
|
new_struct = new_cat0.search([('parent_id', 'child_of', new_cat0.id)])
|
||
|
self.assertEqual(len(new_struct), 4, "After duplication, the new object must have the childs records")
|
||
|
old_struct = new_cat0.search([('parent_id', 'child_of', self.cat0.id)])
|
||
|
self.assertEqual(len(old_struct), 4, "After duplication, previous record must have old childs records only")
|
||
|
self.assertFalse(new_struct & old_struct, "After duplication, nodes should not be mixed")
|
||
|
|
||
|
def test_duplicate_children_01(self):
|
||
|
""" Duplicate the children then reassign them to the new parent (1st method). """
|
||
|
new_cat1 = self.cat1.copy()
|
||
|
new_cat2 = self.cat2.copy()
|
||
|
new_cat0 = self.cat0.copy({'child_ids': []})
|
||
|
(new_cat1 + new_cat2).write({'parent_id': new_cat0.id})
|
||
|
new_struct = new_cat0.search([('parent_id', 'child_of', new_cat0.id)])
|
||
|
self.assertEqual(len(new_struct), 4, "After duplication, the new object must have the childs records")
|
||
|
old_struct = new_cat0.search([('parent_id', 'child_of', self.cat0.id)])
|
||
|
self.assertEqual(len(old_struct), 4, "After duplication, previous record must have old childs records only")
|
||
|
self.assertFalse(new_struct & old_struct, "After duplication, nodes should not be mixed")
|
||
|
|
||
|
def test_duplicate_children_02(self):
|
||
|
""" Duplicate the children then reassign them to the new parent (2nd method). """
|
||
|
new_cat1 = self.cat1.copy()
|
||
|
new_cat2 = self.cat2.copy()
|
||
|
new_cat0 = self.cat0.copy({'child_ids': [Command.set((new_cat1 + new_cat2).ids)]})
|
||
|
new_struct = new_cat0.search([('parent_id', 'child_of', new_cat0.id)])
|
||
|
self.assertEqual(len(new_struct), 4, "After duplication, the new object must have the childs records")
|
||
|
old_struct = new_cat0.search([('parent_id', 'child_of', self.cat0.id)])
|
||
|
self.assertEqual(len(old_struct), 4, "After duplication, previous record must have old childs records only")
|
||
|
self.assertFalse(new_struct & old_struct, "After duplication, nodes should not be mixed")
|
||
|
|
||
|
def test_duplicate_children_03(self):
|
||
|
""" Duplicate the children then reassign them to the new parent (3rd method). """
|
||
|
new_cat1 = self.cat1.copy()
|
||
|
new_cat2 = self.cat2.copy()
|
||
|
new_cat0 = self.cat0.copy({'child_ids': []})
|
||
|
new_cat0.write({'child_ids': [Command.link(new_cat1.id), Command.link(new_cat2.id)]})
|
||
|
new_struct = new_cat0.search([('parent_id', 'child_of', new_cat0.id)])
|
||
|
self.assertEqual(len(new_struct), 4, "After duplication, the new object must have the childs records")
|
||
|
old_struct = new_cat0.search([('parent_id', 'child_of', self.cat0.id)])
|
||
|
self.assertEqual(len(old_struct), 4, "After duplication, previous record must have old childs records only")
|
||
|
self.assertFalse(new_struct & old_struct, "After duplication, nodes should not be mixed")
|
||
|
|
||
|
|
||
|
class TestGroups(TransactionCase):
|
||
|
|
||
|
def test_res_groups_fullname_search(self):
|
||
|
all_groups = self.env['res.groups'].search([])
|
||
|
|
||
|
groups = all_groups.search([('full_name', 'like', 'Sale')])
|
||
|
self.assertItemsEqual(groups.ids, [g.id for g in all_groups if 'Sale' in g.full_name],
|
||
|
"did not match search for 'Sale'")
|
||
|
|
||
|
groups = all_groups.search([('full_name', 'like', 'Technical')])
|
||
|
self.assertItemsEqual(groups.ids, [g.id for g in all_groups if 'Technical' in g.full_name],
|
||
|
"did not match search for 'Technical'")
|
||
|
|
||
|
groups = all_groups.search([('full_name', 'like', 'Sales /')])
|
||
|
self.assertItemsEqual(groups.ids, [g.id for g in all_groups if 'Sales /' in g.full_name],
|
||
|
"did not match search for 'Sales /'")
|
||
|
|
||
|
groups = all_groups.search([('full_name', 'in', ['Administration / Access Rights','Contact Creation'])])
|
||
|
self.assertTrue(groups, "did not match search for 'Administration / Access Rights' and 'Contact Creation'")
|
||
|
|
||
|
def test_res_group_recursion(self):
|
||
|
# four groups with no cycle, check them all together
|
||
|
a = self.env['res.groups'].create({'name': 'A'})
|
||
|
b = self.env['res.groups'].create({'name': 'B'})
|
||
|
c = self.env['res.groups'].create({'name': 'G', 'implied_ids': [Command.set((a + b).ids)]})
|
||
|
d = self.env['res.groups'].create({'name': 'D', 'implied_ids': [Command.set(c.ids)]})
|
||
|
self.assertTrue((a + b + c + d)._check_m2m_recursion('implied_ids'))
|
||
|
|
||
|
# create a cycle and check
|
||
|
a.implied_ids = d
|
||
|
self.assertFalse(a._check_m2m_recursion('implied_ids'))
|
||
|
|
||
|
def test_res_group_copy(self):
|
||
|
a = self.env['res.groups'].with_context(lang='en_US').create({'name': 'A'})
|
||
|
b = a.copy()
|
||
|
self.assertFalse(a.name == b.name)
|
||
|
|
||
|
def test_apply_groups(self):
|
||
|
a = self.env['res.groups'].create({'name': 'A'})
|
||
|
b = self.env['res.groups'].create({'name': 'B'})
|
||
|
c = self.env['res.groups'].create({'name': 'C', 'implied_ids': [Command.set(a.ids)]})
|
||
|
|
||
|
# C already implies A, we want both B+C to imply A
|
||
|
(b + c)._apply_group(a)
|
||
|
|
||
|
self.assertIn(a, b.implied_ids)
|
||
|
self.assertIn(a, c.implied_ids)
|
||
|
|
||
|
def test_remove_groups(self):
|
||
|
u1 = self.env['res.users'].create({'login': 'u1', 'name': 'U1'})
|
||
|
u2 = self.env['res.users'].create({'login': 'u2', 'name': 'U2'})
|
||
|
default = self.env.ref('base.default_user')
|
||
|
portal = self.env.ref('base.group_portal')
|
||
|
p = self.env['res.users'].create({'login': 'p', 'name': 'P', 'groups_id': [Command.set([portal.id])]})
|
||
|
|
||
|
a = self.env['res.groups'].create({'name': 'A', 'users': [Command.set(u1.ids)]})
|
||
|
b = self.env['res.groups'].create({'name': 'B', 'users': [Command.set(u1.ids)]})
|
||
|
c = self.env['res.groups'].create({'name': 'C', 'implied_ids': [Command.set(a.ids)], 'users': [Command.set([p.id, u2.id, default.id])]})
|
||
|
d = self.env['res.groups'].create({'name': 'D', 'implied_ids': [Command.set(a.ids)], 'users': [Command.set([u2.id, default.id])]})
|
||
|
|
||
|
def assertUsersEqual(users, group):
|
||
|
self.assertEqual(
|
||
|
sorted([r.login for r in users]),
|
||
|
sorted([r.login for r in group.with_context(active_test=False).users])
|
||
|
)
|
||
|
# sanity checks
|
||
|
assertUsersEqual([u1, u2, p, default], a)
|
||
|
assertUsersEqual([u1], b)
|
||
|
assertUsersEqual([u2, p, default], c)
|
||
|
assertUsersEqual([u2, default], d)
|
||
|
|
||
|
# C already implies A, we want none of B+C to imply A
|
||
|
(b + c)._remove_group(a)
|
||
|
|
||
|
self.assertNotIn(a, b.implied_ids)
|
||
|
self.assertNotIn(a, c.implied_ids)
|
||
|
self.assertIn(a, d.implied_ids)
|
||
|
|
||
|
# - Since B didn't imply A, removing A from the implied groups of (B+C)
|
||
|
# should not remove user U1 from A, even though C implied A, since C does
|
||
|
# not have U1 as a user
|
||
|
# - P should be removed as was only added via inheritance to C
|
||
|
# - U2 should not be removed from A since it is implied via C but also via D
|
||
|
assertUsersEqual([u1, u2, default], a)
|
||
|
assertUsersEqual([u1], b)
|
||
|
assertUsersEqual([u2, p, default], c)
|
||
|
assertUsersEqual([u2, default], d)
|
||
|
|
||
|
# When adding the template user to a new group, it should add it to existing internal users
|
||
|
e = self.env['res.groups'].create({'name': 'E'})
|
||
|
default.write({'groups_id': [Command.link(e.id)]})
|
||
|
self.assertIn(u1, e.users)
|
||
|
self.assertIn(u2, e.users)
|
||
|
self.assertIn(default, e.with_context(active_test=False).users)
|
||
|
self.assertNotIn(p, e.users)
|