Mise à jour automatique (Gitea + Watchtower) et conteneurisation Docker

- Détection de version via l'API Gitea, application via Watchtower
- Router système, UI admin (intégrations), bandeau de mise à jour
- Dockerfile multi-étapes, docker-compose, scripts/release.sh
- Version centralisée dans app/version.py (pyproject ou OHM_VERSION au build)
This commit is contained in:
Roman
2026-09-22 11:58:03 +00:00
parent 41566ab5fb
commit 9148b5fb6a
21 changed files with 1092 additions and 49 deletions
+83 -18
View File
@@ -1,25 +1,8 @@
"""Tests d'intégration API (auth, downloads, favoris, streaming, admin)."""
import pytest
from httpx import ASGITransport, AsyncClient
from app.config import get_settings
from app.db import db
from app.main import app
@pytest.fixture
async def client() -> AsyncClient:
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as c:
yield c
@pytest.fixture
async def admin_cookies(client: AsyncClient):
r = await client.post("/auth/register", data={"username": "admin", "password": "secret123"})
assert r.status_code == 303
return r.cookies
from app.scrapers.base import ScrapeError, SearchResult
async def test_health(client):
@@ -121,3 +104,85 @@ async def test_source_toggle(client, admin_cookies):
states = {s["name"]: s["enabled"] for s in r.json()}
assert states["vostfree"] is False
assert states["french_manga"] is True
async def test_source_health(client, admin_cookies, monkeypatch):
# source inconnue → 404 (pas de 500)
r = await client.post("/api/admin/sources/inconnue/health", cookies=admin_cookies)
assert r.status_code == 404
# état initial : aucune santé connue
r = await client.get("/api/admin/sources", cookies=admin_cookies)
assert all(s["health"] is None for s in r.json())
async def ok_search(self, query: str) -> list[SearchResult]:
return [SearchResult(source=self.name, source_id="x", title="X", url="http://x")]
monkeypatch.setattr("app.scrapers.sources.vostfree.VostfreeScraper.search", ok_search)
r = await client.post("/api/admin/sources/vostfree/health", cookies=admin_cookies)
data = r.json()
assert data["healthy"] is True and data["detail"] == "1 résultats" and data["checked_at"]
# résultat persisté et exposé par GET /sources
r = await client.get("/api/admin/sources", cookies=admin_cookies)
health = {s["name"]: s["health"] for s in r.json()}
assert health["vostfree"]["healthy"] is True
assert health["french_manga"] is None
async def failing_search(self, query: str) -> list[SearchResult]:
raise ScrapeError("site indisponible")
monkeypatch.setattr("app.scrapers.sources.vostfree.VostfreeScraper.search", failing_search)
r = await client.post("/api/admin/sources/vostfree/health", cookies=admin_cookies)
data = r.json()
assert r.status_code == 200 and data["healthy"] is False and data["detail"] == "site indisponible"
async def broken_search(self, query: str) -> list[SearchResult]:
raise RuntimeError("boom")
monkeypatch.setattr("app.scrapers.sources.vostfree.VostfreeScraper.search", broken_search)
r = await client.post("/api/admin/sources/vostfree/health", cookies=admin_cookies)
data = r.json()
assert r.status_code == 200 and data["healthy"] is False and "boom" in data["detail"]
async def test_source_toggle_unknown(client, admin_cookies):
r = await client.post(
"/api/admin/sources/inconnue/toggle", json={"enabled": True}, cookies=admin_cookies
)
assert r.status_code == 404
async def test_source_url_override(client, admin_cookies):
from app.scrapers.base import get_source
r = await client.get("/api/admin/sources", cookies=admin_cookies)
src = next(s for s in r.json() if s["name"] == "vostfree")
default = src["default_base_url"]
assert src["base_url"] == default and src["overridden"] is False
# source inconnue → 404
r = await client.put("/api/admin/sources/inconnue/url", json={"url": "https://x.org"}, cookies=admin_cookies)
assert r.status_code == 404
# URL invalide → 422
r = await client.put("/api/admin/sources/vostfree/url", json={"url": "pas-une-url"}, cookies=admin_cookies)
assert r.status_code == 422
# changement de domaine (slash final toléré) → appliqué à l'instance + persisté
r = await client.put("/api/admin/sources/vostfree/url", json={"url": "https://exemple.org/"},
cookies=admin_cookies)
assert r.status_code == 200
data = r.json()
assert data["base_url"] == "https://exemple.org" and data["overridden"] is True
assert get_source("vostfree").base_url == "https://exemple.org"
r = await client.get("/api/admin/sources", cookies=admin_cookies)
src = next(s for s in r.json() if s["name"] == "vostfree")
assert src["base_url"] == "https://exemple.org" and src["overridden"] is True
# URL vide → retour au défaut du code
r = await client.put("/api/admin/sources/vostfree/url", json={"url": ""}, cookies=admin_cookies)
data = r.json()
assert data["base_url"] == default and data["overridden"] is False
assert get_source("vostfree").base_url == default