feat(storage): add Evernote destination (#862)

* feat(storage): add Evernote destination

* style: apply ruff auto-fix

- Auto-formatted code with ruff format
- Applied ruff linting fixes with --fix

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: Christian Krakau-Louis <christian@Christians-Mac-mini-7.local>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Christian Krakau-Louis
2026-05-22 12:13:46 +02:00
committed by GitHub
parent 8e744c076d
commit 4b46c4baf8
20 changed files with 507 additions and 2 deletions
+8
View File
@@ -35,6 +35,7 @@ class TestValidateStorageConfigs:
"google_drive",
"onedrive",
"email",
"evernote",
"paperless",
"uptime_kuma",
]
@@ -83,6 +84,13 @@ class TestValidateStorageConfigs:
assert "DEST_EMAIL_HOST is not configured" in result["email"]
assert "DEST_EMAIL_DEFAULT_RECIPIENT is not configured" in result["email"]
def test_evernote_missing_token(self):
"""Test validation when Evernote destination auth token is missing."""
with patch("app.utils.config_validator.validators.settings") as mock_settings:
mock_settings.evernote_auth_token = None
result = validate_storage_configs()
assert "EVERNOTE_AUTH_TOKEN is not configured" in result["evernote"]
@pytest.mark.unit
class TestValidateEmailConfig:
@@ -663,6 +663,7 @@ def _all_should_upload_false():
"ftp",
"sftp",
"email",
"evernote",
"onedrive",
"s3",
"sharepoint",
+19
View File
@@ -7,6 +7,7 @@ import pytest
from app.tasks.send_to_all import (
_should_upload_to_dropbox,
_should_upload_to_email,
_should_upload_to_evernote,
_should_upload_to_ftp,
_should_upload_to_google_drive,
_should_upload_to_icloud,
@@ -128,6 +129,14 @@ class TestShouldUploadFunctions:
assert _should_upload_to_email() is True
@patch("app.tasks.send_to_all.settings")
def test_should_upload_to_evernote_configured(self, mock_settings):
"""Test Evernote upload check."""
mock_settings.evernote_enabled = True
mock_settings.evernote_auth_token = "token"
assert _should_upload_to_evernote() is True
@patch("app.tasks.send_to_all.settings")
def test_should_upload_to_onedrive_configured(self, mock_settings):
"""Test OneDrive upload check."""
@@ -248,6 +257,14 @@ class TestShouldUploadEnabledFlag:
assert _should_upload_to_email() is False
@patch("app.tasks.send_to_all.settings")
def test_evernote_disabled_with_credentials(self, mock_settings):
"""Test Evernote upload is blocked when disabled even with valid credentials."""
mock_settings.evernote_enabled = False
mock_settings.evernote_auth_token = "token"
assert _should_upload_to_evernote() is False
@patch("app.tasks.send_to_all.settings")
def test_onedrive_disabled_with_credentials(self, mock_settings):
"""Test OneDrive upload is blocked when disabled even with valid credentials."""
@@ -289,6 +306,7 @@ class TestGetConfiguredServicesFromValidator:
"Dropbox": {"configured": True, "enabled": True},
"NextCloud": {"configured": False, "enabled": True},
"S3 Storage": {"configured": True, "enabled": True},
"Evernote": {"configured": True, "enabled": True},
}
result = get_configured_services_from_validator()
@@ -296,6 +314,7 @@ class TestGetConfiguredServicesFromValidator:
assert result["dropbox"] is True
assert result["nextcloud"] is False
assert result["s3"] is True
assert result["evernote"] is True
@patch("app.tasks.send_to_all.get_provider_status")
def test_handles_missing_providers(self, mock_get_status):
+2
View File
@@ -8,6 +8,7 @@ from unittest.mock import Mock, patch
import pytest
from app.tasks.upload_to_email import upload_to_email
from app.tasks.upload_to_evernote import upload_to_evernote
from app.tasks.upload_to_ftp import upload_to_ftp
from app.tasks.upload_to_google_drive import upload_to_google_drive
from app.tasks.upload_to_onedrive import upload_to_onedrive
@@ -447,6 +448,7 @@ def test_all_upload_tasks_have_consistent_signature(sample_text_file):
(upload_to_webdav, "app.tasks.upload_to_webdav"),
(upload_to_google_drive, "app.tasks.upload_to_google_drive"),
(upload_to_email, "app.tasks.upload_to_email"),
(upload_to_evernote, "app.tasks.upload_to_evernote"),
]
import inspect
+113
View File
@@ -0,0 +1,113 @@
import hashlib
import json
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from app.tasks.upload_to_evernote import _build_enml, upload_to_evernote
class _FakeTypes:
class Data:
pass
class Resource:
pass
class ResourceAttributes:
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
class Note:
pass
class _FakeNoteStore:
def __init__(self):
self.calls = []
def createNote(self, auth_token, note):
self.calls.append((auth_token, note))
note.guid = "note-guid-123"
return note
_fake_note_store = _FakeNoteStore()
@pytest.fixture(autouse=True)
def reset_fake_store():
global _fake_note_store
_fake_note_store = _FakeNoteStore()
def test_build_enml_escapes_metadata():
enml = _build_enml({"sender": "A&B <Corp>"}, "abc123", "application/pdf", include_metadata=True)
assert "A&amp;B &lt;Corp&gt;" in enml
assert '<en-media type="application/pdf" hash="abc123"/>' in enml
@pytest.mark.unit
def test_upload_to_evernote_creates_note_with_metadata_and_pdf(tmp_path):
pdf_path = tmp_path / "invoice.pdf"
pdf_bytes = b"%PDF-1.4 test content"
pdf_path.write_bytes(pdf_bytes)
pdf_path.with_suffix(".json").write_text(
json.dumps(
{
"title": "Invoice May",
"absender": "Example GmbH",
"tags": ["invoice", "finance"],
"empty": "Unknown",
}
),
encoding="utf-8",
)
with (
patch("app.tasks.upload_to_evernote._get_note_store", return_value=(_fake_note_store, _FakeTypes)),
patch("app.tasks.upload_to_evernote.log_task_progress"),
patch("app.tasks.upload_to_evernote.settings") as mock_settings,
):
mock_settings.evernote_auth_token = "auth-token"
mock_settings.evernote_sandbox = True
mock_settings.evernote_notebook_guid = "notebook-guid"
mock_settings.evernote_default_tags = "docuelevate,archive"
mock_settings.evernote_include_metadata = True
result = upload_to_evernote.apply(args=[str(pdf_path)], kwargs={"file_id": 7}).get()
created_note = _fake_note_store.calls[0][1]
resource = created_note.resources[0]
assert result["status"] == "Completed"
assert result["evernote_note_guid"] == "note-guid-123"
assert created_note.title == "Invoice May"
assert created_note.notebookGuid == "notebook-guid"
assert created_note.tagNames == ["docuelevate", "archive", "invoice", "finance"]
assert "Example GmbH" in created_note.content
assert "empty" not in created_note.content
assert f'hash="{hashlib.md5(pdf_bytes).hexdigest()}"' in created_note.content # noqa: S324
assert resource.mime == "application/pdf"
assert resource.attributes.fileName == "invoice.pdf"
assert resource.data.body == pdf_bytes
assert resource.data.bodyHash == hashlib.md5(pdf_bytes).digest() # noqa: S324
@pytest.mark.unit
def test_upload_to_evernote_requires_token(tmp_path):
pdf_path = tmp_path / "document.pdf"
pdf_path.write_bytes(b"%PDF-1.4")
with (
patch("app.tasks.upload_to_evernote.log_task_progress"),
patch("app.tasks.upload_to_evernote.settings", SimpleNamespace(evernote_auth_token=None)),
):
result = upload_to_evernote.apply(args=[str(pdf_path)])
assert result.failed()
assert isinstance(result.result, ValueError)
assert "EVERNOTE_AUTH_TOKEN" in str(result.result)