# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import ast from textwrap import dedent 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) # Test RETURN_CONST self.assertEqual(const_eval('10'), 10) 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_expr_eval_opcodes(self): for expr, expected in [ ('3', 3), # RETURN_CONST ('[1,2,3,4][1:3]', [2, 3]), # BINARY_SLICE ]: self.assertEqual(expr_eval(expr), expected) def test_safe_eval_opcodes(self): for expr, locals_dict, expected in [ ('[x for x in (1,2)]', {}, [1, 2]), # LOAD_FAST_AND_CLEAR ('list(x for x in (1,2))', {}, [1, 2]), # END_FOR, CALL_INTRINSIC_1 ('v if v is None else w', {'v': False, 'w': 'foo'}, 'foo'), # POP_JUMP_IF_NONE ('v if v is not None else w', {'v': None, 'w': 'foo'}, 'foo'), # POP_JUMP_IF_NOT_NONE ('{a for a in (1, 2)}', {}, {1, 2}), # RERAISE ]: self.assertEqual(safe_eval(expr, locals_dict=locals_dict), expected) def test_safe_eval_exec_opcodes(self): for expr, locals_dict, expected in [ (""" def f(v): if v: x = 1 return x result = f(42) """, {}, 1), # LOAD_FAST_CHECK ]: safe_eval(dedent(expr), locals_dict=locals_dict, mode="exec", nocopy=True) self.assertEqual(locals_dict['result'], 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") 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_has_cycle(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.assertFalse((a + b + c + d)._has_cycle('implied_ids')) # create a cycle and check a.implied_ids = d self.assertTrue(a._has_cycle('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.assertNotEqual(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)