feat(deploy): healthcheck api и worker, отдельный migrate-сервис, лимит загрузки отчёта

Миграции вынесены из команды api в one-shot сервис migrate, api и worker стартуют после него. /health отвечает 503 при недоступной БД. Отчёт больше MAX_UPLOAD_BYTES (25 МиБ) получает 413, Caddy режет на 30 МБ раньше. Том uploads убран, секреты env_file передаются сервисам явно. В CI добавлена сборка образа бэкенда без push, test_migrations сверяет модели с историей Alembic.
This commit is contained in:
Dmitry
2026-09-19 21:55:33 +03:00
parent 600496048f
commit 4a0b4e0532
11 changed files with 139 additions and 21 deletions
+13
View File
@@ -459,3 +459,16 @@ async def test_pending_rows_are_created_by_the_preview_alone(app, http, auth_hea
async with get_sessionmaker()() as session:
assert await session.scalar(select(func.count()).select_from(PendingInstrument)) == 1
assert await session.scalar(select(func.count()).select_from(Event)) == 0
async def test_oversized_report_is_413(app, http, auth_headers, parser, monkeypatch):
from fintracker.config import get_settings
monkeypatch.setenv("MAX_UPLOAD_BYTES", "1024")
get_settings.cache_clear()
try:
response = await upload(http, auth_headers, b"x" * 2048)
finally:
monkeypatch.undo()
get_settings.cache_clear()
assert response.status_code == 413, response.text
+17
View File
@@ -4,3 +4,20 @@ async def test_health_reports_db(client):
body = r.json()
assert body["status"] == "ok"
assert body["database"] == "ok"
async def test_health_is_503_when_the_database_is_down(app, client):
from fintracker.api.deps import get_session
class _Broken:
async def execute(self, *_a, **_k):
raise ConnectionRefusedError
async def broken():
yield _Broken()
app.dependency_overrides[get_session] = broken
r = await client.get("/api/v1/health")
assert r.status_code == 503
assert r.json()["status"] == "degraded"
assert r.json()["database"].startswith("error")
+20
View File
@@ -0,0 +1,20 @@
"""Models and Alembic history must describe the same schema — the `alembic check` of this
project. `migrated` upgrades a throwaway database to head, so a model change committed
without its revision shows up here as a non-empty diff."""
from __future__ import annotations
from alembic.autogenerate import compare_metadata
from alembic.migration import MigrationContext
async def test_migrations_match_the_models(migrated: str):
import fintracker.models # noqa: F401 — registers every table on Base.metadata
from fintracker.db import get_engine
from fintracker.db.base import Base
async with get_engine().connect() as conn:
diff = await conn.run_sync(
lambda c: compare_metadata(MigrationContext.configure(c), Base.metadata)
)
assert diff == [], f"models changed without a migration (run `just revision`): {diff}"