diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml index d47ea1c..25cc4f3 100644 --- a/.github/workflows/documentation.yaml +++ b/.github/workflows/documentation.yaml @@ -30,9 +30,7 @@ jobs: - name: Installation des dépendances run: | python -m pip install --upgrade pip - pip install sphinx - pip install sphinx_rtd_theme --break-system-packages - pip install colorama + pip install -r requirements.txt # Étape 4 : Générer la documentation - name: Construction de la documentation (avec Sphinx) diff --git a/.github/workflows/qualite.yaml b/.github/workflows/qualite.yaml index 6e6ad7b..906d3e3 100644 --- a/.github/workflows/qualite.yaml +++ b/.github/workflows/qualite.yaml @@ -4,7 +4,7 @@ on: pull_request: branches: - main - - develop + - 'release/**' jobs: lint: @@ -25,8 +25,7 @@ jobs: - name: Installation des dépendances run: | python -m pip install --upgrade pip - pip install pylint - pip install colorama + pip install -r requirements.txt # Étape 4 : Lancement de l'analyse - name: Analyse avec Pylint (note >= 9.0 requise) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 13b33c9..d1c3aea 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -4,7 +4,7 @@ on: pull_request: branches: - main - - develop + - 'release/**' # Permissions (lecture uniquement) permissions: @@ -33,6 +33,8 @@ jobs: run: | python -m pip install --upgrade pip pip install colorama + pip install altair + pip install pandas pip install pytest pip install pytest-cov pip install pytest-mock diff --git a/README.md b/README.md index e02d302..293e1ad 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Bienvenue dans le monde de LogBuster, l'outil ultime pour analyser, décortiquer et sauver vos logs Apache des griffes du chaos. Vous avez des logs qui traînent, qui sont indéchiffrables ou tout simplement encombrants ? Pas de panique, LogBuster est là pour les attraper, les analyser et vous offrir des statistiques claires et précises, comme jamais auparavant ! +Le document de réflexion est disponible [ici](https://drive.google.com/file/d/1j_F8MizttT8etW3joW23XR5ZI5k-uNqB/view?usp=sharing). + ## 📋 Table des matières - [👻 Fonctionnalités](#-fonctionnalités) @@ -28,6 +30,8 @@ Bienvenue dans le monde de LogBuster, l'outil ultime pour analyser, décortiquer - 📄 Parsing avancé de logs Apache. - 📉 Extraire des statistiques clés. +- 🥧 Génération de graphiques camemberts. +- 🧽 Filtrer les analyses. - 🗂️ Ranger les données par catégorie. - 🧹 Indiquer les erreurs de format avec précision. - 🚚 Exporter les données en JSON. @@ -39,7 +43,7 @@ Bienvenue dans le monde de LogBuster, l'outil ultime pour analyser, décortiquer git clone https://github.com/AnthonyGuillauma/code_source cd code_source python -m venv .venv -source .venv/bin/activate # Activation de l'environnement virtuel sous Bash +source .venv/bin/activate pip install -r requirements.txt ``` @@ -48,17 +52,20 @@ pip install -r requirements.txt git clone https://github.com/AnthonyGuillauma/code_source cd code_source python -m venv .venv -.venv\Scripts\activate # Activation de l'environnement virtuel sous Windows +.venv\Scripts\activate pip install -r requirements.txt ``` ## 🛠️ Utilisation de base ``` -python app/main.py chemin_log [-s SORTIE] +python app/main.py chemin_log [-s SORTIE] [-i IP] [-c CODE_STATUT_HTTP] [--camembert CAMEMBERT] ``` - `chemin_log` : Le chemin vers le fichier de log Apache à analyser. - `-s SORTIE` (optionnel) : Le chemin où sauvegarder les résultats de l'analyse. Si non spécifié, les résultats seront sauvegardés dans un fichier `analyse-log-apache.json`. +- `-i IP` (optionnel) : Le filtre à appliquer sur les adresses IP des entrées du fichier de log. Uniquement les entrées avec cette adresse IP seront analysées. +- `-c CODE_STATUT_HTTP` (optionnel) : Le filtre à appliquer sur les code de statut http des entrées du fichier de log. Uniquement les entrées avec ce code de statut http seront analysées. +- `--camembert CAMEMBERT` (optionnel) : Active la génération de graphiques camemberts dans lors de l'analyse pour les statistiques compatibles (plus d'infos [ici](https://anthonyguillauma.github.io/code_source/#o-o-format-de-l-analyse)). ## ⚠️ Précautions diff --git a/app/analyse/analyseur_log_apache.py b/app/analyse/analyseur_log_apache.py index f00552f..cf7b727 100644 --- a/app/analyse/analyseur_log_apache.py +++ b/app/analyse/analyseur_log_apache.py @@ -2,8 +2,10 @@ Module pour l'analyse statistique d'un fichier log Apache. """ +from os.path import abspath from collections import Counter from parse.fichier_log_apache import FichierLogApache +from analyse.filtre_log_apache import FiltreLogApache class AnalyseurLogApache: @@ -17,24 +19,29 @@ class AnalyseurLogApache: les statistiques des classements (tops). """ - def __init__(self, fichier_log_apache: FichierLogApache, nombre_par_top: int = 3): + def __init__(self, + fichier_log_apache: FichierLogApache, + filtre: FiltreLogApache, + nombre_par_top: int = 3): """ Initialise un nouveau analysateur de fichier log Apache. Args: fichier_log_apache (FichierLogApache): Le fichier à analyser. + filtre (FiltreLogApache): Le filtre à appliquer dans l'analyse. Si une entrée ne + passe pas le filtre, elle ne sera pas pris en compte dans l'analyse. nombre_par_top (int): Le nombre maximal d'éléments à inclure dans les statistiques des classements (tops). Par défaut, sa valeur est égale à ``3``. Raises: - TypeError: Si l'argument ``fichier_log_apache`` n'est pas une instance - de :class:`FichierLogApache` - ou si l'argument ``nombre_par_top`` n'est pas un entier. + TypeError: Les paramètres ne sont pas du type attendu. ValueError: Si l'argument ``nombre_par_top`` est inférieur à ``0``. """ # Vérification du type des paramètres if not isinstance(fichier_log_apache, FichierLogApache): raise TypeError("La représentation du fichier doit être de type FichierLogApache.") + if not isinstance(filtre, FiltreLogApache): + raise TypeError("Le filtre à appliquer aux entrées doit être de type FiltreLogApache.") if not isinstance(nombre_par_top, int) or isinstance(nombre_par_top, bool): raise TypeError("Le nombre par top doit être un entier.") # Vérification de la valeur du paramètre @@ -43,8 +50,22 @@ def __init__(self, fichier_log_apache: FichierLogApache, nombre_par_top: int = 3 # Ajout des données self.fichier = fichier_log_apache + self.filtre = filtre self.nombre_par_top = nombre_par_top + def _get_entrees_passent_filtre(self) -> list: + """ + Retourne les entrées qui passent le filtre. + + Returns: + list: La liste des entrées qui passent le filtre. + """ + entrees_valides = [] + for entree in self.fichier.entrees: + if self.filtre.entree_passe_filtre(entree): + entrees_valides.append(entree) + return entrees_valides + def _get_repartition_elements(self, liste_elements: list, nom_elements: str, @@ -93,22 +114,29 @@ def get_analyse_complete(self) -> dict: Retourne l'analyse complète du fichier de log Apache. L'analyse suit la structure suivante : - - chemin: chemin du fichier + - chemin: chemin absolu du fichier + - total_entrees: voir :meth:`get_total_entrees` + - filtre: filtre appliqué à l'analyse - statistiques: - - requetes: - - top_urls: voir :meth:`get_top_urls` - - repartition_code_statut_http: - voir :meth:`get_total_par_code_statut_http` + - total_entrees_filtre: voir :meth:`get_total_entrees_filtre` + - requetes: + - top_urls: voir :meth:`get_top_urls` + - reponses: + - repartition_code_statut_http: voir :meth:`get_total_par_code_statut_http` Returns: dict: L'analyse sous forme d'un dictionnaire. """ return { - "chemin": self.fichier.chemin, + "chemin": abspath(self.fichier.chemin), + "total_entrees": self.get_total_entrees(), + "filtre": self.filtre.get_dict_filtre(), "statistiques": { - "total_entrees": self.get_total_entrees(), + "total_entrees_filtre": self.get_total_entrees_filtre(), "requetes": { "top_urls": self.get_top_urls(), + }, + "reponses": { "repartition_code_statut_http": self.get_total_par_code_statut_http() } } @@ -123,9 +151,19 @@ def get_total_entrees(self) -> int: """ return len(self.fichier.entrees) + def get_total_entrees_filtre(self) -> int: + """ + Retourne le nombre d'entrées qui ont passées le filtre dans le fichier. + + Returns: + int: Le nombre total d'entrées. + """ + return len(self._get_entrees_passent_filtre()) + def get_top_urls(self) -> list: """ Retourne le top :attr:`nombre_par_top` des urls les plus demandées. + Les entrées prisent en compte sont uniquement celles qui ont passées le filtre. Returns: list: Une liste de dictionnaires où chaque clé contient : @@ -136,14 +174,15 @@ def get_top_urls(self) -> list: La liste est triée dans l'ordre décroissant du nombre total d'apparitions. """ return self._get_repartition_elements( - [entree.requete.url for entree in self.fichier.entrees], + [entree.requete.url for entree in self._get_entrees_passent_filtre()], "url", True ) def get_total_par_code_statut_http(self) -> list: """ - Retourne la répartition des réponses par code de statut htpp retourné. + Retourne la répartition des réponses par code de statut http retourné. + Les entrées prisent en compte sont uniquement celles qui ont passées le filtre. Returns: list: Une liste de dictionnaires où chaque clé contient : @@ -154,6 +193,21 @@ def get_total_par_code_statut_http(self) -> list: La liste est triée dans l'ordre décroissant du nombre total d'apparitions. """ return self._get_repartition_elements( - [entree.reponse.code_statut_http for entree in self.fichier.entrees], + [entree.reponse.code_statut_http for entree in self._get_entrees_passent_filtre()], "code" ) + + def get_total_par_code_statut_http_camembert(self) -> list: + """ + Retourne la répartition des réponses par code de statut http retourné sous + un format utilisable par un camembert. + Les entrées prisent en compte sont uniquement celles qui ont passées le filtre. + + Returns: + list: Une liste de liste de deux éléments où l'index 0 est le code et l'index 1 + son total d'apparition. + """ + return [ + [stat["code"], stat["total"]] + for stat in self.get_total_par_code_statut_http() + ] diff --git a/app/analyse/filtre_log_apache.py b/app/analyse/filtre_log_apache.py new file mode 100644 index 0000000..0851381 --- /dev/null +++ b/app/analyse/filtre_log_apache.py @@ -0,0 +1,91 @@ +""" +Module pour les filtres lors d'une analyse d'un fichier log Apache. +""" + +from typing import Optional +from parse.entree_log_apache import EntreeLogApache + + +class FiltreLogApache: + """ + Représente le filtre à appliquer lors d'une analyse d'un fichier de log Apache. + + Attributes: + adresse_ip (Optional[str]): L'adresse IP que doit avoir une entrée pour + pouvoir passer le filtre. Si sa valeur est ``None``, ce filtre ne sera + pas appliqué. + code_statut_http (Optional[int]): Le code de statut http que doit avoir une entrée + pour pouvoir passer le filtre. Si sa valeur est ``None``, ce filtre ne sera + pas appliqué. + """ + + def __init__(self, filtre_adresse_ip: Optional[str], filtre_code_statut_http: Optional[int]): + """ + Initalise le filtre à appliquer lors d'une analyse. + + Args: + filtre_adresse_ip (Optional[str]): L'adresse IP que doit avoir une entrée pour + pouvoir passer le filtre. Si sa valeur est ``None``, cette vérification ne sera + pas appliqué. + filtre_code_statut_http (Optional[int]): Le code de statut http que doit + avoir une entrée pour pouvoir passer le filtre. Si sa valeur est ``None``, + cette vérification ne sera pas appliqué. + + Raises: + TypeError: Les paramètres ne sont pas du type attendu. + """ + # Vérification des paramètres + if filtre_adresse_ip is not None and not isinstance(filtre_adresse_ip, str): + raise TypeError("L'adresse IP dans un filtre doit être une chaîne de caractère.") + if (filtre_code_statut_http is not None + and not isinstance(filtre_code_statut_http, int) + or isinstance(filtre_code_statut_http, bool)): + raise TypeError("Un code de statut http dans un filtre doit être un entier.") + + # Ajout des filtres + self.adresse_ip = filtre_adresse_ip + self.code_statut_http = filtre_code_statut_http + + def entree_passe_filtre(self, entree: EntreeLogApache) -> bool: + """ + Indique si l'entrée passée en paramètre passe le filtre. + + Args: + entree (EntreeLogApache): L'entrée à vérifier. + + Returns: + bool: True si l'entrée passe le filtre, False sinon. + + Raises: + TypeError: L'``entrée`` n'est pas de type :class:`EntreeLogApache` + """ + # Vérification du paramètre + if not isinstance(entree, EntreeLogApache): + raise TypeError("L'entrée à vérifier pour le filtre doit être de type EntreeLogApache") + + # Vérification que l'entrée passe le filtre + # Application du filtre sur l'adresse IP si activé + if self.adresse_ip is not None: + if self.adresse_ip != entree.client.adresse_ip: + return False + # Application du filtre sur le code de statut http si activé + if self.code_statut_http is not None: + if self.code_statut_http != entree.reponse.code_statut_http: + return False + + return True + + def get_dict_filtre(self) -> dict: + """ + Retourne le filtre sous forme d'un dictionnaire. + Les clés représentent le champs d'une entrée et leur valeur la valeur + que doit avoir ce champs. Si la valeur d'un filtre est ``None``, cela signifie que + cette vérification n'est pas activé. + + Returns: + dict: Les filtres sous forme d'un dictionnaire. + """ + return { + "adresse_ip": self.adresse_ip, + "code_statut_http": self.code_statut_http + } diff --git a/app/cli/parseur_arguments_cli.py b/app/cli/parseur_arguments_cli.py index 795e0f2..01895ae 100644 --- a/app/cli/parseur_arguments_cli.py +++ b/app/cli/parseur_arguments_cli.py @@ -40,9 +40,26 @@ def __set_arguments(self) -> None: "-s", "--sortie", type=str, - default="./analyse-log-apache.json", - help="Fichier JSON où sera écrit l'analyse. Par défaut, un fichier avec le " - "nom 'analyse-log-apache.json' dans le repertoire courant sera crée.", + default="./", + help="Dossier où sera écrit l'analyse du fichier de log Apache. Par défaut," + "sa valeur est le répertoire d'exécution du script.", + ) + self.add_argument( + "-i", + "--ip", + type=str, + help="L'adresse IP que doivent avoir les entrées à analyser." + ) + self.add_argument( + "-c", + "--code-statut-http", + type=int, + help="Le code de statut http que doivent avoir les entrées à analyser." + ) + self.add_argument( + "--camembert", + action="store_true", + help="Active la génération d'histogrammes pour les statistiques compatibles." ) def parse_args(self, @@ -89,16 +106,11 @@ def parse_args(self, if not match(regex_chemin, arguments_parses.sortie): raise ArgumentCLIException( - "Le chemin du fichier de sortie doit uniquement contenir les caractères " + "Le chemin du dossier de sortie doit uniquement contenir les caractères " "autorisés. Les caractères autorisés sont les minuscules, majuscules, " "chiffres ou les caractères spéciaux suivants: _, \\, -, /." ) - if not arguments_parses.sortie.endswith(".json"): - raise ArgumentCLIException( - "Le fichier de sortie doit obligatoirement être un fichier au format json." - ) - return arguments_parses diff --git a/app/export/exporteur.py b/app/export/exporteur.py index bbcc3c7..75de743 100644 --- a/app/export/exporteur.py +++ b/app/export/exporteur.py @@ -2,8 +2,10 @@ Module pour l'exportation des données. """ -from os.path import abspath, dirname, isdir +from os.path import abspath, isdir, join from json import dump +from altair import Chart, Theta, Color +from pandas import DataFrame class Exporteur: @@ -12,88 +14,136 @@ class Exporteur: vers un fichier de sortie. Attributes: - _chemin_sortie (str): Le chemin du fichier vers lequel - les données seront exportées. + _chemin_sortie (str): Le chemin du dossier vers lequel les données + vont être exportées. """ def __init__(self, chemin_sortie: str): - """ - Initialise un exporteur de données. - - Args: - chemin_sortie (str): Le chemin du fichier vers lequel - les données seront exportées. - - Raises: - TypeError: Le chemin de sortie n'est pas une chaîne de caractère. - ExportationDossierParentException: Exportation impossible à cause de - l'inexistance du dossier parent du fichier d'exportation. - """ - # Vérification du type du paramètre + # Vérification du paramètre if not isinstance(chemin_sortie, str): - raise TypeError("Le chemin de sortie doit être une chaîne de caractère.") - # Vérification du chemin d'exportation - self.verification_exportation_possible(chemin_sortie) - # Ajout du chemin d'exportation + raise TypeError("Le chemin de sortie doit être une chaîne de caractères.") + # Vérification du chemin + chemin_sortie_absolue = abspath(chemin_sortie) + if not isdir(chemin_sortie_absolue): + raise ExportationDossierIntrouvableException(f"Impossible d'exporter vers le " + f"dossier {chemin_sortie} ({chemin_sortie_absolue}), " + "le dossier n'existe pas.") + # Ajout du chemin self._chemin_sortie = chemin_sortie - def verification_exportation_possible(self, chemin_sortie: str) -> None: + def export_vers_json(self, donnees: dict, nom_fichier: str) -> None: """ - Vérifie qu'une exportation est possible vers le chemin du fichier indiqué. Renvoie une - exception expliquant le problème si elle n'est pas possible. + Export le dictionnaire fourni vers le ``chemin de sortie``. Args: - chemin_sortie (str): Le chemin du fichier d'exportation. + donnees (dict): Le dictionnaire qui contient les données. + nom_fichier (str): Le nom du fichier JSON. Returns: None Raises: - ExportationDossierParentException: Le dossier parent du fichier n'existe pas. + TypeError: Le paramètre ``donnees`` n'est pas un dictionnaire. + ExportationJsonException: Une erreur lors de l'écriture dans le fichier JSON. """ - # Vérification du type du paramètre - if not isinstance(chemin_sortie, str): - raise TypeError("Le chemin de sortie doit être une chaîne de caractères.") - # Vérification du chemin - chemin_sortie_absolue = abspath(chemin_sortie) - dossier_parent = dirname(chemin_sortie_absolue) - if not isdir(dossier_parent): - raise ExportationDossierParentException(f"Impossible d'exporter vers le " - f"fichier {chemin_sortie}, son dossier parent " - f"{dossier_parent} n'existe pas.") + # Vérification du type des paramètres + if not isinstance(donnees, dict): + raise TypeError("Les statistiques à exporter doivent être sous une forme " + "de dictionnaire.") + if not isinstance(nom_fichier, str): + raise TypeError("Le nom du fichier doit être une chaîne de caractère.") + # Vérification du nom du fichier + if not nom_fichier.endswith(".json"): + raise ValueError("Le fichier JSON doit terminé par l'extention '.json'.") + # Exportation + chemin_fichier = join(self._chemin_sortie, nom_fichier) + try: + with open(chemin_fichier, 'w', encoding="utf-8") as fichier: + dump(donnees, fichier, indent=4) + except Exception as ex: + raise ExportationJsonException(str(ex)) from ex - def export_vers_json(self, donnees: dict) -> None: + def export_vers_html_camembert(self, + donnees: list, + nom_fichier: str) -> None: """ - Export le dictionnaire fourni vers le :attr:`chemin de sortie`. + Export la liste fournie vers un camembert HTML vers le ``chemin de sortie``. Args: - donnees (dict): Le dictionnaire qui contient les données. + donnees (list): Les données du camembert. La liste doit contenir + des listes de deux éléments où le premier reprèsente le nom de cette + partie du camembert et le deuxième sa valeur. + nom_fichier (str): Le nom du fichier HTML. Returns: None Raises: - TypeError: Le paramètre ``donnees`` n'est pas un dictionnaire. - ExportationException: Une erreur lors de l'écriture dans le fichier JSON. + TypeError: Les paramètres ne sont pas du type attendu ou la liste ``donnees`` + contient un élément qui n'est pas une liste. + ValueError: Le paramètre ``nom_fichier`` ne termine pas par .html ou le paramètre + ``donnees`` ne contient pas des listes de longueur 2. + ExportationCamembertHtmlException: Erreur lors de l'exportation du camembert. """ - # Vérification du type du paramètre - if not isinstance(donnees, dict): - raise TypeError("Les données à exporter doivent être sous une forme " - "de dictionnaire.") + # Vérification du type des paramètres + if not isinstance(donnees, list): + raise TypeError("Les données de l'histogramme à exporter doit être sous une forme " + "de liste.") + if not isinstance(nom_fichier, str): + raise TypeError("Le nom du fichier doit être une chaîne de caractère.") + # Vérification du nom du fichier + if not nom_fichier.endswith(".html"): + raise ValueError("Le fichier HTML doit terminé par l'extention '.html'.") + # Récupération des axes du graphique + axe_x = [] + axe_y = [] + for donnee in donnees: + if not isinstance(donnee, list): + raise ValueError("La liste des données de l'histogramme à exporter ne doit " + "contenir que des listes.") + if not len(donnee) == 2: + raise ValueError("La liste des données de l'histogramme à exporter ne doit " + "contenir que des listes de deux éléments (x, y).") + axe_x.append(donnee[0]) + axe_y.append(donnee[1]) + axes = DataFrame({"x": axe_x, "y": axe_y}) # Exportation try: - with open(self._chemin_sortie, 'w', encoding="utf-8") as fichier: - dump(donnees, fichier, indent=4) + chemin_fichier = join(self._chemin_sortie, nom_fichier) + base = Chart(axes).encode( + theta=Theta(field="y", type="quantitative"), + color=Color(field="x", type="nominal"), + tooltip=['x:N', 'y:Q'] + ) + camembert = base.mark_arc(outerRadius=200) + graphique = (camembert).properties( + width=400, + height=400 + ) + graphique.save(chemin_fichier) except Exception as ex: - raise ExportationException(str(ex)) from ex + raise ExportationCamembertHtmlException("Erreur lors de l'exportation " + f"du camembert {nom_fichier} {ex}.") from ex + class ExportationException(Exception): """ Représente une erreur lors de l'exportation de données. """ -class ExportationDossierParentException(ExportationException): +class ExportationJsonException(ExportationException): + """ + Représente une erreur lors de l'exportation de données vers un format JSON. + """ + +class ExportationCamembertHtmlException(ExportationException): + """ + Représente une erreur lors de l'exportation de données vers un histogramme + au format HTML. + """ + +class ExportationDossierIntrouvableException(ExportationException): """ Représente une erreur lorsque une exportation est impossible - lorsque le dossier parent du fichier d'exportation n'existe pas. + lorsque le dossier de l'exportation n'existe pas. """ diff --git a/app/main.py b/app/main.py index 55f5911..9114f28 100644 --- a/app/main.py +++ b/app/main.py @@ -4,10 +4,10 @@ from cli.afficheur_cli import AfficheurCLI from cli.parseur_arguments_cli import ParseurArgumentsCLI, ArgumentCLIException from parse.parseur_log_apache import ParseurLogApache, ParsageLogApacheException +from analyse.filtre_log_apache import FiltreLogApache from analyse.analyseur_log_apache import AnalyseurLogApache from export.exporteur import Exporteur, ExportationException - def main() -> None: """ Point d'entrée de l'application. @@ -26,12 +26,20 @@ def main() -> None: # Analyse syntaxique du fichier log parseur_log = ParseurLogApache(arguments_cli.chemin_log) fichier_log = parseur_log.parse_fichier() + # Filtre à appliquer lors de l'analyse + filtre_log = FiltreLogApache(arguments_cli.ip, arguments_cli.code_statut_http) # Analyse statistique du fichier log - analyseur_log = AnalyseurLogApache(fichier_log) + analyseur_log = AnalyseurLogApache(fichier_log, filtre_log) analyse = analyseur_log.get_analyse_complete() - # Exportation de l'analyse + # Exportation JSON exporteur = Exporteur(arguments_cli.sortie) - exporteur.export_vers_json(analyse) + exporteur.export_vers_json(analyse, "analyse-log-apache.json") + # Exportation Camembert + if arguments_cli.camembert: + exporteur.export_vers_html_camembert( + analyseur_log.get_total_par_code_statut_http_camembert(), + "camembert-code_statut_http.html" + ) # Termine l'animation de chargement afficheur_cli.stop_animation_chargement() except ArgumentCLIException as ex: diff --git a/docs/source/index.rst b/docs/source/index.rst index c15e472..a56e540 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -23,17 +23,56 @@ Grâce à LogBuster, vous pouvez extraire des informations essentielles, gérer - **Extraire des statistiques clés** : Obtenez des données précieuses sur vos fichiers de logs. - **Exporter les données en JSON** : Accédez à un format structuré pour vos analyses. +- **Exporter des graphiques** : Générez des graphiques de vos fichiers de logs. +- **Filtrer les analyses** : Filtrez les analyses en toute simplicité. - **Gérer les erreurs de format avec précision** : Identifiez rapidement les anomalies et les erreurs de vos fichiers log. **d(■᎑■⌐) Utilisation** --------------------------- ``` -python app/main.py chemin_log [-s SORTIE] +python app/main.py chemin_log [-s SORTIE] [-i IP] [-c CODE_STATUT_HTTP] [--camembert CAMEMBERT] ``` - `chemin_log` : Le chemin vers le fichier de log Apache à analyser. - `-s SORTIE` (optionnel) : Le chemin où sauvegarder les résultats de l'analyse. Si non spécifié, les résultats seront sauvegardés dans un fichier `analyse-log-apache.json`. +- `-i IP` (optionnel) : Le filtre à appliquer sur les adresses IP des entrées du fichier de log. Uniquement les entrées avec cette adresse IP seront analysées. +- `-c CODE_STATUT_HTTP` (optionnel) : Le filtre à appliquer sur les code de statut http des entrées du fichier de log. Uniquement les entrées avec ce code de statut http seront analysées. +- `--camembert CAMEMBERT` : (optionnel) : Active la génération de graphiques camemberts dans lors de l'analyse pour les statistiques compatibles. Les statistiques comptatibles. + +**(ò_ó)⊃ Format de l'analyse** +-------------------------------- + +Lors de l'analyse des fichiers de logs, LogBuster fait en sorte de regrouper les statistiques clés par catégories. + +Voici ci-dessous le format de l'analyse en JSON: + + - chemin: chemin absolu du fichier + - total_entrees: nombre total d'entrées dans le fichier + - filtre: filtres appliqués à l'analyse + - adresse_ip: filtre sur l'adresse IP (None si désactivé) + - code_statut_http: filtre sur le code de statut http (None si désactivé) + - statistiques: + - total_entrees_filtre: nombre total d'entrées analysées + - requetes: + - top_urls: top 3 des urls + - dictionnaires contenant: + - url: ressource demandée + - total: nombre d'entrée avec cette ressource demandée + - taux: pourcentage d'entrée avec cette ressource demandée + - reponses: + - repartition_code_statut_http: répartition des codes de statut http + - dictionnaires contenant: + - code: code de statut http retourné + - total: nombre d'entrée avec ce code de statut http retourné + - taux: pourcentage d'entrée avec ce code de statut http retourné + +Pour les graphiques camemberts, un fichier HTML est généré avec ce graphique. +Néanmoins, toutes les statistiques ne sont pas compatibles avec ce type d'affichage. + +Voici la liste des statistiques comptatibles avec les graphiques: + +- `repartition_code_statut_http` : Répartition des codes de statut HTTP. (nom: camembert-code_statut_http) **(ง'̀᎑'́)ง Format des fichier de log Apache** ------------------------------------------------ @@ -62,7 +101,7 @@ Dans LogBuster, l'**IP de l'utilisateur** et la **Date et heure de la requête** Format combiné (Combined Log Format) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Le format étendu permet d'ajouter plus de détails sur chaque requête. +Le format combiné permet d'ajouter plus de détails sur chaque requête. Voici un exemple de ligne de log avec ce format : ``127.0.0.1 - - [10/Oct/2025:13:55:36 +0000] "GET /index.html HTTP/1.1" 200 2326 "http://referrer.com" "Mozilla/5.0"`` diff --git a/docs/source/modules/analyse/filtre_log_apache.rst b/docs/source/modules/analyse/filtre_log_apache.rst new file mode 100644 index 0000000..994d34f --- /dev/null +++ b/docs/source/modules/analyse/filtre_log_apache.rst @@ -0,0 +1,7 @@ +FiltreLogApache +====================== + +.. automodule:: analyse.filtre_log_apache + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/source/modules/analyse/index_analyse.rst b/docs/source/modules/analyse/index_analyse.rst index 64ad521..54b7b65 100644 --- a/docs/source/modules/analyse/index_analyse.rst +++ b/docs/source/modules/analyse/index_analyse.rst @@ -4,5 +4,6 @@ Analyse .. toctree:: :maxdepth: 4 + filtre_log_apache.rst analyseur_log_apache.rst \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2c8dc61..bcc9c0b 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/tests/conftest.py b/tests/conftest.py index 9dbaba8..44d8a5d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,7 @@ from cli.afficheur_cli import AfficheurCLI from cli.parseur_arguments_cli import ParseurArgumentsCLI from parse.parseur_log_apache import ParseurLogApache +from analyse.filtre_log_apache import FiltreLogApache from analyse.analyseur_log_apache import AnalyseurLogApache from export.exporteur import Exporteur @@ -148,21 +149,34 @@ def entree_log_apache(fichier_log_apache): """ return fichier_log_apache.entrees[0] +@pytest.fixture +def filtre_log_apache(): + """ + Fixture pour initialiser un filtre de fichier de log Apache. + Par défaut, toutes les vérifications de ce filtre sont à ``None``. + + Returns: + FiltreLogApache: Une instance de la classe :class:`FiltreLogApache`. + """ + return FiltreLogApache(None, None) + @pytest.fixture() -def analyseur_log_apache(fichier_log_apache): +def analyseur_log_apache(fichier_log_apache, filtre_log_apache): """ Fixture pour initialiser un analyseur statistique de fichier de log Apache. Le fichier qu'analyse cet analyseur comprend par défaut les entrées parsées de la liste - ``lignes_valides``. + ``lignes_valides`` et toutes les vérifications de sont filtres sont à ``None``. Args: fichier_log_apache (FichierLogApache): Fixture pour l'instance de la classe :class:`FichierLogApache`. + filtre_log_apache (FiltreLogApache): Fixture pour l'instance + de la classe :class:`FiltreLogApache`. Returns: AnalyseurLogApache: Une instance de la classe :class:`AnalyseurLogApache`. """ - return AnalyseurLogApache(fichier_log_apache) + return AnalyseurLogApache(fichier_log_apache, filtre_log_apache) @pytest.fixture def fichier_json(tmp_path): @@ -179,7 +193,7 @@ def fichier_json(tmp_path): return fichier_temp @pytest.fixture -def exporteur(fichier_json): +def exporteur(tmp_path): """ Fixture pour initialiser un exportateur de données. @@ -190,4 +204,4 @@ def exporteur(fichier_json): Returns: Exporteur: Une instance de la classe :class:`Exportateur`. """ - return Exporteur(str(fichier_json)) \ No newline at end of file + return Exporteur(str(tmp_path)) \ No newline at end of file diff --git a/tests/test_analyseur_log_apache.py b/tests/test_analyseur_log_apache.py index 5defd19..aabdf38 100644 --- a/tests/test_analyseur_log_apache.py +++ b/tests/test_analyseur_log_apache.py @@ -3,23 +3,27 @@ """ import pytest +from os.path import abspath from parse.fichier_log_apache import FichierLogApache +from analyse.filtre_log_apache import FiltreLogApache from analyse.analyseur_log_apache import AnalyseurLogApache # Tests unitaires -@pytest.mark.parametrize("fichier, nombre_par_top", [ - (False, 3), - (FichierLogApache("test.log"), False) +@pytest.mark.parametrize("fichier, filtre, nombre_par_top", [ + (False, FiltreLogApache(None, None), 3), + (FichierLogApache("test.log"), False, 3), + (FichierLogApache("test.log"), FiltreLogApache(None, None), False) ]) -def test_analyseur_log_exception_type_invalide(fichier, nombre_par_top): +def test_analyseur_log_exception_type_invalide(fichier, filtre, nombre_par_top): """ Vérifie que la classe AnalyseurLogApache lève une :class:`TypeError` si les types des paramètres du constructeur sont invalides. Scénarios testés: - Type incorrect pour le paramètre ``fichier``. + - Type incorrect pour le paramètre ``filtre``. - Type incorrect pour le paramètre ``nombre_par_top``. Asserts: @@ -30,7 +34,7 @@ def test_analyseur_log_exception_type_invalide(fichier, nombre_par_top): nombre_par_top (any): Nombre maximum d'éléments dans le top classement. """ with pytest.raises(TypeError): - analyseur = AnalyseurLogApache(fichier, nombre_par_top) + analyseur = AnalyseurLogApache(fichier, filtre, nombre_par_top) def test_analyseur_log_exception_valeur_nombre_par_top_invalide(): """ @@ -44,7 +48,9 @@ def test_analyseur_log_exception_valeur_nombre_par_top_invalide(): - Une exception :class:`ValueError` est levée. """ with pytest.raises(ValueError): - analyseur = AnalyseurLogApache(FichierLogApache("test.log"), -4) + analyseur = AnalyseurLogApache(FichierLogApache("test.log"), + FiltreLogApache(None, None), + -4) @pytest.mark.parametrize("liste_elements, nom_element, mode_top_classement", [ (0, "test", True), @@ -189,7 +195,7 @@ def test_analyseur_top_urls_valide(analyseur_log_apache): assert top_urls[1]["total"] == 2 assert top_urls[1]["taux"] == 40.0 -def test_analyseur_repartition_code_statut_htpp_valide(analyseur_log_apache): +def test_analyseur_repartition_code_statut_http_valide(analyseur_log_apache): """ Vérifie que ``get_total_par_code_statut_http`` retourne la répartition correcte des codes HTTP. @@ -213,6 +219,29 @@ def test_analyseur_repartition_code_statut_htpp_valide(analyseur_log_apache): assert repartition[1]["total"] == 1 assert repartition[1]["taux"] == 20.0 +def test_analyseur_repartition_code_statut_http_camembert_valide(analyseur_log_apache): + """ + Vérifie que ``get_total_par_code_statut_http_camembert`` retourne la répartition + correcte des codes HTTP. + + Scénarios testés: + - Vérification du tri et des indexs des listes dans la liste. + + Asserts: + - La liste est triée dans l'ordre attendu. + - Le nombre d'éléments dans le résultat correspond à celui attendu. + + Args: + analyseur_log_apache (AnalyseurLogApache): Fixture pour l'instance + de la classe ParseurLogApache. + """ + repartition = analyseur_log_apache.get_total_par_code_statut_http_camembert() + assert len(repartition) == 2 + assert repartition[0][0] == 500 + assert repartition[0][1] == 4 + assert repartition[1][0] == 200 + assert repartition[1][1] == 1 + @pytest.mark.parametrize("nombre_entrees", [ (0), (3), (100) ]) @@ -241,6 +270,45 @@ def test_analyseur_get_total_entrees_valide(analyseur_log_apache, fichier_log_apache.entrees = [entree_log_apache] * nombre_entrees assert analyseur_log_apache.get_total_entrees() == nombre_entrees +@pytest.mark.parametrize("nombre_entrees_valides", [ + (0), (3), (100) +]) +def test_analyseur_get_entrees_passent_filtre_valide(mocker, + analyseur_log_apache, + entree_log_apache, + nombre_entrees_valides): + """ + Vérifie que ``_get_entrees_passent_filtre`` retourne la liste des entrées + qui passent le filtre. + + Scénarios testés: + - Passage d'entrée à la méthode ``_get_entrees_passent_filtre``. + + Asserts: + - Le nombre d'entrées retourné est égale au nombre de True retourné. + + Args: + mocker (any): Fixture pour simuler des attributs et retours de méthode. + analyseur_log_apache (AnalyseurLogApache): Fixture pour l'instance + de la classe :class:`AnalyseurLogApache`. + entree_log_apache (EntreeLogApache): Fixture pour l'instance + de la classe :class:`EntreeLogApache`. + nombre_entrees_valides (int): Le nombre d'entrées valides que retourne la méthode. + """ + analyseur_log_apache.fichier = mocker.MagicMock() + analyseur_log_apache.fichier.entrees = [entree_log_apache] * (nombre_entrees_valides * 2) + + retour_methode = [True] * nombre_entrees_valides + retour_methode += [False] * nombre_entrees_valides + + analyseur_log_apache.filtre = mocker.MagicMock() + analyseur_log_apache.filtre.entree_passe_filtre.side_effect = retour_methode + + entrees_filtre = analyseur_log_apache._get_entrees_passent_filtre() + + assert entrees_filtre == [entree_log_apache] * nombre_entrees_valides + assert len(entrees_filtre) == nombre_entrees_valides + def test_analyseur_get_analyse_complete_valide(analyseur_log_apache): """ Vérifie que ``get_analyse_complete`` retourne un rapport de l'analyse correct @@ -255,18 +323,16 @@ def test_analyseur_get_analyse_complete_valide(analyseur_log_apache): Args: analyseur_log_apache (AnalyseurLogApache): Fixture pour l'instance de la classe :class:`AnalyseurLogApache`. - fichier_log_apache (FichierLogApache): Fixture pour l'instance - de la classe :class:`FichierLogApache`. - entree_log_apache (EntreeLogApache): Fixture pour l'instance - de la classe :class:`EntreeLogApache`. - nombre_entrees (int): Le nombre total d'entrées dans le fichier. """ analyse = analyseur_log_apache.get_analyse_complete() - assert analyse["chemin"] == analyseur_log_apache.fichier.chemin + assert analyse["chemin"] == abspath(analyseur_log_apache.fichier.chemin) + assert analyse["total_entrees"] == analyseur_log_apache.get_total_entrees() + assert analyse["filtre"] == analyseur_log_apache.filtre.get_dict_filtre() statistiques = analyse["statistiques"] - assert statistiques["total_entrees"] == analyseur_log_apache.get_total_entrees() + assert statistiques["total_entrees_filtre"] == analyseur_log_apache.get_total_entrees_filtre() statistiques_requetes = statistiques["requetes"] assert statistiques_requetes["top_urls"] == analyseur_log_apache.get_top_urls() - assert (statistiques_requetes["repartition_code_statut_http"] + statistiques_reponses = statistiques["reponses"] + assert (statistiques_reponses["repartition_code_statut_http"] == analyseur_log_apache.get_total_par_code_statut_http()) \ No newline at end of file diff --git a/tests/test_exporteur.py b/tests/test_exporteur.py index d5b6819..3e4a180 100644 --- a/tests/test_exporteur.py +++ b/tests/test_exporteur.py @@ -4,7 +4,10 @@ import pytest from json import load -from export.exporteur import Exporteur, ExportationException +from export.exporteur import (Exporteur, + ExportationJsonException, + ExportationCamembertHtmlException, + ExportationDossierIntrouvableException) @pytest.mark.parametrize("chemin_sortie", [ (0), (None), ([]) @@ -28,39 +31,22 @@ def test_exporteur_type_chemin_invalide(chemin_sortie): def test_exporteur_emplacement_inexistant(): """ - Vérifie que la classe renvoie une erreur lorsque un chemin de fichier invalide + Vérifie que la classe renvoie une erreur lorsque un chemin de dossier invalide est passé dans le constructeur. Scénarios testés: - Chemin invalide pour le paramètre ``chemin_sortie``. Asserts: - - Une exception :class:`ExportationException` est levée. + - Une exception :class:`ExportationDossierIntrouvableException` est levée. """ - with pytest.raises(ExportationException): - exporteur = Exporteur("dossier/inexistant/sortie.json") + with pytest.raises(ExportationDossierIntrouvableException): + exporteur = Exporteur("dossier/inexistant/") -def test_exporteur_verification_exception_exportation_possible_type_invalide(exporteur): - """ - Vérifie que la méthode verification_exportation_possible renvoie une erreur lorsque le type - de son paramètre est invalide. - - Scénarios testés: - - Paramètre ``chemin_sortie`` avec un mauvais type. - - Asserts: - - Une exception :class:`TypeError` est levée. - - Args: - exporteur (Exporteur) : Fixture pour l'instance de la classe :class:`Exporteur`. - """ - with pytest.raises(TypeError): - exporteur.verification_exportation_possible(False) - -@pytest.mark.parametrize("donnees", [ - (0), (None), ([]) +@pytest.mark.parametrize("donnees, nom_fichier", [ + (False, "fichier.json"), ({}, False) ]) -def test_exporteur_export_json_type_donnees_invalide(exporteur, donnees): +def test_exporteur_export_json_type_donnees_invalide(exporteur, donnees, nom_fichier): """ Vérifie que la classe renvoie une erreur lorsque un argument de type invalide est passé dans la méthode ``export_vers_json``. @@ -76,7 +62,15 @@ def test_exporteur_export_json_type_donnees_invalide(exporteur, donnees): donnees (any): Les données à exporter. """ with pytest.raises(TypeError): - exporteur.export_vers_json("type invalide") + exporteur.export_vers_json(donnees, nom_fichier) + +@pytest.mark.parametrize("nom_fichier", [ + ("fichier.rst"), ("sorite.html") +]) +def test_exporteur_exeption_export_vers_json_fichier_invalide(exporteur, + nom_fichier): + with pytest.raises(ValueError): + exporteur.export_vers_json({}, nom_fichier) @pytest.mark.parametrize("exception", [ (PermissionError("Pas les droits")), @@ -85,7 +79,7 @@ def test_exporteur_export_json_type_donnees_invalide(exporteur, donnees): ]) def test_exporteur_export_json_exception_exportation(exporteur, mocker, exception): """ - Vérifie que la classe renvoie l'exception :class:`ExportationException` lorsque + Vérifie que la classe renvoie l'exception :class:`ExportationJsonException` lorsque une erreur apparait durant l'exportation des données. Scénarios testés: @@ -94,16 +88,16 @@ def test_exporteur_export_json_exception_exportation(exporteur, mocker, exceptio - Une exception :class:`Exception` survient. Asserts: - - Une exception :class:`ExportationException` est levée. + - Une exception :class:`ExportationJsonException` est levée. Args: exporteur (Exporteur) : Fixture pour l'instance de la classe :class:`Exporteur`. mocker (any): Une fixture pour simuler des exceptions. - donnees (any): Les données à exporter. + exception (any): L'exception levée dans la méthode. """ mocker.patch("builtins.open", side_effect=exception) - with pytest.raises(ExportationException): - exporteur.export_vers_json({}) + with pytest.raises(ExportationJsonException): + exporteur.export_vers_json({}, "fichier.json") def test_exporteur_exportation_json_valide(exporteur, fichier_json): """ @@ -122,8 +116,155 @@ def test_exporteur_exportation_json_valide(exporteur, fichier_json): fichier_json (Path): Le chemin du fichier. """ donnees = {"cle": {"valeur": [1, 2, 3]}} - exporteur.export_vers_json(donnees) + exporteur.export_vers_json(donnees, "sortie.json") assert fichier_json.exists() with open(fichier_json, "r") as exportation: contenu_exportation = load(exportation) - assert contenu_exportation == donnees \ No newline at end of file + assert contenu_exportation == donnees + +@pytest.mark.parametrize("donnees, nom_fichier", [ + (False, "fichier.html"), + ([], False) +]) +def test_exporteur_exception_export_vers_html_camembert_type_invalide(exporteur, + donnees, + nom_fichier): + """ + Vérifie qu'une exception est levée lorsque les paramètres ont des types incorrects. + + Scénarios testés: + - Type incorrect pour le paramètre ``donnees``. + - Type incorrect pour le paramètre ``nom_fichier``. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + exporteur (Exporteur) : Fixture pour l'instance de la classe :class:`Exporteur`. + donnees (any): Les données à mettre sous forme de camembert. + nom_fichier (any): Le nom du fichier HTML qui contient le camembert. + """ + with pytest.raises(TypeError): + exporteur.export_vers_html_camembert(donnees, nom_fichier) + +@pytest.mark.parametrize("nom_fichier", [ + ("fichier.rst"), ("sorite.json") +]) +def test_exporteur_exception_export_vers_html_camembert_fichier_invalide(exporteur, + nom_fichier): + """ + Vérifie qu'une exception est levée lorsque le nom du fichier n'est pas un + fichier HTML. + + Scénarios testés: + - Passage d'un nom de fichier qui ne termine pas par '.html'. + + Asserts: + - Une exception :class:`ValueError` est levée. + + Args: + exporteur (Exporteur) : Fixture pour l'instance de la classe :class:`Exporteur`. + nom_fichier (any): Le nom du fichier HTML qui contient le camembert. + """ + with pytest.raises(ValueError): + exporteur.export_vers_html_camembert([], nom_fichier) + +@pytest.mark.parametrize("donnees", [ + (["test"]), ([False]) +]) +def test_exporteur_exception_export_vers_html_camembert_elements_donnees_invalide( + exporteur, + donnees): + """ + Vérifie qu'une exception est levée lorsque le paramètre ``donnees`` n'est pas une liste + de listes. + + Scénarios testés: + - Passage d'une liste de données ne contenant pas de liste.. + + Asserts: + - Une exception :class:`ValueError` est levée. + + Args: + exporteur (Exporteur) : Fixture pour l'instance de la classe :class:`Exporteur`. + donnees (any): Le nom du fichier HTML qui contient le camembert. + """ + with pytest.raises(ValueError): + exporteur.export_vers_html_camembert(donnees, "sortie.html") + +@pytest.mark.parametrize("donnees", [ + ([[1, 1, 1]]), ([[1]]), ([[]]) +]) +def test_exporteur_exception_export_vers_html_camembert_listes_non_2_elements_invalide( + exporteur, + donnees): + """ + Vérifie qu'une exception est levée lorsque la liste des données contient des listes + ne contenant pas deux éléments. + + Scénarios testés: + - Lancement de la méthode avec des listes incorrectes. + + Asserts: + - Une exception :class:`ValueError` est levée. + + Args: + exporteur (Exporteur) : Fixture pour l'instance de la classe :class:`Exporteur`. + donnees (dict): Les données à mettre sous forme de camembert. + """ + with pytest.raises(ValueError): + exporteur.export_vers_html_camembert(donnees, "sortie.html") + +@pytest.mark.parametrize("donnees, nom_fichier", [ + ([[200, 1], [404, 10]], "fichier.html"), + ([["test", 12], ["essaie", 50], ], "fichier.html") +]) +def test_exporteur_export_vers_html_camembert_valide(exporteur, + donnees, + nom_fichier): + """ + Vérifie qu'aucune exception n'est levée lorsque du déroulement normale + de la méthode. + + Scénarios testés: + - Lancement de la méthode avec des paramètres correctes. + + Asserts: + - Aucune exception n'est levée. + + Args: + exporteur (Exporteur) : Fixture pour l'instance de la classe :class:`Exporteur`. + donnees (dict): Les données à mettre sous forme de camembert. + nom_fichier (str): Le nom du fichier HTML du camembert. + """ + try: + exporteur.export_vers_html_camembert(donnees, nom_fichier) + except Exception: + pytest.fail("Aucune exception ne doit être levée ici") + +@pytest.mark.parametrize("exception", [ + (PermissionError("Pas les droits")), + (FileNotFoundError("Fichier non trouvé.")), + (Exception("Toutes exceptions")) +]) +def test_exporteur_export_vers_html_camembert_exception_exportation(exporteur, mocker, exception): + """ + Vérifie que la classe renvoie l'exception :class:`ExportationCamembertHtmlException` lorsque + une erreur apparait durant l'exportation des données. + + Scénarios testés: + - Une exception :class:`PermissionError` survient. + - Une exception :class:`FileNotFoundError` survient. + - Une exception :class:`Exception` survient. + + Asserts: + - Une exception :class:`ExportationCamembertHtmlException` est levée. + + Args: + exporteur (Exporteur) : Fixture pour l'instance de la classe :class:`Exporteur`. + mocker (any): Une fixture pour simuler des exceptions. + exception (any): L'exception levée dans la méthode. + """ + mocker.patch("altair.Chart.save", side_effect=exception) + with pytest.raises(ExportationCamembertHtmlException): + exporteur.export_vers_html_camembert([[200, 1], [404, 3]], "fichier.html") diff --git a/tests/test_filtre_log_apache.py b/tests/test_filtre_log_apache.py new file mode 100644 index 0000000..5b5d858 --- /dev/null +++ b/tests/test_filtre_log_apache.py @@ -0,0 +1,118 @@ +""" +Module des tests unitaires pour le filtre de log Apache. +""" + +import pytest +from analyse.filtre_log_apache import FiltreLogApache + + +# Tests unitaires + +@pytest.mark.parametrize("filtre_adresse_ip, filtre_code_statut_http", [ + (False, 200), + ("127.0.0.1", False) +]) +def test_filtre_log_type_invalide(filtre_adresse_ip, filtre_code_statut_http): + """ + Vérifie que la classe renvoie une erreur lorsque un argument de type invalide + est passé dans le constructeur. + + Scénarios testés: + - Type incorrect pour le paramètre ``filtre_adresse_ip``. + - Type incorrect pour le paramètre ``filtre_code_statut_http``. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + filtre_adresse_ip (any): La vérification sur l'adresse IP. + filtre_code_statut_http (any): La vérification sur le code de statut http. + """ + with pytest.raises(TypeError): + filtre = FiltreLogApache(filtre_adresse_ip, filtre_code_statut_http) + +def test_filtre_log_entree_passe_filtre_type_invalide(filtre_log_apache): + """ + Vérifie que la méthode ``entree_passe_filtre`` renvoie une erreur en cas + de type de paramètre invalide. + + Scénarios testés: + - Type incorrect pour le paramètre ``entree``. + + Asserts: + - Une exception :class:`TypeError` est levée. + + Args: + filtre_log_apache (FiltreLogApache): Fixture pour l'instance + de la classe :class:`FiltreLogApache`. + """ + with pytest.raises(TypeError): + filtre_log_apache.entree_passe_filtre(False) + +@pytest.mark.parametrize("filtre_adresse_ip, adresse_ip_entree, retour_attendu", [ + ("127.0.0.1", "127.0.0.1", True), + ("127.0.0.2", "127.0.0.1", False), + ("127.0.0.1", "127.0.0.2", False) +]) +def test_filtre_log_entree_passe_filtre_adresse_ip_valide(filtre_log_apache, + entree_log_apache, + filtre_adresse_ip, + adresse_ip_entree, + retour_attendu): + """ + Vérifie que la méthode ``entree_passe_filtre`` applique correctement la vérification + sur l'adresse IP. + + Scénarios testés: + - L'adresse IP de l'entrée égale à celle du filtre. + - L'adresse IP de l'entrée différente de celle du filtre. + + Asserts: + - La méthode ``entree_passe_filtre`` est égale à ``retour_attendu``. + + Args: + filtre_log_apache (FiltreLogApache): Fixture pour l'instance + de la classe :class:`FiltreLogApache`. + entree_log_apache (EntreeLogApache): Fixture pour l'instance + de la classe :class:`EntreeLogApache`. + filtre_adresse_ip (str): La valeur du filtre de l'adresse IP. + adresse_ip_entree (str): L'adresse IP de l'entrée. + retour_attendu (bool): Le retour attendu par la méthode. + """ + filtre_log_apache.adresse_ip = filtre_adresse_ip + entree_log_apache.client.adresse_ip = adresse_ip_entree + assert filtre_log_apache.entree_passe_filtre(entree_log_apache) == retour_attendu + +@pytest.mark.parametrize("filtre_code_statut_http, code_statut_http_entree, retour_attendu", [ + (200, 200, True), + (404, 200, False), + (200, 404, False) +]) +def test_filtre_log_entree_passe_filtre_code_statut_http_valide(filtre_log_apache, + entree_log_apache, + filtre_code_statut_http, + code_statut_http_entree, + retour_attendu): + """ + Vérifie que la méthode ``entree_passe_filtre`` applique correctement la vérification + sur le code de statut http. + + Scénarios testés: + - Le code de statut http de l'entrée égale à celle du filtre. + - Le code de statut http de l'entrée différente de celle du filtre. + + Asserts: + - La méthode ``entree_passe_filtre`` est égale à ``retour_attendu``. + + Args: + filtre_log_apache (FiltreLogApache): Fixture pour l'instance + de la classe :class:`FiltreLogApache`. + entree_log_apache (EntreeLogApache): Fixture pour l'instance + de la classe :class:`EntreeLogApache`. + filtre_code_statut_http (str): La valeur du filtre du code de statut http. + code_statut_http_entree (str): Le code de statut http de l'entrée. + retour_attendu (bool): Le retour attendu par la méthode. + """ + filtre_log_apache.code_statut_http = filtre_code_statut_http + entree_log_apache.reponse.code_statut_http = code_statut_http_entree + assert filtre_log_apache.entree_passe_filtre(entree_log_apache) == retour_attendu \ No newline at end of file diff --git a/tests/test_main.py b/tests/test_main.py index 80d5f9c..26253b0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -52,6 +52,8 @@ def test_main_succes(mocker): chemin_log="test.log" ) + mocker.patch("main.FiltreLogApache") + mock_parseur_log = mocker.patch("main.ParseurLogApache") mock_parseur_log.return_value.parse_fichier.return_value = mocker.MagicMock() diff --git a/tests/test_parseur_arguments_cli.py b/tests/test_parseur_arguments_cli.py index 0cf8fa9..aa63c23 100644 --- a/tests/test_parseur_arguments_cli.py +++ b/tests/test_parseur_arguments_cli.py @@ -22,9 +22,9 @@ ] sorties_valides = [ - "fichier.json", - "./fichier.json", - "C:/Users/fest/fichier.json" + "./", + "./dossier/", + "C:/Users/fest/dossier/" ] arguments_invalides = [ @@ -124,11 +124,11 @@ def test_parseur_cli_exception_chemin_log_invalide(parseur_arguments_cli, chemin @pytest.mark.parametrize("chemin_sortie", sorties_valides) def test_parseur_cli_recuperation_chemin_sortie_valide(parseur_arguments_cli, chemin_sortie): """ - Vérifie que le chemin du fichier de sortie JSON fourni depuis la ligne de commande est bien + Vérifie que le chemin du dossier de sortie fourni depuis la ligne de commande est bien récupéré par le parseur. Scénarios testés: - - Demande de parsage d'un chemin de fichier de sortie. + - Demande de parsage d'un chemin de dossier de sortie. Asserts: - La valeur du chemin de sortie est bien récupérée et conforme à l'entrée. @@ -144,11 +144,11 @@ def test_parseur_cli_recuperation_chemin_sortie_valide(parseur_arguments_cli, ch @pytest.mark.parametrize("chemin_sortie", chemins_invalides) def test_parseur_cli_exception_chemin_sortie_invalide(parseur_arguments_cli, chemin_sortie): """ - Vérifie qu'une erreur est retournée lorsque le chemin du fichier de sortie fourni contient + Vérifie qu'une erreur est retournée lorsque le chemin du dossier de sortie fourni contient au moins un caractère non autorisé. Scénarios testés: - - Demande de parsage d'un chemin de fichier de sortie invalide. + - Demande de parsage d'un chemin du dossier de sortie invalide. Asserts: - Une exception :class:`ArgumentCLIException` est levée. @@ -163,11 +163,11 @@ def test_parseur_cli_exception_chemin_sortie_invalide(parseur_arguments_cli, che def test_parseur_cli_recuperation_chemin_sortie_defaut_valide(parseur_arguments_cli): """ - Vérifie que le chemin du fichier de sortie JSON par défaut est bien appliqué lorsque - aucun chemin de sortie n'est donné. + Vérifie que le chemin du dossier de sortie par défaut est bien appliqué lorsque + aucun dossier de sortie n'est donné. Scénarios testés: - - Demande de parsage avec aucun fichier de sortie indiqué. + - Demande de parsage avec aucun dossier de sortie indiqué. Asserts: - La bonne valeur par défaut pour le chemin de sortie à été appliquée. @@ -177,15 +177,66 @@ def test_parseur_cli_recuperation_chemin_sortie_defaut_valide(parseur_arguments_ de la classe :class:`ParseurArgumentsCLI`. """ argument = parseur_arguments_cli.parse_args(args=["fichier.txt"]) - assert argument.sortie == "./analyse-log-apache.json" + assert argument.sortie == "./" -def test_parseur_cli_verification_extention_chemin_sortie(parseur_arguments_cli): +@pytest.mark.parametrize("adresse_ip", [ + ("127.0.0.1"), ("192.168.0.0"), ("10.0.0.8") +]) +def test_parseur_cli_recuperation_filtre_adresse_ip_valide(parseur_arguments_cli, adresse_ip): + """ + Vérifie que le filtre sur l'adresse IP fourni depuis la ligne de commande est bien + récupéré par le parseur. + + Scénarios testés: + - Ajout d'un filtre sur l'adresse IP dans les arguments de la CLI. + + Asserts: + - La valeur de l'adresse IP est bien récupérée et conforme à l'entrée. + + Args: + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe :class:`ParseurArgumentsCLI`. + adresse_ip (str): Le filtre sur l'adresse IP. + """ + arguments = parseur_arguments_cli.parse_args(args=["fichier.txt", "-i", adresse_ip]) + assert arguments.ip == adresse_ip + +@pytest.mark.parametrize("code_statut_http", [ + ("200"), ("302"), ("404") +]) +def test_parseur_cli_recuperation_filtre_code_statut_http_valide(parseur_arguments_cli, + code_statut_http): + """ + Vérifie que le filtre sur le code de statut http fourni depuis la ligne de commande est bien + récupéré par le parseur. + + Scénarios testés: + - Ajout d'un filtre sur le code de statut http dans les arguments de la CLI. + + Asserts: + - La valeur du code de statut http est bien récupérée et conforme à l'entrée. + - La valeur du code de statut http est bien convertie en entier. + + Args: + parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance + de la classe :class:`ParseurArgumentsCLI`. + code_statut_http (str): Le filtre sur le code de statut http. + """ + arguments = parseur_arguments_cli.parse_args(args=["fichier.txt", "-c", code_statut_http]) + assert arguments.code_statut_http == int(code_statut_http) + +@pytest.mark.parametrize("code_statut_http_invalide", [ + ("test"), ("invalide") +]) +def test_parseur_cli_exception_recuperation_filtre_code_statut_http_invalide( + parseur_arguments_cli, + code_statut_http_invalide): """ - Vérifie qu'une erreur est retournée lorsque le fichier de sortie fourni ne possède - pas l'extension '.json'. + Vérifie qu'une erreur se produit lorsque le filtre sur le code de statut http n'est pas + convertisable en un entier. Scénarios testés: - - Demande de parsage d'un fichier de sortie qui n'est pas un fichier .json. + - Ajout d'un filtre sur le code de statut http invalide dans les arguments de la CLI. Asserts: - Une exception :class:`ArgumentCLIException` est levée. @@ -193,6 +244,8 @@ def test_parseur_cli_verification_extention_chemin_sortie(parseur_arguments_cli) Args: parseur_arguments_cli (ParseurArgumentsCLI): Fixture pour l'instance de la classe :class:`ParseurArgumentsCLI`. + code_statut_http_invalide (str): Le filtre invalide sur le code de statut http. """ with pytest.raises(ArgumentCLIException): - parseur_arguments_cli.parse_args(args=["fichier.txt", "-s", "invalide.txt"]) + arguments = parseur_arguments_cli.parse_args( + args=["fichier.txt", "-c", code_statut_http_invalide]) \ No newline at end of file