================================================================================ TESTS UNITAIRES — Agent Immobilier ReAct Fichier : tests/test_tools.py Exécution : pytest tests/test_tools.py -v Total : 76 tests | 7 classes | 0 échec ================================================================================ LÉGENDE [N] Cas nominal — fonctionnement attendu sur des données valides [L] Cas limite — valeurs aux bornes, seuils, zéros, très grandes valeurs [E] Cas d'erreur — champs manquants, types invalides, JSON malformé, SQL dangereux ================================================================================ CLASSE 1 — TestAnalyzeProperty (tools/analyze_property.py) Objectif : calcul du prix au m² à partir du prix et de la surface d'un bien. Formule : prix_m2 = round(prix / surface, 2) ================================================================================ [N] test_calcul_prix_m2_basique Entrée : prix=200000, surface=50 Attendu : prix_m2 == 4000.0 [N] test_resultat_contient_tous_les_champs Entrée : prix=200000, surface=50, ville="Toulouse", type_bien="Appartement" Attendu : dict contient ville, type_bien, surface_m2, prix_total, prix_m2 [N] test_arrondi_deux_decimales Entrée : prix=150000, surface=7 Attendu : prix_m2 == round(150000/7, 2) == 21428.57 [N] test_valeurs_par_defaut_ville_type Entrée : prix=100000, surface=30 (sans ville ni type_bien) Attendu : ville == "inconnue", type_bien == "inconnu" [L] test_surface_un_m2 Entrée : prix=50000, surface=1 (surface minimale valide) Attendu : prix_m2 == 50000.0, pas d'erreur [L] test_prix_tres_eleve Entrée : prix=5000000, surface=200 Attendu : prix_m2 == 25000.0, "erreur" absent du résultat [E] test_erreur_prix_manquant Entrée : {"surface": 50} (champ obligatoire absent) Attendu : "erreur" dans le résultat [E] test_erreur_surface_manquante Entrée : {"prix": 200000} (champ obligatoire absent) Attendu : "erreur" dans le résultat [E] test_erreur_surface_zero Entrée : surface=0 (division par zéro) Attendu : "erreur" dans le résultat [E] test_erreur_surface_negative Entrée : surface=-10 Attendu : "erreur" dans le résultat [E] test_erreur_parametre_non_json Entrée : "je veux analyser un appartement" (texte libre) Attendu : "erreur" dans le résultat [E] test_erreur_parametre_vide Entrée : "" (chaîne vide) Attendu : "erreur" dans le résultat ================================================================================ CLASSE 2 — TestCalculateProfitability (tools/calculate_profitability.py) Objectif : calcul de la rentabilité locative brute et nette. Formules : brute = (loyer_annuel / prix) * 100 nette = ((loyer_annuel - charges - taxe - frais) / prix) * 100 ================================================================================ [N] test_rentabilite_brute_sans_charges Entrée : prix=200000, loyer_mensuel=800 Attendu : brute == 4.8 %, nette == 4.8 % (sans charges : brute = nette) [N] test_loyer_annuel_est_mensuel_fois_12 Entrée : prix=200000, loyer_mensuel=650 Attendu : loyer_annuel == 7800 (650 × 12) [N] test_rentabilite_nette_avec_toutes_charges Entrée : prix=200000, loyer=800, charges=1200, taxe=800, frais=600 Attendu : brute == 4.8 %, nette == 3.5 %, revenu_net == 7000.0 [N] test_rentabilite_elevee Entrée : prix=100000, loyer_mensuel=2000 Attendu : brute == 24.0 % [L] test_charges_superieures_au_loyer_nette_negative Entrée : prix=100000, loyer=300, charges=5000 Attendu : rentabilite_nette_pct < 0 [L] test_charges_optionnelles_par_defaut_zero Entrée : prix=100000, loyer=500 (sans charges, taxe, frais) Attendu : charges_annuelles == 0, taxe_fonciere == 0, frais_gestion == 0 [E] test_erreur_prix_manquant Entrée : {"loyer_mensuel": 800} Attendu : "erreur" dans le résultat [E] test_erreur_loyer_manquant Entrée : {"prix": 200000} Attendu : "erreur" dans le résultat [E] test_erreur_prix_zero Entrée : prix=0 (division par zéro) Attendu : "erreur" dans le résultat [E] test_erreur_parametre_non_json Entrée : "pas du json" Attendu : "erreur" dans le résultat ================================================================================ CLASSE 3 — TestCompareWithMarket (tools/compare_with_market.py) Objectif : comparer le prix au m² d'un bien avec le prix moyen du marché. Formule : ecart_pct = round(((prix_bien - prix_marche) / prix_marche) * 100, 1) Règles : ecart_pct < -5 → "sous-évalué" ecart_pct > +5 → "surévalué" sinon → "prix marché" ================================================================================ [N] test_statut_sous_evalue Entrée : prix_bien=3000, prix_marche=4000 Attendu : statut == "sous-évalué", ecart_pct == -25.0 [N] test_statut_sureevalue Entrée : prix_bien=5000, prix_marche=4000 Attendu : statut == "surévalué", ecart_pct == 25.0 [N] test_statut_prix_marche Entrée : prix_bien=4080, prix_marche=4000 (écart=+2 %) Attendu : statut == "prix marché" [N] test_calcul_ecart_euros Entrée : prix_bien=3800, prix_marche=4000 Attendu : ecart_euros == -200.0 [L] test_seuil_exactement_moins_5pct_est_prix_marche Entrée : prix_bien=3800, prix_marche=4000 → ecart_pct == -5.0 (exact) Attendu : statut == "prix marché" (seuil strict : < -5 et non <= -5) [L] test_seuil_exactement_plus_5pct_est_prix_marche Entrée : prix_bien=4200, prix_marche=4000 → ecart_pct == +5.0 (exact) Attendu : statut == "prix marché" (seuil strict : > +5 et non >= +5) [L] test_juste_sous_moins_5pct_est_sous_evalue Entrée : prix_bien=2000, prix_marche=4000 → ecart_pct == -50.0 Attendu : statut == "sous-évalué" [L] test_juste_au_dessus_plus_5pct_est_sureevalue Entrée : prix_bien=4600, prix_marche=4000 → ecart_pct == +15.0 Attendu : statut == "surévalué" [E] test_erreur_prix_bien_manquant Entrée : {"prix_m2_marche": 4000} Attendu : "erreur" dans le résultat [E] test_erreur_prix_marche_manquant Entrée : {"prix_m2_bien": 4000} Attendu : "erreur" dans le résultat [E] test_erreur_prix_marche_zero Entrée : prix_m2_marche=0 (division par zéro) Attendu : "erreur" dans le résultat [E] test_erreur_parametre_non_json Entrée : "texte invalide" Attendu : "erreur" dans le résultat ================================================================================ CLASSE 4 — TestInvestmentScore (tools/investment_score.py) Objectif : score pondéré 0–10 selon 4 critères. Pondération : rentabilité 40 %, prix marché 30 %, demande 20 %, risque 10 % Score rentabilité (score_renta) : >= 7 % → 10.0 >= 5 % → 6 + (r - 5) × 2 >= 3 % → 3 + (r - 3) × 1.5 < 3 % → max(0, r) Niveaux du score final : >= 8 → excellent | >= 6 → bon | >= 4 → correct | >= 2 → faible | < 2 → mauvais ================================================================================ [N] test_niveau_excellent Entrée : rentabilite=8, sous-évalué, très forte, faible Attendu : score >= 8, niveau == "excellent" Calcul : 10×0.4 + 9×0.3 + 10×0.2 + 9×0.1 = 9.6 [N] test_niveau_bon Entrée : rentabilite=5, prix marché, forte, moyen Attendu : 6 <= score < 8, niveau == "bon" Calcul : 6×0.4 + 6×0.3 + 8×0.2 + 6×0.1 = 6.4 [N] test_niveau_correct Entrée : rentabilite=3, prix marché, moyenne, moyen Attendu : 4 <= score < 6, niveau == "correct" Calcul : 3×0.4 + 6×0.3 + 5×0.2 + 6×0.1 = 4.6 [N] test_niveau_faible Entrée : rentabilite=0, prix marché, faible, moyen Attendu : 2 <= score < 4, niveau == "faible" Calcul : 0×0.4 + 6×0.3 + 2×0.2 + 6×0.1 = 2.8 [N] test_niveau_mauvais Entrée : rentabilite=0, surévalué, faible, élevé Attendu : score < 2, niveau == "mauvais" Calcul : 0×0.4 + 2×0.3 + 2×0.2 + 2×0.1 = 1.2 [L] test_seuil_renta_7_et_plus_donne_10 Entrée : rentabilite_brute=7 Attendu : details["rentabilite"] == 10.0 (palier maximal) [L] test_seuil_renta_entre_5_et_7 Entrée : rentabilite_brute=6 Attendu : details["rentabilite"] == 8.0 (6 + (6-5)×2) [L] test_seuil_renta_entre_3_et_5 Entrée : rentabilite_brute=4 Attendu : details["rentabilite"] == 4.5 (3 + (4-3)×1.5) [L] test_seuil_renta_inferieure_3 Entrée : rentabilite_brute=2 Attendu : details["rentabilite"] == 2.0 (max(0, 2)) [L] test_score_marche_sous_evalue Entrée : statut_marche="sous-évalué" Attendu : details["prix_marche"] == 9 [L] test_score_marche_prix_marche Entrée : statut_marche="prix marché" Attendu : details["prix_marche"] == 6 [L] test_score_marche_sureevalue Entrée : statut_marche="surévalué" Attendu : details["prix_marche"] == 2 [L] test_valeurs_par_defaut_parametres_manquants Entrée : "{}" (paramètres vides) Attendu : retour valide sans exception, "erreur" absent [L] test_ponderation_score_final Entrée : rentabilite=5, sous-évalué, forte, faible Attendu : score == round(6×0.40 + 9×0.30 + 8×0.20 + 9×0.10, 1) == 7.6 ================================================================================ CLASSE 5 — TestGenerateReport (tools/generate_report.py) Objectif : génération du rapport d'investissement en Markdown. ================================================================================ [N] test_format_retour Entrée : données complètes (bien, marche, financiers, score, conclusion) Attendu : result["format"] == "markdown", result["rapport"] est une str [N] test_sections_markdown_presentes Entrée : données complètes Attendu : 6 sections présentes (Property Summary, Market Analysis, Financial Indicators, Investment Score, Conclusion, Recommandation) [N] test_valeurs_bien_dans_rapport Entrée : ville="Toulouse", type_bien="Appartement", prix_total=200000 Attendu : ces valeurs apparaissent dans le texte du rapport [N] test_score_dans_rapport Entrée : score_investissement.score = 6.5 Attendu : "6.5" présent dans le rapport [L] test_valeurs_none_affichees_na Entrée : plusieurs champs à None Attendu : "N/A" apparaît dans le rapport, pas d'exception [E] test_erreur_parametre_vide Entrée : "" (chaîne vide) Attendu : "erreur" dans le résultat [E] test_erreur_parametre_non_json Entrée : "texte invalide" Attendu : "erreur" dans le résultat ================================================================================ CLASSE 6 — TestQueryDb (tools/query_db.py) Objectif : exécution de requêtes SQL SELECT sur la base DVF (immobilier.db). Base de test : 3 mutations fictives (2 Toulouse, 1 Tournefeuille). Sécurité intégrée : validation via security.valider_sql() avant exécution. ================================================================================ [N] test_select_count_retourne_toutes_lignes Requête : SELECT COUNT(*) AS n FROM mutations Attendu : result[0]["n"] == 3 [N] test_select_filtre_commune_toulouse Requête : SELECT * FROM mutations WHERE commune = 'TOULOUSE' Attendu : len(result) == 2 [N] test_select_retourne_liste_de_dicts Requête : SELECT commune, valeur_fonciere FROM mutations Attendu : list de dict avec clés "commune" et "valeur_fonciere" [N] test_select_vide_retourne_liste_vide Requête : WHERE commune = 'INEXISTANT' Attendu : result == [] [N] test_avg_prix_m2 Requête : AVG(valeur_fonciere / surface_reelle_bati) WHERE commune='TOULOUSE' Attendu : 4000.0 (200000/50 et 320000/80 → moyenne = 4000) [E] test_delete_bloque Requête : DELETE FROM mutations Attendu : "erreur" dans le résultat (non-SELECT bloqué) [E] test_insert_bloque Requête : INSERT INTO mutations ... Attendu : "erreur" dans le résultat [E] test_drop_bloque Requête : DROP TABLE mutations Attendu : "erreur" dans le résultat [E] test_injection_union_select_bloque Requête : SELECT commune FROM mutations UNION SELECT api_key FROM config Attendu : "erreur" dans le résultat (bloqué par security.valider_sql) [E] test_injection_stacked_query_bloque Requête : SELECT 1; DROP TABLE mutations Attendu : "erreur" dans le résultat (stacked query bloquée) [E] test_sql_invalide_retourne_erreur Requête : "SELEC * FORM mutations" (syntaxe invalide) Attendu : "erreur" dans le résultat ================================================================================ CLASSE 7 — TestGetLoyerData (tools/get_loyer_data.py) Objectif : interrogation de la table 'loyers' (Observatoire des Loyers 2025). Base de test : 5 lignes fictives couvrant global, Ville centre, Périphérie, Appart 2P, Ville centre + Appart 2P. ================================================================================ [N] test_sans_filtre_retourne_agregat_global Entrée : "{}" (aucun filtre) Attendu : loyer_m2_moyen == 11.7 (ligne globale, tous NULL) [N] test_filtre_zone_ville_centre Entrée : {"zone": "Ville centre"} Attendu : loyer_m2_moyen == 12.5 [N] test_filtre_zone_peripherie Entrée : {"zone": "Périphérie"} Attendu : loyer_m2_moyen == 10.5 [N] test_filtre_nombre_pieces Entrée : {"nombre_pieces": "Appart 2P"} Attendu : loyer_m2_moyen == 12.9 [N] test_filtre_combine_zone_et_pieces Entrée : {"zone": "Ville centre", "nombre_pieces": "Appart 2P"} Attendu : loyer_m2_moyen == 13.3 [N] test_source_presente_dans_retour Entrée : "{}" Attendu : "source" dans le résultat, "Observatoire" dans la valeur [L] test_parametre_vide_chaine Entrée : "" (chaîne vide → params={} → agrégat global) Attendu : loyer_m2_moyen == 11.7 [L] test_parametre_non_json_fallback_global Entrée : "texte invalide" (JSON invalide → params={} → agrégat global) Attendu : loyer_m2_moyen == 11.7 [L] test_zone_inexistante_retourne_liste_vide Entrée : {"zone": "Zone Inconnue"} (inexistant) Attendu : "erreur" absent, "resultats" présent et vide [] Note : comportement réel du tool — pas d'exception, liste vide signalée [L] test_retour_une_ligne_est_un_dict_plat Entrée : {"zone": "Ville centre"} → 1 résultat Attendu : "resultats" absent, "loyer_m2_moyen" directement dans le dict ================================================================================ RÉSULTAT GLOBAL : 76 tests — 76 PASSED — 0 FAILED ================================================================================