feat(gherkin): ajouter features API pour jauges d'intérêt
Création de 3 features Gherkin pour les tests backend des jauges d'intérêt: - evolution-jauges.feature: Tests API pour calculs de jauges (likes auto/manuels, abonnements créateurs, skips), persistence PostgreSQL, bornes 0-100%, cache Redis - jauge-initiale.feature: Tests API pour initialisation à 50% lors inscription, questionnaire optionnel post-MVP, recommandations cold start - degradation-temporelle.feature: Tests API confirmant absence de dégradation automatique, réinitialisation manuelle avec snapshot et audit log Complète les features UI existantes avec les aspects techniques backend.
This commit is contained in:
337
features/api/interest-gauges/jauge-initiale.feature
Normal file
337
features/api/interest-gauges/jauge-initiale.feature
Normal file
@@ -0,0 +1,337 @@
|
||||
# language: fr
|
||||
Fonctionnalité: API - Jauge initiale et cold start
|
||||
En tant qu'API backend
|
||||
Je veux initialiser toutes les jauges à 50% lors de l'inscription
|
||||
Afin de garantir un démarrage neutre et équitable
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est disponible
|
||||
Et que la base de données PostgreSQL est accessible
|
||||
|
||||
Scénario: API initialise toutes les jauges à 50% lors inscription
|
||||
Quand je POST /api/v1/auth/register
|
||||
"""json
|
||||
{
|
||||
"email": "nouveau@example.com",
|
||||
"password": "SecureP@ss123",
|
||||
"birth_date": "1990-01-15",
|
||||
"username": "nouveau_user"
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 201
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"user_id": "<uuid>",
|
||||
"email": "nouveau@example.com",
|
||||
"username": "nouveau_user"
|
||||
}
|
||||
"""
|
||||
Et en base de données, la table interest_gauges contient 12 lignes pour ce user_id:
|
||||
| category | level |
|
||||
| Automobile | 50 |
|
||||
| Voyage | 50 |
|
||||
| Famille | 50 |
|
||||
| Amour | 50 |
|
||||
| Musique | 50 |
|
||||
| Économie | 50 |
|
||||
| Cryptomonnaie | 50 |
|
||||
| Politique | 50 |
|
||||
| Culture générale | 50 |
|
||||
| Sport | 50 |
|
||||
| Technologie | 50 |
|
||||
| Santé | 50 |
|
||||
|
||||
Scénario: API retourne les 12 catégories disponibles
|
||||
Quand je GET /api/v1/interest-gauges/categories
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"categories": [
|
||||
{"id": "automobile", "name": "Automobile", "icon": "car"},
|
||||
{"id": "voyage", "name": "Voyage", "icon": "plane"},
|
||||
{"id": "famille", "name": "Famille", "icon": "users"},
|
||||
{"id": "amour", "name": "Amour", "icon": "heart"},
|
||||
{"id": "musique", "name": "Musique", "icon": "music"},
|
||||
{"id": "economie", "name": "Économie", "icon": "chart"},
|
||||
{"id": "cryptomonnaie", "name": "Cryptomonnaie", "icon": "bitcoin"},
|
||||
{"id": "politique", "name": "Politique", "icon": "landmark"},
|
||||
{"id": "culture-generale", "name": "Culture générale", "icon": "book"},
|
||||
{"id": "sport", "name": "Sport", "icon": "running"},
|
||||
{"id": "technologie", "name": "Technologie", "icon": "cpu"},
|
||||
{"id": "sante", "name": "Santé", "icon": "heart-pulse"}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API GET retourne jauges utilisateur nouvellement inscrit
|
||||
Étant donné qu'un utilisateur "user_new" vient de s'inscrire
|
||||
Quand je GET /api/v1/users/user_new/interest-gauges
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient 12 jauges toutes à 50%:
|
||||
"""json
|
||||
{
|
||||
"user_id": "user_new",
|
||||
"gauges": [
|
||||
{"category": "Automobile", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Voyage", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Famille", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Amour", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Musique", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Économie", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Cryptomonnaie", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Politique", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Culture générale", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Sport", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Technologie", "level": 50, "evolution_since_signup": 0},
|
||||
{"category": "Santé", "level": 50, "evolution_since_signup": 0}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API calcule recommandations avec jauges à 50% - pas de biais
|
||||
Étant donné qu'un utilisateur "user_new" vient de s'inscrire
|
||||
Et que toutes ses jauges sont à 50%
|
||||
Et qu'il est à la position GPS (48.8566, 2.3522) - Paris
|
||||
Quand je POST /api/v1/recommendations
|
||||
"""json
|
||||
{
|
||||
"user_id": "user_new",
|
||||
"latitude": 48.8566,
|
||||
"longitude": 2.3522,
|
||||
"limit": 10
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient 10 contenus
|
||||
Et le scoring est basé uniquement sur:
|
||||
| critère | poids |
|
||||
| Distance géographique| 100% |
|
||||
| Intérêts (50% égal) | 0% |
|
||||
Et aucune catégorie n'a d'avantage initial
|
||||
|
||||
Scénario: API permet ajout de nouvelles catégories
|
||||
Étant donné qu'un admin ajoute une nouvelle catégorie "Gastronomie"
|
||||
Quand je POST /api/v1/admin/interest-gauges/categories
|
||||
"""json
|
||||
{
|
||||
"id": "gastronomie",
|
||||
"name": "Gastronomie",
|
||||
"icon": "utensils"
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 201
|
||||
Et pour tous les utilisateurs existants:
|
||||
| action |
|
||||
| Une ligne est créée dans interest_gauges |
|
||||
| category = "Gastronomie" |
|
||||
| level = 50 |
|
||||
Et les nouveaux utilisateurs auront aussi cette catégorie à 50%
|
||||
|
||||
Scénario: API calcule évolution depuis inscription
|
||||
Étant donné qu'un utilisateur "user123" s'est inscrit il y a 30 jours
|
||||
Et qu'il a les jauges suivantes en base:
|
||||
| catégorie | niveau | initial |
|
||||
| Automobile | 67% | 50% |
|
||||
| Voyage | 82% | 50% |
|
||||
| Économie | 34% | 50% |
|
||||
| Sport | 50% | 50% |
|
||||
Quand je GET /api/v1/users/user123/interest-gauges
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"user_id": "user123",
|
||||
"signup_date": "2026-01-03T10:00:00Z",
|
||||
"gauges": [
|
||||
{
|
||||
"category": "Automobile",
|
||||
"level": 67,
|
||||
"evolution_since_signup": 17
|
||||
},
|
||||
{
|
||||
"category": "Voyage",
|
||||
"level": 82,
|
||||
"evolution_since_signup": 32
|
||||
},
|
||||
{
|
||||
"category": "Économie",
|
||||
"level": 34,
|
||||
"evolution_since_signup": -16
|
||||
},
|
||||
{
|
||||
"category": "Sport",
|
||||
"level": 50,
|
||||
"evolution_since_signup": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API transaction atomique lors inscription
|
||||
Quand je POST /api/v1/auth/register
|
||||
"""json
|
||||
{
|
||||
"email": "test@example.com",
|
||||
"password": "SecureP@ss123",
|
||||
"birth_date": "1995-03-20",
|
||||
"username": "test_user"
|
||||
}
|
||||
"""
|
||||
Alors l'insertion en base de données est atomique:
|
||||
| action |
|
||||
| INSERT INTO users |
|
||||
| INSERT INTO interest_gauges (12 lignes) |
|
||||
| Tout ou rien (transaction) |
|
||||
Et si une erreur survient, aucune donnée partielle n'est créée
|
||||
|
||||
Scénario: API rollback si initialisation jauges échoue
|
||||
Étant donné que la table interest_gauges a une contrainte violée
|
||||
Quand je POST /api/v1/auth/register avec données valides
|
||||
Alors le statut de réponse est 500
|
||||
Et aucune ligne n'est créée dans la table users
|
||||
Et aucune ligne n'est créée dans la table interest_gauges
|
||||
Et la transaction est rollback complètement
|
||||
|
||||
Scénario: API POST questionnaire optionnel post-MVP
|
||||
Étant donné qu'un utilisateur "user123" a écouté 3 contenus
|
||||
Et qu'il décide de remplir le questionnaire optionnel
|
||||
Quand je POST /api/v1/users/user123/interest-gauges/quick-setup
|
||||
"""json
|
||||
{
|
||||
"selected_categories": ["Automobile", "Voyage", "Sport"]
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et en base de données:
|
||||
| catégorie | niveau |
|
||||
| Automobile | 70 |
|
||||
| Voyage | 70 |
|
||||
| Sport | 70 |
|
||||
| Musique | 30 |
|
||||
| Économie | 30 |
|
||||
| Cryptomonnaie | 30 |
|
||||
| Politique | 30 |
|
||||
| Culture générale | 30 |
|
||||
| Technologie | 30 |
|
||||
| Santé | 30 |
|
||||
| Famille | 30 |
|
||||
| Amour | 30 |
|
||||
Et un flag quick_setup_completed = true est enregistré
|
||||
|
||||
Scénario: API rejette questionnaire optionnel si déjà rempli
|
||||
Étant donné qu'un utilisateur "user123" a déjà rempli le questionnaire optionnel
|
||||
Quand je POST /api/v1/users/user123/interest-gauges/quick-setup
|
||||
"""json
|
||||
{
|
||||
"selected_categories": ["Musique", "Technologie"]
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 409
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"error": "QUICK_SETUP_ALREADY_COMPLETED",
|
||||
"message": "Le questionnaire a déjà été rempli"
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API valide nombre de catégories sélectionnées
|
||||
Quand je POST /api/v1/users/user123/interest-gauges/quick-setup
|
||||
"""json
|
||||
{
|
||||
"selected_categories": ["Automobile"]
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 400
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"error": "VALIDATION_ERROR",
|
||||
"message": "Vous devez sélectionner entre 2 et 5 catégories"
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API déterministe - deux users identiques
|
||||
Étant donné que l'utilisateur "userA" s'inscrit à 10:00:00
|
||||
Et que l'utilisateur "userB" s'inscrit à 10:00:01
|
||||
Quand je GET /api/v1/users/userA/interest-gauges
|
||||
Et je GET /api/v1/users/userB/interest-gauges
|
||||
Alors les deux réponses ont des jauges identiques (toutes à 50%)
|
||||
Et le comportement est déterministe
|
||||
|
||||
Scénario: API retourne statistiques cold start
|
||||
Étant donné qu'un utilisateur "user_new" vient de s'inscrire
|
||||
Quand je GET /api/v1/users/user_new/stats
|
||||
Alors le statut de réponse est 200
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"user_id": "user_new",
|
||||
"signup_date": "2026-02-02T14:00:00Z",
|
||||
"total_listened_content": 0,
|
||||
"gauges_summary": {
|
||||
"all_at_default": true,
|
||||
"default_value": 50,
|
||||
"total_categories": 12,
|
||||
"personalization_level": "none"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API recommandations cold start priorité géo
|
||||
Étant donné qu'un utilisateur "user_new" vient de s'inscrire
|
||||
Et qu'il est à Paris avec 100 contenus disponibles dans un rayon de 5km
|
||||
Et que ces contenus ont des catégories variées
|
||||
Quand je POST /api/v1/recommendations
|
||||
"""json
|
||||
{
|
||||
"user_id": "user_new",
|
||||
"latitude": 48.8566,
|
||||
"longitude": 2.3522,
|
||||
"limit": 10
|
||||
}
|
||||
"""
|
||||
Alors le statut de réponse est 200
|
||||
Et les 10 contenus retournés sont les plus proches géographiquement
|
||||
Et les catégories sont variées (pas de biais intérêts)
|
||||
Et la réponse contient:
|
||||
"""json
|
||||
{
|
||||
"recommendations": [
|
||||
{
|
||||
"content_id": "<uuid>",
|
||||
"distance_meters": 150,
|
||||
"interest_match": 50,
|
||||
"final_score": 0.95
|
||||
}
|
||||
],
|
||||
"cold_start": true,
|
||||
"personalization_level": "none"
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: API index optimisé pour lecture jauges
|
||||
Étant donné que la table interest_gauges a 1 million de lignes
|
||||
Quand je GET /api/v1/users/user123/interest-gauges
|
||||
Alors la requête SQL utilise l'index (user_id, category)
|
||||
Et le temps de réponse est < 50ms
|
||||
Et le plan d'exécution confirme l'utilisation de l'index
|
||||
|
||||
Scénario: API cache jauges utilisateur en Redis
|
||||
Étant donné qu'un utilisateur "user123" a ses jauges en base
|
||||
Quand je GET /api/v1/users/user123/interest-gauges
|
||||
Alors le backend vérifie d'abord Redis avec clé "user:user123:gauges"
|
||||
Et si absent, lit depuis PostgreSQL
|
||||
Et met en cache dans Redis avec TTL = 300 secondes
|
||||
Quand je GET à nouveau dans les 5 minutes
|
||||
Alors la réponse vient directement de Redis
|
||||
Et aucune requête PostgreSQL n'est faite
|
||||
|
||||
Scénario: API invalide cache Redis lors mise à jour jauge
|
||||
Étant donné que les jauges de "user123" sont en cache Redis
|
||||
Quand je POST /api/v1/listening-events qui modifie une jauge
|
||||
Alors le cache Redis "user:user123:gauges" est supprimé
|
||||
Et le prochain GET recharge depuis PostgreSQL
|
||||
Et remet en cache avec nouvelles valeurs
|
||||
Reference in New Issue
Block a user