From 718581b954a4474a366697f2e2a2d2fc1227f072 Mon Sep 17 00:00:00 2001 From: jpgiannetti Date: Mon, 2 Feb 2026 22:32:29 +0100 Subject: [PATCH] =?UTF-8?q?feat(gherkin):=20ajouter=20features=20API=20pou?= =?UTF-8?q?r=20jauges=20d'int=C3=A9r=C3=AAt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../degradation-temporelle.feature | 296 +++++++++++ .../interest-gauges/evolution-jauges.feature | 481 ++++++++++++++++++ .../interest-gauges/jauge-initiale.feature | 337 ++++++++++++ 3 files changed, 1114 insertions(+) create mode 100644 features/api/interest-gauges/degradation-temporelle.feature create mode 100644 features/api/interest-gauges/evolution-jauges.feature create mode 100644 features/api/interest-gauges/jauge-initiale.feature diff --git a/features/api/interest-gauges/degradation-temporelle.feature b/features/api/interest-gauges/degradation-temporelle.feature new file mode 100644 index 0000000..2134123 --- /dev/null +++ b/features/api/interest-gauges/degradation-temporelle.feature @@ -0,0 +1,296 @@ +# language: fr +Fonctionnalité: API - Pas de dégradation temporelle + En tant qu'API backend + Je veux que les jauges n'évoluent que par les actions utilisateur + Afin d'avoir un comportement prévisible sans automatisme caché + + Contexte: + Étant donné que l'API RoadWave est disponible + Et que la base de données PostgreSQL est accessible + + Scénario: API ne dégrade jamais les jauges automatiquement + Étant donné qu'un utilisateur "user123" a une jauge "Économie" à 80% en base + Et que la colonne updated_at = "2026-01-01T10:00:00Z" + Quand 30 jours s'écoulent sans activité + Et je GET /api/v1/users/user123/interest-gauges le "2026-02-01T10:00:00Z" + Alors le statut de réponse est 200 + Et la jauge "Économie" est toujours à 80% + Et aucune dégradation temporelle n'a été appliquée + Et la colonne updated_at n'a pas changé + + Scénario: Aucun cron job de dégradation n'existe + Étant donné que le système vérifie les tâches cron planifiées + Quand je liste tous les cron jobs du backend + Alors aucun job nommé "degrade_interest_gauges" n'existe + Et aucun job périodique ne modifie la table interest_gauges + Et aucune ressource CPU n'est consommée pour la dégradation + + Scénario: API GET jauges après 6 mois d'inactivité + Étant donné qu'un utilisateur "user123" a les jauges suivantes: + | catégorie | niveau | updated_at | + | Automobile | 75% | 2025-08-01T10:00:00 | + | Voyage | 60% | 2025-08-01T10:00:00 | + | Musique | 45% | 2025-08-01T10:00:00 | + Et qu'il ne se connecte pas pendant 6 mois + Quand je GET /api/v1/users/user123/interest-gauges le "2026-02-01T10:00:00Z" + Alors le statut de réponse est 200 + Et les jauges sont exactement les mêmes: + | catégorie | niveau | + | Automobile | 75% | + | Voyage | 60% | + | Musique | 45% | + Et aucune modification n'a été appliquée + + Scénario: Évolution par actions utilisateur uniquement + Étant donné qu'un utilisateur "user123" a une jauge "Économie" à 80% + Et qu'il skip 50 contenus "Économie" en 1 an + Quand je calcule l'évolution via les events + Alors la jauge "Économie" descend via les skips: + | action | impact | nouveau_niveau | + | 50 skips × -0.5%| -25% | 55% | + Et la dégradation vient des actions, pas du temps + Et la colonne updated_at reflète la date du dernier skip + + Scénario: API POST réinitialiser centres d'intérêt + Étant donné qu'un utilisateur "user123" a des jauges personnalisées: + | catégorie | niveau | + | Automobile | 75% | + | Voyage | 60% | + | Économie | 34% | + | Sport | 88% | + Quand je POST /api/v1/users/user123/interest-gauges/reset + """json + { + "confirmation": true + } + """ + Alors le statut de réponse est 200 + Et la réponse contient: + """json + { + "message": "Vos centres d'intérêt ont été réinitialisés", + "previous_gauges_saved": true, + "new_gauges": { + "all_categories": 50 + } + } + """ + Et en base de données, toutes les jauges de "user123" sont à 50%: + | catégorie | niveau | + | Automobile | 50 | + | Voyage | 50 | + | Économie | 50 | + | Sport | 50 | + | Musique | 50 | + | Technologie | 50 | + | Santé | 50 | + | Politique | 50 | + | Cryptomonnaie | 50 | + | Culture générale | 50 | + | Famille | 50 | + | Amour | 50 | + + Scénario: API sauvegarde jauges précédentes avant réinitialisation + Étant donné qu'un utilisateur "user123" a des jauges personnalisées + Quand je POST /api/v1/users/user123/interest-gauges/reset + """json + { + "confirmation": true + } + """ + Alors le statut de réponse est 200 + Et une ligne est insérée dans interest_gauges_snapshots: + | user_id | snapshot_type | snapshot_date | gauges_json | + | user123 | manual_reset | 2026-02-02T14:00:00 | {"Automobile": 75, "Voyage": 60, ...} | + Et l'historique permet de restaurer si besoin + + Scénario: API rejette réinitialisation sans confirmation + Quand je POST /api/v1/users/user123/interest-gauges/reset + """json + { + "confirmation": false + } + """ + Alors le statut de réponse est 400 + Et la réponse contient: + """json + { + "error": "CONFIRMATION_REQUIRED", + "message": "Vous devez confirmer la réinitialisation" + } + """ + Et les jauges ne sont pas modifiées + + Scénario: API recommandations après réinitialisation + Étant donné qu'un utilisateur "user123" avait "Économie" à 85% + Et qu'il réinitialise ses jauges (toutes à 50%) + Quand je POST /api/v1/recommendations + """json + { + "user_id": "user123", + "latitude": 48.8566, + "longitude": 2.3522, + "limit": 10 + } + """ + Alors le statut de réponse est 200 + Et les recommandations utilisent 50% pour toutes les catégories + Et plus aucun biais "Économie" n'est appliqué + Et la géolocalisation redevient le critère principal + + Scénario: Historique d'écoute conservé après réinitialisation + Étant donné qu'un utilisateur "user123" a écouté 500 contenus + Et que la table listening_history contient 500 lignes pour "user123" + Quand je POST /api/v1/users/user123/interest-gauges/reset + Alors le statut de réponse est 200 + Et la table listening_history conserve toujours les 500 lignes + Et aucune donnée d'historique n'est supprimée + Et l'utilisateur peut toujours consulter ses anciens contenus écoutés + + Scénario: API GET historique après réinitialisation + Étant donné qu'un utilisateur "user123" a réinitialisé ses jauges + Quand je GET /api/v1/users/user123/listening-history + Alors le statut de réponse est 200 + Et la réponse contient tous les anciens contenus écoutés + Et l'historique est intact + + Scénario: API enregistre timestamp réinitialisation + Étant donné qu'un utilisateur "user123" réinitialise ses jauges + Quand je POST /api/v1/users/user123/interest-gauges/reset + Alors en base de données, la table users est mise à jour: + | user_id | interest_gauges_reset_at | reset_count | + | user123 | 2026-02-02T14:00:00Z | 1 | + Et un compteur permet de tracker les réinitialisations multiples + + Scénario: API permet réinitialisations multiples + Étant donné qu'un utilisateur "user123" a déjà réinitialisé une fois + Quand je POST /api/v1/users/user123/interest-gauges/reset une 2ème fois + Alors le statut de réponse est 200 + Et le reset_count passe à 2 + Et toutes les jauges reviennent à 50% + + Scénario: API n'envoie jamais de suggestion de réinitialisation + Étant donné qu'un utilisateur "user123" n'a pas utilisé l'app depuis 1 an + Quand je GET /api/v1/users/user123/notifications + Alors le statut de réponse est 200 + Et aucune notification "Réinitialiser vos centres d'intérêt" n'est présente + Et le système ne suggère jamais de réinitialisation automatique + + Scénario: API GET statistiques respect historique utilisateur + Étant donné qu'un utilisateur "user123" aime "Cryptomonnaie" depuis 2 ans + Et que sa jauge est à 90% + Et qu'il n'a pas écouté de contenu "Cryptomonnaie" depuis 6 mois + Quand je GET /api/v1/users/user123/interest-gauges + Alors le statut de réponse est 200 + Et la jauge "Cryptomonnaie" est toujours à 90% + Et la réponse contient: + """json + { + "gauges": [ + { + "category": "Cryptomonnaie", + "level": 90, + "last_updated": "2025-08-02T10:00:00Z", + "days_since_update": 183, + "preserved": true + } + ] + } + """ + Et le système respecte l'historique des goûts + + Scénario: API métrique temps écoulé sans modifier la jauge + Étant donné qu'un utilisateur "user123" a une jauge "Sport" à 65% + Et que la dernière modification date de 90 jours + Quand je GET /api/v1/users/user123/interest-gauges/sport + Alors le statut de réponse est 200 + Et la réponse contient: + """json + { + "category": "Sport", + "level": 65, + "last_updated": "2025-11-03T12:00:00Z", + "days_since_update": 90 + } + """ + Et la métrique days_since_update est informative uniquement + Et elle ne modifie jamais la jauge + + Scénario: Requête SQL n'utilise jamais de calcul temporel + Étant donné que je trace les requêtes SQL du backend + Quand je GET /api/v1/users/user123/interest-gauges + Alors la requête SQL exécutée est: + """sql + SELECT category, level, updated_at + FROM interest_gauges + WHERE user_id = $1 + """ + Et aucune clause WHERE avec date/timestamp n'est présente + Et aucune fonction NOW(), CURRENT_TIMESTAMP, ou DATEDIFF n'est utilisée + Et le calcul est minimal (simple SELECT) + + Scénario: API coût CPU minimal - pas de calcul de dates + Étant donné que 10000 utilisateurs consultent leurs jauges simultanément + Quand les requêtes /api/v1/users/{id}/interest-gauges sont exécutées + Alors aucun calcul de date n'est nécessaire + Et aucun appel à time.Now() ou time.Since() n'est fait + Et le coût CPU par requête est < 1ms + Et aucune dégradation de performance liée aux dates + + Scénario: API pas de risque de bug fuseau horaire + Étant donné qu'aucune logique temporelle n'existe + Quand un utilisateur change de fuseau horaire (Paris → Tokyo) + Alors ses jauges ne sont pas affectées + Et aucun bug de conversion UTC/local ne peut survenir + Et le comportement reste déterministe + + Scénario: Audit log réinitialisation manuelle + Étant donné qu'un utilisateur "user123" réinitialise ses jauges + Quand je POST /api/v1/users/user123/interest-gauges/reset + Alors une ligne est insérée dans audit_log: + | user_id | action | timestamp | details | + | user123 | interest_gauges_reset | 2026-02-02T14:00:00 | {"previous_snapshot_id": 42} | + Et l'audit permet de tracer toutes les réinitialisations + + Scénario: API empêche réinitialisation trop fréquente + Étant donné qu'un utilisateur "user123" a réinitialisé il y a 10 minutes + Quand je POST /api/v1/users/user123/interest-gauges/reset + Alors le statut de réponse est 429 + Et la réponse contient: + """json + { + "error": "RATE_LIMIT_EXCEEDED", + "message": "Vous ne pouvez réinitialiser qu'une fois par heure", + "retry_after_seconds": 3000 + } + """ + + Scénario: API documentation endpoints réinitialisation + Quand je GET /api/v1/openapi.json + Alors le endpoint POST /api/v1/users/{id}/interest-gauges/reset est documenté: + """yaml + /users/{id}/interest-gauges/reset: + post: + summary: Réinitialise toutes les jauges à 50% + description: | + Remet toutes les jauges d'intérêt à leur valeur par défaut (50%). + Cette action est manuelle et requiert une confirmation. + Les jauges précédentes sont sauvegardées. + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + confirmation: + type: boolean + description: Doit être true + responses: + 200: + description: Réinitialisation réussie + 400: + description: Confirmation manquante + 429: + description: Trop de réinitialisations + """ diff --git a/features/api/interest-gauges/evolution-jauges.feature b/features/api/interest-gauges/evolution-jauges.feature new file mode 100644 index 0000000..23016d5 --- /dev/null +++ b/features/api/interest-gauges/evolution-jauges.feature @@ -0,0 +1,481 @@ +# language: fr +Fonctionnalité: API - Évolution des jauges d'intérêt + En tant qu'API backend + Je veux calculer et persister les évolutions de jauges d'intérêt + Afin d'alimenter l'algorithme de recommandation + + Contexte: + Étant donné que l'API RoadWave est disponible + Et que la base de données PostgreSQL est accessible + Et qu'un utilisateur "user123" existe avec token JWT valide + + Scénario: API calcule like automatique renforcé (≥80% écoute) + Étant donné que l'utilisateur "user123" a une jauge "Automobile" à 45% en base + Et qu'un contenu "content456" de 5 minutes est tagué "Automobile" + Quand je POST /api/v1/listening-events + """json + { + "user_id": "user123", + "content_id": "content456", + "listened_duration_seconds": 270, + "total_duration_seconds": 300, + "completion_percentage": 90 + } + """ + Alors le statut de réponse est 201 + Et la réponse contient: + """json + { + "like_type": "automatic_reinforced", + "gauge_updates": [ + { + "category": "Automobile", + "previous_value": 45, + "delta": 2, + "new_value": 47 + } + ] + } + """ + Et en base de données, la jauge "Automobile" de "user123" est à 47% + + Scénario: API calcule like automatique standard (30-79% écoute) + Étant donné que l'utilisateur "user123" a une jauge "Voyage" à 60% en base + Et qu'un contenu "content789" de 10 minutes est tagué "Voyage" + Quand je POST /api/v1/listening-events + """json + { + "user_id": "user123", + "content_id": "content789", + "listened_duration_seconds": 300, + "total_duration_seconds": 600, + "completion_percentage": 50 + } + """ + Alors le statut de réponse est 201 + Et la réponse contient: + """json + { + "like_type": "automatic_standard", + "gauge_updates": [ + { + "category": "Voyage", + "previous_value": 60, + "delta": 1, + "new_value": 61 + } + ] + } + """ + Et en base de données, la jauge "Voyage" de "user123" est à 61% + + Scénario: API applique like manuel explicite (+2%) + Étant donné que l'utilisateur "user123" a une jauge "Musique" à 55% en base + Et qu'un contenu "content999" est tagué "Musique" + Quand je POST /api/v1/likes + """json + { + "user_id": "user123", + "content_id": "content999", + "like_type": "manual" + } + """ + Alors le statut de réponse est 201 + Et la réponse contient: + """json + { + "like_id": "", + "gauge_updates": [ + { + "category": "Musique", + "previous_value": 55, + "delta": 2, + "new_value": 57 + } + ] + } + """ + Et en base de données, la jauge "Musique" de "user123" est à 57% + + Scénario: API applique unlike (retire like manuel) + Étant donné que l'utilisateur "user123" a une jauge "Sport" à 57% en base + Et qu'il a liké manuellement le contenu "content888" tagué "Sport" + Et que le like a l'ID "like_abc123" + Quand je DELETE /api/v1/likes/like_abc123 + Alors le statut de réponse est 204 + Et en base de données, la jauge "Sport" de "user123" est à 55% + Et le like "like_abc123" est supprimé de la table likes + + Scénario: API refuse unlike d'un like automatique + Étant donné que l'utilisateur "user123" a écouté un contenu à 90% + Et qu'il a reçu un like automatique "like_auto456" + Quand je DELETE /api/v1/likes/like_auto456 + Alors le statut de réponse est 403 + Et la réponse contient: + """json + { + "error": "CANNOT_UNLIKE_AUTOMATIC", + "message": "Les likes automatiques ne peuvent pas être retirés" + } + """ + Et en base de données, le like "like_auto456" existe toujours + + Scénario: API cumule like automatique + like manuel + Étant donné que l'utilisateur "user123" a une jauge "Technologie" à 50% en base + Et qu'un contenu "content777" de 10 minutes est tagué "Technologie" + Quand je POST /api/v1/listening-events + """json + { + "user_id": "user123", + "content_id": "content777", + "completion_percentage": 50 + } + """ + Alors la jauge "Technologie" passe à 51% (+1% auto) + Quand je POST ensuite /api/v1/likes + """json + { + "user_id": "user123", + "content_id": "content777", + "like_type": "manual" + } + """ + Alors la jauge "Technologie" passe à 53% (+2% manuel) + Et le delta total est de 3% + + Scénario: API applique bonus abonnement créateur (+5% tous tags) + Étant donné que l'utilisateur "user123" a les jauges suivantes: + | catégorie | niveau | + | Automobile | 50% | + | Technologie | 45% | + Et qu'un créateur "creator456" publie des contenus tagués "Automobile" et "Technologie" + Quand je POST /api/v1/subscriptions + """json + { + "user_id": "user123", + "creator_id": "creator456" + } + """ + Alors le statut de réponse est 201 + Et en base de données: + | catégorie | niveau | + | Automobile | 55% | + | Technologie | 50% | + Et l'abonnement est créé avec bonus appliqué + + Scénario: API retire bonus désabonnement créateur (-5% tous tags) + Étant donné que l'utilisateur "user123" a les jauges suivantes: + | catégorie | niveau | + | Voyage | 65% | + | Culture | 58% | + Et qu'il est abonné au créateur "creator789" qui publie "Voyage" et "Culture" + Quand je DELETE /api/v1/subscriptions/creator789 + Alors le statut de réponse est 204 + Et en base de données: + | catégorie | niveau | + | Voyage | 60% | + | Culture | 53% | + + Scénario: API applique pénalité skip rapide (<10s) + Étant donné que l'utilisateur "user123" a une jauge "Politique" à 50% en base + Et qu'un contenu "content555" de 300 secondes est tagué "Politique" + Quand je POST /api/v1/skip-events + """json + { + "user_id": "user123", + "content_id": "content555", + "listened_duration_seconds": 5, + "total_duration_seconds": 300 + } + """ + Alors le statut de réponse est 201 + Et la réponse contient: + """json + { + "skip_type": "early", + "gauge_updates": [ + { + "category": "Politique", + "previous_value": 50, + "delta": -0.5, + "new_value": 49.5 + } + ] + } + """ + Et en base de données, la jauge "Politique" de "user123" est à 49.5% + + Scénario: API n'applique pas de pénalité pour skip ≥10s et <30% + Étant donné que l'utilisateur "user123" a une jauge "Économie" à 60% en base + Et qu'un contenu "content333" de 600 secondes est tagué "Économie" + Quand je POST /api/v1/skip-events + """json + { + "user_id": "user123", + "content_id": "content333", + "listened_duration_seconds": 120, + "total_duration_seconds": 600, + "completion_percentage": 20 + } + """ + Alors le statut de réponse est 201 + Et la réponse contient: + """json + { + "skip_type": "neutral", + "gauge_updates": [] + } + """ + Et en base de données, la jauge "Économie" de "user123" reste à 60% + + Scénario: API n'applique pas de pénalité pour skip ≥30% + Étant donné que l'utilisateur "user123" a une jauge "Sport" à 55% en base + Et qu'un contenu "content222" de 600 secondes est tagué "Sport" + Quand je POST /api/v1/skip-events + """json + { + "user_id": "user123", + "content_id": "content222", + "listened_duration_seconds": 300, + "total_duration_seconds": 600, + "completion_percentage": 50 + } + """ + Alors le statut de réponse est 201 + Et la réponse contient: + """json + { + "skip_type": "late", + "gauge_updates": [] + } + """ + Et en base de données, la jauge "Sport" de "user123" reste à 55% + + Scénario: API applique évolution sur plusieurs tags simultanément + Étant donné que l'utilisateur "user123" a les jauges suivantes: + | catégorie | niveau | + | Automobile | 45% | + | Voyage | 60% | + Et qu'un contenu "content111" est tagué "Automobile" et "Voyage" + Quand je POST /api/v1/listening-events + """json + { + "user_id": "user123", + "content_id": "content111", + "completion_percentage": 90 + } + """ + Alors le statut de réponse est 201 + Et la réponse contient: + """json + { + "like_type": "automatic_reinforced", + "gauge_updates": [ + { + "category": "Automobile", + "previous_value": 45, + "delta": 2, + "new_value": 47 + }, + { + "category": "Voyage", + "previous_value": 60, + "delta": 2, + "new_value": 62 + } + ] + } + """ + Et en base de données: + | catégorie | niveau | + | Automobile | 47% | + | Voyage | 62% | + + Scénario: API respecte la borne maximum 100% + Étant donné que l'utilisateur "user123" a une jauge "Cryptomonnaie" à 99% en base + Et qu'un contenu "content_crypto" est tagué "Cryptomonnaie" + Quand je POST /api/v1/listening-events + """json + { + "user_id": "user123", + "content_id": "content_crypto", + "completion_percentage": 95 + } + """ + Alors le statut de réponse est 201 + Et la réponse contient: + """json + { + "like_type": "automatic_reinforced", + "gauge_updates": [ + { + "category": "Cryptomonnaie", + "previous_value": 99, + "delta": 2, + "new_value": 100, + "capped": true + } + ] + } + """ + Et en base de données, la jauge "Cryptomonnaie" de "user123" est à 100% + Et la jauge n'a pas dépassé 100% + + Scénario: API respecte la borne minimum 0% + Étant donné que l'utilisateur "user123" a une jauge "Politique" à 0.3% en base + Et qu'un contenu "content_pol" est tagué "Politique" + Quand je POST /api/v1/skip-events + """json + { + "user_id": "user123", + "content_id": "content_pol", + "listened_duration_seconds": 3, + "total_duration_seconds": 300 + } + """ + Alors le statut de réponse est 201 + Et la réponse contient: + """json + { + "skip_type": "early", + "gauge_updates": [ + { + "category": "Politique", + "previous_value": 0.3, + "delta": -0.5, + "new_value": 0, + "capped": true + } + ] + } + """ + Et en base de données, la jauge "Politique" de "user123" est à 0% + Et la jauge n'est pas devenue négative + + Scénario: API respecte borne minimum lors désabonnement + Étant donné que l'utilisateur "user123" a une jauge "Économie" à 3% en base + Et qu'il est abonné au créateur "creator_eco" qui publie "Économie" + Quand je DELETE /api/v1/subscriptions/creator_eco + Alors le statut de réponse est 204 + Et en base de données, la jauge "Économie" de "user123" est à 0% (et non -2%) + + Scénario: API GET retourne toutes les jauges utilisateur + Étant donné que l'utilisateur "user123" a les jauges suivantes en base: + | catégorie | niveau | + | Automobile | 67% | + | Voyage | 82% | + | Économie | 34% | + | Sport | 50% | + | Musique | 45% | + | Technologie | 71% | + Quand je GET /api/v1/users/user123/interest-gauges + Alors le statut de réponse est 200 + Et la réponse contient les 12 catégories avec leurs niveaux: + """json + { + "user_id": "user123", + "gauges": [ + {"category": "Automobile", "level": 67}, + {"category": "Voyage", "level": 82}, + {"category": "Économie", "level": 34}, + {"category": "Sport", "level": 50}, + {"category": "Musique", "level": 45}, + {"category": "Technologie", "level": 71} + ] + } + """ + + Scénario: API calcule évolution immédiate (pas de batch différé) + Étant donné que l'utilisateur "user123" a une jauge "Voyage" à 50% en base + Quand je POST /api/v1/listening-events à 12:00:00 + """json + { + "user_id": "user123", + "content_id": "content_travel", + "completion_percentage": 85 + } + """ + Alors le statut de réponse est 201 + Quand je GET /api/v1/users/user123/interest-gauges à 12:00:01 (1 seconde après) + Alors la jauge "Voyage" est à 52% + Et la mise à jour est visible immédiatement + + Scénario: API rejette token JWT invalide + Quand je POST /api/v1/listening-events sans token JWT + Alors le statut de réponse est 401 + Et la réponse contient: + """json + { + "error": "UNAUTHORIZED", + "message": "Token JWT manquant ou invalide" + } + """ + + Scénario: API valide format des données d'entrée + Quand je POST /api/v1/listening-events + """json + { + "user_id": "user123", + "content_id": "content456", + "completion_percentage": 150 + } + """ + Alors le statut de réponse est 400 + Et la réponse contient: + """json + { + "error": "VALIDATION_ERROR", + "message": "completion_percentage doit être entre 0 et 100" + } + """ + + Scénario: API gère contenu avec tags inexistants en base + Étant donné qu'un contenu "content_new" est tagué "NouvelleCategorie" (non encore en base) + Quand je POST /api/v1/listening-events + """json + { + "user_id": "user123", + "content_id": "content_new", + "completion_percentage": 90 + } + """ + Alors le statut de réponse est 201 + Et une nouvelle ligne est créée dans la table interest_gauges: + | user_id | category | level | + | user123 | NouvelleCategorie | 52 | + Et l'initialisation démarre à 50% + 2% de like auto = 52% + + Scénario: API persiste historique des modifications de jauges + Étant donné que l'utilisateur "user123" a une jauge "Sport" à 50% + Quand je POST /api/v1/listening-events qui applique +2% + Alors une ligne est insérée dans interest_gauge_history: + | user_id | category | previous_value | delta | new_value | event_type | event_id | timestamp | + | user123 | Sport | 50 | 2 | 52 | listening_event| | 2026-02-02T12:00:00 | + Et cet historique permet d'auditer les évolutions + + Scénario: API retourne métriques d'évolution utilisateur + Étant donné que l'utilisateur "user123" a un historique d'évolution en base + Quand je GET /api/v1/users/user123/interest-gauges/evolution?since=7d + Alors le statut de réponse est 200 + Et la réponse contient: + """json + { + "period": "7d", + "evolution": [ + { + "category": "Automobile", + "start_value": 60, + "end_value": 67, + "delta": 7, + "events_count": 15 + }, + { + "category": "Voyage", + "start_value": 80, + "end_value": 82, + "delta": 2, + "events_count": 3 + } + ] + } + """ diff --git a/features/api/interest-gauges/jauge-initiale.feature b/features/api/interest-gauges/jauge-initiale.feature new file mode 100644 index 0000000..b3bbb77 --- /dev/null +++ b/features/api/interest-gauges/jauge-initiale.feature @@ -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": "", + "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": "", + "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