- Snooze (with a Snoozed folder), keyboard shortcuts with a Settings section, mail templates (Compose > More > Templates), a Subscriptions folder with bulk unsubscribe/delete, and tracker blocking with a Privacy toggle (on by default) - Own dialogs instead of browser popups; emoji picker in compose; compose windows survive a visit to Settings - Sentence completion learned from the user's sent mail (no AI) - Interface in English and Polish (one file per language, English as the fallback), picked from the browser on first run, then kept - Email translation on the server via LibreTranslate: Translate button and "Translate to...", messages in several languages translated part by part, names/addresses/emoji/file names/phone numbers kept, an animated line while translating - Antispam "Mark as spam" loads with the message, not when the menu opens Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
87 lines
3.8 KiB
Python
87 lines
3.8 KiB
Python
"""Email translation over the API, with LibreTranslate replaced by a stand-in."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from app import translation
|
|
from tests.api.conftest import MailCtx, raw_mail
|
|
|
|
TEXT = "Hello Lukas, I would like to ask about the project schedule for next month."
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_service(monkeypatch: pytest.MonkeyPatch) -> dict[str, object]:
|
|
calls: dict[str, object] = {"detect": 0, "translate": 0}
|
|
|
|
async def detect(text: str) -> str:
|
|
calls["detect"] = int(calls["detect"]) + 1 # type: ignore[call-overload]
|
|
return "en"
|
|
|
|
async def translate_html(
|
|
html: str, source: str, target: str, keep: object = (), names: object = ()
|
|
) -> str:
|
|
calls["translate"] = int(calls["translate"]) + 1 # type: ignore[call-overload]
|
|
calls["keep"] = list(keep) # type: ignore[call-overload]
|
|
calls["names"] = list(names) # type: ignore[call-overload]
|
|
assert source == "en" and target == "pl"
|
|
return "<p>Cześć Łukasz</p><script>alert(1)</script>"
|
|
|
|
monkeypatch.setattr(translation, "enabled", lambda: True)
|
|
monkeypatch.setattr(translation, "detect", detect)
|
|
monkeypatch.setattr(translation, "translate_html", translate_html)
|
|
return calls
|
|
|
|
|
|
async def test_language_is_found_once_and_kept(
|
|
mail_ctx: MailCtx, fake_service: dict[str, object]
|
|
) -> None:
|
|
msg = await mail_ctx.deliver(raw_mail("Project", html=f"<p>{TEXT}</p>"), 1)
|
|
for _ in range(2):
|
|
resp = await mail_ctx.client.get(f"/api/messages/{msg.id}/html")
|
|
assert resp.headers["X-Content-Language"] == "en"
|
|
assert fake_service["detect"] == 1
|
|
(env,) = (await mail_ctx.client.get(f"/api/threads/{msg.thread_id}")).json()["messages"]
|
|
assert env["content_language"] == "en"
|
|
|
|
|
|
async def test_translation_is_cleaned_and_kept(
|
|
mail_ctx: MailCtx, fake_service: dict[str, object]
|
|
) -> None:
|
|
msg = await mail_ctx.deliver(raw_mail("Project", html=f"<p>{TEXT}</p>"), 1)
|
|
for _ in range(2):
|
|
resp = await mail_ctx.client.get(f"/api/messages/{msg.id}/html", params={"translate": "pl"})
|
|
assert resp.status_code == 200
|
|
assert "Cześć Łukasz" in resp.text and "alert(1)" not in resp.text
|
|
assert resp.headers["X-Translated-From"] == "en"
|
|
assert resp.headers["X-Translated-To"] == "pl"
|
|
assert fake_service["translate"] == 1 # the second came from the cache
|
|
# The people on the message are names to keep if the model would change them.
|
|
assert "News" in fake_service["names"] # type: ignore[operator]
|
|
|
|
|
|
async def test_service_trouble_is_a_clear_error(
|
|
mail_ctx: MailCtx, monkeypatch: pytest.MonkeyPatch, fake_service: dict[str, object]
|
|
) -> None:
|
|
async def broken(
|
|
html: str, source: str, target: str, keep: object = (), names: object = ()
|
|
) -> str:
|
|
raise translation.TranslationError("the translation service is unreachable")
|
|
|
|
monkeypatch.setattr(translation, "translate_html", broken)
|
|
msg = await mail_ctx.deliver(raw_mail("Project", html=f"<p>{TEXT}</p>"), 1)
|
|
resp = await mail_ctx.client.get(f"/api/messages/{msg.id}/html", params={"translate": "pl"})
|
|
assert resp.status_code == 503
|
|
assert resp.json()["error"]["code"] == "translation_failed"
|
|
# The message itself still shows.
|
|
assert (await mail_ctx.client.get(f"/api/messages/{msg.id}/html")).status_code == 200
|
|
|
|
|
|
async def test_off_without_a_service(mail_ctx: MailCtx, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(translation, "enabled", lambda: False)
|
|
msg = await mail_ctx.deliver(raw_mail("Project", html=f"<p>{TEXT}</p>"), 1)
|
|
resp = await mail_ctx.client.get(f"/api/messages/{msg.id}/html")
|
|
assert "X-Content-Language" not in resp.headers
|
|
info = (await mail_ctx.client.get("/api/translation/languages")).json()
|
|
assert info == {"available": False, "languages": []}
|