55from unittest .mock import patch
66
77from odoo import fields
8+ from odoo .exceptions import UserError , ValidationError
89from odoo .fields import Command
910from odoo .tests .common import TransactionCase
11+ from odoo .tools .mail import email_normalize
1012
1113from ..match_algorithm import email_domain
1214from .common import get_message_body
1719
1820
1921class 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