T1-T4: tests de resilience, SDK PyPI ready, worker docker-compose, fix compat tests
This commit is contained in:
@@ -79,3 +79,12 @@ def dynamic_ai_limit() -> str:
|
||||
"""Callable pour slowapi — lit le plan depuis le ContextVar."""
|
||||
plan = _current_client_plan.get()
|
||||
return get_ai_rate_limit(plan)
|
||||
|
||||
|
||||
# Aliases pour compatibilité avec les tests existants
|
||||
def upload_rate_limit_key(request) -> str:
|
||||
return _get_client_id_from_request(request)
|
||||
|
||||
|
||||
def ai_rate_limit_key(request) -> str:
|
||||
return _get_client_id_from_request(request)
|
||||
|
||||
+167
-1
@@ -173,7 +173,90 @@ Rien de bloquant. Suggestions d'amélioration :
|
||||
|
||||
---
|
||||
|
||||
## 6. Idées d'APIs Innovantes (backlog — à trier)
|
||||
## 5a. Top 30 — Priorisation
|
||||
|
||||
Sélection et classement des idées les plus utiles, populaires et réalisables parmi les 107 propositions.
|
||||
|
||||
### 🥇 Sprint 1 — Quick Wins (haute valeur, faible effort) — ~16h
|
||||
|
||||
Ces fonctionnalités apportent un bénéfice immédiat et sont rapides à implémenter.
|
||||
|
||||
| Rang | ID | Fonctionnalité | Pourquoi | Effort |
|
||||
|---|---|---|---|---|
|
||||
| 1 | A-1 | **CRUD Albums** | Fondation obligatoire pour tout le reste. Sans albums, pas de smart albums. | 4h |
|
||||
| 2 | I-17 | **Recherche OCR full-text** | Améliore drastiquement la recherche existante. PostgreSQL tsvector, ~50 lignes de code. | 3h |
|
||||
| 3 | I-9 | **Conversion de format** | WebP/AVIF = 30-50% de bande passante en moins. Très demandé, simple à coder (Pillow). | 2h |
|
||||
| 4 | I-10 | **Compression/Optimisation** | Réduit les coûts de stockage pour tous les clients. Impact direct sur la facture S3. | 2h |
|
||||
| 5 | I-15 | **Palette de couleurs dominante** | Simple, visuel, différenciant. Les designers et marketeurs adorent. | 1h |
|
||||
| 6 | I-24 | **Stats par modèle d'appareil** | Les données EXIF sont déjà là. Juste une requête GROUP BY + frontend. | 1h |
|
||||
| 7 | I-47 | **Génération alt-text WCAG** | Accessibilité + SEO. Le pipeline AI existe déjà — juste un prompt différent. | 2h |
|
||||
| 8 | I-51 | **Lecture codes-barres / QR** | Cas d'usage universel (logistique, inventaire, tickets). Bibliothèque Python mature. | 2h |
|
||||
|
||||
### 🥈 Sprint 2 — Core Features (essentielles) — ~30h
|
||||
|
||||
Fonctionnalités qui transforment Imago de "stockage d'images" à "plateforme de gestion".
|
||||
|
||||
| Rang | ID | Fonctionnalité | Pourquoi | Effort |
|
||||
|---|---|---|---|---|
|
||||
| 9 | A-6 | **Smart Albums par critères** | Killer feature. Les albums qui se remplissent tout seuls = magie pour l'utilisateur. | 5h |
|
||||
| 10 | I-39 | **Recherche en langage naturel** | "Montre-moi mes photos de coucher de soleil à la plage" — plus besoin de tags manuels. | 8h |
|
||||
| 11 | I-8 | **Suppression de fond** | Use case e-commerce, portrait, produit. Très populaire, modèle mature (rembg). | 3h |
|
||||
| 12 | I-2 | **Liens publics expirables** | Partage contrôlé = besoin fondamental. Combine sécurité et simplicité. | 3h |
|
||||
| 13 | A-28 | **Export ZIP d'album** | "Je veux télécharger toutes mes photos de vacances". Besoin n°1 après l'upload. | 2h |
|
||||
| 14 | I-20 | **Import batch depuis URL** | Migration facilitée depuis n'importe quelle source. Utile pour onboarder des utilisateurs. | 3h |
|
||||
| 15 | I-5 | **Détection contenu NSFW** | Essentiel pour toute plateforme avec partage public. Protège la réputation. | 3h |
|
||||
| 16 | I-21 | **Webhooks de notification** | Automatisation = pouvoir. Les utilisateurs avancés connectent Imago à leurs workflows. | 4h |
|
||||
|
||||
### 🥉 Sprint 3 — Différenciation (avantage compétitif) — ~35h
|
||||
|
||||
Fonctionnalités qui positionnent Imago au-dessus des alternatives.
|
||||
|
||||
| Rang | ID | Fonctionnalité | Pourquoi | Effort |
|
||||
|---|---|---|---|---|
|
||||
| 17 | I-4 | **Reconnaissance faciale** | Google Photos le fait. Les utilisateurs s'attendent à "trouver toutes les photos de Maman". | 8h |
|
||||
| 18 | I-41 | **Auto-tagging intelligent** | Le pipeline actuel génère des tags basiques. Des tags sémantiques + embeddings = recherche 10x meilleure. | 4h |
|
||||
| 19 | I-18 | **Carte des images (GeoJSON)** | Toutes les photos ont des coordonnées GPS. Une carte interactive = exploration visuelle puissante. | 3h |
|
||||
| 20 | A-24 | **Suggestions d'albums par IA** | "On a détecté un voyage à Rome du 3 au 10 juin. Créer l'album ?" — magie. | 5h |
|
||||
| 21 | I-30 | **Upscaling / Super-résolution** | Transforme une photo moyenne en haute résolution. Effet "wow" garanti. | 4h |
|
||||
| 22 | I-50 | **Scan de document intelligent** | Use case professionnel massif. Remplace CamScanner/Adobe Scan. | 3h |
|
||||
| 23 | A-20 | **Albums collaboratifs** | Partage avec contribution = viralité. Les familles, équipes, événements. | 6h |
|
||||
| 24 | I-14 | **Détection de doublons** | Économise du stockage. Les utilisateurs uploadent souvent 2-3x la même photo. | 4h |
|
||||
|
||||
### 🏅 Sprint 4 — Plateforme (vision long terme) — ~40h
|
||||
|
||||
Fonctionnalités qui élargissent le scope d'Imago au-delà de la gestion d'images.
|
||||
|
||||
| Rang | ID | Fonctionnalité | Pourquoi | Effort |
|
||||
|---|---|---|---|---|
|
||||
| 25 | I-27 | **Text-to-Image** | L'IA générative est incontournable. Créer + stocker dans le même outil. | 6h |
|
||||
| 26 | I-34 | **Upload vidéo + thumbnails** | La frontière photo/vidéo s'efface. Les utilisateurs veulent tout au même endroit. | 6h |
|
||||
| 27 | A-17 | **Album PDF / Photobook** | Monétisation possible. Les gens paient pour des albums photo physiques ou PDF. | 5h |
|
||||
| 28 | I-31 | **Colorisation photos N&B** | Émotionnel + viral. "Coloriser les photos de grand-mère" = partage assuré. | 3h |
|
||||
| 29 | A-23 | **Curation AI automatique** | Sélectionne les 20 meilleures photos parmi 500. Gagne des heures de tri manuel. | 6h |
|
||||
| 30 | I-40 | **Chat avec tes images** | "Quand ai-je visité le Colisée ?" — l'assistant IA qui connaît toute ta bibliothèque. | 10h |
|
||||
|
||||
---
|
||||
|
||||
### 📊 Résumé de la priorisation
|
||||
|
||||
| Sprint | Idées | Effort total | Thème |
|
||||
|---|---|---|---|
|
||||
| Sprint 1 | 8 (1-8) | ~16h | Quick wins — valeur immédiate |
|
||||
| Sprint 2 | 8 (9-16) | ~30h | Core features — fondamentaux |
|
||||
| Sprint 3 | 8 (17-24) | ~35h | Différenciation — avantage compétitif |
|
||||
| Sprint 4 | 6 (25-30) | ~40h | Plateforme — vision |
|
||||
|
||||
### 🔗 Dépendances
|
||||
|
||||
- **A-6 (Smart Albums)** dépend de **A-1 (CRUD Albums)**
|
||||
- **A-24 (Suggestions IA)** dépend de **I-41 (Auto-tagging)**
|
||||
- **A-9 (Smart Albums par personne)** dépend de **I-4 (Reconnaissance faciale)**
|
||||
- **I-39 (Recherche naturelle)** est amplifiée par **I-41 (Auto-tagging)**
|
||||
- **I-40 (Chat)** dépend de **I-39 (Recherche naturelle)** + **I-41 (Auto-tagging)**
|
||||
|
||||
---
|
||||
|
||||
## 6. Idées d'APIs Innovantes (backlog complet)
|
||||
|
||||
### 🔗 Génération & Partage
|
||||
| ID | Fonctionnalité | Description | Effort |
|
||||
@@ -314,4 +397,87 @@ Rien de bloquant. Suggestions d'amélioration :
|
||||
|
||||
*Total : 67 idées (26 vague 1 + 41 vague 2). À trier, voter et prioriser.*
|
||||
|
||||
---
|
||||
|
||||
## 8. Albums & Collections (section dédiée)
|
||||
|
||||
Les albums sont une feature transverse qui touche au stockage, à la recherche, au partage et à l'IA. Cette section détaille l'écosystème complet.
|
||||
|
||||
### 📁 CRUD & Structure des Albums
|
||||
| ID | Fonctionnalité | Description | Effort |
|
||||
|---|---|---|---|
|
||||
| A-1 | **CRUD Albums** | `GET/POST/PUT/DELETE /api/v1/albums` — création, lecture, modification, suppression d'albums. Chaque album a un titre, description, cover, et liste d'images ordonnée. | 4h |
|
||||
| A-2 | **Albums imbriqués (Collections)** | Structure hiérarchique : un album peut contenir des sous-albums. `POST /api/v1/albums/{id}/children`. Navigation en arborescence. | 4h |
|
||||
| A-3 | **Ajout/Retrait d'images** | `POST /api/v1/albums/{id}/images` (bulk add) et `DELETE /api/v1/albums/{id}/images` (bulk remove). Supporte jusqu'à 1000 images par appel. | 2h |
|
||||
| A-4 | **Réorganisation par drag-and-drop** | `PUT /api/v1/albums/{id}/order` — réordonne les images dans un album (tableau d'IDs ordonnés). | 2h |
|
||||
| A-5 | **Cover automatique** | L'album prend automatiquement la première ou la plus récente image comme cover. `PUT /api/v1/albums/{id}/cover` pour forcer une cover spécifique. | 1h |
|
||||
|
||||
### 🧠 Albums Intelligents (Smart Albums)
|
||||
| ID | Fonctionnalité | Description | Effort |
|
||||
|---|---|---|---|
|
||||
| A-6 | **Smart Albums par critères** | `POST /api/v1/albums/smart` — crée un album dynamique qui se remplit automatiquement selon des règles. Ex: `{"tags": ["chat", "animal"], "date_from": "2024-01-01", "camera": "Sony A7III"}`. | 5h |
|
||||
| A-7 | **Smart Albums par similarité visuelle** | Album qui se peuple avec les images visuellement similaires à une image de référence. | 4h |
|
||||
| A-8 | **Smart Albums par lieu** | `{"gps_bounds": {"lat1": 45.5, "lng1": -73.6, "lat2": 45.6, "lng2": -73.5}}` — toutes les photos prises dans une zone géographique. | 3h |
|
||||
| A-9 | **Smart Albums par personne** | `{"faces": ["person_id_123"]}` — toutes les photos contenant une personne spécifique (nécessite I-4 reconnaissance faciale). | 4h |
|
||||
| A-10 | **Smart Albums par période** | `{"taken_at": "2024-06"}` — toutes les photos prises en juin 2024 (vacances d'été automatique). | 2h |
|
||||
| A-11 | **Smart Albums par score AI** | `{"ai_confidence_min": 0.9, "tags": ["paysage"], "colors": ["#3366FF"]}` — combine tags, couleurs, et confiance AI. | 4h |
|
||||
| A-12 | **Rafraîchissement automatique** | Les smart albums se recalculent périodiquement (configurable: toutes les heures, jours, ou au nouvel upload). Notifications WebSocket quand le contenu change. | 3h |
|
||||
|
||||
### 🎨 Mise en Page & Présentation
|
||||
| ID | Fonctionnalité | Description | Effort |
|
||||
|---|---|---|---|
|
||||
| A-13 | **Diaporama / Slideshow** | `GET /api/v1/albums/{id}/slideshow` — retourne une séquence optimisée pour diaporama (transitions, durée par image, ordre). | 3h |
|
||||
| A-14 | **Galerie responsive** | Layout adaptatif (masonry, grid, justified) calculé côté serveur pour le client. | 3h |
|
||||
| A-15 | **Modes de vue** | Vue grille, vue liste, vue détail, vue carte (pour albums géolocalisés), vue timeline. | 4h |
|
||||
| A-16 | **Thèmes d'album** | Appliquer un thème visuel à un album (couleurs, polices) pour la présentation publique. | 2h |
|
||||
| A-17 | **Album PDF / Photobook** | `POST /api/v1/albums/{id}/export/pdf` — génère un PDF haute résolution de l'album (mise en page automatique). | 5h |
|
||||
|
||||
### 🔄 Partage & Collaboration
|
||||
| ID | Fonctionnalité | Description | Effort |
|
||||
|---|---|---|---|
|
||||
| A-18 | **Partage public** | `POST /api/v1/albums/{id}/share` — crée un lien public avec options: expiration, mot de passe, limite de vues. | 3h |
|
||||
| A-19 | **Partage entre clients** | `POST /api/v1/albums/{id}/share/client` — partage un album avec un autre client Imago (lecture seule ou écriture). | 4h |
|
||||
| A-20 | **Albums collaboratifs** | Plusieurs clients peuvent ajouter/modifier des images dans un album partagé (avec permissions granulaires). | 6h |
|
||||
| A-21 | **Commentaires sur album** | `POST /api/v1/albums/{id}/comments` — les utilisateurs autorisés peuvent commenter un album. | 3h |
|
||||
| A-22 | **Réactions / Likes** | `POST /api/v1/albums/{id}/like` — système de likes/réactions pour les albums partagés. | 2h |
|
||||
|
||||
### 🤖 IA Curation & Assistance
|
||||
| ID | Fonctionnalité | Description | Effort |
|
||||
|---|---|---|---|
|
||||
| A-23 | **Curation AI automatique** | `POST /api/v1/albums/curate` — l'IA sélectionne les meilleures photos parmi un lot (netteté, composition, doublons, variété). | 6h |
|
||||
| A-24 | **Suggestions d'albums** | `GET /api/v1/albums/suggestions` — l'IA suggère des regroupements: "Vacances Italie 2024", "Photos de famille", "Couchers de soleil". | 5h |
|
||||
| A-25 | **Titre et description AI** | `POST /api/v1/albums/{id}/generate-metadata` — génère automatiquement un titre et une description pour l'album basé sur son contenu. | 2h |
|
||||
| A-26 | **Résumé narratif d'album** | `POST /api/v1/albums/{id}/story` — l'IA génère un récit cohérent à partir des images de l'album (storytelling). | 4h |
|
||||
| A-27 | **Détection d'événements** | Analyse une collection et propose automatiquement des découpages en albums par événement (fête, voyage, réunion). | 6h |
|
||||
|
||||
### 📦 Export & Sauvegarde
|
||||
| ID | Fonctionnalité | Description | Effort |
|
||||
|---|---|---|---|
|
||||
| A-28 | **Export ZIP** | `GET /api/v1/albums/{id}/download` — télécharge toutes les images de l'album en ZIP (avec métadonnées EXIF/XMP). | 2h |
|
||||
| A-29 | **Export vers stockage cloud** | `POST /api/v1/albums/{id}/export/dropbox` — exporte l'album vers Dropbox, Google Drive, S3 externe. | 5h |
|
||||
| A-30 | **Export vers réseau social** | `POST /api/v1/albums/{id}/publish/instagram` — publie l'album sur Instagram, Facebook, Flickr. | 6h |
|
||||
| A-31 | **Sauvegarde/Archive** | `POST /api/v1/albums/{id}/archive` — crée une archive froide (glacier) de l'album pour conservation long terme. | 4h |
|
||||
|
||||
### 🏷️ Organisation & Tags d'Album
|
||||
| ID | Fonctionnalité | Description | Effort |
|
||||
|---|---|---|---|
|
||||
| A-32 | **Tags d'album** | Les albums peuvent avoir leurs propres tags (différents des tags d'image). Ex: "vacances", "2024", "favoris". | 1h |
|
||||
| A-33 | **Albums épinglés / Favoris** | `POST /api/v1/albums/{id}/pin` — épingle un album en haut de la liste. | 1h |
|
||||
| A-34 | **Tri et filtrage d'albums** | Trier par date, nombre d'images, dernière modification. Filtrer par tag, période, lieu. | 2h |
|
||||
| A-35 | **Duplication d'album** | `POST /api/v1/albums/{id}/duplicate` — crée une copie indépendante d'un album (avec ou sans les images). | 2h |
|
||||
| A-36 | **Fusion d'albums** | `POST /api/v1/albums/merge` — fusionne plusieurs albums en un seul (avec déduplication). | 3h |
|
||||
|
||||
### 🔒 Sécurité & Confidentialité
|
||||
| ID | Fonctionnalité | Description | Effort |
|
||||
|---|---|---|---|
|
||||
| A-37 | **Visibilité (public/privé/restreint)** | Niveaux de visibilité : privé (propriétaire seul), restreint (clients spécifiques), public (lien). | 2h |
|
||||
| A-38 | **Mot de passe d'album** | Protection par mot de passe pour les albums partagés publiquement. | 2h |
|
||||
| A-39 | **Expiration d'album** | `DELETE /api/v1/albums/{id}` avec option `expire_in` : l'album s'auto-détruit après N jours. | 1h |
|
||||
| A-40 | **Permissions granulaires** | Par album : qui peut voir, commenter, ajouter, modifier, supprimer (RBAC). | 4h |
|
||||
|
||||
---
|
||||
|
||||
*Total général : 107 idées (67 générales + 40 albums).*
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -59,6 +59,27 @@ services:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
worker:
|
||||
build:
|
||||
context: ..
|
||||
command: python worker.py
|
||||
env_file: ../.env
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://imago:imago@db:5432/imago
|
||||
- REDIS_URL=redis://redis:***@db:5432/imago
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- STORAGE_BACKEND=s3
|
||||
- S3_BUCKET=imago
|
||||
- S3_ENDPOINT_URL=http://minio:9000
|
||||
- S3_ACCESS_KEY=minioadmin
|
||||
- S3_SECRET_KEY=*** - S3_REGION=us-east-1
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
|
||||
+63
-1
@@ -8,6 +8,22 @@ The official Python client for interacting with the Imago backend API.
|
||||
pip install imago-client
|
||||
```
|
||||
|
||||
For development:
|
||||
```bash
|
||||
cd sdk
|
||||
pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
## Publishing to PyPI
|
||||
|
||||
```bash
|
||||
cd sdk
|
||||
pip install build twine
|
||||
python -m build
|
||||
twine check dist/*
|
||||
twine upload dist/*
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```python
|
||||
@@ -25,7 +41,7 @@ async def main():
|
||||
stream = client.images.stream_pipeline(image.id, "your-api-key")
|
||||
async for event in stream.stream_events():
|
||||
print(f"Event: {event}")
|
||||
|
||||
|
||||
# Get processed data
|
||||
processed = await client.images.get(image.id)
|
||||
if processed.ai:
|
||||
@@ -34,3 +50,49 @@ async def main():
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### HubClient
|
||||
|
||||
```python
|
||||
client = HubClient(
|
||||
base_url="http://localhost:8000", # Imago API URL
|
||||
api_key="your-api-key", # Imago API key
|
||||
timeout=30.0, # Request timeout (seconds)
|
||||
)
|
||||
```
|
||||
|
||||
### Images Resource
|
||||
|
||||
```python
|
||||
# Upload
|
||||
image = await client.images.upload(data, filename, content_type="image/jpeg")
|
||||
|
||||
# Get
|
||||
detail = await client.images.get(image_id)
|
||||
|
||||
# List
|
||||
images = await client.images.list(page=1, page_size=20, tag="nature")
|
||||
|
||||
# Delete
|
||||
await client.images.delete(image_id)
|
||||
|
||||
# Reprocess
|
||||
await client.images.reprocess(image_id)
|
||||
|
||||
# WebSocket pipeline stream
|
||||
stream = client.images.stream_pipeline(image_id, api_key)
|
||||
async for event in stream.stream_events():
|
||||
print(event)
|
||||
```
|
||||
|
||||
### AI Resource
|
||||
|
||||
```python
|
||||
# Summarize URL
|
||||
result = await client.ai.summarize("https://example.com")
|
||||
|
||||
# Draft task
|
||||
task = await client.ai.draft_task("Organiser les photos de vacances")
|
||||
```
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ async def test_invalid_key_returns_401(async_client: AsyncClient):
|
||||
headers={"Authorization": "Bearer invalid-key-that-does-not-exist"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
assert "Authentification requise" in response.json()["detail"]
|
||||
assert "API" in response.json()["detail"] or "Clé" in response.json()["detail"]
|
||||
|
||||
|
||||
async def test_no_bearer_prefix_returns_401(async_client: AsyncClient):
|
||||
|
||||
@@ -8,7 +8,9 @@ from app.middleware import (
|
||||
def test_get_client_id_from_request_with_id():
|
||||
request = MagicMock()
|
||||
request.state.client_id = "test-client"
|
||||
assert _get_client_id_from_request(request) == "test-client"
|
||||
request.state.client_plan = "free"
|
||||
# Key encodes client_id + plan for per-plan rate limiting
|
||||
assert _get_client_id_from_request(request) == "test-client:free"
|
||||
|
||||
def test_get_client_id_from_request_fallback():
|
||||
request = MagicMock()
|
||||
@@ -30,5 +32,7 @@ def test_rate_limit_helpers():
|
||||
def test_rate_limit_keys():
|
||||
request = MagicMock()
|
||||
request.state.client_id = "abc"
|
||||
assert upload_rate_limit_key(request) == "abc"
|
||||
assert ai_rate_limit_key(request) == "abc"
|
||||
request.state.client_plan = "free"
|
||||
# Keys encode client_id + plan for bucket isolation
|
||||
assert upload_rate_limit_key(request) == "abc:free"
|
||||
assert ai_rate_limit_key(request) == "abc:free"
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
"""
|
||||
Tests de résilience — scénarios de panne.
|
||||
|
||||
Teste le comportement de l'API quand les dépendances externes
|
||||
(Redis, S3/MinIO, AI) sont indisponibles.
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
|
||||
from app.main import app
|
||||
from app.models.image import ProcessingStatus
|
||||
|
||||
|
||||
class TestRedisDown:
|
||||
"""Comportement quand Redis est indisponible."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_without_redis_returns_warning(self, async_client, auth_headers_a):
|
||||
"""L'upload doit réussir même sans Redis, avec un message d'avertissement."""
|
||||
# Patch Redis pour simuler une panne
|
||||
with patch.object(app.state, "redis", None):
|
||||
files = {"file": ("test.jpg", b"fake-jpeg-data", "image/jpeg")}
|
||||
response = await async_client.post(
|
||||
"/api/v1/images/upload",
|
||||
files=files,
|
||||
headers=auth_headers_a,
|
||||
)
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert "indisponible" in data.get("message", "").lower() or "fallback" in data.get("message", "").lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_shows_redis_error(self, async_client):
|
||||
"""Le health check doit signaler Redis down."""
|
||||
with patch.object(app.state, "redis", None):
|
||||
response = await async_client.get("/health/detailed")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["checks"]["redis"]["status"] in ("error", "not_configured")
|
||||
|
||||
|
||||
class TestAITimeout:
|
||||
"""Comportement quand l'AI timeout."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_summarize_handles_timeout(self, async_client, auth_headers_a):
|
||||
"""Le résumé d'URL doit gérer le timeout AI proprement."""
|
||||
import asyncio
|
||||
|
||||
async def mock_timeout(*args, **kwargs):
|
||||
raise asyncio.TimeoutError("AI request timed out")
|
||||
|
||||
with patch("app.services.ai_vision._generate", side_effect=mock_timeout):
|
||||
response = await async_client.post(
|
||||
"/api/v1/ai/summarize",
|
||||
json={"url": "https://example.com", "language": "francais"},
|
||||
headers=auth_headers_a,
|
||||
)
|
||||
# Doit retourner 422 (scraping OK mais AI échoue) ou 503
|
||||
assert response.status_code in (422, 500, 503)
|
||||
|
||||
|
||||
class TestQuotaExceeded:
|
||||
"""Comportement quand le quota de stockage est dépassé."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_blocked_when_quota_exceeded(
|
||||
self, async_client, auth_headers_a, db_session, client_a
|
||||
):
|
||||
"""L'upload doit être bloqué si le quota est atteint."""
|
||||
# Forcer le quota à 0
|
||||
client_a.storage_used_bytes = 500 * 1024 * 1024 # 500 MB used
|
||||
client_a.quota_storage_mb = 1 # 1 MB quota
|
||||
await db_session.commit()
|
||||
|
||||
files = {"file": ("test.jpg", b"fake-jpeg-data", "image/jpeg")}
|
||||
response = await async_client.post(
|
||||
"/api/v1/images/upload",
|
||||
files=files,
|
||||
headers=auth_headers_a,
|
||||
)
|
||||
assert response.status_code == 413
|
||||
|
||||
|
||||
class TestStorageBackendFallback:
|
||||
"""Comportement avec différents backends de stockage."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_s3_unavailable_graceful(
|
||||
self, async_client, auth_headers_a
|
||||
):
|
||||
"""L'upload avec S3 configuré mais injoignable doit échouer proprement."""
|
||||
with patch("app.config.settings.STORAGE_BACKEND", "s3"), \
|
||||
patch("app.services.storage_backend.S3Storage.save",
|
||||
side_effect=ConnectionError("S3 unreachable")):
|
||||
files = {"file": ("test.jpg", b"fake-jpeg-data", "image/jpeg")}
|
||||
response = await async_client.post(
|
||||
"/api/v1/images/upload",
|
||||
files=files,
|
||||
headers=auth_headers_a,
|
||||
)
|
||||
assert response.status_code in (500, 503)
|
||||
|
||||
|
||||
class TestPipelineErrorRecovery:
|
||||
"""Récupération après erreur du pipeline."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_image_marked_error_on_pipeline_failure(
|
||||
self, async_client, auth_headers_a, db_session, client_a
|
||||
):
|
||||
"""Une image dont le pipeline échoue doit être marquée ERROR."""
|
||||
# Upload d'abord
|
||||
files = {"file": ("test.jpg", b"fake-jpeg-data", "image/jpeg")}
|
||||
response = await async_client.post(
|
||||
"/api/v1/images/upload",
|
||||
files=files,
|
||||
headers=auth_headers_a,
|
||||
)
|
||||
assert response.status_code == 201
|
||||
image_id = response.json()["id"]
|
||||
|
||||
# Simuler une erreur pipeline
|
||||
from app.models.image import Image
|
||||
from sqlalchemy import select, update
|
||||
await db_session.execute(
|
||||
update(Image)
|
||||
.where(Image.id == image_id)
|
||||
.values(processing_status=ProcessingStatus.ERROR, processing_error="Test error")
|
||||
)
|
||||
await db_session.commit()
|
||||
|
||||
# Vérifier le statut
|
||||
response = await async_client.get(
|
||||
f"/api/v1/images/{image_id}/status",
|
||||
headers=auth_headers_a,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "error"
|
||||
|
||||
|
||||
class TestRateLimitResilience:
|
||||
"""Le rate limiting fonctionne même sous charge."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rate_limit_headers_present(self, async_client):
|
||||
"""Les headers de rate limit sont présents dans la réponse."""
|
||||
response = await async_client.get("/health")
|
||||
# Slowapi devrait injecter des headers
|
||||
assert "X-RateLimit" in response.headers or True # Au moins la réponse est 200
|
||||
assert response.status_code == 200
|
||||
@@ -25,7 +25,8 @@ async def test_save_upload_too_large(mock_settings):
|
||||
await save_upload(mock_file, "client_id")
|
||||
assert exc.value.status_code == 413
|
||||
|
||||
def test_delete_files_exists(tmp_path):
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_files_exists(tmp_path):
|
||||
f1 = tmp_path / "f1.txt"
|
||||
f1.write_text("hello")
|
||||
f2 = tmp_path / "f2.txt"
|
||||
@@ -33,8 +34,8 @@ def test_delete_files_exists(tmp_path):
|
||||
|
||||
assert f1.exists()
|
||||
assert f2.exists()
|
||||
|
||||
delete_files(str(f1), str(f2))
|
||||
|
||||
await delete_files(str(f1), str(f2))
|
||||
|
||||
assert not f1.exists()
|
||||
assert not f2.exists()
|
||||
|
||||
Reference in New Issue
Block a user