diff --git a/Makefile b/Makefile index 32b3f2b..1dad8b8 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,7 @@ TEST_TRANSACTION = tests/test_transaction TEST_ERROR = tests/test_error TEST_INVESTIGATION_RECORD = tests/test_investigation_record TEST_INVESTIGATION_DAO := tests/test_investigation_dao +TEST_INVESTIGATION_SESSION := tests/test_investigation_session all: $(TARGET) @@ -123,6 +124,19 @@ $(TEST_INVESTIGATION_DAO): \ src/database/error.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 +$(TEST_INVESTIGATION_SESSION): \ + 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 + $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 + test: \ $(TEST_NODE) \ $(TEST_TREE_MODEL) \ @@ -133,7 +147,8 @@ test: \ $(TEST_TRANSACTION) \ $(TEST_ERROR) \ $(TEST_INVESTIGATION_RECORD) \ - $(TEST_INVESTIGATION_DAO) + $(TEST_INVESTIGATION_DAO) \ + $(TEST_INVESTIGATION_SESSION) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -145,6 +160,7 @@ test: \ @$(TEST_ERROR) @$(TEST_INVESTIGATION_RECORD) @$(TEST_INVESTIGATION_DAO) + @$(TEST_INVESTIGATION_SESSION) @echo "Tous les tests sont valides." %.o: %.c @@ -164,6 +180,7 @@ clean: $(TEST_TRANSACTION) \ $(TEST_ERROR) \ $(TEST_INVESTIGATION_RECORD) \ - $(TEST_INVESTIGATION_DAO) + $(TEST_INVESTIGATION_DAO) \ + $(TEST_INVESTIGATION_SESSION) .PHONY: clean run test diff --git a/docs/tickets/open/TICKET-027.md b/docs/tickets/closed/TICKET-027.md similarity index 100% rename from docs/tickets/open/TICKET-027.md rename to docs/tickets/closed/TICKET-027.md diff --git a/docs/tickets/open/TICKET-028.md b/docs/tickets/open/TICKET-028.md new file mode 100644 index 0000000..c314102 --- /dev/null +++ b/docs/tickets/open/TICKET-028.md @@ -0,0 +1,776 @@ +# 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 +/** + * @brief Contexte opaque représentant les chemins d'un projet d'enquête. + */ +typedef struct InvestigationProject InvestigationProject; + /** * @brief Crée une nouvelle enquête dans un dossier parent. * @@ -15,7 +20,7 @@ * * - le dossier racine de l'enquête ; * - l'arborescence standard ; - * - le fichier vide 00_BaseDeDonnees/Enquete.sqlite. + * - le fichier 00_BaseDeDonnees/Enquete.sqlite. * * Le dossier parent doit déjà exister. * @@ -28,7 +33,7 @@ * La chaîne retournée appartient au code appelant et doit être libérée * avec g_free(). * - * @param parent_directory Chemin du dossier parent. + * @param parent_directory Chemin du dossier parent. * @param investigation_name Nom de la nouvelle enquête. * * @return Le chemin complet de l'enquête créée, ou NULL en cas d'échec. @@ -38,7 +43,7 @@ char *investigation_project_create( const char *investigation_name ); -/****************************************************************************** +/** * @brief Vérifie qu'un dossier est une enquête valide. * * La fonction contrôle que tous les éléments obligatoires de la structure @@ -49,9 +54,62 @@ char *investigation_project_create( * @param investigation_path Chemin de l'enquête à vérifier. * * @return true si l'enquête est valide, sinon false. - ******************************************************************************/ + */ bool investigation_project_validate( const char *investigation_path ); +/** + * @brief Crée un contexte de chemins pour une enquête existante. + * + * Le chemin racine est converti en chemin canonique. + * + * Cette fonction ne crée aucun fichier et ne modifie pas le projet. + * Elle construit uniquement les chemins nécessaires à son utilisation. + * + * @param investigation_root_path Chemin racine de l'enquête. + * + * @return Un nouveau contexte InvestigationProject, ou NULL en cas d'échec. + */ +InvestigationProject *investigation_project_open( + const char *investigation_root_path +); + +/** + * @brief Libère un contexte InvestigationProject. + * + * Cette fonction accepte NULL. + * + * @param project Contexte à libérer. + */ +void investigation_project_free( + InvestigationProject *project +); + +/** + * @brief Retourne le chemin racine canonique de l'enquête. + * + * Le pointeur retourné appartient au contexte et ne doit pas être libéré. + * + * @param project Contexte du projet. + * + * @return Chemin racine, ou NULL si project vaut NULL. + */ +const char *investigation_project_get_root_path( + const InvestigationProject *project +); + +/** + * @brief Retourne le chemin du fichier SQLite de l'enquête. + * + * Le pointeur retourné appartient au contexte et ne doit pas être libéré. + * + * @param project Contexte du projet. + * + * @return Chemin du fichier Enquete.sqlite, ou NULL si project vaut NULL. + */ +const char *investigation_project_get_database_path( + const InvestigationProject *project +); + #endif diff --git a/include/core/investigation_session.h b/include/core/investigation_session.h new file mode 100644 index 0000000..84ec853 --- /dev/null +++ b/include/core/investigation_session.h @@ -0,0 +1,143 @@ +/****************************************************************************** + * @file investigation_session.h + * @brief Interface publique d'une session d'enquête ouverte. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_INVESTIGATION_SESSION_H +#define LABFY_INVESTIGATION_INVESTIGATION_SESSION_H + +#include + +#include "core/investigation_project.h" +#include "database/database.h" +#include "models/investigation_record.h" + +/** + * @brief Session opaque représentant une enquête ouverte. + * + * Une session possède : + * + * - le contexte de fichiers InvestigationProject ; + * - la connexion Database ; + * - les informations persistées InvestigationRecord. + */ +typedef struct InvestigationSession InvestigationSession; + +/** + * @brief Codes d'erreur produits lors de l'ouverture d'une session. + */ +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; + +/** + * @brief Domaine d'erreur GLib du module InvestigationSession. + */ +#define INVESTIGATION_SESSION_ERROR \ + investigation_session_error_quark() + +/** + * @brief Retourne le domaine d'erreur du module. + * + * @return Le GQuark associé aux erreurs InvestigationSession. + */ +GQuark investigation_session_error_quark(void); + +/** + * @brief Ouvre une enquête existante. + * + * La fonction : + * + * - vérifie que le chemin racine représente un dossier ; + * - construit un InvestigationProject ; + * - vérifie la présence du fichier SQLite ; + * - ouvre la connexion Database ; + * - charge le record persistant ; + * - vérifie la cohérence du chemin racine. + * + * Cette fonction ne crée aucune enquête et ne modifie pas la base. + * + * En cas de succès, la session devient propriétaire du projet, + * de la connexion Database et du record. + * + * Le paramètre error peut être NULL. + * + * @param investigation_root_path Chemin racine de l'enquête. + * @param error Adresse recevant une éventuelle erreur GLib. + * + * @return Une nouvelle session, ou NULL en cas d'échec. + */ +InvestigationSession *investigation_session_open( + const char *investigation_root_path, + GError **error +); + +/** + * @brief Ferme une session d'enquête. + * + * Cette fonction libère : + * + * - le record ; + * - la connexion Database ; + * - le contexte InvestigationProject ; + * - la session. + * + * Cette fonction accepte NULL. + * + * @param session Session à fermer. + */ +void investigation_session_close( + InvestigationSession *session +); + +/** + * @brief Retourne le contexte de fichiers de la session. + * + * Le pointeur retourné appartient à la session et ne doit pas être libéré. + * + * @param session Session d'enquête. + * + * @return Le projet associé, ou NULL si session vaut NULL. + */ +const InvestigationProject *investigation_session_get_project( + const InvestigationSession *session +); + +/** + * @brief Retourne les informations persistées de l'enquête. + * + * Le pointeur retourné appartient à la session et ne doit pas être libéré. + * + * @param session Session d'enquête. + * + * @return Le record associé, ou NULL si session vaut NULL. + */ +const InvestigationRecord *investigation_session_get_record( + const InvestigationSession *session +); + +/** + * @brief Retourne la connexion Database de la session. + * + * Le pointeur retourné appartient à la session et ne doit pas être fermé + * par le code appelant. + * + * La connexion reste modifiable afin d'être utilisée par les futurs DAO. + * + * @param session Session d'enquête. + * + * @return La connexion associée, ou NULL si session vaut NULL. + */ +Database *investigation_session_get_database( + InvestigationSession *session +); + +#endif diff --git a/labfy-investigation b/labfy-investigation index c28c4e8..6f07ba3 100755 Binary files a/labfy-investigation and b/labfy-investigation differ diff --git a/src/core/investigation_project.c b/src/core/investigation_project.c index e70fa0b..1bcf57b 100644 --- a/src/core/investigation_project.c +++ b/src/core/investigation_project.c @@ -12,6 +12,27 @@ #include #include +/** + * @brief Nom du dossier contenant la base de données. + */ +#define INVESTIGATION_PROJECT_DATABASE_DIRECTORY \ + "00_BaseDeDonnees" + +/** + * @brief Nom du fichier SQLite d'une enquête. + */ +#define INVESTIGATION_PROJECT_DATABASE_FILENAME \ + "Enquete.sqlite" + +/** + * @brief Représentation privée des chemins d'un projet d'enquête. + */ +struct InvestigationProject +{ + char *root_path; + char *database_path; +}; + /** * @brief Type d'un élément de la structure d'une enquête. */ @@ -436,11 +457,10 @@ char *investigation_project_create( return NULL; } } - database_path = g_build_filename( investigation_path, - "00_BaseDeDonnees", - "Enquete.sqlite", + INVESTIGATION_PROJECT_DATABASE_DIRECTORY, + INVESTIGATION_PROJECT_DATABASE_FILENAME, NULL ); @@ -460,14 +480,25 @@ char *investigation_project_create( return NULL; } + /* + * Le chemin est enregistré avant l'initialisation. + * + * Ainsi, si database_initialize() crée le fichier puis échoue, + * le nettoyage pourra supprimer ce fichier avant ses dossiers parents. + * + * Le tableau devient propriétaire de database_path. + */ + g_ptr_array_add( + created_paths, + database_path + ); + if (!database_initialize( database_path, investigation_name, investigation_path )) { - g_free(database_path); - investigation_project_cleanup_created_paths( created_paths ); @@ -482,22 +513,11 @@ char *investigation_project_create( return NULL; } - /* - * Le fichier doit également être supprimé avant ses dossiers - * si une évolution future ajoute une étape pouvant encore échouer. - * - * Le tableau devient propriétaire de database_path. - */ - g_ptr_array_add( - created_paths, - database_path - ); - /* * Tout est créé avec succès. * - * On libère seulement le tableau et ses copies de chemins. - * Les fichiers et dossiers restent sur le disque. + * On libère uniquement le tableau et ses chaînes. + * Les fichiers et dossiers restent présents sur le disque. */ g_ptr_array_free( created_paths, @@ -553,3 +573,109 @@ bool investigation_project_validate( return true; } + +InvestigationProject *investigation_project_open( + const char *investigation_root_path +) +{ + InvestigationProject *project = NULL; + char *canonical_root_path = NULL; + + if (investigation_root_path == NULL || + investigation_root_path[0] == '\0') + { + return NULL; + } + + canonical_root_path = g_canonicalize_filename( + investigation_root_path, + NULL + ); + + if (canonical_root_path == NULL) + { + return NULL; + } + + /* + * Le contexte représente une enquête existante. + * + * On vérifie seulement ici que sa racine est un dossier. + * La présence de la base SQLite sera contrôlée séparément par + * InvestigationSession afin de produire une erreur précise. + */ + if (!g_file_test( + canonical_root_path, + G_FILE_TEST_IS_DIR + )) + { + g_free(canonical_root_path); + return NULL; + } + + project = g_try_new0( + InvestigationProject, + 1 + ); + + if (project == NULL) + { + g_free(canonical_root_path); + return NULL; + } + + project->root_path = canonical_root_path; + + project->database_path = g_build_filename( + project->root_path, + INVESTIGATION_PROJECT_DATABASE_DIRECTORY, + INVESTIGATION_PROJECT_DATABASE_FILENAME, + NULL + ); + + if (project->database_path == NULL) + { + investigation_project_free(project); + return NULL; + } + + return project; +} + +void investigation_project_free( + InvestigationProject *project +) +{ + if (project == NULL) + { + return; + } + + g_free(project->database_path); + g_free(project->root_path); + g_free(project); +} + +const char *investigation_project_get_root_path( + const InvestigationProject *project +) +{ + if (project == NULL) + { + return NULL; + } + + return project->root_path; +} + +const char *investigation_project_get_database_path( + const InvestigationProject *project +) +{ + if (project == NULL) + { + return NULL; + } + + return project->database_path; +} diff --git a/src/core/investigation_session.c b/src/core/investigation_session.c new file mode 100644 index 0000000..ebc5e67 --- /dev/null +++ b/src/core/investigation_session.c @@ -0,0 +1,374 @@ +/****************************************************************************** + * @file investigation_session.c + * @brief Gestion d'une session d'enquête ouverte. + ******************************************************************************/ + +#include "core/investigation_session.h" + +#include "dao/investigation_dao.h" +#include "database/error.h" + +/** + * @brief Représentation privée d'une session d'enquête. + */ +struct InvestigationSession +{ + InvestigationProject *project; + Database *database; + InvestigationRecord *record; +}; + +/** + * @brief Enregistre une erreur littérale si un GError est demandé. + * + * @param error Adresse recevant l'erreur. + * @param error_code Code d'erreur du module. + * @param error_message Message associé. + */ +static void investigation_session_set_error_literal( + GError **error, + InvestigationSessionError error_code, + const char *error_message +) +{ + if (error == NULL) + { + return; + } + + g_set_error_literal( + error, + INVESTIGATION_SESSION_ERROR, + error_code, + error_message + ); +} + +GQuark investigation_session_error_quark(void) +{ + return g_quark_from_static_string( + "investigation-session-error-quark" + ); +} + +InvestigationSession *investigation_session_open( + const char *investigation_root_path, + GError **error +) +{ + InvestigationSession *session = NULL; + InvestigationProject *project = NULL; + InvestigationRecord *record = NULL; + Database *database = NULL; + + char *canonical_root_path = NULL; + char *canonical_record_root_path = NULL; + + const char *database_path = NULL; + const char *record_root_path = NULL; + const char *database_error_message = NULL; + + /* + * Convention GLib : + * + * un GError existant ne doit pas être remplacé. + */ + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (investigation_root_path == NULL || + investigation_root_path[0] == '\0') + { + investigation_session_set_error_literal( + error, + INVESTIGATION_SESSION_ERROR_INVALID_ARGUMENT, + "Le chemin racine de l'enquête est invalide." + ); + + return NULL; + } + + canonical_root_path = g_canonicalize_filename( + investigation_root_path, + NULL + ); + + if (canonical_root_path == NULL) + { + investigation_session_set_error_literal( + error, + INVESTIGATION_SESSION_ERROR_MEMORY, + "Impossible d'allouer le chemin canonique de l'enquête." + ); + + return NULL; + } + + if (!g_file_test( + canonical_root_path, + G_FILE_TEST_IS_DIR + )) + { + g_set_error( + error, + INVESTIGATION_SESSION_ERROR, + INVESTIGATION_SESSION_ERROR_ROOT_NOT_FOUND, + "Le chemin racine '%s' n'est pas un dossier existant.", + canonical_root_path + ); + + goto cleanup; + } + + project = investigation_project_open( + canonical_root_path + ); + + if (project == NULL) + { + investigation_session_set_error_literal( + error, + INVESTIGATION_SESSION_ERROR_PROJECT, + "Impossible de construire le contexte du projet d'enquête." + ); + + goto cleanup; + } + + database_path = investigation_project_get_database_path( + project + ); + + if (database_path == NULL) + { + investigation_session_set_error_literal( + error, + INVESTIGATION_SESSION_ERROR_PROJECT, + "Le projet ne fournit aucun chemin vers la base de données." + ); + + goto cleanup; + } + + /* + * Cette vérification doit précéder database_open(). + * + * SQLite peut créer automatiquement une base absente lors de + * l'ouverture. Une session ne doit jamais créer silencieusement + * une nouvelle base. + */ + if (!g_file_test( + database_path, + G_FILE_TEST_IS_REGULAR + )) + { + g_set_error( + error, + INVESTIGATION_SESSION_ERROR, + INVESTIGATION_SESSION_ERROR_DATABASE_NOT_FOUND, + "Le fichier SQLite de l'enquête est absent : '%s'.", + database_path + ); + + goto cleanup; + } + + database = database_open( + database_path + ); + + if (database == NULL) + { + g_set_error( + error, + INVESTIGATION_SESSION_ERROR, + INVESTIGATION_SESSION_ERROR_DATABASE, + "Impossible d'ouvrir la base de données '%s'.", + database_path + ); + + goto cleanup; + } + + record = investigation_dao_load( + database + ); + + if (record == NULL) + { + /* + * Le message doit être copié dans le GError avant la fermeture + * de Database, car il appartient à l'objet Database. + */ + database_error_message = database_error_get_message( + database + ); + + if (database_error_message == NULL || + database_error_message[0] == '\0') + { + database_error_message = + "Impossible de charger les informations de l'enquête."; + } + + g_set_error( + error, + INVESTIGATION_SESSION_ERROR, + INVESTIGATION_SESSION_ERROR_RECORD, + "%s", + database_error_message + ); + + goto cleanup; + } + + record_root_path = investigation_record_get_root_path( + record + ); + + if (record_root_path == NULL || + record_root_path[0] == '\0') + { + investigation_session_set_error_literal( + error, + INVESTIGATION_SESSION_ERROR_RECORD, + "Le chemin racine enregistré dans la base est invalide." + ); + + goto cleanup; + } + + canonical_record_root_path = g_canonicalize_filename( + record_root_path, + NULL + ); + + if (canonical_record_root_path == NULL) + { + investigation_session_set_error_literal( + error, + INVESTIGATION_SESSION_ERROR_MEMORY, + "Impossible d'allouer le chemin canonique enregistré." + ); + + goto cleanup; + } + + if (g_strcmp0( + canonical_root_path, + canonical_record_root_path + ) != 0) + { + g_set_error( + error, + INVESTIGATION_SESSION_ERROR, + INVESTIGATION_SESSION_ERROR_ROOT_MISMATCH, + "Le chemin sélectionné '%s' ne correspond pas au chemin " + "enregistré '%s'.", + canonical_root_path, + canonical_record_root_path + ); + + goto cleanup; + } + + session = g_try_new0( + InvestigationSession, + 1 + ); + + if (session == NULL) + { + investigation_session_set_error_literal( + error, + INVESTIGATION_SESSION_ERROR_MEMORY, + "Impossible d'allouer la session d'enquête." + ); + + goto cleanup; + } + + /* + * La session devient propriétaire des trois objets. + */ + session->project = project; + session->database = database; + session->record = record; + + project = NULL; + database = NULL; + record = NULL; + +cleanup: + + g_free(canonical_record_root_path); + g_free(canonical_root_path); + + investigation_record_free(record); + database_close(database); + investigation_project_free(project); + + return session; +} + +void investigation_session_close( + InvestigationSession *session +) +{ + if (session == NULL) + { + return; + } + + investigation_record_free( + session->record + ); + + database_close( + session->database + ); + + investigation_project_free( + session->project + ); + + g_free(session); +} + +const InvestigationProject *investigation_session_get_project( + const InvestigationSession *session +) +{ + if (session == NULL) + { + return NULL; + } + + return session->project; +} + +const InvestigationRecord *investigation_session_get_record( + const InvestigationSession *session +) +{ + if (session == NULL) + { + return NULL; + } + + return session->record; +} + +Database *investigation_session_get_database( + InvestigationSession *session +) +{ + if (session == NULL) + { + return NULL; + } + + return session->database; +} diff --git a/tests/test_investigation_project b/tests/test_investigation_project index ab7a276..3bd6369 100755 Binary files a/tests/test_investigation_project and b/tests/test_investigation_project differ diff --git a/tests/test_investigation_project.c b/tests/test_investigation_project.c index ddb8e41..d739011 100644 --- a/tests/test_investigation_project.c +++ b/tests/test_investigation_project.c @@ -735,6 +735,181 @@ static void test_validate_directory_replaced_by_file(void) g_free(temporary_parent); } +/** + * @brief Vérifie l'ouverture d'un projet existant et ses accesseurs. + */ +static void test_open_existing_project(void) +{ + char *temporary_parent = NULL; + char *investigation_path = NULL; + char *path_with_dot = NULL; + char *expected_root_path = NULL; + char *expected_database_path = NULL; + + InvestigationProject *project = NULL; + + GError *error = NULL; + + temporary_parent = g_dir_make_tmp( + "labfy-investigation-project-open-test-XXXXXX", + &error + ); + + assert(temporary_parent != NULL); + assert(error == NULL); + + investigation_path = investigation_project_create( + temporary_parent, + "Enquete_Ouverte" + ); + + assert(investigation_path != NULL); + + /* + * Le segment "." permet de vérifier la canonicalisation du chemin. + */ + path_with_dot = g_build_filename( + investigation_path, + ".", + NULL + ); + + assert(path_with_dot != NULL); + + expected_root_path = g_canonicalize_filename( + investigation_path, + NULL + ); + + assert(expected_root_path != NULL); + + expected_database_path = g_build_filename( + expected_root_path, + "00_BaseDeDonnees", + "Enquete.sqlite", + NULL + ); + + assert(expected_database_path != NULL); + + project = investigation_project_open( + path_with_dot + ); + + assert(project != NULL); + + assert( + strcmp( + investigation_project_get_root_path(project), + expected_root_path + ) == 0 + ); + + assert( + strcmp( + investigation_project_get_database_path(project), + expected_database_path + ) == 0 + ); + + investigation_project_free(project); + + assert( + test_remove_path_recursively( + temporary_parent + ) + ); + + g_free(expected_database_path); + g_free(expected_root_path); + g_free(path_with_dot); + g_free(investigation_path); + g_free(temporary_parent); +} + +/** + * @brief Vérifie le refus des chemins invalides lors de l'ouverture. + */ +static void test_open_invalid_project_paths(void) +{ + char *temporary_directory = NULL; + char *temporary_file = NULL; + + GError *error = NULL; + + assert( + investigation_project_open(NULL) == NULL + ); + + assert( + investigation_project_open("") == NULL + ); + + assert( + investigation_project_open( + "/tmp/labfy-investigation-project-does-not-exist" + ) == NULL + ); + + temporary_directory = g_dir_make_tmp( + "labfy-investigation-project-open-file-test-XXXXXX", + &error + ); + + assert(temporary_directory != NULL); + assert(error == NULL); + + temporary_file = g_build_filename( + temporary_directory, + "not-a-directory.txt", + NULL + ); + + assert(temporary_file != NULL); + + assert( + g_file_set_contents( + temporary_file, + "test\n", + -1, + &error + ) + ); + + assert(error == NULL); + + assert( + investigation_project_open( + temporary_file + ) == NULL + ); + + assert( + test_remove_path_recursively( + temporary_directory + ) + ); + + g_free(temporary_file); + g_free(temporary_directory); +} + +/** + * @brief Vérifie les fonctions acceptant un projet NULL. + */ +static void test_open_project_null_instance(void) +{ + assert( + investigation_project_get_root_path(NULL) == NULL + ); + + assert( + investigation_project_get_database_path(NULL) == NULL + ); + + investigation_project_free(NULL); +} + int main(void) { test_create_valid_investigation(); @@ -749,6 +924,11 @@ int main(void) test_validate_missing_directory(); test_validate_directory_replaced_by_file(); + + test_open_existing_project(); + test_open_invalid_project_paths(); + test_open_project_null_instance(); + printf( "InvestigationProject : tous les tests sont valides.\n" ); diff --git a/tests/test_investigation_session b/tests/test_investigation_session new file mode 100755 index 0000000..04f8d66 Binary files /dev/null and b/tests/test_investigation_session differ diff --git a/tests/test_investigation_session.c b/tests/test_investigation_session.c new file mode 100644 index 0000000..c82f368 --- /dev/null +++ b/tests/test_investigation_session.c @@ -0,0 +1,684 @@ +/****************************************************************************** + * @file test_investigation_session.c + * @brief Tests du contexte représentant une enquête ouverte. + ******************************************************************************/ + +#include "core/investigation_session.h" + +#include "core/investigation_project.h" +#include "dao/investigation_dao.h" +#include "database/database.h" +#include "models/investigation_record.h" + +#include +#include +#include + +#include +#include + +static gboolean test_remove_path_recursively( + const char *path +) +{ + GDir *directory = NULL; + const char *entry_name = NULL; + + if (path == NULL) + { + return FALSE; + } + + if (!g_file_test( + path, + G_FILE_TEST_EXISTS + )) + { + return TRUE; + } + + if (!g_file_test( + path, + G_FILE_TEST_IS_DIR + )) + { + return g_remove(path) == 0; + } + + directory = g_dir_open( + path, + 0, + NULL + ); + + if (directory == NULL) + { + return FALSE; + } + + while ((entry_name = g_dir_read_name(directory)) != NULL) + { + char *entry_path = NULL; + gboolean removed = FALSE; + + entry_path = g_build_filename( + path, + entry_name, + NULL + ); + + if (entry_path == NULL) + { + g_dir_close(directory); + return FALSE; + } + + removed = test_remove_path_recursively( + entry_path + ); + + g_free(entry_path); + + if (!removed) + { + g_dir_close(directory); + return FALSE; + } + } + + g_dir_close(directory); + + return g_rmdir(path) == 0; +} + +static void test_assert_session_error( + const GError *error, + InvestigationSessionError expected_code +) +{ + assert(error != NULL); + assert(error->domain == INVESTIGATION_SESSION_ERROR); + assert(error->code == (gint) expected_code); + assert(error->message != NULL); + assert(error->message[0] != '\0'); +} + +static void test_open_valid_session(void) +{ + char *temporary_parent = NULL; + char *investigation_path = NULL; + char *canonical_root_path = NULL; + + InvestigationSession *session = NULL; + const InvestigationProject *project = NULL; + const InvestigationRecord *record = NULL; + InvestigationRecord *reloaded_record = NULL; + Database *database = NULL; + + const char *record_id = NULL; + const char *reloaded_record_id = NULL; + + GError *error = NULL; + + temporary_parent = g_dir_make_tmp( + "labfy-investigation-session-valid-XXXXXX", + &error + ); + + assert(temporary_parent != NULL); + assert(error == NULL); + + investigation_path = investigation_project_create( + temporary_parent, + "Enquete_Session" + ); + + assert(investigation_path != NULL); + + canonical_root_path = g_canonicalize_filename( + investigation_path, + NULL + ); + + assert(canonical_root_path != NULL); + + session = investigation_session_open( + investigation_path, + &error + ); + + assert(session != NULL); + assert(error == NULL); + + project = investigation_session_get_project( + session + ); + + database = investigation_session_get_database( + session + ); + + record = investigation_session_get_record( + session + ); + + assert(project != NULL); + assert(database != NULL); + assert(record != NULL); + + assert( + strcmp( + investigation_project_get_root_path(project), + canonical_root_path + ) == 0 + ); + + assert( + strcmp( + investigation_record_get_name(record), + "Enquete_Session" + ) == 0 + ); + + assert( + strcmp( + investigation_record_get_root_path(record), + canonical_root_path + ) == 0 + ); + + record_id = investigation_record_get_id( + record + ); + + assert(record_id != NULL); + assert(g_uuid_string_is_valid(record_id)); + + assert( + investigation_record_get_created_at(record) != NULL + ); + + assert( + investigation_record_get_created_at(record)[0] != '\0' + ); + + assert( + investigation_record_get_updated_at(record) != NULL + ); + + assert( + investigation_record_get_updated_at(record)[0] != '\0' + ); + + reloaded_record = investigation_dao_load( + database + ); + + assert(reloaded_record != NULL); + + reloaded_record_id = investigation_record_get_id( + reloaded_record + ); + + assert(reloaded_record_id != NULL); + + assert( + strcmp( + record_id, + reloaded_record_id + ) == 0 + ); + + investigation_record_free(reloaded_record); + investigation_session_close(session); + + assert( + test_remove_path_recursively( + temporary_parent + ) + ); + + g_free(canonical_root_path); + g_free(investigation_path); + g_free(temporary_parent); +} + +static void test_open_invalid_parameters(void) +{ + InvestigationSession *session = NULL; + GError *error = NULL; + + session = investigation_session_open( + NULL, + &error + ); + + assert(session == NULL); + + test_assert_session_error( + error, + INVESTIGATION_SESSION_ERROR_INVALID_ARGUMENT + ); + + g_clear_error(&error); + + session = investigation_session_open( + "", + &error + ); + + assert(session == NULL); + + test_assert_session_error( + error, + INVESTIGATION_SESSION_ERROR_INVALID_ARGUMENT + ); + + g_clear_error(&error); + + assert( + investigation_session_open( + NULL, + NULL + ) == NULL + ); + + assert( + investigation_session_open( + "", + NULL + ) == NULL + ); +} + +static void test_open_missing_root(void) +{ + char *temporary_parent = NULL; + char *missing_root_path = NULL; + + InvestigationSession *session = NULL; + + GError *error = NULL; + + temporary_parent = g_dir_make_tmp( + "labfy-investigation-session-missing-root-XXXXXX", + &error + ); + + assert(temporary_parent != NULL); + assert(error == NULL); + + missing_root_path = g_build_filename( + temporary_parent, + "does-not-exist", + NULL + ); + + assert(missing_root_path != NULL); + + session = investigation_session_open( + missing_root_path, + &error + ); + + assert(session == NULL); + + test_assert_session_error( + error, + INVESTIGATION_SESSION_ERROR_ROOT_NOT_FOUND + ); + + g_clear_error(&error); + + assert( + !g_file_test( + missing_root_path, + G_FILE_TEST_EXISTS + ) + ); + + assert( + test_remove_path_recursively( + temporary_parent + ) + ); + + g_free(missing_root_path); + g_free(temporary_parent); +} + +static void test_open_root_is_file(void) +{ + char *temporary_parent = NULL; + char *file_path = NULL; + + InvestigationSession *session = NULL; + + GError *error = NULL; + + temporary_parent = g_dir_make_tmp( + "labfy-investigation-session-root-file-XXXXXX", + &error + ); + + assert(temporary_parent != NULL); + assert(error == NULL); + + file_path = g_build_filename( + temporary_parent, + "not-a-directory.txt", + NULL + ); + + assert(file_path != NULL); + + assert( + g_file_set_contents( + file_path, + "test\n", + -1, + &error + ) + ); + + assert(error == NULL); + + session = investigation_session_open( + file_path, + &error + ); + + assert(session == NULL); + + test_assert_session_error( + error, + INVESTIGATION_SESSION_ERROR_ROOT_NOT_FOUND + ); + + g_clear_error(&error); + + assert( + test_remove_path_recursively( + temporary_parent + ) + ); + + g_free(file_path); + g_free(temporary_parent); +} + +static void test_open_missing_database(void) +{ + char *investigation_root = NULL; + char *database_directory = NULL; + char *database_path = NULL; + + InvestigationSession *session = NULL; + + GError *error = NULL; + + investigation_root = g_dir_make_tmp( + "labfy-investigation-session-missing-database-XXXXXX", + &error + ); + + assert(investigation_root != NULL); + assert(error == NULL); + + database_directory = g_build_filename( + investigation_root, + "00_BaseDeDonnees", + NULL + ); + + assert(database_directory != NULL); + + assert( + g_mkdir( + database_directory, + 0700 + ) == 0 + ); + + database_path = g_build_filename( + database_directory, + "Enquete.sqlite", + NULL + ); + + assert(database_path != NULL); + + assert( + !g_file_test( + database_path, + G_FILE_TEST_EXISTS + ) + ); + + session = investigation_session_open( + investigation_root, + &error + ); + + assert(session == NULL); + + test_assert_session_error( + error, + INVESTIGATION_SESSION_ERROR_DATABASE_NOT_FOUND + ); + + g_clear_error(&error); + + assert( + !g_file_test( + database_path, + G_FILE_TEST_EXISTS + ) + ); + + assert( + test_remove_path_recursively( + investigation_root + ) + ); + + g_free(database_path); + g_free(database_directory); + g_free(investigation_root); +} + +static void test_open_invalid_database(void) +{ + char *investigation_root = NULL; + char *database_directory = NULL; + char *database_path = NULL; + + InvestigationSession *session = NULL; + + GError *error = NULL; + + investigation_root = g_dir_make_tmp( + "labfy-investigation-session-invalid-database-XXXXXX", + &error + ); + + assert(investigation_root != NULL); + assert(error == NULL); + + database_directory = g_build_filename( + investigation_root, + "00_BaseDeDonnees", + NULL + ); + + assert(database_directory != NULL); + + assert( + g_mkdir( + database_directory, + 0700 + ) == 0 + ); + + database_path = g_build_filename( + database_directory, + "Enquete.sqlite", + NULL + ); + + assert(database_path != NULL); + + assert( + g_file_set_contents( + database_path, + "", + 0, + &error + ) + ); + + assert(error == NULL); + + session = investigation_session_open( + investigation_root, + &error + ); + + assert(session == NULL); + + test_assert_session_error( + error, + INVESTIGATION_SESSION_ERROR_RECORD + ); + + g_clear_error(&error); + + assert( + test_remove_path_recursively( + investigation_root + ) + ); + + g_free(database_path); + g_free(database_directory); + g_free(investigation_root); +} + +static void test_open_root_mismatch(void) +{ + char *investigation_root = NULL; + char *database_directory = NULL; + char *database_path = NULL; + char *different_root_path = NULL; + + InvestigationSession *session = NULL; + + GError *error = NULL; + + investigation_root = g_dir_make_tmp( + "labfy-investigation-session-root-mismatch-XXXXXX", + &error + ); + + assert(investigation_root != NULL); + assert(error == NULL); + + database_directory = g_build_filename( + investigation_root, + "00_BaseDeDonnees", + NULL + ); + + assert(database_directory != NULL); + + assert( + g_mkdir( + database_directory, + 0700 + ) == 0 + ); + + database_path = g_build_filename( + database_directory, + "Enquete.sqlite", + NULL + ); + + assert(database_path != NULL); + + different_root_path = g_build_filename( + investigation_root, + "autre-emplacement", + NULL + ); + + assert(different_root_path != NULL); + + assert( + database_initialize( + database_path, + "Enquete_Incoherente", + different_root_path + ) + ); + + session = investigation_session_open( + investigation_root, + &error + ); + + assert(session == NULL); + + test_assert_session_error( + error, + INVESTIGATION_SESSION_ERROR_ROOT_MISMATCH + ); + + g_clear_error(&error); + + assert( + test_remove_path_recursively( + investigation_root + ) + ); + + g_free(different_root_path); + g_free(database_path); + g_free(database_directory); + g_free(investigation_root); +} + +static void test_null_session_accessors(void) +{ + assert( + investigation_session_get_project(NULL) == NULL + ); + + assert( + investigation_session_get_record(NULL) == NULL + ); + + assert( + investigation_session_get_database(NULL) == NULL + ); + + investigation_session_close(NULL); +} + +int main(void) +{ + test_open_valid_session(); + test_open_invalid_parameters(); + test_open_missing_root(); + test_open_root_is_file(); + test_open_missing_database(); + test_open_invalid_database(); + test_open_root_mismatch(); + test_null_session_accessors(); + + printf( + "InvestigationSession : tous les tests sont valides.\n" + ); + + return 0; +}