diff --git a/tests/test_imap_tasks.py b/tests/test_imap_tasks.py index 3fb7b7f9..9c46d1a4 100644 --- a/tests/test_imap_tasks.py +++ b/tests/test_imap_tasks.py @@ -687,6 +687,59 @@ class TestPullInbox: mock_star.assert_called_once_with(mock_mail, b"1") mock_label.assert_called_once_with(mock_mail, b"1", label="Ingested") + @patch("app.tasks.imap_tasks.email_already_has_label") + @patch("app.tasks.imap_tasks.mark_as_processed_with_label") + @patch("app.tasks.imap_tasks.mark_as_processed_with_star") + @patch("app.tasks.imap_tasks.fetch_attachments_and_enqueue") + @patch("app.tasks.imap_tasks.imaplib.IMAP4_SSL") + @patch("app.tasks.imap_tasks.load_processed_emails") + @patch("app.tasks.imap_tasks.save_processed_emails") + @patch("app.tasks.imap_tasks.settings") + def test_gmail_labels_disabled_when_gmail_apply_labels_false( + self, + mock_settings, + mock_save, + mock_load, + mock_imap_class, + mock_fetch, + mock_star, + mock_label, + mock_has_label, + ): + """Gmail star/label operations should be skipped when gmail_apply_labels=False.""" + mock_settings.workdir = "/tmp" + mock_settings.imap_readonly_mode = False + mock_load.return_value = {} + mock_mail = MagicMock() + mock_imap_class.return_value = mock_mail + + import email + + msg = email.message.EmailMessage() + msg["Message-ID"] = "" + raw_email = msg.as_bytes() + + mock_mail.login.return_value = ("OK", []) + mock_mail.select.return_value = ("OK", []) + mock_mail.search.return_value = ("OK", [b"1"]) + mock_mail.fetch.return_value = ("OK", [[None, raw_email]]) + + pull_inbox( + mailbox_key="imap2", + host="imap.gmail.com", + port=993, + username="user@gmail.com", + password=_TEST_CREDENTIAL, + use_ssl=True, + delete_after_process=False, + gmail_apply_labels=False, + ) + + mock_star.assert_not_called() + mock_label.assert_not_called() + mock_has_label.assert_not_called() + mock_fetch.assert_called() + @patch("app.tasks.imap_tasks.email_already_has_label") @patch("app.tasks.imap_tasks.imaplib.IMAP4_SSL") @patch("app.tasks.imap_tasks.load_processed_emails") diff --git a/tests/test_watch_folder_tasks.py b/tests/test_watch_folder_tasks.py index fccef1ec..72a08423 100644 --- a/tests/test_watch_folder_tasks.py +++ b/tests/test_watch_folder_tasks.py @@ -3843,6 +3843,134 @@ class TestPullUserIntegrationWatchFolders: result = _pull_user_integration_watch_folders() assert result["status"] == "ok" + @patch("app.tasks.watch_folder_tasks._get_db_session") + @patch("app.tasks.watch_folder_tasks._load_cache", return_value={}) + @patch("app.tasks.watch_folder_tasks._save_cache") + def test_dispatches_s3_source_type(self, mock_save, mock_load, mock_session_factory): + """WATCH_FOLDER with source_type 's3' should dispatch to _scan_user_s3_folder.""" + from app.tasks.watch_folder_tasks import ( + _USER_WF_CLOUD_HANDLERS, + _pull_user_integration_watch_folders, + ) + + mock_integ = MagicMock() + mock_integ.id = 30 + mock_integ.owner_id = "owner-s3" + mock_integ.config = ( + '{"source_type": "s3", "bucket": "test-bucket", "prefix": "inbox/", "delete_after_process": false}' + ) + mock_integ.is_active = True + mock_integ.credentials = "encrypted-s3-creds" + + mock_db = MagicMock() + mock_db.query.return_value.filter.return_value.all.return_value = [mock_integ] + mock_session_factory.return_value = mock_db + + mock_s3_handler = MagicMock(return_value=3) + original_handler = _USER_WF_CLOUD_HANDLERS.get("s3") + _USER_WF_CLOUD_HANDLERS["s3"] = mock_s3_handler + try: + with patch( + "app.utils.encryption.decrypt_value", + return_value='{"access_key_id": "AKI", "secret_access_key": "SK"}', + ): + result = _pull_user_integration_watch_folders() + + assert result["status"] == "ok" + assert result["files_enqueued"] == 3 + mock_s3_handler.assert_called_once() + args = mock_s3_handler.call_args + assert args[0][0]["source_type"] == "s3" + assert args[0][4] == "owner-s3" + finally: + if original_handler is not None: + _USER_WF_CLOUD_HANDLERS["s3"] = original_handler + + @patch("app.tasks.watch_folder_tasks._get_db_session") + @patch("app.tasks.watch_folder_tasks._load_cache", return_value={}) + @patch("app.tasks.watch_folder_tasks._save_cache") + def test_dispatches_dropbox_source_type(self, mock_save, mock_load, mock_session_factory): + """WATCH_FOLDER with source_type 'dropbox' should dispatch to _scan_user_dropbox_folder.""" + from app.tasks.watch_folder_tasks import ( + _USER_WF_CLOUD_HANDLERS, + _pull_user_integration_watch_folders, + ) + + mock_integ = MagicMock() + mock_integ.id = 31 + mock_integ.owner_id = "owner-dbx" + mock_integ.config = '{"source_type": "dropbox", "folder_path": "/Inbox", "delete_after_process": false}' + mock_integ.is_active = True + mock_integ.credentials = "encrypted-dbx-creds" + + mock_db = MagicMock() + mock_db.query.return_value.filter.return_value.all.return_value = [mock_integ] + mock_session_factory.return_value = mock_db + + mock_dbx_handler = MagicMock(return_value=5) + original_handler = _USER_WF_CLOUD_HANDLERS.get("dropbox") + _USER_WF_CLOUD_HANDLERS["dropbox"] = mock_dbx_handler + try: + with patch( + "app.utils.encryption.decrypt_value", + return_value='{"refresh_token": "tok", "app_key": "ak", "app_secret": "as"}', + ): + result = _pull_user_integration_watch_folders() + + assert result["status"] == "ok" + assert result["files_enqueued"] == 5 + mock_dbx_handler.assert_called_once() + args = mock_dbx_handler.call_args + assert args[0][0]["source_type"] == "dropbox" + assert args[0][4] == "owner-dbx" + finally: + if original_handler is not None: + _USER_WF_CLOUD_HANDLERS["dropbox"] = original_handler + + @patch("app.tasks.watch_folder_tasks._get_db_session") + def test_unknown_source_type_skipped(self, mock_session_factory): + """Unknown source_type should be skipped gracefully.""" + from app.tasks.watch_folder_tasks import _pull_user_integration_watch_folders + + mock_integ = MagicMock() + mock_integ.id = 32 + mock_integ.owner_id = "owner-unknown" + mock_integ.config = '{"source_type": "unknown_provider", "delete_after_process": false}' + mock_integ.is_active = True + + mock_db = MagicMock() + mock_db.query.return_value.filter.return_value.all.return_value = [mock_integ] + mock_session_factory.return_value = mock_db + + result = _pull_user_integration_watch_folders() + assert result["status"] == "ok" + assert result["files_enqueued"] == 0 + + @patch("app.tasks.watch_folder_tasks._get_db_session") + @patch("app.tasks.watch_folder_tasks._scan_user_watch_folder", return_value=4) + @patch("app.tasks.watch_folder_tasks._load_cache", return_value={}) + @patch("app.tasks.watch_folder_tasks._save_cache") + def test_local_source_type_uses_local_scanner(self, mock_save, mock_load, mock_scan_local, mock_session_factory): + """Explicit source_type 'local' should use the local filesystem scanner.""" + from app.tasks.watch_folder_tasks import _pull_user_integration_watch_folders + + mock_integ = MagicMock() + mock_integ.id = 33 + mock_integ.owner_id = "owner-local" + mock_integ.config = '{"source_type": "local", "folder_path": "/data/scans", "delete_after_process": false}' + mock_integ.is_active = True + + mock_db = MagicMock() + mock_db.query.return_value.filter.return_value.all.return_value = [mock_integ] + mock_session_factory.return_value = mock_db + + result = _pull_user_integration_watch_folders() + assert result["status"] == "ok" + assert result["files_enqueued"] == 4 + mock_scan_local.assert_called_once() + assert mock_scan_local.call_args[0][0] == "/data/scans" + assert mock_scan_local.call_args[0][3] == "owner-local" + @pytest.mark.unit class TestScanAllWatchFoldersIncludesUserIntegrations: