# -*- 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)