Skip to content

Commit f417d6b

Browse files
committed
[TST] fetchmail_from_imap_folder: more tests
1 parent 322b16e commit f417d6b

4 files changed

Lines changed: 174 additions & 23 deletions

File tree

fetchmail_from_imap_folder/match_algorithm/email_exact.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def _get_mailaddress_search_domain(
1919
self, folder, message_dict, operator="=", values=None
2020
):
2121
mailaddresses = values or self._get_mailaddresses(folder, message_dict)
22-
if not mailaddresses:
22+
if not mailaddresses: # pragma: no cover
2323
return [(0, "=", 1)]
2424
search_domain = (
2525
(["|"] * (len(mailaddresses) - 1))

fetchmail_from_imap_folder/models/fetchmail_server.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ def parse_list_response(line):
7070
object_id = fields.Many2one(required=False) # comodel_name='ir.model'
7171
server_type = fields.Selection(default="imap")
7272

73+
def write(self, vals):
74+
"""Should reset to draft if server type changes and state not specified."""
75+
if "state" not in vals and (
76+
"server_type" in vals or "is_ssl" in vals or "object_id" in vals
77+
):
78+
vals["state"] = "draft"
79+
return super().write(vals)
80+
7381
@api.onchange("server_type", "is_ssl", "object_id")
7482
def onchange_server_type(self):
7583
result = super().onchange_server_type()
@@ -79,7 +87,7 @@ def onchange_server_type(self):
7987
def fetch_mail(self, **kwargs):
8088
result = True
8189
for this in self:
82-
if not this.folders_only:
90+
if not this.folders_only: # pragma: no cover
8391
result = result and super(FetchmailServer, this).fetch_mail(**kwargs)
8492
this.folder_ids.fetch_mail()
8593
return result

fetchmail_from_imap_folder/models/fetchmail_server_folder.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def fetch_mail(self):
175175
this.check_imap_archive_folder(connection)
176176
this.retrieve_imap_folder(connection)
177177
connection.close()
178-
except Exception:
178+
except Exception: # pragma: no cover
179179
_logger.error(
180180
(
181181
"General failure when trying to connect to"
@@ -228,7 +228,7 @@ def retrieve_imap_folder(self, connection):
228228
self.env.cr.execute("savepoint apply_matching")
229229
self.apply_matching(connection, message_uid)
230230
self.env.cr.execute("release savepoint apply_matching")
231-
except Exception:
231+
except Exception: # pragma: no cover
232232
self.env.cr.execute("rollback to savepoint apply_matching")
233233
_logger.exception(
234234
"Failed to fetch mail %(message_uid)s from server %(server)s",
@@ -260,9 +260,11 @@ def get_message_uids(self, connection, criteria):
260260
if result != "OK":
261261
raise UserError(
262262
self.env._(
263-
"Could not search folder %(folder)s on server %(server)s",
263+
"Could not search folder %(folder)s on server %(server)s"
264+
" with criteria %(criteria)s",
264265
folder=self.path,
265266
server=server.name,
267+
criteria=criteria,
266268
)
267269
)
268270
_logger.info(
@@ -305,7 +307,8 @@ def run_server_action(self, matched_object_ids):
305307
return
306308
records = self.env[self.model_id.model].browse(matched_object_ids)
307309
for record in records:
308-
if not record.exists():
310+
if not record.exists(): # pragma: no cover
311+
# We have encountered cases where record not existed IRL.
309312
continue
310313
action.with_context(
311314
**{
@@ -396,8 +399,6 @@ def _find_match(self, message_dict):
396399
"""Try to find existing object to link mail to."""
397400
self.ensure_one()
398401
matcher = self._get_algorithm()
399-
if not matcher:
400-
return None
401402
matches = matcher.search_matches(self, message_dict)
402403
if not matches:
403404
_logger.info(
@@ -435,12 +436,7 @@ def _get_algorithm(self):
435436
self.ensure_one()
436437
if self.match_algorithm == "email_domain":
437438
return match_algorithm.email_domain.EmailDomain()
438-
if self.match_algorithm == "email_exact":
439-
return match_algorithm.email_exact.EmailExact()
440-
_logger.error(
441-
"Unknown algorithm %(algorithm)s", {"algorithm": self.match_algorithm}
442-
)
443-
return None
439+
return match_algorithm.email_exact.EmailExact()
444440

445441
@api.model
446442
def attach_mail(self, match_object, message_dict):

fetchmail_from_imap_folder/tests/test_match_algorithms.py

Lines changed: 156 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
from unittest.mock import patch
66

77
from odoo import fields
8+
from odoo.exceptions import UserError, ValidationError
89
from odoo.fields import Command
910
from odoo.tests.common import TransactionCase
11+
from odoo.tools.mail import email_normalize
1012

1113
from ..match_algorithm import email_domain
1214
from .common import get_message_body
@@ -17,15 +19,30 @@
1719

1820

1921
class MockConnection:
22+
_msg_store = {}
23+
2024
def select(self, path=None):
21-
return ("OK",)
25+
if path in ("INBOX", "customers", "archived_messages"):
26+
return ("OK",)
27+
return ("NO",)
2228

2329
def create(self, path):
2430
"""Mock creating a folder."""
2531
return ("OK",)
2632

2733
def store(self, message_uid, msg_item, value):
28-
"""Mock store command."""
34+
"""Mock store command.
35+
36+
Actually simplified. We only store te latest flag set (or unset), in the
37+
real IMAP folder there can be multiple flags on a message.
38+
"""
39+
if message_uid not in self._msg_store:
40+
self._msg_store[message_uid] = {}
41+
msg_key = msg_item[1:] # Remove + or -
42+
if msg_item == "-FLAGS": # This is to remove flags
43+
self._msg_store[message_uid][msg_key] = ""
44+
else:
45+
self._msg_store[message_uid][msg_key] = value
2946
return "OK"
3047

3148
def copy(self, message_uid, folder_path):
@@ -38,6 +55,8 @@ def fetch(self, message_uid, parts):
3855
By passing special values in message_uid, we can manipulate
3956
the body returned.
4057
"""
58+
if message_uid == "deleted_uid":
59+
return ("BAD", "")
4160
email = (
4261
"the_smart_red_one@reynaerde.waesland"
4362
if message_uid == "test no match"
@@ -47,11 +66,16 @@ def fetch(self, message_uid, parts):
4766

4867
def search(self, charset, criteria):
4968
"""Return some message uid's."""
69+
if criteria == "test_invalid":
70+
return ("BAD", [])
5071
return ("OK", ["123 456"])
5172

5273
def close(self):
5374
pass
5475

76+
def logout(self):
77+
pass
78+
5579
def expunge(self):
5680
"""Mock an IMAP4.expunge action"""
5781
return ("OK", None)
@@ -77,18 +101,19 @@ def setUpClass(cls):
77101
"category_id": [Command.clear()],
78102
}
79103
)
80-
cls.server_model = cls.env["fetchmail.server"]
81-
cls.folder_model = cls.env["fetchmail.server.folder"]
82-
cls.server = cls.server_model.create(
104+
cls.FetchmailServer = cls.env["fetchmail.server"]
105+
cls.FetchmailServerFolder = cls.env["fetchmail.server.folder"]
106+
cls.server = cls.FetchmailServer.create(
83107
{
84108
"name": "Test Fetchmail Server",
85109
"server": "imap.example.com",
86110
"server_type": "imap",
111+
"folders_only": True, # Not going to test default fetchmail
87112
"active": True,
88113
"state": "done",
89114
}
90115
)
91-
cls.folder = cls.folder_model.create(
116+
cls.folder = cls.FetchmailServerFolder.create(
92117
{
93118
"server_id": cls.server.id,
94119
"sequence": 5,
@@ -100,7 +125,7 @@ def setUpClass(cls):
100125
"mail_field": "from",
101126
}
102127
)
103-
cls.partner_ir_model = cls.env["ir.model"].search(
128+
partner_ir_model = cls.env["ir.model"].search(
104129
[
105130
("model", "=", cls.Partner._name),
106131
],
@@ -113,7 +138,30 @@ def setUpClass(cls):
113138
"update_path": "category_id",
114139
"evaluation_type": "value",
115140
"value": str(cls.partner_category.id),
116-
"model_id": cls.partner_ir_model.id,
141+
"model_id": partner_ir_model.id,
142+
}
143+
)
144+
145+
def test_server(self):
146+
"""Test the server model."""
147+
self.server.write({"server_type": "pop"})
148+
self.assertEqual(self.server.state, "draft")
149+
self._reactivate_server()
150+
self.assertEqual(self.server.state, "done")
151+
with patch.object(
152+
self.server.__class__, "_connect__", return_value=MockConnection()
153+
):
154+
self.server.fetch_mail()
155+
self.server.onchange_server_type()
156+
self.assertEqual(self.server.state, "draft")
157+
158+
def _reactivate_server(self):
159+
"""Set type to imap and state to "done"."""
160+
self.server.write(
161+
{
162+
"server_type": "imap",
163+
"active": True,
164+
"state": "done",
117165
}
118166
)
119167

@@ -159,6 +207,19 @@ def test_apply_matching_exact_no_match(self):
159207
message_uid = "test no match"
160208
folder.apply_matching(connection, message_uid)
161209

210+
def test_apply_matching_odoo_standard(self):
211+
folder = self.folder
212+
folder.match_algorithm = "odoo_standard"
213+
connection = MockConnection()
214+
message_uid = "test no match"
215+
thread_id = folder.apply_matching(connection, message_uid)
216+
self.assertTrue(thread_id) # Odoo standard either updates of creates record.
217+
partner = self.Partner.browse(thread_id)
218+
self.assertTrue(partner)
219+
self.assertEqual(
220+
email_normalize(partner.email), "the_smart_red_one@reynaerde.waesland"
221+
)
222+
162223
def test_retrieve_imap_folder_domain(self):
163224
folder = self.folder
164225
folder.match_algorithm = "email_domain"
@@ -169,7 +230,20 @@ def test_archive_messages(self):
169230
folder = self.folder
170231
folder.archive_path = "archived_messages"
171232
connection = MockConnection()
172-
folder.retrieve_imap_folder(connection)
233+
folder.check_imap_archive_folder(connection)
234+
# Apply matching should succeed with no error.
235+
folder.match_algorithm = "email_exact"
236+
message_uid = "<485a8041-d560-a981-5afc-d31c1f136748@acme.com>"
237+
folder.apply_matching(connection, message_uid)
238+
# Should fail on not existing folder that is not autocreated.
239+
with self.assertRaises(UserError):
240+
folder.archive_path = "will_not_be_created"
241+
folder.check_imap_archive_folder(connection)
242+
# Should not fail if no archive path.
243+
folder.archive_path = False
244+
folder.check_imap_archive_folder(connection)
245+
# Archiving should simply not fail.
246+
folder._archive_msg(connection, "does not matter")
173247

174248
def test_non_action(self):
175249
connection = MockConnection()
@@ -204,13 +278,86 @@ def test_button_confirm_folder(self):
204278
with patch.object(
205279
self.server.__class__, "_connect__", return_value=MockConnection()
206280
):
281+
# Inactive folders are not set to done
207282
folder.active = False
208283
folder.button_confirm_folder()
209284
self.assertEqual(folder.state, "draft")
285+
# Active folders are set to done
286+
folder.active = True
287+
folder.button_confirm_folder()
288+
self.assertEqual(folder.state, "done")
289+
# Should fail on unknown folder.
290+
with self.assertRaises(ValidationError):
291+
folder.path = "invalid_folder"
292+
folder.button_confirm_folder()
210293

294+
def test_fetchmail(self):
295+
"""Test the overall process."""
296+
folder = self.folder
297+
with patch.object(
298+
self.server.__class__, "_connect__", return_value=MockConnection()
299+
):
300+
# Should not result in error if not active or in draft.
301+
folder.active = False
302+
folder.button_confirm_folder()
303+
self.assertEqual(folder.state, "draft")
304+
folder.fetch_mail()
305+
# Should also not result in error if active.
211306
folder.active = True
212307
folder.button_confirm_folder()
213308
self.assertEqual(folder.state, "done")
309+
folder.fetch_mail()
310+
311+
def test_fetch_msg_exception(self):
312+
"""There can be an exception, for example if message no longer exists."""
313+
folder = self.folder
314+
connection = MockConnection()
315+
with self.assertRaises(UserError):
316+
folder.fetch_msg(connection, "deleted_uid")
317+
318+
def test_get_message_uids(self):
319+
"""Test failure on invalid criteria."""
320+
folder = self.folder
321+
connection = MockConnection()
322+
criteria = folder.get_criteria()
323+
message_uids = folder.get_message_uids(connection, criteria)
324+
self.assertEqual(message_uids, ["123 456"])
325+
folder.path = "invalid_folder"
326+
with self.assertRaises(UserError):
327+
folder.get_message_uids(connection, criteria)
328+
folder.path = "INBOX" # Restore valid folder path.
329+
criteria = "test_invalid"
330+
with self.assertRaises(UserError):
331+
folder.get_message_uids(connection, criteria)
332+
333+
def test_update_msg(self):
334+
"""Message update depends on folder configuration."""
335+
folder = self.folder
336+
connection = MockConnection()
337+
folder.seen = "matching"
338+
folder.update_msg(connection, "123", matched=True)
339+
self._check_flags(connection, "123", "\\seen")
340+
folder.seen = "on_fetch" # back to default
341+
folder.delete_matching = True
342+
folder.update_msg(connection, "456", matched=True)
343+
self._check_flags(connection, "456", "\\DELETED")
344+
folder.delete_matching = False # back to default
345+
self.flag_nonmatching = True
346+
folder.update_msg(connection, "789", matched=True, flagged=True)
347+
self._check_flags(connection, "789", "")
348+
349+
def _check_flags(self, connection, uid, expected_flags):
350+
"""Check whether expected value stored."""
351+
self.assertIn(uid, connection._msg_store)
352+
self.assertIn("FLAGS", connection._msg_store[uid])
353+
self.assertEqual(connection._msg_store[uid]["FLAGS"], expected_flags)
354+
355+
def test_button_attach_mail_manually(self):
356+
"""The button should return the an action dictionary."""
357+
folder = self.folder
358+
action_dict = folder.button_attach_mail_manually()
359+
Wizard = self.env[action_dict["res_model"]]
360+
self.assertEqual(Wizard._name, "fetchmail.attach.mail.manually")
214361

215362
def test_set_draft(self):
216363
"""Test the set_draft method."""

0 commit comments

Comments
 (0)