Files
test/web_studio/tests/test_approval.py
2023-04-14 17:42:23 +08:00

467 lines
24 KiB
Python

from psycopg2 import IntegrityError
from odoo.addons.mail.tests.common import mail_new_test_user
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tests.common import TransactionCase
from odoo.tools import mute_logger
class TestStudioApproval(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
creation_context = {
"studio": True,
'no_reset_password': True,
'mail_create_nosubscribe': True,
'mail_create_nolog': True
}
# setup 2 users with custom groups
cls.group_user = cls.env['res.groups'].with_context(**creation_context).create({
'name': 'Approval User',
'implied_ids': [(4, cls.env.ref('base.group_user').id)],
})
cls.group_manager = cls.env['res.groups'].with_context(**creation_context).create({
'name': 'Approval Manager',
'implied_ids': [(4, cls.group_user.id)],
})
cls.user = mail_new_test_user(
cls.env, login='Employee',
groups="base.group_user,base.group_partner_manager", context=creation_context)
cls.manager = mail_new_test_user(
cls.env, login='Manager',
groups="base.group_user,base.group_partner_manager", context=creation_context)
cls.user.write({
'groups_id': [(4, cls.group_user.id)]
})
cls.manager.write({
'groups_id': [(4, cls.group_manager.id)]
})
cls.record = cls.user.partner_id
# setup validation rules; inactive by default, they'll get
# activated in the tests when they're needed
# i'll use the 'open_parent' method on partners because why not
partner_model = cls.env.ref('base.model_res_partner')
cls.MODEL = 'res.partner'
cls.METHOD = 'open_parent'
cls.rule = cls.env['studio.approval.rule'].create({
'active': False,
'model_id': partner_model.id,
'method': cls.METHOD,
'message': "You didn't say the magic word!",
'group_id': cls.group_manager.id,
})
cls.rule_with_domain = cls.env['studio.approval.rule'].create({
'active': False,
'model_id': partner_model.id,
'method': cls.METHOD,
'message': "You didn't say the magic word!",
'group_id': cls.group_manager.id,
'domain': '[("is_company", "=", True)]',
})
cls.rule_exclusive = cls.env['studio.approval.rule'].create({
'active': False,
'model_id': partner_model.id,
'method': cls.METHOD,
'message': "You didn't say the magic word!",
'group_id': cls.group_manager.id,
'exclusive_user': True,
})
cls.rule_exclusive_with_domain = cls.env['studio.approval.rule'].create({
'active': False,
'model_id': partner_model.id,
'method': cls.METHOD,
'message': "You didn't say the magic word!",
'group_id': cls.group_manager.id,
'domain': '[("is_company", "=", True)]',
'exclusive_user': True,
})
def test_00_constraints(self):
"""Check that constraints on the model apply as expected."""
self.rule.active = True
# check that approval rules on non-existing methods are not allowed
with self.assertRaises(ValidationError, msg="Shouldn't have approval on non-existing method"):
self.rule.method = 'atomize'
# check that there cannot be 2 entries for the same rule+record
self.rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=False)
with mute_logger('odoo.sql_db'):
with self.assertRaises(IntegrityError, msg="Shouldn't have 2 entries for the same rule+record"):
with self.cr.savepoint():
self.env['studio.approval.entry'].with_user(self.manager).create({
'rule_id': self.rule.id,
'user_id': self.manager.id,
'res_id': self.record.id,
})
# check that modifying forbidden fields is prevented when entries exist
with self.assertRaises(UserError):
self.rule.method = 'unlink'
with self.assertRaises(UserError):
self.rule.group_id = self.env.ref('base.group_user')
with self.assertRaises(UserError):
self.rule.model_id = self.env.ref('base.model_res_partner_bank')
with self.assertRaises(UserError):
self.rule.action_id = self.env['ir.actions.actions'].search([], limit=1)
# check that deleting a rule that has entries is prevented
with self.assertRaises(UserError):
self.rule.unlink()
def test_01_single_rule(self):
""" - normal user can't validate
- normal user can't proceed
- admin user can validate
- normal user can proceed after that
"""
rule = self.rule
rule.active = True
with self.assertRaises(UserError, msg="Should'nt validate without required group"):
rule.with_user(self.user).set_approval(res_id=self.record.id, approved=True)
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False)
self.assertFalse(approval_result.get('approved'), "Shouldn't have approved automatically")
rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=True)
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False)
self.assertTrue(approval_result.get('approved'), "Should be able to proceed after manager approval")
def test_02_single_rule_on_action(self):
""" Same as previous test, but with approval on an action instead of a method."""
rule = self.rule
# there's no constraint on actions, since any action could be set
# in the form view's arch; take a random action
ACTION = self.env.ref('base.action_partner_form')
rule.write({
'active': True,
'method': False,
'action_id': ACTION.id,
})
with self.assertRaises(UserError, msg="Should'nt validate without required group"):
rule.with_user(self.user).set_approval(res_id=self.record.id, approved=True)
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=False,
action_id=ACTION.id)
self.assertFalse(approval_result.get('approved'), "Shouldn't have approved automatically")
rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=True)
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=False,
action_id=ACTION.id)
self.assertTrue(approval_result.get('approved'), "Should be able to proceed after manager approval")
def test_03_single_rule_with_domain(self):
""" - rule not triggered if no domain match
- rule triggered if domain match
"""
rule = self.rule_with_domain
rule.active = True
matching_record = self.env.ref('base.main_company').partner_id
non_matching_record = self.record
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
model=self.MODEL,
res_id=matching_record.id,
method=self.METHOD,
action_id=False)
self.assertFalse(approval_result.get('approved'), "Shouldn't be able to proceed on record that matches the rule's domain")
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
model=self.MODEL,
res_id=non_matching_record.id,
method=self.METHOD,
action_id=False)
self.assertTrue(approval_result.get('approved'), "Should be able to proceed on record that doesn't match the rule's domain")
def test_04_rule_rejection(self):
""" - admin rejects rule
- normal user can't proceed
- admin user can't proceed
- admin approves rule
- normal user can proceed
"""
rule = self.rule
rule.active = True
rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=False)
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False)
self.assertFalse(approval_result.get('approved'), "User shouldn't be able to proceed following manager rejection")
approval_result = self.env['studio.approval.rule'].with_user(self.manager).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False)
self.assertFalse(approval_result.get('approved'), "Manager shouldn't be able to proceed following own rejection")
rule.with_user(self.manager).delete_approval(res_id=self.record.id)
rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=True)
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False)
self.assertTrue(approval_result.get('approved'), "Should be able to proceed after manager changed their mind")
def test_05_different_users(self):
""" - user cannot proceed
- admin cannot proceed
- admin can validate their rule and proceed
- user can approve their rule and proceed after manager approval
"""
# set the base rule for users and the 'exlusive_user' rules for admin
self.rule.active = True
self.rule.group_id = self.group_user
self.rule_exclusive.active = True
user_rule = self.rule
manager_rule = self.rule_exclusive
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False)
self.assertFalse(approval_result.get('approved'), "User shouldn't be able to proceed")
# cancel the user's approval which was implicitely done in the previous call
user_rule.with_user(self.user).delete_approval(res_id=self.record.id)
approval_result = self.env['studio.approval.rule'].with_user(self.manager).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False)
self.assertFalse(approval_result.get('approved'), "Manager shouldn't be able to proceed")
approval_info = self.env['studio.approval.rule'].with_user(self.manager).get_approval_spec(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False
)
manager_entry = list(filter(lambda e: e['user_id'][0] == self.manager.id, approval_info['entries']))
self.assertEqual(len(manager_entry), 1, "Only one rule should have been validated by the manager")
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False)
self.assertTrue(approval_result.get('approved'), "Should be able to proceed after everybody approved")
def test_06_security(self):
""" - user cannot remove entry from admin
- admin cannot remove entry from user
- rule already validated/rejected doesn't accept new validation
"""
# set the base rule for users and the 'exlusive_user' rules for admin
self.rule.active = True
self.rule.group_id = self.group_user
self.rule_exclusive.active = True
user_rule = self.rule
manager_rule = self.rule_exclusive
user_rule.with_user(self.user).set_approval(res_id=self.record.id, approved=False)
manager_rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=False)
with self.assertRaises(UserError, msg="Shouldn't be able to cancel approval of someone else"):
user_rule.with_user(self.manager).delete_approval(res_id=self.record.id)
with self.assertRaises(UserError, msg="Shouldn't be able to create a second entry for the same record+rule"):
user_rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=True)
self.env.flush_all()
with self.assertRaises(UserError, msg="Shouldn't be able to cancel approval of someone else"):
manager_rule.with_user(self.user).delete_approval(res_id=self.record.id)
with self.assertRaises(UserError, msg="Shouldn't be able to create a second entry for the same record+rule"):
manager_rule.with_user(self.user).set_approval(res_id=self.record.id, approved=True)
self.env.flush_all()
def test_07_forbidden_record(self):
"""Getting/setting approval on records to which you don't have access."""
MODEL = 'res.company'
METHOD = 'create'
self.rule.write({
'active': True,
'method': METHOD,
'model_id': self.env.ref('base.model_res_company').id,
})
main_company = self.manager.company_id
alternate_company = self.env['res.company'].create({'name': 'SomeCompany'})
# I don't need to assert anything: raise = failure
self.env['studio.approval.rule'].with_user(self.manager).get_approval_spec(
model=MODEL,
res_id=main_company.id,
method=METHOD,
action_id=False
)
with self.assertRaises(AccessError, msg="Shouldn't be able to get approval spec on record I can't read"):
self.env['studio.approval.rule'].with_user(self.manager).get_approval_spec(
model=MODEL,
res_id=alternate_company.id,
method=METHOD,
action_id=False
)
with self.assertRaises(AccessError, msg="Shouldn't be able to set approval on record I can't write on"):
self.rule.with_user(self.manager).set_approval(res_id=main_company.id, approved=True)
def test_08_archive(self):
"""Archiving of approvals should be applied even with active_test disabled."""
# set the base rule for users and the 'exclusive_user' rules for admin
self.rule.active = True
self.rule.group_id = self.group_user
self.rule_exclusive.active = True
manager_rule = self.rule_exclusive
approval_result = self.env['studio.approval.rule'].with_user(self.manager).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False)
self.assertFalse(approval_result.get('approved'), "Manager shouldn't be able to proceed")
manager_rule.active = False
approval_result = self.env['studio.approval.rule'].with_context(active_test=False).with_user(self.manager).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False)
self.assertTrue(approval_result.get('approved'), "Manager should be able to proceed with archived rule even with active_test disabled")
def test_09_archive_reverse(self):
"""Archiving of rules should not prevent rules with 'exclusive_user' from working."""
# set the base rule for users and the 'exclusive_user' rules for admin
self.rule.active = True
self.rule.group_id = self.group_user
self.rule_exclusive.active = True
non_exlusive_rule = self.rule
# validate a rule that is not exclusive then archive it
non_exlusive_rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=True)
non_exlusive_rule.active = False
# try to approve an exclusive rule which is still remaining
approval_result = self.env['studio.approval.rule'].with_user(self.manager).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False)
self.assertTrue(approval_result.get('approved'), "should be able to proceed with an exclusive rule if another entry was archived")
def test_10_exclusive_collision(self):
"""Test that exclusive rules for different methods does not interact unexpectdedly."""
# set the base rule for users and the 'exlusive_user' rules for admin
self.rule.active = True
self.rule.group_id = self.group_user
self.rule_exclusive.active = True
self.rule_exclusive.group_id = self.group_user
other_exclusive_rule = self.env['studio.approval.rule'].create({
'active': True,
'model_id': self.rule_exclusive.model_id.id,
'method': 'unlink',
'message': "You didn't say the magic word!",
'group_id': self.group_user.id,
'exclusive_user': True,
})
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False)
self.assertFalse(approval_result.get('approved'), "User shouldn't be able to proceed")
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
model=self.MODEL,
res_id=self.record.id,
method='unlink',
action_id=False)
# check that the rule on 'unlink' is not prevented by another entry
# for a rule that is not related to the same action/method
self.assertTrue(approval_result.get('approved'), "User should be able to unlink")
def test_11_approval_activity(self):
"""Test the integration between approvals and next activities"""
self.rule.active = True
self.rule.responsible_id = self.manager
# generate a next activity for the rule's responsible by asking for approval
self.env['studio.approval.rule'].with_user(self.user).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False)
approval_request = self.env['studio.approval.request'].search(
[('rule_id', '=', self.rule.id), ('res_id', '=', self.record.id)])
self.assertEqual(len(approval_request), 1, "There should be exactly one approval request")
activity = approval_request.mail_activity_id
# mark the activity as done, the approval should go through and the request should be deleted
activity.with_user(self.manager).action_done()
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False)
self.assertTrue(approval_result.get('approved'),
"The approval should have been granted upon validation of the activity")
self.assertFalse(approval_request.exists(),
"The approval request should have been deleted upon the activity's confirmation")
def test_12_approval_activity_spoof(self):
"""Test that validating an approval activity as another user will not leak approval rights"""
self.rule.active = True
self.rule.responsible_id = self.manager
# generate a next activity for the rule's responsible by asking for approval
self.env['studio.approval.rule'].with_user(self.user).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False)
approval_request = self.env['studio.approval.request'].search(
[('rule_id', '=', self.rule.id), ('res_id', '=', self.record.id)])
activity = approval_request.mail_activity_id
# mark the manager's activity as done with the non-manager user
# the approval should *not* go through and the request should be deleted (and no errors should be raised)
activity.with_user(self.user).action_done()
approval_result = self.env['studio.approval.rule'].with_user(self.user).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False)
self.assertFalse(approval_result.get('approved'),
"The approval should not have been granted upon validation of the activity by anohter user")
self.assertFalse(approval_request.exists(),
"The approval request should have been deleted upon the activity's confirmation")
def test_13_approval_activity_dismissal(self):
"""Test that granting approval unlinks the activity that was created for that purpose"""
self.rule.active = True
self.rule.responsible_id = self.manager
# generate a next activity for the rule's responsible by asking for approval
self.env['studio.approval.rule'].with_user(self.user).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False)
approval_request = self.env['studio.approval.request'].search(
[('rule_id', '=', self.rule.id), ('res_id', '=', self.record.id)])
activity = approval_request.mail_activity_id
# grant approval as the manager
# both the mail activity and the approval requested should be deleted
self.rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=True)
self.assertFalse(activity.exists(),
"The activity should have been deleted if approval was granted through another channel")
self.assertFalse(approval_request.exists(),
"The approval request should have been deleted when the approval was granted")
def test_14_approval_activity_dismissal_refused(self):
"""Test that granting approval unlinks the activity that was created for that purpose"""
self.rule.active = True
self.rule.responsible_id = self.manager
# generate a next activity for the rule's responsible by asking for approval
self.env['studio.approval.rule'].with_user(self.user).check_approval(
model=self.MODEL,
res_id=self.record.id,
method=self.METHOD,
action_id=False)
approval_request = self.env['studio.approval.request'].search(
[('rule_id', '=', self.rule.id), ('res_id', '=', self.record.id)])
activity = approval_request.mail_activity_id
# refuse the approval as the manager
# both the mail activity and the approval requested should be deleted
self.rule.with_user(self.manager).set_approval(res_id=self.record.id, approved=False)
self.assertFalse(activity.exists(),
"The activity should have been deleted if approval was refused through another channel")
self.assertFalse(approval_request.exists(),
"The approval request should have been deleted when the approval was refused")