"""Spelling/grammar check proxy (compose window -> LanguageTool). LanguageTool is replaced by a stub transport, so these run without the engine. """ from __future__ import annotations import uuid from collections.abc import AsyncIterator from typing import Any import httpx import pytest import pytest_asyncio from httpx import ASGITransport from sqlalchemy import delete from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.pool import NullPool from app.api import spellcheck from app.config import get_settings from app.identity.passwords import hash_password from app.identity.sessions import create_session from app.main import create_app from app.models import User LT_REPLY: dict[str, Any] = { "language": { "code": "auto", "name": "Auto", "detectedLanguage": {"code": "pl-PL", "name": "Polish"}, }, "matches": [ { "offset": 12, "length": 6, "message": "Wyraz „prosze” to najczęściej literówka", "shortMessage": "Literówka", "replacements": [{"value": "proszę"}], "rule": {"id": "PL_PROSZE", "issueType": "misspelling"}, }, { "offset": 40, "length": 11, "message": "Powtórzony wyraz", "shortMessage": "", "replacements": [{"value": v} for v in "abcdefgh"], "rule": {"id": "WORD_REPEAT", "issueType": "duplication"}, }, ], } @pytest_asyncio.fixture async def client(monkeypatch: pytest.MonkeyPatch) -> AsyncIterator[httpx.AsyncClient]: engine = create_async_engine(get_settings().database_url, poolclass=NullPool) session = async_sessionmaker(engine, expire_on_commit=False)() user = User(handle=f"sp{uuid.uuid4().hex[:8]}@test.local", password_hash=hash_password("pw")) session.add(user) await session.commit() token = await create_session(user.id, "pytest") async with httpx.AsyncClient( transport=ASGITransport(app=create_app()), base_url="http://testserver", cookies={"session": token, "csrf": "c"}, headers={"X-CSRF": "c"}, ) as c: yield c await session.execute(delete(User).where(User.id == user.id)) await session.commit() await session.close() await engine.dispose() def _stub(monkeypatch: pytest.MonkeyPatch, handler: Any) -> list[httpx.Request]: """Route the module's outbound calls to `handler`; return what was sent.""" seen: list[httpx.Request] = [] real = httpx.AsyncClient def wrapped(request: httpx.Request) -> httpx.Response: seen.append(request) return handler(request) monkeypatch.setattr( spellcheck.httpx, "AsyncClient", lambda **kw: real(transport=httpx.MockTransport(wrapped), **kw), ) monkeypatch.setattr(get_settings(), "languagetool_url", "http://lt.test:8010") return seen async def test_check_returns_trimmed_typed_issues( client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch ) -> None: sent = _stub(monkeypatch, lambda _r: httpx.Response(200, json=LT_REPLY)) resp = await client.post( "/api/spellcheck", json={"text": "Dzień dobry prosze o pliki ważne ważne", "language": "auto"}, ) assert resp.status_code == 200 body = resp.json() assert (body["language"], body["language_name"]) == ("pl-PL", "Polish") first, second = body["issues"] assert first["kind"] == "spelling" and first["replacements"] == ["proszę"] assert second["kind"] == "grammar" and len(second["replacements"]) == 6 assert b"language=auto" in sent[0].content async def test_unconfigured_or_down_engine_is_a_503( client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch ) -> None: def down(_r: httpx.Request) -> httpx.Response: raise httpx.ConnectError("refused") _stub(monkeypatch, down) resp = await client.post("/api/spellcheck", json={"text": "hello"}) assert resp.json()["error"]["code"] == "spellcheck_unavailable" monkeypatch.setattr(get_settings(), "languagetool_url", "") resp = await client.post("/api/spellcheck", json={"text": "hello"}) assert resp.status_code == 503 async def test_empty_text_never_reaches_the_engine( client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch ) -> None: sent = _stub(monkeypatch, lambda _r: httpx.Response(200, json=LT_REPLY)) resp = await client.post("/api/spellcheck", json={"text": " \n "}) assert resp.json()["issues"] == [] and sent == [] async def test_settings_store_the_spellcheck_choice(client: httpx.AsyncClient) -> None: out = (await client.patch("/api/settings", json={"spellcheck_language": "pl-PL"})).json() assert out["spellcheck_language"] == "pl-PL" and out["spellcheck_enabled"] is True out = (await client.patch("/api/settings", json={"spellcheck_enabled": False})).json() assert out["spellcheck_enabled"] is False bad = (await client.patch("/api/settings", json={"spellcheck_language": "