357 lines
19 KiB
Python
357 lines
19 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from unittest.mock import patch
|
|
from freezegun import freeze_time
|
|
from json import dumps
|
|
|
|
from odoo import Command
|
|
from odoo.exceptions import AccessError, ValidationError
|
|
from odoo.addons.http_routing.tests.common import MockRequest
|
|
from odoo.tests import tagged
|
|
from odoo.tools import formataddr
|
|
from .sign_controller_common import TestSignControllerCommon
|
|
|
|
@tagged('post_install', '-at_install')
|
|
class TestSignController(TestSignControllerCommon):
|
|
# test float auto_field display
|
|
def test_sign_controller_float(self):
|
|
sign_request = self.create_sign_request_no_item(signer=self.partner_1, cc_partners=self.partner_4)
|
|
text_type = self.env['sign.item.type'].search([('name', '=', 'Text')])
|
|
# the partner_latitude expects 7 zeros of decimal precision
|
|
text_type.auto_field = 'partner_latitude'
|
|
text_type.model_id = self.env['ir.model']._get('res.partner').id
|
|
token_a = self.env["sign.request.item"].search([('sign_request_id', '=', sign_request.id)]).access_token
|
|
with MockRequest(sign_request.env):
|
|
values = self.SignController.get_document_qweb_context(sign_request.id, token=token_a)
|
|
sign_type = next(filter(lambda sign_type: sign_type["name"] == "Text", values.get("rendering_context")["sign_item_types"]))
|
|
latitude = sign_type["auto_value"]
|
|
self.assertEqual(latitude, 0)
|
|
|
|
# test auto_field with wrong partner field
|
|
def test_sign_controller_dummy_fields(self):
|
|
text_type = self.env['sign.item.type'].search([('name', '=', 'Text')])
|
|
# we set a dummy field that raises an error
|
|
text_type.model_id = self.env['ir.model']._get('res.partner').id
|
|
with self.assertRaises(ValidationError):
|
|
text_type.auto_field = 'this_is_not_a_partner_field'
|
|
|
|
# we set a field the demo user does not have access and must not be able to set as auto_field
|
|
self.patch(type(self.env['res.partner']).function, 'groups', 'base.group_system')
|
|
with self.assertRaises(AccessError):
|
|
text_type.with_user(self.user_demo).auto_field = 'function'
|
|
|
|
# test auto_field with multiple sub steps
|
|
def test_sign_controller_multi_step_auto_field(self):
|
|
self.partner_1.company_id = self.env.ref('base.main_company')
|
|
self.partner_1.company_id.country_id = self.env.ref('base.be').id
|
|
sign_request = self.create_sign_request_no_item(signer=self.partner_1, cc_partners=self.partner_4)
|
|
text_type = self.env['sign.item.type'].search([('name', '=', 'Text')])
|
|
text_type.model_id = self.env['ir.model']._get('res.partner').id
|
|
text_type.auto_field = 'company_id.country_id.name'
|
|
token_a = self.env["sign.request.item"].search([('sign_request_id', '=', sign_request.id)]).access_token
|
|
with MockRequest(sign_request.env):
|
|
values = self.SignController.get_document_qweb_context(sign_request.id, token=token_a)
|
|
sign_type = next(filter(lambda sign_type: sign_type["name"] == "Text", values.get("rendering_context")["sign_item_types"]))
|
|
country = sign_type["auto_value"]
|
|
self.assertEqual(country, "Belgium")
|
|
|
|
def test_sign_request_requires_auth_when_credits_are_available(self):
|
|
sign_request = self.create_sign_request_1_role_sms_auth(self.partner_1, self.env['res.partner'])
|
|
sign_request_item = sign_request.request_item_ids[0]
|
|
|
|
self.assertFalse(sign_request_item.signed_without_extra_auth)
|
|
self.assertEqual(sign_request_item.role_id.auth_method, 'sms')
|
|
|
|
sign_vals = self.create_sign_values(sign_request.template_id.sign_item_ids, sign_request_item.role_id.id)
|
|
with patch('odoo.addons.iap.models.iap_account.IapAccount.get_credits', lambda self, x: 10):
|
|
response = self._json_url_open(
|
|
'/sign/sign/%d/%s' % (sign_request.id, sign_request_item.access_token),
|
|
data={'signature': sign_vals}
|
|
).json()['result']
|
|
|
|
self.assertFalse(response.get('success'))
|
|
self.assertTrue(sign_request_item.state, 'sent')
|
|
self.assertFalse(sign_request_item.signed_without_extra_auth)
|
|
|
|
def test_sign_request_allows_no_auth_when_credits_are_not_available(self):
|
|
sign_request = self.create_sign_request_1_role_sms_auth(self.partner_1, self.env['res.partner'])
|
|
sign_request_item = sign_request.request_item_ids[0]
|
|
|
|
self.assertFalse(sign_request_item.signed_without_extra_auth)
|
|
self.assertEqual(sign_request_item.role_id.auth_method, 'sms')
|
|
|
|
sign_vals = self.create_sign_values(sign_request.template_id.sign_item_ids, sign_request_item.role_id.id)
|
|
with patch('odoo.addons.iap.models.iap_account.IapAccount.get_credits', lambda self, x: 0):
|
|
response = self._json_url_open(
|
|
'/sign/sign/%d/%s' % (sign_request.id, sign_request_item.access_token),
|
|
data={'signature': sign_vals}
|
|
).json()['result']
|
|
|
|
self.assertTrue(response.get('success'))
|
|
self.assertTrue(sign_request_item.state, 'completed')
|
|
self.assertTrue(sign_request.state, 'done')
|
|
self.assertTrue(sign_request_item.signed_without_extra_auth)
|
|
|
|
def test_sign_from_mail_no_expiry_params(self):
|
|
sign_request = self.create_sign_request_1_role(self.partner_1, self.env['res.partner'])
|
|
url = '/sign/document/mail/%s/%s' % (sign_request.id, sign_request.request_item_ids[0].access_token)
|
|
response = self.url_open(url)
|
|
self.assertEqual(response.status_code, 404)
|
|
self.assertTrue('The signature request might have been deleted or modified.' in response.text)
|
|
|
|
def test_sign_from_mail_link_not_expired(self):
|
|
with freeze_time('2020-01-01'):
|
|
sign_request = self.create_sign_request_1_role(self.partner_1, self.env['res.partner'])
|
|
sign_request_item_id = sign_request.request_item_ids[0]
|
|
timestamp = sign_request_item_id._generate_expiry_link_timestamp()
|
|
expiry_hash = sign_request_item_id._generate_expiry_signature(sign_request_item_id.id, timestamp)
|
|
|
|
url = '/sign/document/mail/%(sign_request_id)s/%(access_token)s?timestamp=%(timestamp)s&exp=%(exp)s' % {
|
|
'sign_request_id': sign_request.id,
|
|
'access_token': sign_request.request_item_ids[0].access_token,
|
|
'timestamp': timestamp,
|
|
'exp': expiry_hash
|
|
}
|
|
response = self.url_open(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertTrue('/sign/document/%s/%s' % (sign_request.id, sign_request_item_id.access_token) in response.url)
|
|
|
|
def test_sign_from_mail_with_expired_link(self):
|
|
with freeze_time('2020-01-01'):
|
|
sign_request = self.create_sign_request_1_role(self.partner_1, self.env['res.partner'])
|
|
sign_request_item_id = sign_request.request_item_ids[0]
|
|
timestamp = sign_request_item_id._generate_expiry_link_timestamp()
|
|
expiry_hash = sign_request_item_id._generate_expiry_signature(sign_request_item_id.id, timestamp)
|
|
|
|
with freeze_time('2020-01-17'):
|
|
url = '/sign/document/mail/%(sign_request_id)s/%(access_token)s?timestamp=%(timestamp)s&exp=%(exp)s' % {
|
|
'sign_request_id': sign_request.id,
|
|
'access_token': sign_request.request_item_ids[0].access_token,
|
|
'timestamp': timestamp,
|
|
'exp': expiry_hash
|
|
}
|
|
response = self.url_open(url)
|
|
self.assertEqual(response.status_code, 403)
|
|
self.assertTrue('This link has expired' in response.text)
|
|
|
|
def test_shared_sign_request_without_expiry_params(self):
|
|
sign_request = self.create_sign_request_1_role(self.partner_1, self.env['res.partner'])
|
|
sign_request.state = 'shared'
|
|
sign_request_item_id = sign_request.request_item_ids[0]
|
|
url = '/sign/document/mail/%s/%s' % (sign_request.id, sign_request_item_id.access_token)
|
|
response = self.url_open(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertTrue('/sign/document/%s/%s' % (sign_request.id, sign_request_item_id.access_token) in response.url)
|
|
|
|
def test_sign_from_resend_expired_link(self):
|
|
with freeze_time('2020-01-01'):
|
|
sign_request = self.create_sign_request_1_role(self.partner_1, self.env['res.partner'])
|
|
sign_request_item_id = sign_request.request_item_ids[0]
|
|
timestamp = sign_request_item_id._generate_expiry_link_timestamp()
|
|
expiry_hash = sign_request_item_id._generate_expiry_signature(sign_request_item_id.id, timestamp)
|
|
|
|
url = '/sign/document/mail/%(sign_request_id)s/%(access_token)s?timestamp=%(timestamp)s&exp=%(exp)s' % {
|
|
'sign_request_id': sign_request.id,
|
|
'access_token': sign_request.request_item_ids[0].access_token,
|
|
'timestamp': timestamp,
|
|
'exp': expiry_hash
|
|
}
|
|
response = self.url_open(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertTrue('/sign/document/%s/%s' % (sign_request.id, sign_request_item_id.access_token) in response.url)
|
|
|
|
sign_request_item = {sign_request_item.role_id: sign_request_item for sign_request_item in sign_request.request_item_ids}
|
|
sign_request_item_signer_1 = sign_request_item[self.role_signer_1]
|
|
|
|
sign_request_item_signer_1.sudo().sign(self.single_signer_sign_values)
|
|
mail = self.env['mail.mail'].search([('email_to', '=', formataddr((self.partner_1.name, self.partner_1.email)))])
|
|
self.assertEqual(len(mail.ids), 2)
|
|
|
|
with freeze_time('2020-01-17'):
|
|
self.start_tour(url, 'sign_resend_expired_link_tour', login='demo')
|
|
|
|
def test_cancel_request_as_public_user(self):
|
|
"""
|
|
Test that a public user can cancel a sign request and that a cancellation log is recorded on the partner_id.
|
|
"""
|
|
sign_request = self.create_sign_request_1_role(self.partner_1, self.env['res.partner'])
|
|
sign_request_item = sign_request.request_item_ids[0]
|
|
|
|
data = {
|
|
'params': {
|
|
'refusal_reason': 'No.',
|
|
},
|
|
}
|
|
headers = {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
url = '/sign/refuse/%(item_id)s/%(token)s?refusal_reason="test"' % {
|
|
'item_id': sign_request.id,
|
|
'token': sign_request_item.access_token
|
|
}
|
|
|
|
# Set the environment user as the public user
|
|
self.uid = self.public_user
|
|
|
|
# Send a request to cancel the sign request item
|
|
self.authenticate(None, None)
|
|
response = self.url_open(url, data=dumps(data), headers=headers)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertEqual(sign_request.state, 'canceled', "Sign request state should be 'canceled'")
|
|
self.assertEqual(sign_request_item.state, 'canceled', "Sign request item state should be 'canceled'")
|
|
|
|
sign_cancel_log = self.env['sign.log'].sudo().search([
|
|
('sign_request_id', '=', sign_request.id),
|
|
('action', '=', 'cancel')
|
|
])
|
|
|
|
self.assertTrue(sign_cancel_log, "A sign cancel log should be created")
|
|
self.assertEqual(sign_cancel_log.request_state, 'canceled',
|
|
"Log request state should be 'canceled'")
|
|
self.assertEqual(sign_cancel_log.partner_id, self.partner_1,
|
|
"Log partner_id should match partner_1")
|
|
self.assertEqual(sign_cancel_log.sign_request_item_id.id, sign_request_item.id,
|
|
"Log should reference the correct request item")
|
|
|
|
def test_make_public_user_with_duplicate_contact(self):
|
|
"""
|
|
Test make_public_user behavior with duplicate contacts.
|
|
Ensures that when a user has a duplicate contact with the same email and name,
|
|
the sign request associates the correct partner ID which contains user_ids.
|
|
"""
|
|
user = self.env['res.users'].create({
|
|
'name': 'Test User',
|
|
'login': 'test_user@example.com',
|
|
'email': 'test_user@example.com',
|
|
})
|
|
contact = user.partner_id
|
|
|
|
# Duplicate contact and set the user's partner ID to the duplicate contact
|
|
duplicate_contact = contact.copy()
|
|
user.partner_id = duplicate_contact
|
|
self.assertEqual(user.partner_id, duplicate_contact)
|
|
self.assertEqual(contact.email, duplicate_contact.email)
|
|
|
|
# Create a sign request and linked to the user's(Test User) contact
|
|
sign_request = self.env['sign.request'].with_context(no_sign_mail=False).create({
|
|
'template_id': self.template_no_item.id,
|
|
'reference': self.template_no_item.display_name,
|
|
'state': 'shared',
|
|
'request_item_ids': [Command.create({
|
|
'role_id': self.env.ref('sign.sign_item_role_default').id,
|
|
})],
|
|
})
|
|
|
|
response = self._json_url_open(
|
|
'/sign/send_public/%s/%s' % (sign_request.id, sign_request.access_token),
|
|
data={'name': contact.name, 'mail': contact.email}
|
|
).json().get('result')
|
|
|
|
self.assertTrue(response.get('requestID'), 'Request ID should be returned')
|
|
|
|
sign_request_partner_id = self.env['sign.request'].search(
|
|
[('id', '=', response.get('requestID'))]
|
|
).request_item_ids.partner_id
|
|
|
|
self.assertEqual(
|
|
duplicate_contact.id, sign_request_partner_id.id,
|
|
'The sign request partner ID should match the duplicate contact'
|
|
)
|
|
self.assertEqual(
|
|
sign_request_partner_id.email, contact.email,
|
|
'The email of the sign request partner should match the original contact email'
|
|
)
|
|
self.assertEqual(
|
|
sign_request_partner_id.name, duplicate_contact.name,
|
|
'The name of the sign request partner should match the original contact name'
|
|
)
|
|
self.assertEqual(
|
|
len(sign_request.request_item_ids), 1,
|
|
'There should be exactly one request item in the sign request'
|
|
)
|
|
|
|
def test_get_sign_request_items_with_public_user(self):
|
|
""" Test the RPC call to get sign request items as a public user. """
|
|
# Create two sign requests. The second one will be returned as 'the next' to be signed.
|
|
self.sign_request = self.create_sign_request_1_role(self.partner_1, self.env['res.partner'])
|
|
self.next_sign_request = self.create_sign_request_1_role(self.partner_1, self.env['res.partner'])
|
|
self.sign_request_item = self.sign_request.request_item_ids[0]
|
|
|
|
self.authenticate(None, None) # Ensure the current user for the request is public
|
|
response = self._json_url_open(
|
|
'/sign/sign_request_items',
|
|
{
|
|
'request_id': self.sign_request.id,
|
|
'token': self.sign_request_item.access_token,
|
|
'sign_item_id': self.sign_request_item.id,
|
|
}
|
|
)
|
|
self.assertEqual(response.status_code, 200, f"Expected 200 OK, got {response.status_code}")
|
|
response_json = response.json() # Get the JSON content of the response
|
|
self.assertEqual(response_json['jsonrpc'], '2.0', "Expected JSON-RPC 2.0 response")
|
|
self.assertNotIn('error', response_json, f"RPC call returned an error: {response_json.get('error')}")
|
|
|
|
result = response_json['result']
|
|
self.assertIsInstance(result, list, "Result should be a list")
|
|
self.assertGreaterEqual(len(result), 1, "Should find at least one sign request item")
|
|
|
|
# Verify that the found item is the one associated with the test partner's email
|
|
found_item_ids = [item['id'] for item in result]
|
|
self.assertIn(
|
|
self.next_sign_request.request_item_ids[0].id, found_item_ids,
|
|
"The 'next' sign request item must be able to be accessed through the public route."
|
|
)
|
|
|
|
def test_sign_request_local(self):
|
|
# Make sure the stored signature of an user can't be used by another user locally
|
|
# make sure the log user_id is correct
|
|
# user_1 is linked to partner1
|
|
self.user_1.sign_signature = b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNgYGAAAAAEAAH2FzhVAAAAAElFTkSuQmCC'
|
|
sign_request = self.create_sign_request_1_role(self.partner_1, self.env['res.partner'])
|
|
sign_request_item = sign_request.request_item_ids[0]
|
|
res = sign_request_item.with_user(self.user_2)._get_user_signature(signature_type='sign_signature')
|
|
self.assertFalse(res, "The signature of user1 is not available to user 2")
|
|
res = sign_request_item.with_user(self.user_1)._get_user_signature(signature_type='sign_signature')
|
|
self.assertTrue(res, "The signature of user1 is available to user1")
|
|
|
|
sign_vals = self.create_sign_values(sign_request.template_id.sign_item_ids, sign_request_item.role_id.id)
|
|
self.authenticate(self.user_2.login, "user_2!user_2")
|
|
response = self._json_url_open(
|
|
'/sign/sign/%d/%s' % (sign_request.id, sign_request_item.access_token),
|
|
data={'signature': sign_vals}
|
|
).json()['result']
|
|
self.assertTrue(response['success'], "success request")
|
|
logs = sign_request.sign_log_ids
|
|
create_log = logs.filtered(lambda l: l.action == 'create')
|
|
signature_log = logs.filtered(lambda l: l.action == 'sign')
|
|
self.assertEqual(create_log.create_uid, self.env.user, "the signature is created by current user")
|
|
self.assertEqual(create_log.user_id, self.env.user, "The sign request is created by current user")
|
|
self.assertEqual(signature_log.create_uid, self.user_1, "the signature is created by the signing user")
|
|
self.assertEqual(signature_log.user_id, self.user_2, "the signature log user_id is the logged in user")
|
|
|
|
def test_sign_completed_redirection(self):
|
|
""" Test that internal users are redirected to the backend when a sign request is completed. """
|
|
sign_request = self.create_sign_request_1_role(self.partner_1, self.env['res.partner'])
|
|
sign_request_item = sign_request.request_item_ids[0]
|
|
sign_vals = self.create_sign_values(sign_request.template_id.sign_item_ids, sign_request_item.role_id.id)
|
|
sign_request_item._sign(sign_vals)
|
|
url = '/sign/document/%s/%s' % (sign_request.id, sign_request_item.access_token)
|
|
|
|
# Test redirection for a logged-in user with a sign.group_sign_user
|
|
self.authenticate("user_1", "user_1")
|
|
response = self.url_open(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertTrue(f'/odoo/sign.request/{sign_request.id}/action-sign.Document' in response.url)
|
|
|
|
# Test no redirection for a public user
|
|
self.authenticate(None, None)
|
|
response = self.url_open(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertFalse('/odoo/sign.request/' in response.url)
|
|
|
|
# Test no redirection for an internal user without sign.group_sign_user
|
|
self.authenticate("user_5", "user_5")
|
|
response = self.url_open(url)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertFalse('/odoo/sign.request/' in response.url)
|