fixup(pipeline-hardening M4/M5): purge count, shared toast helper, clean imports
- Fix RabbitMQ purge_queue to report the pre-purge message count instead of the post-purge depth (which was always 0). - Add shared attach_toast helper in admin templating; remove duplicated inline _attach_toast implementations in review.py and mq.py. - Keep purge confirmation case-sensitive so the typed safeguard works as intended. - Fix test imports to use known-first-party 'contract_check' per isort config.
This commit is contained in:
parent
c00f3cf9f9
commit
0b2e7665f4
5 changed files with 29 additions and 29 deletions
|
|
@ -12,7 +12,7 @@ from fastapi import APIRouter, Depends, Form, HTTPException, Request, status
|
|||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
|
||||
from src.contract_check.api.admin.auth import AdminUser, HtmxGuard, require_admin
|
||||
from src.contract_check.api.admin.templating import templates
|
||||
from src.contract_check.api.admin.templating import attach_toast, templates
|
||||
from src.contract_check.core.logging import get_logger
|
||||
from src.contract_check.core.mq.management import (
|
||||
MqManagementDisabledError,
|
||||
|
|
@ -36,12 +36,6 @@ def _client() -> RabbitMQManagementClient:
|
|||
return RabbitMQManagementClient()
|
||||
|
||||
|
||||
def _attach_toast(response: RedirectResponse, toast: str) -> None:
|
||||
import json
|
||||
|
||||
response.headers["HX-Trigger"] = json.dumps({"showToast": toast})
|
||||
|
||||
|
||||
def _safe_toast(toast: str) -> str:
|
||||
# Keep toast text short and URL-safe for redirects.
|
||||
return toast[:200]
|
||||
|
|
@ -184,7 +178,7 @@ async def requeue_dlq(
|
|||
response = RedirectResponse(
|
||||
url=f"/admin/mq/dlq/{queue_name}", status_code=status.HTTP_303_SEE_OTHER
|
||||
)
|
||||
_attach_toast(response, toast)
|
||||
attach_toast(response, toast)
|
||||
return response
|
||||
|
||||
|
||||
|
|
@ -221,11 +215,14 @@ async def purge_dlq(
|
|||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="unknown DLQ")
|
||||
|
||||
expected = f"purge {queue_name}"
|
||||
if confirmation.strip().lower() != expected.lower():
|
||||
return RedirectResponse(
|
||||
url=f"/admin/mq/dlq/{queue_name}/purge/confirm?toast=Неверное+подтверждение.+Введите+%27{expected}%27",
|
||||
if confirmation.strip() != expected:
|
||||
toast = f"Неверное подтверждение. Введите '{expected}'"
|
||||
response = RedirectResponse(
|
||||
url=f"/admin/mq/dlq/{queue_name}/purge/confirm",
|
||||
status_code=status.HTTP_303_SEE_OTHER,
|
||||
)
|
||||
attach_toast(response, toast)
|
||||
return response
|
||||
|
||||
client = _client()
|
||||
try:
|
||||
|
|
@ -252,5 +249,5 @@ async def purge_dlq(
|
|||
response = RedirectResponse(
|
||||
url=f"/admin/mq/dlq/{queue_name}", status_code=status.HTTP_303_SEE_OTHER
|
||||
)
|
||||
_attach_toast(response, toast)
|
||||
attach_toast(response, toast)
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ All mutations are HTMX-driven and protected by the ``HX-Request`` CSRF guard.
|
|||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import json
|
||||
import uuid
|
||||
from decimal import Decimal
|
||||
from typing import Annotated
|
||||
|
|
@ -16,7 +15,7 @@ from fastapi import APIRouter, Depends, Form, HTTPException, Request, status
|
|||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
|
||||
from src.contract_check.api.admin.auth import AdminUser, HtmxGuard, require_admin
|
||||
from src.contract_check.api.admin.templating import templates
|
||||
from src.contract_check.api.admin.templating import attach_toast, templates
|
||||
from src.contract_check.api.deps import AsyncSessionDep, PublisherDep
|
||||
from src.contract_check.core.config import get_settings
|
||||
from src.contract_check.core.db.repositories import ReviewQueueRepository
|
||||
|
|
@ -124,7 +123,7 @@ async def list_review_queue(
|
|||
"conf_max": conf_max,
|
||||
},
|
||||
)
|
||||
_attach_toast(response, request.query_params.get("toast"))
|
||||
attach_toast(response, request.query_params.get("toast"))
|
||||
return response
|
||||
|
||||
|
||||
|
|
@ -150,15 +149,10 @@ async def review_detail(
|
|||
"toast": request.query_params.get("toast"),
|
||||
},
|
||||
)
|
||||
_attach_toast(response, request.query_params.get("toast"))
|
||||
attach_toast(response, request.query_params.get("toast"))
|
||||
return response
|
||||
|
||||
|
||||
def _attach_toast(response: HTMLResponse, toast: str | None) -> None:
|
||||
if toast:
|
||||
response.headers["HX-Trigger"] = json.dumps({"showToast": toast})
|
||||
|
||||
|
||||
def _review_action_redirect(
|
||||
*,
|
||||
document_id: uuid.UUID,
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ datetime rendering consistent.
|
|||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import Response
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
_TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
|
||||
|
|
@ -33,5 +35,11 @@ def _yes_no_none(value: object) -> str:
|
|||
return "—"
|
||||
|
||||
|
||||
def attach_toast(response: Response, toast: str | None) -> None:
|
||||
"""Attach an HTMX toast trigger to any response that supports headers."""
|
||||
if toast:
|
||||
response.headers["HX-Trigger"] = json.dumps({"showToast": toast})
|
||||
|
||||
|
||||
templates.env.filters["dt"] = _format_dt
|
||||
templates.env.filters["yn"] = _yes_no_none
|
||||
|
|
|
|||
|
|
@ -240,6 +240,9 @@ class RabbitMQManagementClient:
|
|||
"""Empty a queue and return the number of messages removed."""
|
||||
self._ensure_enabled()
|
||||
url = f"/api/queues/{self._vhost}/{quote(queue_name, safe='')}/contents"
|
||||
# Capture the pre-purge depth so we can report how many were removed.
|
||||
before_info = await self.get_queue_info(queue_name)
|
||||
messages_before = max(0, before_info.messages)
|
||||
try:
|
||||
resp = await self.client.delete(url)
|
||||
except httpx.NetworkError as exc:
|
||||
|
|
@ -254,9 +257,7 @@ class RabbitMQManagementClient:
|
|||
status_code=resp.status_code,
|
||||
body=resp.text[:500],
|
||||
)
|
||||
# Successful purge returns no body; the queue is now empty.
|
||||
info = await self.get_queue_info(queue_name)
|
||||
return PurgeResult(messages_removed=max(0, info.messages))
|
||||
return PurgeResult(messages_removed=messages_before)
|
||||
|
||||
async def requeue_batch(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import httpx
|
|||
import pytest
|
||||
import respx
|
||||
|
||||
from src.contract_check.core.mq.management import (
|
||||
from contract_check.core.mq.management import (
|
||||
MqManagementDisabledError,
|
||||
MqManagementNotFoundError,
|
||||
MqManagementResponseError,
|
||||
|
|
@ -23,7 +23,7 @@ from src.contract_check.core.mq.management import (
|
|||
RabbitMQManagementClient,
|
||||
RequeueResult,
|
||||
)
|
||||
from src.contract_check.core.mq.topology import DLQ_FOR
|
||||
from contract_check.core.mq.topology import DLQ_FOR
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -132,18 +132,18 @@ async def test_peek_messages_empty_non_list(mgmt_client: RabbitMQManagementClien
|
|||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_purge_queue(mgmt_client: RabbitMQManagementClient) -> None:
|
||||
async def test_purge_queue_reports_pre_purge_count(mgmt_client: RabbitMQManagementClient) -> None:
|
||||
with respx.mock:
|
||||
respx.delete("http://rabbitmq:15672/api/queues/%2F/extract.dlq/contents").mock(
|
||||
return_value=httpx.Response(204)
|
||||
)
|
||||
respx.get("http://rabbitmq:15672/api/queues/%2F/extract.dlq").mock(
|
||||
return_value=httpx.Response(
|
||||
200, json={"name": "extract.dlq", "messages": 0, "state": "running"}
|
||||
200, json={"name": "extract.dlq", "messages": 3, "state": "running"}
|
||||
)
|
||||
)
|
||||
await mgmt_client.connect()
|
||||
assert await mgmt_client.purge_queue("extract.dlq") == PurgeResult(messages_removed=0)
|
||||
assert await mgmt_client.purge_queue("extract.dlq") == PurgeResult(messages_removed=3)
|
||||
await mgmt_client.aclose()
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue