255 lines
12 KiB
Python
255 lines
12 KiB
Python
# -*- 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)
|