fix(i-d): mail-API-Brueche behoben — signatures PATCH/DELETE + labels DELETE im Backend ergaenzt, drafts PATCH->PUT
Check Cross-Plugin Imports / check (push) Has been cancelled

Root-Cause: Frontend-Komponenten (SignatureManager, LabelManager) rufen Endpunkte auf die das Backend nie hatte (404/405 in Production). Anders als ai/sessions sind diese Funktionen ECHT in Komponenten eingebunden -> Backend-Routen nachbestellt statt Frontend-Calls zu loeschen:

(1) PATCH+DELETE /mail/signatures/{id}: MailSignatureUpdate-Schema neu, Tenant-Scoped + Owner-Check (403 bei fremder Signatur), is_default-Exklusivitaet beim Setzen. (2) DELETE /mail/labels/{id}: gleicher Stil. (3) updateDraft Frontend: apiPatch -> apiPut (Backend hat PUT /drafts/{id} bereits). Beweistest tests/test_mail_sig_label_routes.py 5/5 gruen (PATCH-Werte, DELETE+Liste-leer, 404-Faelle).

Verifikation: create_app registriert beide neuen Routen (563 total); ruff clean; tsc exit=0.
This commit is contained in:
Agent Zero
2026-08-25 20:13:40 +02:00
parent 3e5f13f516
commit 86c96f03ca
6 changed files with 230 additions and 218 deletions
+139
View File
@@ -0,0 +1,139 @@
"""Tests for the mail signature PATCH/DELETE and label DELETE endpoints (I-D fix).
Proves the previously missing routes now work end-to-end:
- PATCH /api/v1/mail/signatures/{id} -> 200 with updated values
- DELETE /api/v1/mail/signatures/{id} -> 204, row gone
- DELETE /api/v1/mail/labels/{id} -> 204, row gone
- 404 for unknown IDs
"""
from __future__ import annotations
import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
from app.core.db import close_engine, reset_engine_for_testing
from app.core.permission_registry import init_permission_registry
from app.core.service_container import get_container
from app.main import create_app
from app.plugins.builtins.mail import MailPlugin
from app.plugins.registry import reset_registry_for_testing
from app.services.plugin_service import reset_plugin_service_for_testing
from tests.conftest import ORIGIN_HEADER, login_client, seed_tenant_and_users
@pytest_asyncio.fixture
async def mail_app(engine: AsyncEngine, redis_client):
"""FastAPI app with Mail plugin registered."""
reset_engine_for_testing(engine)
app = create_app()
registry = reset_registry_for_testing()
registry.initialize(engine, app)
init_permission_registry(active_plugin_names={"mail"})
container = get_container()
await container.initialize()
registry.register_plugin(MailPlugin())
reset_plugin_service_for_testing(registry)
sf = async_sessionmaker(bind=engine, expire_on_commit=False, class_=AsyncSession)
async with sf() as session:
await registry.install(session, "mail")
await registry.activate(session, "mail")
await session.commit()
yield app
await close_engine()
@pytest_asyncio.fixture
async def mail_client(mail_app) -> AsyncClient:
transport = ASGITransport(app=mail_app)
async with AsyncClient(transport=transport, base_url="http://test") as c:
yield c
@pytest_asyncio.fixture
async def authed_client(
mail_client: AsyncClient, db_session: AsyncSession
) -> AsyncClient:
seed = await seed_tenant_and_users(db_session)
assert seed is not None
await login_client(mail_client, "admin@tenanta.com")
return mail_client
async def _create_signature(client: AsyncClient) -> dict:
resp = await client.post(
"/api/v1/mail/signatures",
json={"name": "Old Name", "body_html": "<p>old</p>", "is_default": False},
headers=ORIGIN_HEADER,
)
assert resp.status_code == 201, resp.text
return resp.json()
async def _create_label(client: AsyncClient) -> dict:
resp = await client.post(
"/api/v1/mail/labels",
json={"name": "Temp Label", "color": "#ff0000"},
headers=ORIGIN_HEADER,
)
assert resp.status_code == 201, resp.text
return resp.json()
@pytest.mark.asyncio
async def test_update_signature(authed_client: AsyncClient):
sig = await _create_signature(authed_client)
resp = await authed_client.patch(
f"/api/v1/mail/signatures/{sig['id']}",
json={"name": "New Name", "body_html": "<p>new</p>", "is_default": True},
headers=ORIGIN_HEADER,
)
assert resp.status_code == 200, resp.text
data = resp.json()
assert data["name"] == "New Name"
assert data["body_html"] == "<p>new</p>"
assert data["is_default"] is True
@pytest.mark.asyncio
async def test_delete_signature(authed_client: AsyncClient):
sig = await _create_signature(authed_client)
resp = await authed_client.delete(
f"/api/v1/mail/signatures/{sig['id']}",
headers=ORIGIN_HEADER,
)
assert resp.status_code == 204, resp.text
lst = await authed_client.get("/api/v1/mail/signatures", headers=ORIGIN_HEADER)
assert all(s["id"] != sig["id"] for s in lst.json())
@pytest.mark.asyncio
async def test_delete_label(authed_client: AsyncClient):
label = await _create_label(authed_client)
resp = await authed_client.delete(
f"/api/v1/mail/labels/{label['id']}",
headers=ORIGIN_HEADER,
)
assert resp.status_code == 204, resp.text
lst = await authed_client.get("/api/v1/mail/labels", headers=ORIGIN_HEADER)
assert all(lb["id"] != label["id"] for lb in lst.json())
@pytest.mark.asyncio
async def test_delete_unknown_signature_404(authed_client: AsyncClient):
resp = await authed_client.delete(
"/api/v1/mail/signatures/00000000-0000-0000-0000-000000000000",
headers=ORIGIN_HEADER,
)
assert resp.status_code == 404, resp.text
@pytest.mark.asyncio
async def test_delete_unknown_label_404(authed_client: AsyncClient):
resp = await authed_client.delete(
"/api/v1/mail/labels/00000000-0000-0000-0000-000000000000",
headers=ORIGIN_HEADER,
)
assert resp.status_code == 404, resp.text