[MERGE] Sunc with addons/trunk.
[odoo/odoo.git] / addons / mail / tests / test_mail.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Business Applications
5 #    Copyright (c) 2012-TODAY OpenERP S.A. <http://openerp.com>
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import tools
23
24 from openerp.addons.mail.tests import test_mail_mockup
25 from openerp.tools.mail import html_sanitize
26
27 MAIL_TEMPLATE = """Return-Path: <whatever-2a840@postmaster.twitter.com>
28 To: {to}
29 Received: by mail1.openerp.com (Postfix, from userid 10002)
30     id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST)
31 From: Sylvie Lelitre <sylvie.lelitre@agrolait.com>
32 Subject: {subject}
33 MIME-Version: 1.0
34 Content-Type: multipart/alternative;
35     boundary="----=_Part_4200734_24778174.1344608186754"
36 Date: Fri, 10 Aug 2012 14:16:26 +0000
37 Message-ID: <1198923581.41972151344608186760.JavaMail@agrolait.com>
38 {extra}
39 ------=_Part_4200734_24778174.1344608186754
40 Content-Type: text/plain; charset=utf-8
41 Content-Transfer-Encoding: quoted-printable
42
43 Please call me as soon as possible this afternoon!
44
45 --
46 Sylvie
47 ------=_Part_4200734_24778174.1344608186754
48 Content-Type: text/html; charset=utf-8
49 Content-Transfer-Encoding: quoted-printable
50
51 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
52 <html>
53  <head>=20
54   <meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8" />
55  </head>=20
56  <body style=3D"margin: 0; padding: 0; background: #ffffff;-webkit-text-size-adjust: 100%;">=20
57
58   <p>Please call me as soon as possible this afternoon!</p>
59
60   <p>--<br/>
61      Sylvie
62   <p>
63  </body>
64 </html>
65 ------=_Part_4200734_24778174.1344608186754--
66 """
67
68 MAIL_TEMPLATE_PLAINTEXT = """Return-Path: <whatever-2a840@postmaster.twitter.com>
69 To: {to}
70 Received: by mail1.openerp.com (Postfix, from userid 10002)
71     id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST)
72 From: Sylvie Lelitre <sylvie.lelitre@agrolait.com>
73 Subject: {subject}
74 MIME-Version: 1.0
75 Content-Type: text/plain
76 Date: Fri, 10 Aug 2012 14:16:26 +0000
77 Message-ID: {msg_id}
78 {extra}
79
80 Please call me as soon as possible this afternoon!
81
82 --
83 Sylvie
84 """
85
86
87 class test_mail(test_mail_mockup.TestMailMockups):
88
89     def _mock_send_get_mail_body(self, *args, **kwargs):
90         # def _send_get_mail_body(self, cr, uid, mail, partner=None, context=None)
91         body = tools.append_content_to_html(args[2].body_html, kwargs.get('partner').name if kwargs.get('partner') else 'No specific partner')
92         return body
93
94     def setUp(self):
95         super(test_mail, self).setUp()
96         cr, uid = self.cr, self.uid
97         self.ir_model = self.registry('ir.model')
98         self.mail_alias = self.registry('mail.alias')
99         self.mail_thread = self.registry('mail.thread')
100         self.mail_group = self.registry('mail.group')
101         self.mail_mail = self.registry('mail.mail')
102         self.mail_message = self.registry('mail.message')
103         self.mail_notification = self.registry('mail.notification')
104         self.mail_followers = self.registry('mail.followers')
105         self.mail_message_subtype = self.registry('mail.message.subtype')
106         self.res_users = self.registry('res.users')
107         self.res_partner = self.registry('res.partner')
108
109         # Find Employee group
110         group_employee_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user')
111         group_employee_id = group_employee_ref and group_employee_ref[1] or False
112         # Test users
113         self.user_raoul_id = self.res_users.create(cr, uid,
114             {'name': 'Raoul Grosbedon', 'email': 'raoul@raoul.fr', 'login': 'raoul', 'groups_id': [(6, 0, [group_employee_id])]})
115         self.user_raoul = self.res_users.browse(cr, uid, self.user_raoul_id)
116         self.user_admin = self.res_users.browse(cr, uid, uid)
117
118         # Mock send_get_mail_body to test its functionality without other addons override
119         self._send_get_mail_body = self.registry('mail.mail').send_get_mail_body
120         self.registry('mail.mail').send_get_mail_body = self._mock_send_get_mail_body
121
122         # groups@.. will cause the creation of new mail groups
123         self.mail_group_model_id = self.ir_model.search(cr, uid, [('model', '=', 'mail.group')])[0]
124         self.mail_alias.create(cr, uid, {'alias_name': 'groups',
125                                                    'alias_model_id': self.mail_group_model_id})
126         # create a 'pigs' group that will be used through the various tests
127         self.group_pigs_id = self.mail_group.create(cr, uid,
128             {'name': 'Pigs', 'description': 'Fans of Pigs, unite !'})
129         self.group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
130
131     def tearDown(self):
132         # Remove mocks
133         self.registry('mail.mail').send_get_mail_body = self._send_get_mail_body
134         super(test_mail, self).tearDown()
135
136     def test_00_message_process(self):
137         """ Testing incoming emails processing. """
138         cr, uid, user_raoul = self.cr, self.uid, self.user_raoul
139         # Incoming mail creates a new mail_group "frogs"
140         self.assertEqual(self.mail_group.search(cr, uid, [('name', '=', 'frogs')]), [])
141         mail_frogs = MAIL_TEMPLATE.format(to='groups@example.com, other@gmail.com', subject='frogs', extra='')
142         self.mail_thread.message_process(cr, uid, None, mail_frogs)
143         frog_groups = self.mail_group.search(cr, uid, [('name', '=', 'frogs')])
144         self.assertTrue(len(frog_groups) == 1)
145
146         # Previously-created group can be emailed now - it should have an implicit alias group+frogs@...
147         frog_group = self.mail_group.browse(cr, uid, frog_groups[0])
148         group_messages = frog_group.message_ids
149         self.assertTrue(len(group_messages) == 1, 'New group should only have the original message')
150         mail_frog_news = MAIL_TEMPLATE.format(to='Friendly Frogs <group+frogs@example.com>', subject='news', extra='')
151         self.mail_thread.message_process(cr, uid, None, mail_frog_news)
152         frog_group.refresh()
153         self.assertTrue(len(frog_group.message_ids) == 2, 'Group should contain 2 messages now')
154
155         # Even with a wrong destination, a reply should end up in the correct thread
156         mail_reply = MAIL_TEMPLATE.format(to='erroneous@example.com>', subject='Re: news',
157                                           extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n' % frog_group.id)
158         self.mail_thread.message_process(cr, uid, None, mail_reply)
159         frog_group.refresh()
160         self.assertTrue(len(frog_group.message_ids) == 3, 'Group should contain 3 messages now')
161
162         # No model passed and no matching alias must raise
163         mail_spam = MAIL_TEMPLATE.format(to='noone@example.com', subject='spam', extra='')
164         self.assertRaises(Exception,
165                           self.mail_thread.message_process,
166                           cr, uid, None, mail_spam)
167
168         # plain text content should be wrapped and stored as html
169         test_msg_id = '<deadcafe.1337@smtp.agrolait.com>'
170         mail_text = MAIL_TEMPLATE_PLAINTEXT.format(to='groups@example.com', subject='frogs', extra='', msg_id=test_msg_id)
171         self.mail_thread.message_process(cr, uid, None, mail_text)
172         new_mail = self.mail_message.browse(cr, uid, self.mail_message.search(cr, uid, [('message_id', '=', test_msg_id)])[0])
173         self.assertEqual(new_mail.body, '\n<pre>\nPlease call me as soon as possible this afternoon!\n\n--\nSylvie\n</pre>\n',
174                          'plaintext mail incorrectly parsed')
175
176         # Do: post a new message, with a known partner
177         test_msg_id = '<deadcafe.1337-2@smtp.agrolait.com>'
178         TEMPLATE_MOD = MAIL_TEMPLATE_PLAINTEXT.replace('Sylvie Lelitre <sylvie.lelitre@agrolait.com>', user_raoul.email)
179         mail_new = TEMPLATE_MOD.format(to='Friendly Frogs <group+frogs@example.com>', subject='extra news', extra='', msg_id=test_msg_id)
180         self.mail_thread.message_process(cr, uid, None, mail_new)
181         new_mail = self.mail_message.browse(cr, uid, self.mail_message.search(cr, uid, [('message_id', '=', test_msg_id)])[0])
182         # Test: author_id set, not email_from
183         self.assertEqual(new_mail.author_id, user_raoul.partner_id, 'message process wrong author found')
184         self.assertFalse(new_mail.email_from, 'message process should not set the email_from when an author is found')
185
186         # Do: post a new message, with a unknown partner
187         test_msg_id = '<deadcafe.1337-3@smtp.agrolait.com>'
188         TEMPLATE_MOD = MAIL_TEMPLATE_PLAINTEXT.replace('Sylvie Lelitre <sylvie.lelitre@agrolait.com>', '_abcd_')
189         mail_new = TEMPLATE_MOD.format(to='Friendly Frogs <group+frogs@example.com>', subject='super news', extra='', msg_id=test_msg_id)
190         self.mail_thread.message_process(cr, uid, None, mail_new)
191         new_mail = self.mail_message.browse(cr, uid, self.mail_message.search(cr, uid, [('message_id', '=', test_msg_id)])[0])
192         # Test: author_id set, not email_from
193         self.assertFalse(new_mail.author_id, 'message process shnould not have found a partner for _abcd_ email address')
194         self.assertIn('_abcd_', new_mail.email_from, 'message process should set en email_from when not finding a partner_id')
195
196     def test_10_followers_function_field(self):
197         """ Tests designed for the many2many function field 'follower_ids'.
198             We will test to perform writes using the many2many commands 0, 3, 4,
199             5 and 6. """
200         cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
201
202         # Data: create partner Bert Poilu
203         partner_bert_id = self.res_partner.create(cr, uid, {'name': 'Bert Poilu'})
204         # Data: create 'disturbing' values in mail.followers: same res_id, other res_model; same res_model, other res_id
205         group_dummy_id = self.mail_group.create(cr, uid,
206             {'name': 'Dummy group'})
207         self.mail_followers.create(cr, uid,
208             {'res_model': 'mail.thread', 'res_id': self.group_pigs_id, 'partner_id': partner_bert_id})
209         self.mail_followers.create(cr, uid,
210             {'res_model': 'mail.group', 'res_id': group_dummy_id, 'partner_id': partner_bert_id})
211
212         # Pigs just created: should be only Admin as follower
213         follower_ids = set([follower.id for follower in group_pigs.message_follower_ids])
214         self.assertEqual(follower_ids, set([user_admin.partner_id.id]), 'Admin should be the only Pigs fan')
215
216         # Subscribe Bert through a '4' command
217         group_pigs.write({'message_follower_ids': [(4, partner_bert_id)]})
218         group_pigs.refresh()
219         follower_ids = set([follower.id for follower in group_pigs.message_follower_ids])
220         self.assertEqual(follower_ids, set([partner_bert_id, user_admin.partner_id.id]), 'Bert and Admin should be the only Pigs fans')
221
222         # Unsubscribe Bert through a '3' command
223         group_pigs.write({'message_follower_ids': [(3, partner_bert_id)]})
224         group_pigs.refresh()
225         follower_ids = set([follower.id for follower in group_pigs.message_follower_ids])
226         self.assertEqual(follower_ids, set([user_admin.partner_id.id]), 'Admin should be the only Pigs fan')
227
228         # Set followers through a '6' command
229         group_pigs.write({'message_follower_ids': [(6, 0, [partner_bert_id])]})
230         group_pigs.refresh()
231         follower_ids = set([follower.id for follower in group_pigs.message_follower_ids])
232         self.assertEqual(follower_ids, set([partner_bert_id]), 'Bert should be the only Pigs fan')
233
234         # Add a follower created on the fly through a '0' command
235         group_pigs.write({'message_follower_ids': [(0, 0, {'name': 'Patrick Fiori'})]})
236         partner_patrick_id = self.res_partner.search(cr, uid, [('name', '=', 'Patrick Fiori')])[0]
237         group_pigs.refresh()
238         follower_ids = set([follower.id for follower in group_pigs.message_follower_ids])
239         self.assertEqual(follower_ids, set([partner_bert_id, partner_patrick_id]), 'Bert and Patrick should be the only Pigs fans')
240
241         # Finally, unlink through a '5' command
242         group_pigs.write({'message_follower_ids': [(5, 0)]})
243         group_pigs.refresh()
244         follower_ids = set([follower.id for follower in group_pigs.message_follower_ids])
245         self.assertFalse(follower_ids, 'Pigs group should not have fans anymore')
246
247         # Test dummy data has not been altered
248         fol_obj_ids = self.mail_followers.search(cr, uid, [('res_model', '=', 'mail.thread'), ('res_id', '=', self.group_pigs_id)])
249         follower_ids = set([follower.partner_id.id for follower in self.mail_followers.browse(cr, uid, fol_obj_ids)])
250         self.assertEqual(follower_ids, set([partner_bert_id]), 'Bert should be the follower of dummy mail.thread data')
251         fol_obj_ids = self.mail_followers.search(cr, uid, [('res_model', '=', 'mail.group'), ('res_id', '=', group_dummy_id)])
252         follower_ids = set([follower.partner_id.id for follower in self.mail_followers.browse(cr, uid, fol_obj_ids)])
253         self.assertEqual(follower_ids, set([partner_bert_id, user_admin.partner_id.id]), 'Bert and Admin should be the followers of dummy mail.group data')
254
255     def test_11_message_followers_and_subtypes(self):
256         """ Tests designed for the subscriber API as well as message subtypes """
257         cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
258         # Data: user Raoul
259         user_raoul_id = self.user_raoul_id
260         user_raoul = self.res_users.browse(cr, uid, user_raoul_id)
261         # Data: message subtypes
262         self.mail_message_subtype.create(cr, uid, {'name': 'mt_mg_def', 'default': True, 'res_model': 'mail.group'})
263         self.mail_message_subtype.create(cr, uid, {'name': 'mt_other_def', 'default': True, 'res_model': 'crm.lead'})
264         self.mail_message_subtype.create(cr, uid, {'name': 'mt_all_def', 'default': True, 'res_model': False})
265         mt_mg_nodef = self.mail_message_subtype.create(cr, uid, {'name': 'mt_mg_nodef', 'default': False, 'res_model': 'mail.group'})
266         mt_all_nodef = self.mail_message_subtype.create(cr, uid, {'name': 'mt_all_nodef', 'default': False, 'res_model': False})
267         default_group_subtypes = self.mail_message_subtype.search(cr, uid, [('default', '=', True), '|', ('res_model', '=', 'mail.group'), ('res_model', '=', False)])
268
269         # ----------------------------------------
270         # CASE1: test subscriptions with subtypes
271         # ----------------------------------------
272
273         # Do: Subscribe Raoul three times (niak niak) through message_subscribe_users
274         group_pigs.message_subscribe_users([user_raoul_id, user_raoul_id])
275         group_pigs.message_subscribe_users([user_raoul_id])
276         group_pigs.refresh()
277         # Test: 2 followers (Admin and Raoul)
278         follower_ids = [follower.id for follower in group_pigs.message_follower_ids]
279         self.assertEqual(set(follower_ids), set([user_raoul.partner_id.id, user_admin.partner_id.id]), 'Admin and Raoul should be the only 2 Pigs fans')
280         # Test: Raoul follows default subtypes
281         fol_ids = self.mail_followers.search(cr, uid, [('res_model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id), ('partner_id', '=', user_raoul.partner_id.id)])
282         fol_obj = self.mail_followers.browse(cr, uid, fol_ids)[0]
283         fol_subtype_ids = set([subtype.id for subtype in fol_obj.subtype_ids])
284         self.assertEqual(set(fol_subtype_ids), set(default_group_subtypes), 'subscription subtypes are incorrect')
285
286         # Do: Unsubscribe Raoul twice through message_unsubscribe_users
287         group_pigs.message_unsubscribe_users([user_raoul_id, user_raoul_id])
288         group_pigs.refresh()
289         # Test: 1 follower (Admin)
290         follower_ids = [follower.id for follower in group_pigs.message_follower_ids]
291         self.assertEqual(follower_ids, [user_admin.partner_id.id], 'Admin must be the only Pigs fan')
292
293         # Do: subscribe Admin with subtype_ids
294         group_pigs.message_subscribe_users([uid], [mt_mg_nodef, mt_all_nodef])
295         fol_ids = self.mail_followers.search(cr, uid, [('res_model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id), ('partner_id', '=', user_admin.partner_id.id)])
296         fol_obj = self.mail_followers.browse(cr, uid, fol_ids)[0]
297         fol_subtype_ids = set([subtype.id for subtype in fol_obj.subtype_ids])
298         self.assertEqual(set(fol_subtype_ids), set([mt_mg_nodef, mt_all_nodef]), 'subscription subtypes are incorrect')
299
300         # ----------------------------------------
301         # CASE2: test mail_thread fields
302         # ----------------------------------------
303
304         subtype_data = group_pigs._get_subscription_data(None, None)[group_pigs.id]['message_subtype_data']
305         self.assertEqual(set(subtype_data.keys()), set(['Discussions', 'mt_mg_def', 'mt_all_def', 'mt_mg_nodef', 'mt_all_nodef']), 'mail.group available subtypes incorrect')
306         self.assertFalse(subtype_data['Discussions']['followed'], 'Admin should not follow Discussions in pigs')
307         self.assertTrue(subtype_data['mt_mg_nodef']['followed'], 'Admin should follow mt_mg_nodef in pigs')
308         self.assertTrue(subtype_data['mt_all_nodef']['followed'], 'Admin should follow mt_all_nodef in pigs')
309
310     def test_20_message_quote_context(self):
311         """ Tests designed for message_post. """
312         cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
313
314         msg1_id = self.mail_message.create(cr, uid, {'body': 'Thread header about Zap Brannigan', 'subject': 'My subject'})
315         msg2_id = self.mail_message.create(cr, uid, {'body': 'First answer, should not be displayed', 'subject': 'Re: My subject', 'parent_id': msg1_id})
316         msg3_id = self.mail_message.create(cr, uid, {'body': 'Second answer', 'subject': 'Re: My subject', 'parent_id': msg1_id})
317         msg4_id = self.mail_message.create(cr, uid, {'body': 'Third answer', 'subject': 'Re: My subject', 'parent_id': msg1_id})
318         msg_new_id = self.mail_message.create(cr, uid, {'body': 'My answer I am propagating', 'subject': 'Re: My subject', 'parent_id': msg1_id})
319
320         result = self.mail_message.message_quote_context(cr, uid, msg_new_id, limit=3)
321         self.assertIn('Thread header about Zap Brannigan', result, 'Thread header content should be in quote.')
322         self.assertIn('Second answer', result, 'Answer should be in quote.')
323         self.assertIn('Third answer', result, 'Answer should be in quote.')
324         self.assertIn('expandable', result, 'Expandable should be present.')
325         self.assertNotIn('First answer, should not be displayed', result, 'Old answer should not be in quote.')
326         self.assertNotIn('My answer I am propagating', result, 'Thread header content should be in quote.')
327
328     def test_21_message_post(self):
329         """ Tests designed for message_post. """
330         cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
331         self.res_users.write(cr, uid, [uid], {'signature': 'Admin', 'email': 'a@a'})
332         # 1 - Bert Tartopoils, with email, should receive emails for comments and emails
333         p_b_id = self.res_partner.create(cr, uid, {'name': 'Bert Tartopoils', 'email': 'b@b'})
334         # 2 - Carine Poilvache, with email, should never receive emails
335         p_c_id = self.res_partner.create(cr, uid, {'name': 'Carine Poilvache', 'email': 'c@c', 'notification_email_send': 'email'})
336         # 3 - Dédé Grosbedon, without email, to test email verification; should receive emails for every message
337         p_d_id = self.res_partner.create(cr, uid, {'name': 'Dédé Grosbedon', 'notification_email_send': 'all'})
338
339         # Subscribe #1, #2
340         group_pigs.message_subscribe([p_b_id, p_c_id])
341
342         # Mail data
343         _subject = 'Pigs'
344         _mail_subject = '%s posted on %s' % (user_admin.name, group_pigs.name)
345         _body1 = 'Pigs rules'
346         _mail_body1 = 'Pigs rules\n<pre>Admin</pre>\n'
347         _mail_bodyalt1 = 'Pigs rules\nAdmin'
348         _body2 = '<html>Pigs rules</html>'
349         _mail_body2 = html_sanitize('<html>Pigs rules\n<pre>Admin</pre>\n</html>')
350         _mail_bodyalt2 = 'Pigs rules\nAdmin'
351         _attachments = [('First', 'My first attachment'), ('Second', 'My second attachment')]
352
353         # ----------------------------------------
354         # CASE1: post comment, body and subject specified
355         # ----------------------------------------
356
357         # 1. Post a new comment on Pigs
358         self._init_mock_build_email()
359         msg_id = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body1, subject=_subject, type='comment', subtype='mt_comment')
360         message = self.mail_message.browse(cr, uid, msg_id)
361         sent_emails = self._build_email_kwargs_list
362         # Test: mail.mail notifications have been deleted
363         self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', msg_id)]), 'mail.mail notifications should have been auto-deleted!')
364         # Test: mail_message: subject is _subject, body is _body1 (no formatting done)
365         self.assertEqual(message.subject, _subject, 'mail.message subject incorrect')
366         self.assertEqual(message.body, _body1, 'mail.message body incorrect')
367         # Test: sent_email: email send by server: correct subject, body, body_alternative
368         for sent_email in sent_emails:
369             self.assertEqual(sent_email['subject'], _subject, 'sent_email subject incorrect')
370             self.assertEqual(sent_email['body'], _mail_body1 + '\n<pre>Bert Tartopoils</pre>\n', 'sent_email body incorrect')
371             # the html2plaintext uses etree or beautiful soup, so the result may be slighly different
372             # depending if you have installed beautiful soup.
373             self.assertIn(sent_email['body_alternative'], _mail_bodyalt1 + '\nBert Tartopoils\n', 'sent_email body_alternative is incorrect')
374         # Test: mail_message: notified_partner_ids = group followers
375         message_pids = set([partner.id for partner in message.notified_partner_ids])
376         test_pids = set([p_b_id, p_c_id])
377         self.assertEqual(test_pids, message_pids, 'mail.message partners incorrect')
378         # Test: notification linked to this message = group followers = notified_partner_ids
379         notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', message.id)])
380         notif_pids = set([notif.partner_id.id for notif in self.mail_notification.browse(cr, uid, notif_ids)])
381         self.assertEqual(notif_pids, test_pids, 'mail.message notification partners incorrect')
382         # Test: sent_email: email_to should contain b@b, not c@c (pref email), not a@a (writer)
383         for sent_email in sent_emails:
384             self.assertEqual(sent_email['email_to'], ['b@b'], 'sent_email email_to is incorrect')
385
386         # ----------------------------------------
387         # CASE2: post an email with attachments, parent_id, partner_ids
388         # ----------------------------------------
389
390         # 1. Post a new email comment on Pigs
391         self._init_mock_build_email()
392         msg_id2 = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body2, type='email', subtype='mt_comment',
393             partner_ids=[(6, 0, [p_d_id])], parent_id=msg_id, attachments=_attachments)
394         message = self.mail_message.browse(cr, uid, msg_id2)
395         sent_emails = self._build_email_kwargs_list
396         self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', msg_id2)]), 'mail.mail notifications should have been auto-deleted!')
397         # Test: mail_message: subject is False, body is _body2 (no formatting done), parent_id is msg_id
398         self.assertEqual(message.subject, False, 'mail.message subject incorrect')
399         self.assertEqual(message.body, html_sanitize(_body2), 'mail.message body incorrect')
400         self.assertEqual(message.parent_id.id, msg_id, 'mail.message parent_id incorrect')
401         # Test: sent_email: email send by server: correct automatic subject, body, body_alternative
402         self.assertEqual(len(sent_emails), 2, 'sent_email number of sent emails incorrect')
403         for sent_email in sent_emails:
404             self.assertEqual(sent_email['subject'], _mail_subject, 'sent_email subject incorrect')
405             self.assertIn(_mail_body2, sent_email['body'], 'sent_email body incorrect')
406             self.assertIn(_mail_bodyalt2, sent_email['body_alternative'], 'sent_email body_alternative incorrect')
407         # Test: mail_message: notified_partner_ids = group followers
408         message_pids = set([partner.id for partner in message.notified_partner_ids])
409         test_pids = set([p_b_id, p_c_id, p_d_id])
410         self.assertEqual(message_pids, test_pids, 'mail.message partners incorrect')
411         # Test: notifications linked to this message = group followers = notified_partner_ids
412         notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', message.id)])
413         notif_pids = set([notif.partner_id.id for notif in self.mail_notification.browse(cr, uid, notif_ids)])
414         self.assertEqual(notif_pids, test_pids, 'mail.message notification partners incorrect')
415         # Test: sent_email: email_to should contain b@b, c@c, not a@a (writer)
416         for sent_email in sent_emails:
417             self.assertTrue(set(sent_email['email_to']).issubset(set(['b@b', 'c@c'])), 'sent_email email_to incorrect')
418         # Test: attachments
419         for attach in message.attachment_ids:
420             self.assertEqual(attach.res_model, 'mail.group', 'mail.message attachment res_model incorrect')
421             self.assertEqual(attach.res_id, self.group_pigs_id, 'mail.message attachment res_id incorrect')
422             self.assertIn((attach.name, attach.datas.decode('base64')), _attachments,
423                 'mail.message attachment name / data incorrect')
424
425         # 3. Reply to the last message, check that its parent will be the first message
426         msg_id3 = self.mail_group.message_post(cr, uid, self.group_pigs_id, body='Test', parent_id=msg_id2)
427         message = self.mail_message.browse(cr, uid, msg_id3)
428         self.assertEqual(message.parent_id.id, msg_id, 'message_post did not flatten the thread structure')
429
430     def test_25_message_compose_wizard(self):
431         """ Tests designed for the mail.compose.message wizard. """
432         cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
433         mail_compose = self.registry('mail.compose.message')
434         self.res_users.write(cr, uid, [uid], {'signature': 'Admin', 'email': 'a@a'})
435         group_bird_id = self.mail_group.create(cr, uid, {'name': 'Bird', 'description': 'Bird resistance'})
436         group_bird = self.mail_group.browse(cr, uid, group_bird_id)
437
438         # Mail data
439         _subject = 'Pigs'
440         _body_text = 'Pigs rules'
441         _msg_reply = 'Re: Pigs'
442         _msg_body = '<pre>Pigs rules</pre>'
443         _attachments = [
444             {'name': 'First', 'datas_fname': 'first.txt', 'datas': 'My first attachment'.encode('base64')},
445             {'name': 'Second', 'datas_fname': 'second.txt', 'datas': 'My second attachment'.encode('base64')}
446             ]
447         _attachments_test = [('first.txt', 'My first attachment'), ('second.txt', 'My second attachment')]
448
449         # 1 - Bert Tartopoils, with email, should receive emails for comments and emails
450         p_b_id = self.res_partner.create(cr, uid, {'name': 'Bert Tartopoils', 'email': 'b@b'})
451         # 2 - Carine Poilvache, with email, should never receive emails
452         p_c_id = self.res_partner.create(cr, uid, {'name': 'Carine Poilvache', 'email': 'c@c', 'notification_email_send': 'email'})
453         # 3 - Dédé Grosbedon, without email, to test email verification; should receive emails for every message
454         p_d_id = self.res_partner.create(cr, uid, {'name': 'Dédé Grosbedon', 'notification_email_send': 'all'})
455
456         # Subscribe #1
457         group_pigs.message_subscribe([p_b_id])
458
459         # ----------------------------------------
460         # CASE1: comment on group_pigs
461         # ----------------------------------------
462
463         # 1. Comment group_pigs with body_text and subject
464         compose_id = mail_compose.create(cr, uid,
465             {'subject': _subject, 'body_text': _body_text, 'partner_ids': [(4, p_c_id), (4, p_d_id)]},
466             {'default_composition_mode': 'comment', 'default_model': 'mail.group', 'default_res_id': self.group_pigs_id})
467         compose = mail_compose.browse(cr, uid, compose_id)
468         # Test: mail.compose.message: composition_mode, model, res_id
469         self.assertEqual(compose.composition_mode,  'comment', 'mail.compose.message incorrect composition_mode')
470         self.assertEqual(compose.model,  'mail.group', 'mail.compose.message incorrect model')
471         self.assertEqual(compose.res_id, self.group_pigs_id, 'mail.compose.message incorrect res_id')
472
473         # 2. Post the comment, get created message
474         mail_compose.send_mail(cr, uid, [compose_id])
475         group_pigs.refresh()
476         message = group_pigs.message_ids[0]
477         # Test: mail.message: subject, body inside pre
478         self.assertEqual(message.subject,  False, 'mail.message incorrect subject')
479         self.assertEqual(message.body, _msg_body, 'mail.message incorrect body')
480         # Test: mail.message: notified_partner_ids = entries in mail.notification: group_pigs fans (a, b) + mail.compose.message partner_ids (c, d)
481         msg_pids = [partner.id for partner in message.notified_partner_ids]
482         test_pids = [p_b_id, p_c_id, p_d_id]
483         notif_ids = self.mail_notification.search(cr, uid, [('message_id', '=', message.id)])
484         self.assertEqual(len(notif_ids), 3, 'mail.message: too much notifications created')
485         self.assertEqual(set(msg_pids), set(test_pids), 'mail.message notified_partner_ids incorrect')
486
487         # ----------------------------------------
488         # CASE2: reply to last comment with attachments
489         # ----------------------------------------
490
491         # 1. Update last comment subject, reply with attachments
492         message.write({'subject': _subject})
493         compose_id = mail_compose.create(cr, uid,
494             {'attachment_ids': [(0, 0, _attachments[0]), (0, 0, _attachments[1])]},
495             {'default_composition_mode': 'reply', 'default_model': 'mail.thread', 'default_res_id': self.group_pigs_id, 'default_parent_id': message.id})
496         compose = mail_compose.browse(cr, uid, compose_id)
497         # Test: model, res_id, parent_id, content_subtype
498         self.assertEqual(compose.model,  'mail.group', 'mail.compose.message incorrect model')
499         self.assertEqual(compose.res_id, self.group_pigs_id, 'mail.compose.message incorrect res_id')
500         self.assertEqual(compose.parent_id.id, message.id, 'mail.compose.message incorrect parent_id')
501         self.assertEqual(compose.content_subtype, 'html', 'mail.compose.message incorrect content_subtype')
502         # Test: mail.message: subject as Re:.., body in html, parent_id
503         self.assertEqual(compose.subject, _msg_reply, 'mail.message incorrect subject')
504         # self.assertIn('Administrator wrote:<blockquote><pre>Pigs rules</pre></blockquote>', compose.body, 'mail.message body is incorrect')
505         self.assertEqual(compose.parent_id and compose.parent_id.id, message.id, 'mail.message parent_id incorrect')
506         # Test: mail.message: attachments
507         for attach in compose.attachment_ids:
508             self.assertEqual(attach.res_model, 'mail.group', 'mail.message attachment res_model incorrect')
509             self.assertEqual(attach.res_id, self.group_pigs_id, 'mail.message attachment res_id incorrect')
510             self.assertIn((attach.datas_fname, attach.datas.decode('base64')), _attachments_test, 'mail.message attachment name / data incorrect')
511
512         # ----------------------------------------
513         # CASE3: mass_mail on Pigs and Bird
514         # ----------------------------------------
515
516         # 1. mass_mail on pigs and bird
517         compose_id = mail_compose.create(cr, uid,
518             {'subject': _subject, 'body': '${object.description}'},
519             {'default_composition_mode': 'mass_mail', 'default_model': 'mail.group', 'default_res_id': False,
520                 'active_ids': [self.group_pigs_id, group_bird_id]})
521         compose = mail_compose.browse(cr, uid, compose_id)
522         # Test: content_subtype is html
523         self.assertEqual(compose.content_subtype, 'html', 'mail.compose.message content_subtype incorrect')
524
525         # 2. Post the comment, get created message for each group
526         mail_compose.send_mail(cr, uid, [compose_id],
527             context={'default_res_id': -1, 'active_ids': [self.group_pigs_id, group_bird_id]})
528         group_pigs.refresh()
529         group_bird.refresh()
530         message1 = group_pigs.message_ids[0]
531         message2 = group_bird.message_ids[0]
532         # Test: Pigs and Bird did receive their message
533         test_msg_ids = self.mail_message.search(cr, uid, [], limit=2)
534         self.assertIn(message1.id, test_msg_ids, 'Pigs did not receive its mass mailing message')
535         self.assertIn(message2.id, test_msg_ids, 'Bird did not receive its mass mailing message')
536         # Test: mail.message: subject, body
537         self.assertEqual(message1.subject, _subject, 'mail.message subject incorrect')
538         self.assertEqual(message1.body, group_pigs.description, 'mail.message body incorrect')
539         self.assertEqual(message2.subject, _subject, 'mail.message subject incorrect')
540         self.assertEqual(message2.body, group_bird.description, 'mail.message body incorrect')
541
542     def test_30_message_read(self):
543         """ Tests for message_read and expandables. """
544         cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
545         pigs_domain = [('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)]
546
547         # Data: create a discussion in Pigs (2 messages, one with 2 and one with 3 answers)
548         msg_id0 = self.group_pigs.message_post(body='0', subtype='mt_comment')
549         msg_id1 = self.group_pigs.message_post(body='1', subtype='mt_comment')
550         msg_id2 = self.group_pigs.message_post(body='2', subtype='mt_comment')
551         msg_id3 = self.group_pigs.message_post(body='1-1', subtype='mt_comment', parent_id=msg_id1)
552         msg_id4 = self.group_pigs.message_post(body='2-1', subtype='mt_comment', parent_id=msg_id2)
553         msg_id5 = self.group_pigs.message_post(body='1-2', subtype='mt_comment', parent_id=msg_id1)
554         msg_id6 = self.group_pigs.message_post(body='2-2', subtype='mt_comment', parent_id=msg_id2)
555         msg_id7 = self.group_pigs.message_post(body='1-1-1', subtype='mt_comment', parent_id=msg_id3)
556         msg_id8 = self.group_pigs.message_post(body='2-1-1', subtype='mt_comment', parent_id=msg_id4)
557         msg_id9 = self.group_pigs.message_post(body='1-1-1', subtype='mt_comment', parent_id=msg_id3)
558         msg_id10 = self.group_pigs.message_post(body='2-1-1', subtype='mt_comment', parent_id=msg_id4)
559         msg_ids = [msg_id0, msg_id1, msg_id2, msg_id3, msg_id4, msg_id5, msg_id6, msg_id7, msg_id8, msg_id9, msg_id10]
560
561         # Test: read some specific ids
562         read_msg_list = self.mail_message.message_read(cr, uid, ids=msg_ids[2:4], domain=[('body', 'like', 'dummy')])
563         read_msg_ids = [msg.get('id') for msg in read_msg_list]
564         self.assertEqual(msg_ids[2:4], read_msg_ids, 'message_read with direct ids should read only the requested ids')
565
566         # Test: read messages of Pigs through a domain, being thread or not threaded
567         read_msg_list = self.mail_message.message_read(cr, uid, domain=pigs_domain, limit=200)
568         read_msg_ids = [msg.get('id') for msg in read_msg_list]
569         self.assertEqual(msg_ids, read_msg_ids, 'message_read flat with domain on Pigs should equal all messages of Pigs')
570         read_msg_list = self.mail_message.message_read(cr, uid, domain=pigs_domain, limit=200, thread_level=1)
571         read_msg_ids = [msg.get('id') for msg in read_msg_list]
572         self.assertEqual(msg_ids, read_msg_ids, 'message_read threaded with domain on Pigs should equal all messages of Pigs')
573
574         # ----------------------------------------
575         # CASE1: message_read with domain, threaded
576         # We simulate an entire flow, using the expandables to test them
577         # ----------------------------------------
578
579         # Do: read last message, threaded
580         read_msg_list = self.mail_message.message_read(cr, uid, domain=pigs_domain, limit=1, thread_level=1)
581         read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('type') != 'expandable']
582         # Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is set, 2 expandables
583         self.assertEqual(len(read_msg_list), 4, 'message_read on last Pigs message should return 2 messages and 2 expandables')
584         self.assertEqual(set([msg_id2, msg_id10]), set(read_msg_ids), 'message_read on the last Pigs message should also get its parent')
585         self.assertEqual(read_msg_list[1].get('parent_id'), read_msg_list[0].get('id'), 'message_read should set the ancestor to the thread header')
586         # Data: get expandables
587         new_threads_exp, new_msg_exp = None, None
588         for msg in read_msg_list:
589             if msg.get('type') == 'expandable' and msg.get('nb_messages') == -1 and msg.get('id') == -1:
590                 new_threads_exp = msg
591             elif msg.get('type') == 'expandable':
592                 new_msg_exp = msg
593
594         # Do: fetch new messages in first thread, domain from expandable
595         self.assertIsNotNone(new_msg_exp, 'message_read on last Pigs message should have returned a new messages expandable')
596         domain = new_msg_exp.get('domain', [])
597         # Test: expandable, conditions in domain
598         self.assertIn(('id', 'child_of', msg_id2), domain, 'new messages expandable domain should contain a child_of condition')
599         self.assertIn(('id', '>=', msg_id4), domain, 'new messages expandable domain should contain an id greater than condition')
600         self.assertIn(('id', '<=', msg_id8), domain, 'new messages expandable domain should contain an id less than condition')
601         self.assertEqual(new_msg_exp.get('parent_id'), msg_id2, 'new messages expandable should have ancestor_id set to the thread header')
602         # Do: message_read with domain, thread_level=0, parent_id=msg_id2 (should be imposed by JS)
603         read_msg_list = self.mail_message.message_read(cr, uid, domain=domain, limit=200, thread_level=0, parent_id=msg_id2)
604         read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('type') != 'expandable']
605         # Test: other message in thread have been fetch
606         self.assertEqual(set([msg_id4, msg_id6, msg_id8]), set(read_msg_ids), 'message_read in Pigs thread should return all the previous messages')
607
608         # Do: fetch a new thread, domain from expandable
609         self.assertIsNotNone(new_threads_exp, 'message_read on last Pigs message should have returned a new threads expandable')
610         domain = new_threads_exp.get('domain', [])
611         # Test: expandable, conditions in domain
612         for condition in pigs_domain:
613             self.assertIn(condition, domain, 'new threads expandable domain should contain the message_read domain parameter')
614         self.assertFalse(new_threads_exp.get('parent_id'), 'new threads expandable should not have an ancestor_id')
615         # Do: message_read with domain, thread_level=1 (should be imposed by JS)
616         read_msg_list = self.mail_message.message_read(cr, uid, domain=domain, limit=1, thread_level=1)
617         read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('type') != 'expandable']
618         # Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is set, 2 expandables
619         self.assertEqual(len(read_msg_list), 4, 'message_read on Pigs should return 2 messages and 2 expandables')
620         self.assertEqual(set([msg_id1, msg_id9]), set(read_msg_ids), 'message_read on a Pigs message should also get its parent')
621         self.assertEqual(read_msg_list[1].get('parent_id'), read_msg_list[0].get('id'), 'message_read should set the ancestor to the thread header')
622         # Data: get expandables
623         new_threads_exp, new_msg_exp = None, None
624         for msg in read_msg_list:
625             if msg.get('type') == 'expandable' and msg.get('nb_messages') == -1 and msg.get('id') == -1:
626                 new_threads_exp = msg
627             elif msg.get('type') == 'expandable':
628                 new_msg_exp = msg
629
630         # Do: fetch new messages in second thread, domain from expandable
631         self.assertIsNotNone(new_msg_exp, 'message_read on Pigs message should have returned a new messages expandable')
632         domain = new_msg_exp.get('domain', [])
633         # Test: expandable, conditions in domain
634         self.assertIn(('id', 'child_of', msg_id1), domain, 'new messages expandable domain should contain a child_of condition')
635         self.assertIn(('id', '>=', msg_id3), domain, 'new messages expandable domain should contain an id greater than condition')
636         self.assertIn(('id', '<=', msg_id7), domain, 'new messages expandable domain should contain an id less than condition')
637         self.assertEqual(new_msg_exp.get('parent_id'), msg_id1, 'new messages expandable should have ancestor_id set to the thread header')
638         # Do: message_read with domain, thread_level=0, parent_id=msg_id1 (should be imposed by JS)
639         read_msg_list = self.mail_message.message_read(cr, uid, domain=domain, limit=200, thread_level=0, parent_id=msg_id1)
640         read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('type') != 'expandable']
641         # Test: other message in thread have been fetch
642         self.assertEqual(set([msg_id3, msg_id5, msg_id7]), set(read_msg_ids), 'message_read on the last Pigs message should also get its parent')
643
644         # Test: fetch a new thread, domain from expandable
645         self.assertIsNotNone(new_threads_exp, 'message_read should have returned a new threads expandable')
646         domain = new_threads_exp.get('domain', [])
647         # Test: expandable, conditions in domain
648         for condition in pigs_domain:
649             self.assertIn(condition, domain, 'general expandable domain should contain the message_read domain parameter')
650         # Do: message_read with domain, thread_level=1 (should be imposed by JS)
651         read_msg_list = self.mail_message.message_read(cr, uid, domain=domain, limit=1, thread_level=1)
652         read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('type') != 'expandable']
653         # Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is set, 2 expandables
654         self.assertEqual(len(read_msg_list), 1, 'message_read on Pigs should return 1 message because everything else has been fetched')
655         self.assertEqual([msg_id0], read_msg_ids, 'message_read after 2 More should return only 1 last message')
656
657         # ----------------------------------------
658         # CASE2: message_read with domain, flat
659         # ----------------------------------------
660
661         # Do: read 2 lasts message, flat
662         read_msg_list = self.mail_message.message_read(cr, uid, domain=pigs_domain, limit=2, thread_level=0)
663         read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('type') != 'expandable']
664         # Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is not set, 1 expandable
665         self.assertEqual(len(read_msg_list), 3, 'message_read on last Pigs message should return 2 messages and 1 expandable')
666         self.assertEqual(set([msg_id9, msg_id10]), set(read_msg_ids), 'message_read flat on Pigs last messages should only return those messages')
667         self.assertFalse(read_msg_list[0].get('parent_id'), 'message_read flat should set the ancestor as False')
668         self.assertFalse(read_msg_list[1].get('parent_id'), 'message_read flat should set the ancestor as False')
669         # Data: get expandables
670         new_threads_exp, new_msg_exp = None, None
671         for msg in read_msg_list:
672             if msg.get('type') == 'expandable' and msg.get('nb_messages') == -1 and msg.get('id') == -1:
673                 new_threads_exp = msg
674
675         # Do: fetch new messages, domain from expandable
676         self.assertIsNotNone(new_threads_exp, 'message_read flat on the 2 last Pigs messages should have returns a new threads expandable')
677         domain = new_threads_exp.get('domain', [])
678         # Test: expandable, conditions in domain
679         for condition in pigs_domain:
680             self.assertIn(condition, domain, 'new threads expandable domain should contain the message_read domain parameter')
681         # Do: message_read with domain, thread_level=0 (should be imposed by JS)
682         read_msg_list = self.mail_message.message_read(cr, uid, domain=domain, limit=20, thread_level=0)
683         read_msg_ids = [msg.get('id') for msg in read_msg_list if msg.get('type') != 'expandable']
684         # Test: structure content, ancestor is added to the read messages, ordered by id, ancestor is set, 2 expandables
685         self.assertEqual(len(read_msg_list), 9, 'message_read on Pigs should return 9 messages and 0 expandable')
686         self.assertEqual([msg_id0, msg_id1, msg_id2, msg_id3, msg_id4, msg_id5, msg_id6, msg_id7, msg_id8], read_msg_ids,
687             'message_read, More on flat, should return all remaning messages')
688
689     def test_40_needaction(self):
690         """ Tests for mail.message needaction. """
691         cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
692         user_raoul = self.res_users.browse(cr, uid, self.user_raoul_id)
693         group_pigs_demo = self.mail_group.browse(cr, self.user_raoul_id, self.group_pigs_id)
694         na_admin_base = self.mail_message._needaction_count(cr, uid, domain=[])
695         na_demo_base = self.mail_message._needaction_count(cr, user_raoul.id, domain=[])
696
697         # Test: number of unread notification = needaction on mail.message
698         notif_ids = self.mail_notification.search(cr, uid, [
699             ('partner_id', '=', user_admin.partner_id.id),
700             ('read', '=', False)
701             ])
702         na_count = self.mail_message._needaction_count(cr, uid, domain=[])
703         self.assertEqual(len(notif_ids), na_count, 'unread notifications count does not match needaction count')
704
705         # Do: post 2 message on group_pigs as admin, 3 messages as demo user
706         for dummy in range(2):
707             group_pigs.message_post(body='My Body', subtype='mt_comment')
708         for dummy in range(3):
709             group_pigs_demo.message_post(body='My Demo Body', subtype='mt_comment')
710
711         # Test: admin has 3 new notifications (from demo), and 3 new needaction
712         notif_ids = self.mail_notification.search(cr, uid, [
713             ('partner_id', '=', user_admin.partner_id.id),
714             ('read', '=', False)
715             ])
716         self.assertEqual(len(notif_ids), na_admin_base + 3, 'Admin should have 3 new unread notifications')
717         na_admin = self.mail_message._needaction_count(cr, uid, domain=[])
718         na_admin_group = self.mail_message._needaction_count(cr, uid, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)])
719         self.assertEqual(na_admin, na_admin_base + 3, 'Admin should have 3 new needaction')
720         self.assertEqual(na_admin_group, 3, 'Admin should have 3 needaction related to Pigs')
721         # Test: demo has 0 new notifications (not a follower, not receiving its own messages), and 0 new needaction
722         notif_ids = self.mail_notification.search(cr, uid, [
723             ('partner_id', '=', user_raoul.partner_id.id),
724             ('read', '=', False)
725             ])
726         self.assertEqual(len(notif_ids), na_demo_base + 0, 'Demo should have 0 new unread notifications')
727         na_demo = self.mail_message._needaction_count(cr, user_raoul.id, domain=[])
728         na_demo_group = self.mail_message._needaction_count(cr, user_raoul.id, domain=[('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)])
729         self.assertEqual(na_demo, na_demo_base + 0, 'Demo should have 0 new needaction')
730         self.assertEqual(na_demo_group, 0, 'Demo should have 0 needaction related to Pigs')
731
732     def test_50_thread_parent_resolution(self):
733         """Verify parent/child relationships are correctly established when processing incoming mails"""
734         cr, uid = self.cr, self.uid
735         group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
736         msg1 = group_pigs.message_post(body='My Body', subject='1')
737         msg2 = group_pigs.message_post(body='My Body', subject='2')
738         msg1, msg2 = self.mail_message.browse(cr, uid, [msg1, msg2])
739         self.assertTrue(msg1.message_id, "New message should have a proper message_id")
740
741         # Reply to msg1, make sure the reply is properly attached using the various reply identification mechanisms
742         # 1. In-Reply-To header
743         reply_msg = MAIL_TEMPLATE.format(to='Pretty Pigs <group+pigs@example.com>, other@gmail.com', subject='Re: 1',
744                                          extra='In-Reply-To: %s' % msg1.message_id)
745         self.mail_group.message_process(cr, uid, None, reply_msg)
746
747         # 2. References header
748         reply_msg2 = MAIL_TEMPLATE.format(to='Pretty Pigs <group+pigs@example.com>, other@gmail.com', subject='Re: Re: 1',
749                                          extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % msg1.message_id)
750         self.mail_group.message_process(cr, uid, None, reply_msg2)
751
752         # 3. Subject contains [<ID>] + model passed to message+process -> only attached to group, not to mail
753         reply_msg3 = MAIL_TEMPLATE.format(to='Pretty Pigs <group+pigs@example.com>, other@gmail.com',
754                                           extra='', subject='Re: [%s] 1' % self.group_pigs_id)
755         self.mail_group.message_process(cr, uid, 'mail.group', reply_msg3)
756
757         group_pigs.refresh()
758         msg1.refresh()
759         self.assertEqual(5, len(group_pigs.message_ids), 'group should contain 5 messages')
760         # TDE note: python test + debug because of the random error we see with the next assert
761         if len(msg1.child_ids) != 2:
762             msg_ids = self.mail_message.search(cr, uid, [('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)], limit=10)
763             for new_msg in self.mail_message.browse(cr, uid, msg_ids):
764                 print new_msg.subject, '(id', new_msg.id, ')', 'parent_id:', new_msg.parent_id
765                 print '\tchild_ids', [child.id for child in new_msg.child_ids]
766         self.assertEqual(2, len(msg1.child_ids), 'msg1 should have 2 children now')
767
768     def test_60_message_vote(self):
769         """ Test designed for the vote/unvote feature. """
770         cr, uid, user_admin, user_raoul, group_pigs = self.cr, self.uid, self.user_admin, self.user_raoul, self.group_pigs
771         # Data: post a message on Pigs
772         msg_id = group_pigs.message_post(body='My Body', subject='1')
773         msg = self.mail_message.browse(cr, uid, msg_id)
774
775         # Do: Admin vote for msg
776         self.mail_message.vote_toggle(cr, uid, [msg.id])
777         msg.refresh()
778         # Test: msg has Admin as voter
779         self.assertEqual(set(msg.vote_user_ids), set([user_admin]), 'mail_message vote: after voting, Admin should be in the voter')
780         # Do: Bert vote for msg
781         self.mail_message.vote_toggle(cr, user_raoul.id, [msg.id])
782         msg.refresh()
783         # Test: msg has Admin and Bert as voters
784         self.assertEqual(set(msg.vote_user_ids), set([user_admin, user_raoul]), 'mail_message vote: after voting, Admin and Bert should be in the voters')
785         # Do: Admin unvote for msg
786         self.mail_message.vote_toggle(cr, uid, [msg.id])
787         msg.refresh()
788         # Test: msg has Bert as voter
789         self.assertEqual(set(msg.vote_user_ids), set([user_raoul]), 'mail_message vote: after unvoting, Bert should be in the voter')
790
791     def test_70_message_favorite(self):
792         """ Tests for favorites. """
793         cr, uid, user_admin, user_raoul, group_pigs = self.cr, self.uid, self.user_admin, self.user_raoul, self.group_pigs
794         # Data: post a message on Pigs
795         msg_id = group_pigs.message_post(body='My Body', subject='1')
796         msg = self.mail_message.browse(cr, uid, msg_id)
797
798         # Do: Admin stars msg
799         self.mail_message.favorite_toggle(cr, uid, [msg.id])
800         msg.refresh()
801         # Test: msg starred by Admin
802         self.assertEqual(set(msg.favorite_user_ids), set([user_admin]), 'mail_message favorite: after starring, Admin should be in favorite_user_ids')
803         # Do: Bert stars msg
804         self.mail_message.favorite_toggle(cr, user_raoul.id, [msg.id])
805         msg.refresh()
806         # Test: msg starred by Admin and Raoul
807         self.assertEqual(set(msg.favorite_user_ids), set([user_admin, user_raoul]), 'mail_message favorite: after starring, Admin and Raoul should be in favorite_user_ids')
808         # Do: Admin unvote for msg
809         self.mail_message.favorite_toggle(cr, uid, [msg.id])
810         msg.refresh()
811         # Test: msg starred by Raoul
812         self.assertEqual(set(msg.favorite_user_ids), set([user_raoul]), 'mail_message favorite: after unstarring, Raoul should be in favorite_user_ids')