|
|
|
@@ -1,15 +1,17 @@
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
|
|
|
|
|
|
from werkzeug.urls import url_parse, url_decode
|
|
|
|
|
from werkzeug.urls import url_parse, url_decode, url_encode, url_unparse
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
|
|
|
|
from odoo import http
|
|
|
|
|
from odoo.addons.mail.tests.common import MailCommon
|
|
|
|
|
from odoo.addons.test_mail_full.tests.common import TestMailFullCommon
|
|
|
|
|
from odoo.addons.test_mail_sms.tests.common import TestSMSRecipients
|
|
|
|
|
from odoo.tests import tagged, users
|
|
|
|
|
from odoo.tests.common import HttpCase
|
|
|
|
|
from odoo.tools import html_escape
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@tagged('portal')
|
|
|
|
@@ -145,6 +147,191 @@ class TestPortalControllers(TestPortal):
|
|
|
|
|
self.assertEqual(message.author_id, self.partner_2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@tagged('portal')
|
|
|
|
|
class TestPortalFlow(MailCommon, HttpCase):
|
|
|
|
|
"""Share a link by email to a customer without an account for viewing a record through the portal.
|
|
|
|
|
|
|
|
|
|
The tests consist in sending a mail related to a record to a customer and checking that the record can be viewed
|
|
|
|
|
through the embedded link:
|
|
|
|
|
- either in the backend if the user is connected and has the right to
|
|
|
|
|
- or in the portal otherwise
|
|
|
|
|
"""
|
|
|
|
|
@classmethod
|
|
|
|
|
def setUpClass(cls):
|
|
|
|
|
super().setUpClass()
|
|
|
|
|
cls.customer = cls.env['res.partner'].create({
|
|
|
|
|
'country_id': cls.env.ref('base.fr').id,
|
|
|
|
|
'email': 'mdelvaux34@example.com',
|
|
|
|
|
'lang': 'en_US',
|
|
|
|
|
'mobile': '+33639982325',
|
|
|
|
|
'name': 'Mathias Delvaux',
|
|
|
|
|
'phone': '+33353011823',
|
|
|
|
|
})
|
|
|
|
|
cls.record_portal = cls.env['mail.test.portal'].create({
|
|
|
|
|
'name': 'Test Portal Record',
|
|
|
|
|
'partner_id': cls.customer.id,
|
|
|
|
|
'user_id': cls.user_admin.id,
|
|
|
|
|
})
|
|
|
|
|
cls.mail_template = cls.env['mail.template'].create({
|
|
|
|
|
'auto_delete': True,
|
|
|
|
|
'body_html': '<p>Hello <t t-out="object.partner_id.name"/>, your quotation is ready for review.</p>',
|
|
|
|
|
'email_from': '{{ (object.user_id.email_formatted or user.email_formatted) }}',
|
|
|
|
|
'model_id': cls.env.ref('test_mail_full.model_mail_test_portal').id,
|
|
|
|
|
'name': 'Quotation template',
|
|
|
|
|
'partner_to': '{{ object.partner_id.id }}',
|
|
|
|
|
'subject': 'Your quotation "{{ object.name }}"',
|
|
|
|
|
})
|
|
|
|
|
cls._create_portal_user()
|
|
|
|
|
for group_name, group_func, group_data in cls.record_portal.sudo()._notify_get_recipients_groups():
|
|
|
|
|
if group_name == 'portal_customer' and group_func(cls.customer):
|
|
|
|
|
cls.record_access_url = group_data['button_access']['url']
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
raise AssertionError('Record access URL not found')
|
|
|
|
|
# Build record_access_url_wrong_token with altered access_token for testing security
|
|
|
|
|
parsed_url = url_parse(cls.record_access_url)
|
|
|
|
|
query_params = url_decode(parsed_url.query)
|
|
|
|
|
cls.record_access_url_wrong_token = url_unparse(
|
|
|
|
|
(parsed_url[0], parsed_url[1], parsed_url[2],
|
|
|
|
|
url_encode({**query_params,
|
|
|
|
|
'access_token': query_params['access_token'].translate(
|
|
|
|
|
str.maketrans('0123456789abcdef',
|
|
|
|
|
'9876543210fedcba'))},
|
|
|
|
|
sort=True),
|
|
|
|
|
parsed_url[4]))
|
|
|
|
|
|
|
|
|
|
def assert_URL(self, url, expected_path, expected_fragment_params=None, expected_query=None):
|
|
|
|
|
"""Asserts that the URL has the expected path and if set, the expected fragment parameters and query."""
|
|
|
|
|
parsed_url = url_parse(url)
|
|
|
|
|
fragment_params = url_decode(parsed_url.fragment)
|
|
|
|
|
self.assertEqual(parsed_url.path, expected_path)
|
|
|
|
|
if expected_fragment_params:
|
|
|
|
|
for key, expected_value in expected_fragment_params.items():
|
|
|
|
|
self.assertEqual(fragment_params.get(key), expected_value,
|
|
|
|
|
f'Expected: "{key}={expected_value}" (for path: {expected_path})')
|
|
|
|
|
if expected_query:
|
|
|
|
|
self.assertEqual(expected_query, parsed_url.query,
|
|
|
|
|
f'Expected: query="{expected_query}" (for path: {expected_path})')
|
|
|
|
|
|
|
|
|
|
def _get_composer_with_context(self, template_id=False):
|
|
|
|
|
return self.env['mail.compose.message'].with_context({
|
|
|
|
|
'default_composition_mode': 'comment',
|
|
|
|
|
'default_email_layout_xmlid': 'mail.mail_notification_layout_with_responsible_signature',
|
|
|
|
|
'default_model': self.record_portal._name,
|
|
|
|
|
'default_res_id': self.record_portal.id,
|
|
|
|
|
'default_template_id': template_id,
|
|
|
|
|
'default_use_template': template_id,
|
|
|
|
|
'force_email': True,
|
|
|
|
|
'lang': '{{ object.partner_id.lang }}',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
def test_initial_data(self):
|
|
|
|
|
"""Test some initial values.
|
|
|
|
|
|
|
|
|
|
Test that record_access_url is a valid URL to view the record_portal and that record_access_url_wrong_token
|
|
|
|
|
only differs from record_access_url by a different access_token.
|
|
|
|
|
"""
|
|
|
|
|
parsed_record_access_url = url_parse(self.record_access_url)
|
|
|
|
|
record_access_query_params = url_decode(parsed_record_access_url.query)
|
|
|
|
|
parsed_record_access_url_wrong_token = url_parse(self.record_access_url_wrong_token)
|
|
|
|
|
record_access_wrong_token_query_params = url_decode(parsed_record_access_url_wrong_token.query)
|
|
|
|
|
|
|
|
|
|
self.assertEqual(parsed_record_access_url.path, '/mail/view')
|
|
|
|
|
# Note that pid, hash and auth_signup_token are not tested by this test but may be present in the URL (config).
|
|
|
|
|
self.assertEqual(record_access_query_params.get('model'), 'mail.test.portal')
|
|
|
|
|
self.assertEqual(int(record_access_query_params.get('res_id')), self.record_portal.id)
|
|
|
|
|
self.assertTrue(record_access_query_params.get('access_token'))
|
|
|
|
|
|
|
|
|
|
self.assertNotEqual(self.record_access_url, self.record_access_url_wrong_token)
|
|
|
|
|
self.assertEqual(parsed_record_access_url_wrong_token.path, '/mail/view')
|
|
|
|
|
self.assertTrue(record_access_wrong_token_query_params['access_token'])
|
|
|
|
|
self.assertNotEqual(record_access_query_params['access_token'],
|
|
|
|
|
record_access_wrong_token_query_params['access_token'])
|
|
|
|
|
self.assertEqual({k: v for k, v in record_access_query_params.items() if k != 'access_token'},
|
|
|
|
|
{k: v for k, v in record_access_wrong_token_query_params.items() if k != 'access_token'})
|
|
|
|
|
|
|
|
|
|
@users('portal_test')
|
|
|
|
|
def test_customer_access_logged_without_access(self):
|
|
|
|
|
"""Check that the link redirects the customer (without backend access) to the portal for viewing the record."""
|
|
|
|
|
self.authenticate(self.env.user.login, self.env.user.login)
|
|
|
|
|
res = self.url_open(self.record_access_url)
|
|
|
|
|
self.assertEqual(res.status_code, 200)
|
|
|
|
|
self.assert_URL(res.url, f'/my/test_portal/{self.record_portal.id}')
|
|
|
|
|
|
|
|
|
|
@users('portal_test')
|
|
|
|
|
def test_customer_access_logged_without_access_wrong_token(self):
|
|
|
|
|
"""Check that it redirects to discuss when logged customer has no access to the record and token is invalid."""
|
|
|
|
|
self.authenticate(self.env.user.login, self.env.user.login)
|
|
|
|
|
res = self.url_open(self.record_access_url_wrong_token)
|
|
|
|
|
self.assertEqual(res.status_code, 200)
|
|
|
|
|
self.assert_URL(res.url, '/my', {'action': 'mail.action_discuss'})
|
|
|
|
|
|
|
|
|
|
def test_customer_access_not_logged(self):
|
|
|
|
|
"""Check that the access link redirects the customer (not logged) to the portal for viewing the record."""
|
|
|
|
|
res = self.url_open(self.record_access_url)
|
|
|
|
|
self.assertEqual(res.status_code, 200)
|
|
|
|
|
self.assertIn(f'/my/test_portal/{self.record_portal.id}', res.url)
|
|
|
|
|
self.assert_URL(res.url, f'/my/test_portal/{self.record_portal.id}')
|
|
|
|
|
|
|
|
|
|
def test_customer_access_not_logged_wrong_token(self):
|
|
|
|
|
"""Check that the access link redirect the customer to login when the token is invalid."""
|
|
|
|
|
res = self.url_open(self.record_access_url_wrong_token)
|
|
|
|
|
self.assertEqual(res.status_code, 200)
|
|
|
|
|
self.assert_URL(res.url, '/web/login', {'model': 'mail.test.portal', 'id': str(self.record_portal.id)},
|
|
|
|
|
expected_query='redirect=')
|
|
|
|
|
|
|
|
|
|
@users('employee')
|
|
|
|
|
def test_employee_access(self):
|
|
|
|
|
"""Check that the access link redirects an employee to the backend for viewing the record."""
|
|
|
|
|
self.authenticate(self.env.user.login, self.env.user.login)
|
|
|
|
|
res = self.url_open(self.record_access_url)
|
|
|
|
|
self.assertEqual(res.status_code, 200)
|
|
|
|
|
self.assert_URL(res.url, '/web', {'model': 'mail.test.portal', 'id': str(self.record_portal.id)})
|
|
|
|
|
|
|
|
|
|
@users('employee')
|
|
|
|
|
def test_employee_access_wrong_token(self):
|
|
|
|
|
"""Check that the access link redirects an employee to the record even if the token invalid."""
|
|
|
|
|
self.authenticate(self.env.user.login, self.env.user.login)
|
|
|
|
|
res = self.url_open(self.record_access_url_wrong_token)
|
|
|
|
|
self.assertEqual(res.status_code, 200)
|
|
|
|
|
self.assert_URL(res.url, '/web', {'model': 'mail.test.portal', 'id': str(self.record_portal.id)})
|
|
|
|
|
|
|
|
|
|
@users('employee')
|
|
|
|
|
def test_send_message_to_customer(self):
|
|
|
|
|
"""Same as test_send_message_to_customer_using_template but without a template."""
|
|
|
|
|
composer = self._get_composer_with_context().create({
|
|
|
|
|
'body': '<p>Hello Mathias Delvaux, your quotation is ready for review.</p>',
|
|
|
|
|
'partner_ids': self.customer.ids,
|
|
|
|
|
'subject': 'Your Quotation "a white table"',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
with self.mock_mail_gateway(mail_unlink_sent=True):
|
|
|
|
|
composer._action_send_mail()
|
|
|
|
|
|
|
|
|
|
self.assertEqual(len(self._mails), 1)
|
|
|
|
|
self.assertIn(f'"{html_escape(self.record_access_url)}"', self._mails[0].get('body'))
|
|
|
|
|
# Check that the template is not used (not the same subject)
|
|
|
|
|
self.assertEqual('Your Quotation "a white table"', self._mails[0].get('subject'))
|
|
|
|
|
self.assertIn('Hello Mathias Delvaux', self._mails[0].get('body'))
|
|
|
|
|
|
|
|
|
|
@users('employee')
|
|
|
|
|
def test_send_message_to_customer_using_template(self):
|
|
|
|
|
"""Send a mail to a customer without an account and check that it contains a link to view the record.
|
|
|
|
|
|
|
|
|
|
Other tests below check that that same link has the correct behavior.
|
|
|
|
|
This test follows the common use case by using a template while the next send the mail without a template."""
|
|
|
|
|
composer = self._get_composer_with_context(self.mail_template.id).create({})
|
|
|
|
|
# currently onchange necessary
|
|
|
|
|
composer._onchange_template_id_wrapper()
|
|
|
|
|
|
|
|
|
|
with self.mock_mail_gateway(mail_unlink_sent=True):
|
|
|
|
|
composer._action_send_mail()
|
|
|
|
|
|
|
|
|
|
self.assertEqual(len(self._mails), 1)
|
|
|
|
|
self.assertIn(f'"{html_escape(self.record_access_url)}"', self._mails[0].get('body'))
|
|
|
|
|
self.assertEqual(f'Your quotation "{self.record_portal.name}"', self._mails[0].get('subject')) # Check that the template is used
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@tagged('portal')
|
|
|
|
|
class TestPortalMixin(TestPortal):
|
|
|
|
|
|
|
|
|
|