# Ticket #028 — Ajouter l’ouverture d’une enquête existante ## Contexte Les tickets précédents ont permis de mettre en place : - la création transactionnelle d’une base d’enquête ; - la couche `Database` ; - les requêtes préparées `DatabaseStatement` ; - la gestion des transactions et des erreurs ; - le modèle `InvestigationRecord` ; - le DAO `InvestigationDao` permettant de charger l’unique ligne de la table `investigation`. Le projet possède également un type `InvestigationProject` chargé de représenter les chemins du projet sur le système de fichiers, notamment : - le dossier racine de l’enquête ; - le chemin du fichier `Enquete.sqlite`. Cependant, l’application ne possède pas encore d’objet représentant une enquête réellement ouverte pendant son exécution. Les différentes ressources sont encore séparées : ```text InvestigationProject Database InvestigationRecord ``` Il faut désormais les regrouper dans un contexte cohérent dont la durée de vie correspond à celle d’une enquête ouverte. ## Objectif Créer un type opaque `InvestigationSession` chargé d’ouvrir une enquête existante et de conserver : - son contexte de fichiers `InvestigationProject` ; - sa connexion `Database` ; - ses informations persistées `InvestigationRecord`. L’ouverture doit vérifier que le dossier sélectionné correspond bien aux informations enregistrées dans la base SQLite. La connexion SQLite doit rester ouverte pendant toute la durée de vie de la session afin de permettre les futurs appels aux DAO. ## Architecture attendue ```text Application │ ▼ InvestigationSession ├── InvestigationProject ├── Database └── InvestigationRecord ▲ │ InvestigationDao │ ▼ DatabaseStatement │ ▼ SQLite ``` ## Travail à réaliser ### 1. Créer le type `InvestigationSession` Créer les fichiers : ```text include/core/investigation_session.h src/core/investigation_session.c ``` Le type doit être opaque : ```c typedef struct InvestigationSession InvestigationSession; ``` Sa représentation privée doit contenir au minimum : ```c struct InvestigationSession { InvestigationProject *project; Database *database; InvestigationRecord *record; }; ``` Le header public ne doit pas exposer cette structure. ### 2. Définir les erreurs d’ouverture Créer une énumération dédiée : ```c typedef enum { INVESTIGATION_SESSION_ERROR_INVALID_ARGUMENT, INVESTIGATION_SESSION_ERROR_ROOT_NOT_FOUND, INVESTIGATION_SESSION_ERROR_DATABASE_NOT_FOUND, INVESTIGATION_SESSION_ERROR_PROJECT, INVESTIGATION_SESSION_ERROR_DATABASE, INVESTIGATION_SESSION_ERROR_RECORD, INVESTIGATION_SESSION_ERROR_ROOT_MISMATCH, INVESTIGATION_SESSION_ERROR_MEMORY } InvestigationSessionError; ``` Définir un domaine d’erreur GLib : ```c #define INVESTIGATION_SESSION_ERROR \ investigation_session_error_quark() GQuark investigation_session_error_quark(void); ``` Les erreurs doivent être transmises avec un paramètre : ```c GError **error ``` Lorsqu’une erreur provenant de `Database` doit être propagée, son message doit être copié dans le `GError` avant la fermeture de la connexion. ### 3. Ajouter la fonction d’ouverture Déclarer : ```c InvestigationSession *investigation_session_open( const char *investigation_root_path, GError **error ); ``` La fonction doit ouvrir une enquête déjà existante à partir de son dossier racine. Elle ne doit pas créer une nouvelle enquête. ### 4. Valider les paramètres La fonction doit refuser : ```text investigation_root_path == NULL investigation_root_path vide ``` Le code d’erreur attendu est : ```c INVESTIGATION_SESSION_ERROR_INVALID_ARGUMENT ``` Le paramètre `error` peut être `NULL`. Si `error` n’est pas `NULL`, il doit respecter les conventions GLib : ```c *error == NULL ``` au moment de l’appel. ### 5. Vérifier le dossier racine Le chemin fourni doit correspondre à un dossier existant. La fonction doit vérifier : ```c G_FILE_TEST_IS_DIR ``` Un chemin inexistant ou qui ne représente pas un dossier doit produire : ```c INVESTIGATION_SESSION_ERROR_ROOT_NOT_FOUND ``` Le chemin doit être normalisé avec : ```c g_canonicalize_filename() ``` La session doit travailler à partir de ce chemin canonique. ### 6. Construire `InvestigationProject` Réutiliser l’API existante de `InvestigationProject`. La logique de construction du chemin de la base ne doit pas être dupliquée dans `InvestigationSession`. Le chemin attendu reste géré par `InvestigationProject` : ```text /00_BaseDeDonnees/Enquete.sqlite ``` Si l’API actuelle de `InvestigationProject` ne permet pas de représenter un projet existant, elle peut être étendue de manière minimale. Aucune logique SQLite ne doit être ajoutée dans `InvestigationProject`. ### 7. Vérifier le fichier SQLite Avant l’ouverture de la connexion, vérifier que le chemin retourné par `InvestigationProject` correspond à un fichier régulier : ```c G_FILE_TEST_IS_REGULAR ``` Si le fichier n’existe pas, retourner : ```c INVESTIGATION_SESSION_ERROR_DATABASE_NOT_FOUND ``` L’ouverture d’une enquête existante ne doit jamais créer silencieusement une nouvelle base vide. ### 8. Ouvrir la connexion Database Utiliser : ```c database_open() ``` La fonction `investigation_session_open()` ne doit pas appeler directement : ```c sqlite3_open() sqlite3_open_v2() sqlite3_close() ``` Si `database_open()` échoue, retourner : ```c INVESTIGATION_SESSION_ERROR_DATABASE ``` La connexion doit rester ouverte si la session est créée avec succès. ### 9. Charger l’enquête persistée Utiliser : ```c investigation_dao_load() ``` Le DAO doit retourner un `InvestigationRecord`. Si le chargement échoue : - récupérer le code et le message de la dernière erreur `Database` ; - copier le message dans un `GError` ; - retourner `NULL` ; - fermer proprement la connexion ; - libérer le projet ; - ne laisser aucune ressource allouée. Le code d’erreur de session attendu est : ```c INVESTIGATION_SESSION_ERROR_RECORD ``` ### 10. Vérifier la cohérence du chemin racine Le chemin sélectionné doit correspondre au champ persistant : ```text investigation.root_path ``` Comparer les versions canoniques de : ```text chemin racine sélectionné chemin racine enregistré dans InvestigationRecord ``` La comparaison doit être faite après normalisation avec : ```c g_canonicalize_filename() ``` Si les chemins ne correspondent pas, l’ouverture doit échouer avec : ```c INVESTIGATION_SESSION_ERROR_ROOT_MISMATCH ``` Cette vérification évite d’ouvrir une base copiée ou déplacée sans détecter l’incohérence. Le déplacement volontaire d’une enquête sera traité dans un ticket distinct. ### 11. Construire la session La session ne doit être créée qu’après validation complète : ```text dossier racine valide ↓ InvestigationProject valide ↓ fichier SQLite présent ↓ Database ouverte ↓ InvestigationRecord chargé ↓ chemin racine cohérent ↓ InvestigationSession créée ``` En cas d’échec d’allocation, retourner : ```c INVESTIGATION_SESSION_ERROR_MEMORY ``` ### 12. Ajouter la fonction de fermeture Déclarer : ```c void investigation_session_close( InvestigationSession *session ); ``` Cette fonction doit accepter `NULL`. Elle doit libérer toutes les ressources possédées par la session : ```text InvestigationRecord Database InvestigationProject InvestigationSession ``` La session devient propriétaire de ces trois objets dès que son ouverture réussit. ### 13. Ajouter les accesseurs Ajouter : ```c const InvestigationProject *investigation_session_get_project( const InvestigationSession *session ); const InvestigationRecord *investigation_session_get_record( const InvestigationSession *session ); Database *investigation_session_get_database( InvestigationSession *session ); ``` Les pointeurs retournés appartiennent à la session et ne doivent pas être libérés par l’appelant. Les accesseurs doivent retourner `NULL` si la session reçue est `NULL`. `Database` reste non constante car les futurs DAO auront besoin d’une connexion modifiable. ### 14. Interdire les dépendances SQLite et GTK Le module `InvestigationSession` ne doit pas inclure : ```c #include #include ``` Il doit exclusivement utiliser les abstractions existantes : ```text InvestigationProject Database InvestigationDao InvestigationRecord GLib ``` ## Tests à ajouter Créer : ```text tests/test_investigation_session.c ``` ### Test d’ouverture valide Créer un dossier temporaire. Initialiser une base avec : ```c database_initialize() ``` Ouvrir ensuite l’enquête avec : ```c investigation_session_open() ``` Vérifier : - la session n’est pas `NULL` ; - aucune erreur n’est produite ; - le projet est disponible ; - la connexion Database est disponible ; - le record est disponible ; - le nom de l’enquête est correct ; - le chemin racine est correct ; - l’UUID est valide ; - `created_at` n’est pas vide ; - `updated_at` n’est pas vide ; - la connexion peut encore être utilisée par un DAO ; - la fermeture libère correctement les ressources. ### Test des paramètres invalides Vérifier : ```c investigation_session_open(NULL, &error) == NULL investigation_session_open("", &error) == NULL ``` Le code attendu est : ```c INVESTIGATION_SESSION_ERROR_INVALID_ARGUMENT ``` Vérifier également le comportement avec : ```c error == NULL ``` ### Test d’un dossier inexistant Utiliser un chemin inexistant. Vérifier : ```text résultat == NULL erreur == INVESTIGATION_SESSION_ERROR_ROOT_NOT_FOUND message non vide ``` ### Test d’un chemin qui n’est pas un dossier Créer un fichier temporaire et utiliser son chemin comme racine. Vérifier : ```text résultat == NULL erreur == INVESTIGATION_SESSION_ERROR_ROOT_NOT_FOUND ``` ### Test d’une base absente Créer une structure de projet valide sans fichier : ```text 00_BaseDeDonnees/Enquete.sqlite ``` Vérifier : ```text résultat == NULL erreur == INVESTIGATION_SESSION_ERROR_DATABASE_NOT_FOUND ``` La fonction ne doit pas créer de nouveau fichier SQLite. ### Test d’une base invalide Créer un fichier SQLite vide ou une base ne contenant pas la table : ```text investigation ``` Vérifier : ```text résultat == NULL erreur == INVESTIGATION_SESSION_ERROR_RECORD message non vide ``` ### Test d’un chemin racine incohérent Créer une base avec un chemin racine enregistré différent du dossier utilisé pour l’ouverture. Vérifier : ```text résultat == NULL erreur == INVESTIGATION_SESSION_ERROR_ROOT_MISMATCH message non vide ``` ### Test des accesseurs avec NULL Vérifier : ```c investigation_session_get_project(NULL) == NULL investigation_session_get_record(NULL) == NULL investigation_session_get_database(NULL) == NULL ``` Vérifier également : ```c investigation_session_close(NULL); ``` ### Test de réutilisation du DAO Après l’ouverture valide d’une session, appeler de nouveau : ```c investigation_dao_load( investigation_session_get_database(session) ); ``` Vérifier que la connexion reste fonctionnelle pendant toute la durée de vie de la session. ## Gestion de la mémoire Toutes les sorties d’échec de `investigation_session_open()` doivent libérer les ressources déjà créées. Le nettoyage doit couvrir les cas suivants : ```text échec avant création du projet échec après création du projet échec après ouverture de Database échec après chargement du record échec lors de la comparaison des chemins échec lors de l’allocation de la session ``` Aucun objet ne doit être libéré deux fois. Aucune ressource temporaire ne doit rester allouée : ```text chemins canoniques messages copiés GError temporaires InvestigationProject Database InvestigationRecord ``` ## Makefile Ajouter : ```make TEST_INVESTIGATION_SESSION := tests/test_investigation_session ``` Ajouter une règle compilant au minimum : ```text tests/test_investigation_session.c src/core/investigation_session.c src/core/investigation_project.c src/dao/investigation_dao.c src/models/investigation_record.c src/database/database.c src/database/schema.c src/database/statement.c src/database/transaction.c src/database/error.c ``` Lier avec : ```text GLib SQLite ``` Ajouter le test aux cibles : ```text test clean ``` La nouvelle sortie attendue est : ```text InvestigationSession : tous les tests sont valides. ``` ## Critères d’acceptation - [ ] Le type `InvestigationSession` est opaque. - [ ] Une session possède un `InvestigationProject`. - [ ] Une session possède une connexion `Database`. - [ ] Une session possède un `InvestigationRecord`. - [ ] Le dossier racine est validé avant l’ouverture. - [ ] Le chemin racine est normalisé. - [ ] Le chemin de la base provient de `InvestigationProject`. - [ ] Le fichier SQLite doit exister avant l’appel à `database_open()`. - [ ] L’ouverture ne crée jamais silencieusement une nouvelle base. - [ ] Le record est chargé avec `InvestigationDao`. - [ ] Le chemin enregistré est comparé au chemin sélectionné. - [ ] Une incohérence de chemin empêche l’ouverture. - [ ] La connexion reste ouverte pendant la durée de vie de la session. - [ ] La fermeture libère toutes les ressources. - [ ] Les accesseurs acceptent une session `NULL`. - [ ] Les erreurs sont propagées avec `GError`. - [ ] Aucun type SQLite n’apparaît dans l’API de la session. - [ ] Aucune dépendance GTK n’est ajoutée. - [ ] Les tests d’ouverture valide sont présents. - [ ] Les tests de paramètres invalides sont présents. - [ ] Les tests de dossier absent sont présents. - [ ] Les tests de base absente sont présents. - [ ] Les tests de base invalide sont présents. - [ ] Les tests de chemin incohérent sont présents. - [ ] Les anciens tests restent valides. - [ ] `make` réussit sans erreur. - [ ] `make test` réussit. - [ ] `git diff --check` ne retourne aucune erreur. ## Audit attendu Les commandes suivantes ne doivent rien afficher : ```bash rg -n 'sqlite3_|#include |#include