From 18566280a7319e6eab72eccb69078b0518b36c86 Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Mon, 20 Jul 2026 09:35:36 +0200 Subject: [PATCH] feat: add evidence metadata during import --- .gitignore | 8 +- Makefile | 52 +- include/dao/evidence_type_dao.h | 121 ++ include/models/evidence_type.h | 132 ++ include/views/evidence_import_dialog.h | 176 +++ src/core/application.c | 326 ++++- src/dao/evidence_type_dao.c | 498 +++++++ src/models/evidence_type.c | 266 ++++ src/views/evidence_import_dialog.c | 1705 ++++++++++++++++++++++++ tests/test_evidence_import_dialog.c | 369 +++++ tests/test_evidence_type.c | 339 +++++ tests/test_evidence_type_dao.c | 495 +++++++ 12 files changed, 4469 insertions(+), 18 deletions(-) create mode 100644 include/dao/evidence_type_dao.h create mode 100644 include/models/evidence_type.h create mode 100644 include/views/evidence_import_dialog.h create mode 100644 src/dao/evidence_type_dao.c create mode 100644 src/models/evidence_type.c create mode 100644 src/views/evidence_import_dialog.c create mode 100644 tests/test_evidence_import_dialog.c create mode 100644 tests/test_evidence_type.c create mode 100644 tests/test_evidence_type_dao.c diff --git a/.gitignore b/.gitignore index 3fedcb4..bec1e59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,13 @@ # Build -enquete-gui +labfy-investigation *.o +# Executables de tests +/tests/test_* + +# Conserver les sources des tests +!/tests/test_*.c + # SQLite temporaires *.sqlite-shm *.sqlite-wal diff --git a/Makefile b/Makefile index 9bb8b76..26497c7 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,18 @@ TEST_CFLAGS = -std=c17 \ TEST_LDFLAGS = $(shell $(PKG_CONFIG) --libs glib-2.0 gio-2.0) EVIDENCE_RECORD_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic +EVIDENCE_TYPE_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic +EVIDENCE_TYPE_DAO_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic +EVIDENCE_IMPORT_DIALOG_TEST_CFLAGS := \ + -std=c17 \ + -Wall \ + -Wextra \ + -Werror \ + -Iinclude \ + $(shell $(PKG_CONFIG) --cflags gtk4) + +EVIDENCE_IMPORT_DIALOG_TEST_LDFLAGS := \ + $(shell $(PKG_CONFIG) --libs gtk4) SRC := $(shell find src -name "*.c") @@ -55,6 +67,9 @@ TEST_FILE_HASH := tests/test_file_hash TEST_EVIDENCE_COPY := tests/test_evidence_copy TEST_EVIDENCE_IMPORTER := tests/test_evidence_importer TEST_EVIDENCE_IMPORT_TASK := tests/test_evidence_import_task +TEST_EVIDENCE_TYPE := tests/test_evidence_type +TEST_EVIDENCE_TYPE_DAO := tests/test_evidence_type_dao +TEST_EVIDENCE_IMPORT_DIALOG := tests/test_evidence_import_dialog all: $(TARGET) @@ -265,6 +280,30 @@ $(TEST_EVIDENCE_IMPORT_TASK): \ -DEVIDENCE_IMPORTER_ENABLE_TEST_HOOKS \ $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 +$(TEST_EVIDENCE_TYPE): \ + tests/test_evidence_type.c \ + src/models/evidence_type.c + $(CC) $(EVIDENCE_TYPE_TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) + +$(TEST_EVIDENCE_TYPE_DAO): \ + tests/test_evidence_type_dao.c \ + src/dao/evidence_type_dao.c \ + src/models/evidence_type.c \ + src/database/database.c \ + src/database/schema.c \ + src/database/statement.c \ + src/database/transaction.c \ + src/database/error.c + $(CC) $(EVIDENCE_TYPE_DAO_TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 + +$(TEST_EVIDENCE_IMPORT_DIALOG): \ + tests/test_evidence_import_dialog.c \ + src/views/evidence_import_dialog.c \ + src/models/evidence_type.c + $(CC) $(EVIDENCE_IMPORT_DIALOG_TEST_CFLAGS) $^ \ + -o $@ \ + $(EVIDENCE_IMPORT_DIALOG_TEST_LDFLAGS) + test: \ $(TEST_NODE) \ $(TEST_TREE_MODEL) \ @@ -276,8 +315,10 @@ test: \ $(TEST_ERROR) \ $(TEST_INVESTIGATION_RECORD) \ $(TEST_EVIDENCE_RECORD) \ + $(TEST_EVIDENCE_TYPE) \ $(TEST_INVESTIGATION_DAO) \ $(TEST_EVIDENCE_DAO) \ + $(TEST_EVIDENCE_TYPE_DAO) \ $(TEST_INVESTIGATION_SESSION) \ $(TEST_BACKGROUND_TASK) \ $(TEST_TASK_MANAGER) \ @@ -289,7 +330,8 @@ test: \ $(TEST_FILE_HASH) \ $(TEST_EVIDENCE_COPY) \ $(TEST_EVIDENCE_IMPORTER) \ - $(TEST_EVIDENCE_IMPORT_TASK) + $(TEST_EVIDENCE_IMPORT_TASK) \ + $(TEST_EVIDENCE_IMPORT_DIALOG) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -301,8 +343,10 @@ test: \ @$(TEST_ERROR) @$(TEST_INVESTIGATION_RECORD) @$(TEST_EVIDENCE_RECORD) + @$(TEST_EVIDENCE_TYPE) @$(TEST_INVESTIGATION_DAO) @$(TEST_EVIDENCE_DAO) + @$(TEST_EVIDENCE_TYPE_DAO) @$(TEST_INVESTIGATION_SESSION) @$(TEST_BACKGROUND_TASK) @$(TEST_TASK_MANAGER) @@ -315,6 +359,7 @@ test: \ @$(TEST_EVIDENCE_COPY) @$(TEST_EVIDENCE_IMPORTER) @$(TEST_EVIDENCE_IMPORT_TASK) + @$(TEST_EVIDENCE_IMPORT_DIALOG) @echo "Tous les tests sont valides." %.o: %.c @@ -335,6 +380,8 @@ clean: $(TEST_ERROR) \ $(TEST_INVESTIGATION_RECORD) \ $(TEST_EVIDENCE_RECORD) \ + $(TEST_EVIDENCE_TYPE) \ + $(TEST_EVIDENCE_TYPE_DAO) \ $(TEST_INVESTIGATION_DAO) \ $(TEST_EVIDENCE_DAO) \ $(TEST_INVESTIGATION_SESSION) \ @@ -348,7 +395,8 @@ clean: $(TEST_FILE_HASH) \ $(TEST_EVIDENCE_COPY) \ $(TEST_EVIDENCE_IMPORTER) \ - $(TEST_EVIDENCE_IMPORT_TASK) + $(TEST_EVIDENCE_IMPORT_TASK) \ + $(TEST_EVIDENCE_IMPORT_DIALOG) .PHONY: clean run test diff --git a/include/dao/evidence_type_dao.h b/include/dao/evidence_type_dao.h new file mode 100644 index 0000000..6aea95e --- /dev/null +++ b/include/dao/evidence_type_dao.h @@ -0,0 +1,121 @@ +/****************************************************************************** + * @file evidence_type_dao.h + * @brief Lecture des types de preuve depuis SQLite. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_EVIDENCE_TYPE_DAO_H +#define LABFY_INVESTIGATION_EVIDENCE_TYPE_DAO_H + +#include "database/database.h" + +#include + +G_BEGIN_DECLS + +/** + * @brief DAO opaque donnant accès aux types de preuve. + * + * Le DAO emprunte la connexion Database reçue lors de sa création. + */ +typedef struct EvidenceTypeDao EvidenceTypeDao; + +/** + * @brief Catégories d’erreurs produites par EvidenceTypeDao. + */ +typedef enum +{ + /** + * Un argument public est invalide. + */ + EVIDENCE_TYPE_DAO_ERROR_INVALID_ARGUMENT, + + /** + * L’allocation d’une ressource a échoué. + */ + EVIDENCE_TYPE_DAO_ERROR_MEMORY, + + /** + * La requête SQLite n’a pas pu être préparée. + */ + EVIDENCE_TYPE_DAO_ERROR_PREPARE, + + /** + * Une ligne SQLite n’a pas pu être lue. + */ + EVIDENCE_TYPE_DAO_ERROR_READ, + + /** + * Une ligne SQLite ne peut pas produire un EvidenceType valide. + */ + EVIDENCE_TYPE_DAO_ERROR_MODEL, + + /** + * La table ou le schéma attendu est absent ou invalide. + */ + EVIDENCE_TYPE_DAO_ERROR_SCHEMA +} EvidenceTypeDaoError; + +/** + * @brief Domaine d’erreur du DAO des types de preuve. + */ +#define EVIDENCE_TYPE_DAO_ERROR \ + evidence_type_dao_error_quark() + +/** + * @brief Retourne le domaine d’erreur du DAO. + * + * @return Quark GLib associé à EvidenceTypeDao. + */ +GQuark evidence_type_dao_error_quark(void); + +/** + * @brief Crée un DAO utilisant une connexion Database existante. + * + * La connexion est empruntée. Elle doit rester valide pendant toute + * la durée de vie du DAO. + * + * @param database Connexion SQLite ouverte. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau DAO, ou NULL en cas d’échec. + */ +EvidenceTypeDao *evidence_type_dao_new( + Database *database, + GError **error +); + +/** + * @brief Libère le DAO. + * + * La connexion Database empruntée n’est pas fermée. + * Cette fonction accepte NULL. + * + * @param evidence_type_dao DAO à libérer. + */ +void evidence_type_dao_free( + EvidenceTypeDao *evidence_type_dao +); + +/** + * @brief Charge tous les types de preuve. + * + * Les types sont triés par identifiant numérique croissant. + * + * Le tableau retourné appartient à l’appelant et utilise + * evidence_type_free() comme fonction de destruction. + * + * Une table vide produit un tableau vide, sans erreur. + * + * @param evidence_type_dao DAO valide. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau GPtrArray, ou NULL en cas d’échec. + */ +GPtrArray *evidence_type_dao_list_all( + EvidenceTypeDao *evidence_type_dao, + GError **error +); + +G_END_DECLS + +#endif diff --git a/include/models/evidence_type.h b/include/models/evidence_type.h new file mode 100644 index 0000000..73e1b75 --- /dev/null +++ b/include/models/evidence_type.h @@ -0,0 +1,132 @@ +/****************************************************************************** + * @file evidence_type.h + * @brief Modèle représentant un type de preuve. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_EVIDENCE_TYPE_H +#define LABFY_INVESTIGATION_EVIDENCE_TYPE_H + +#include + +G_BEGIN_DECLS + +/** + * @brief Modèle opaque représentant un type de preuve. + */ +typedef struct EvidenceType EvidenceType; + +/** + * @brief Codes d’erreur produits par EvidenceType. + */ +typedef enum +{ + /** + * Un argument général est invalide. + */ + EVIDENCE_TYPE_ERROR_INVALID_ARGUMENT, + + /** + * L’identifiant numérique est invalide. + */ + EVIDENCE_TYPE_ERROR_INVALID_IDENTIFIER, + + /** + * Le code métier est invalide. + */ + EVIDENCE_TYPE_ERROR_INVALID_CODE, + + /** + * Le libellé utilisateur est invalide. + */ + EVIDENCE_TYPE_ERROR_INVALID_LABEL +} EvidenceTypeError; + +/** + * @brief Domaine d’erreur du modèle EvidenceType. + */ +#define EVIDENCE_TYPE_ERROR \ + evidence_type_error_quark() + +/** + * @brief Retourne le domaine d’erreur du modèle. + * + * @return Quark GLib associé à EvidenceType. + */ +GQuark evidence_type_error_quark(void); + +/** + * @brief Crée un type de preuve. + * + * Les chaînes sont copiées et appartiennent au nouvel objet. + * + * L’identifiant doit être strictement positif. + * + * Le code et le libellé sont obligatoires. + * La description peut être NULL ou vide. + * + * @param identifier Identifiant numérique SQLite. + * @param code Code métier du type. + * @param label Libellé destiné à l’utilisateur. + * @param description Description facultative. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau modèle, ou NULL lorsque les données sont invalides. + */ +EvidenceType *evidence_type_new( + gint64 identifier, + const char *code, + const char *label, + const char *description, + GError **error +); + +/** + * @brief Libère un modèle EvidenceType. + * + * Cette fonction accepte evidence_type == NULL. + * + * @param evidence_type Modèle à libérer. + */ +void evidence_type_free( + EvidenceType *evidence_type +); + +/** + * @brief Retourne l’identifiant numérique du type. + * + * @return Identifiant, ou 0 lorsque evidence_type est NULL. + */ +gint64 evidence_type_get_identifier( + const EvidenceType *evidence_type +); + +/** + * @brief Retourne le code métier du type. + * + * @return Chaîne empruntée, ou NULL. + */ +const char *evidence_type_get_code( + const EvidenceType *evidence_type +); + +/** + * @brief Retourne le libellé utilisateur du type. + * + * @return Chaîne empruntée, ou NULL. + */ +const char *evidence_type_get_label( + const EvidenceType *evidence_type +); + +/** + * @brief Retourne la description du type. + * + * @return Chaîne empruntée, ou NULL. + */ +const char *evidence_type_get_description( + const EvidenceType *evidence_type +); + +G_END_DECLS + +#endif diff --git a/include/views/evidence_import_dialog.h b/include/views/evidence_import_dialog.h new file mode 100644 index 0000000..2e63c95 --- /dev/null +++ b/include/views/evidence_import_dialog.h @@ -0,0 +1,176 @@ +/****************************************************************************** + * @file evidence_import_dialog.h + * @brief Dialogue GTK de saisie des métadonnées d'une preuve. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_EVIDENCE_IMPORT_DIALOG_H +#define LABFY_INVESTIGATION_EVIDENCE_IMPORT_DIALOG_H + +#include + +G_BEGIN_DECLS + +/** + * @brief Résultat opaque produit par le dialogue d'import. + */ +typedef struct EvidenceImportDialogResult EvidenceImportDialogResult; + +/** + * @brief Codes d'erreur produits par EvidenceImportDialog. + */ +typedef enum +{ + /** + * Un argument public est invalide. + */ + EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_ARGUMENT, + + /** + * Aucun type de preuve n'est disponible. + */ + EVIDENCE_IMPORT_DIALOG_ERROR_EMPTY_TYPES, + + /** + * Une allocation ou la création du dialogue a échoué. + */ + EVIDENCE_IMPORT_DIALOG_ERROR_MEMORY, + + /** + * Les métadonnées saisies sont invalides. + */ + EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_DATA +} EvidenceImportDialogError; + +/** + * @brief Domaine d'erreur du dialogue d'import. + */ +#define EVIDENCE_IMPORT_DIALOG_ERROR \ + evidence_import_dialog_error_quark() + +/** + * @brief Callback appelé après la fermeture du dialogue. + * + * En cas de validation, result est non NULL et appartient au callback. + * Il doit être libéré avec evidence_import_dialog_result_free(). + * + * En cas d'annulation, result vaut NULL. + * + * @param result Résultat possédé par le callback, ou NULL. + * @param user_data Données privées fournies lors de l'ouverture. + */ +typedef void (*EvidenceImportDialogCallback)( + EvidenceImportDialogResult *result, + gpointer user_data +); + +/** + * @brief Retourne le domaine d'erreur du dialogue. + * + * @return Quark GLib associé à EvidenceImportDialog. + */ +GQuark evidence_import_dialog_error_quark(void); + +/** + * @brief Présente le dialogue de saisie des métadonnées. + * + * Le chemin du fichier est affiché en lecture seule. + * + * evidence_types doit contenir des pointeurs EvidenceType valides. + * Le dialogue copie les codes et les libellés dont il a besoin : + * le tableau peut donc être libéré après le retour de cette fonction. + * + * Le type dont le code vaut "document" est sélectionné par défaut + * lorsqu'il est disponible. Sinon, le premier type est sélectionné. + * + * @param parent Fenêtre GTK parente. + * @param file_path Chemin du fichier sélectionné. + * @param evidence_types Tableau de EvidenceType. + * @param callback Fonction appelée après validation ou annulation. + * @param user_data Données privées transmises au callback. + * @param error Emplacement facultatif recevant une erreur immédiate. + * + * @return TRUE si le dialogue a été créé et présenté. + */ +gboolean evidence_import_dialog_present( + GtkWindow *parent, + const char *file_path, + const GPtrArray *evidence_types, + EvidenceImportDialogCallback callback, + gpointer user_data, + GError **error +); + +/** + * @brief Valide les valeurs obligatoires d'un import. + * + * Cette fonction ne dépend pas de l'affichage du dialogue et peut être + * testée sans ouvrir de fenêtre GTK. + * + * Le chemin doit désigner un fichier existant. + * Le code du type doit être non vide. + * La date doit être une date ISO 8601 valide comprenant un fuseau horaire. + * + * @param file_path Chemin du fichier à importer. + * @param type_identifier Code métier du type de preuve. + * @param collected_at Date de collecte au format ISO 8601. + * @param error Emplacement facultatif recevant une erreur. + * + * @return TRUE lorsque les valeurs sont valides. + */ +gboolean evidence_import_dialog_validate_values( + const char *file_path, + const char *type_identifier, + const char *collected_at, + GError **error +); + +/** + * @brief Libère le résultat produit par le dialogue. + * + * Cette fonction accepte NULL. + * + * @param result Résultat à libérer. + */ +void evidence_import_dialog_result_free( + EvidenceImportDialogResult *result +); + +/** + * @brief Retourne le code du type sélectionné. + * + * @return Chaîne empruntée, ou NULL. + */ +const char *evidence_import_dialog_result_get_type_identifier( + const EvidenceImportDialogResult *result +); + +/** + * @brief Retourne la date ISO 8601 de collecte. + * + * @return Chaîne empruntée, ou NULL. + */ +const char *evidence_import_dialog_result_get_collected_at( + const EvidenceImportDialogResult *result +); + +/** + * @brief Retourne la source renseignée. + * + * @return Chaîne empruntée, ou NULL lorsque la source est vide. + */ +const char *evidence_import_dialog_result_get_source( + const EvidenceImportDialogResult *result +); + +/** + * @brief Retourne la description renseignée. + * + * @return Chaîne empruntée, ou NULL lorsque la description est vide. + */ +const char *evidence_import_dialog_result_get_description( + const EvidenceImportDialogResult *result +); + +G_END_DECLS + +#endif diff --git a/src/core/application.c b/src/core/application.c index 234b593..03921e5 100644 --- a/src/core/application.c +++ b/src/core/application.c @@ -21,6 +21,8 @@ #include "views/file_dialog.h" #include "core/evidence_import_task.h" #include "models/evidence_record.h" +#include "dao/evidence_type_dao.h" +#include "views/evidence_import_dialog.h" #include #include @@ -36,12 +38,6 @@ #define APPLICATION_EVIDENCE_DIRECTORY \ "01_Preuves_Originales/Documents" -/** - * @brief Type utilisé avant l’ajout du formulaire de métadonnées. - */ -#define APPLICATION_DEFAULT_EVIDENCE_TYPE \ - "document" - /** * @brief Erreurs produites lors du chargement d'une enquête. */ @@ -96,6 +92,43 @@ typedef struct char *investigation_root_path; } ApplicationEvidenceImportContext; +/** + * @brief Contexte conservé pendant le dialogue de métadonnées. + * + * Le chemin fourni par FileDialog n'est valide que pendant son callback. + * Il doit donc être copié avant l'ouverture du second dialogue. + */ +typedef struct +{ + Application *application; + char *file_path; +} ApplicationEvidenceDialogContext; + + +/** + * @brief Libère le contexte du dialogue de métadonnées. + */ +static void application_evidence_dialog_context_free( + gpointer user_data +) +{ + ApplicationEvidenceDialogContext *context = + user_data; + + if (context == NULL) + { + return; + } + + g_free( + context->file_path + ); + + g_free( + context + ); +} + /** * @brief Libère le contexte d’un import asynchrone. */ @@ -1115,7 +1148,11 @@ static void application_on_evidence_import_completed( */ static void application_start_evidence_import( Application *application, - const char *file_path + const char *file_path, + const char *type_identifier, + const char *collected_at, + const char *source, + const char *description ) { const InvestigationProject *project = NULL; @@ -1142,7 +1179,11 @@ static void application_start_evidence_import( application->session == NULL || application->task_manager == NULL || file_path == NULL || - file_path[0] == '\0') + file_path[0] == '\0' || + type_identifier == NULL || + type_identifier[0] == '\0' || + collected_at == NULL || + collected_at[0] == '\0') { return; } @@ -1301,11 +1342,16 @@ static void application_start_evidence_import( APPLICATION_EVIDENCE_DIRECTORY; request.type_identifier = - APPLICATION_DEFAULT_EVIDENCE_TYPE; + type_identifier; - request.collected_at = NULL; - request.source = NULL; - request.description = NULL; + request.collected_at = + collected_at; + + request.source = + source; + + request.description = + description; task = evidence_import_task_start( @@ -1388,6 +1434,91 @@ static void application_start_evidence_import( ); } +/** + * @brief Traite la validation ou l'annulation des métadonnées. + * + * @param result Résultat possédé par ce callback, ou NULL. + * @param user_data Contexte ApplicationEvidenceDialogContext. + */ +static void application_on_evidence_metadata_completed( + EvidenceImportDialogResult *result, + gpointer user_data +) +{ + ApplicationEvidenceDialogContext *context = + user_data; + + Application *application = NULL; + + if (context == NULL) + { + evidence_import_dialog_result_free( + result + ); + + return; + } + + application = + context->application; + + if (application == NULL || + application->main_window == NULL) + { + evidence_import_dialog_result_free( + result + ); + + application_evidence_dialog_context_free( + context + ); + + return; + } + + /* + * Une annulation du dialogue ne doit lancer aucune tâche. + */ + if (result == NULL) + { + main_window_set_status( + application->main_window, + "Import de preuve annulé." + ); + + application_evidence_dialog_context_free( + context + ); + + return; + } + + application_start_evidence_import( + application, + context->file_path, + evidence_import_dialog_result_get_type_identifier( + result + ), + evidence_import_dialog_result_get_collected_at( + result + ), + evidence_import_dialog_result_get_source( + result + ), + evidence_import_dialog_result_get_description( + result + ) + ); + + evidence_import_dialog_result_free( + result + ); + + application_evidence_dialog_context_free( + context + ); +} + /** * @brief Traite le fichier de preuve sélectionné. * @@ -1452,9 +1583,174 @@ static void application_on_evidence_file_selected( return; } - application_start_evidence_import( - application, - file_path + Database *database = NULL; + EvidenceTypeDao *evidence_type_dao = NULL; + GPtrArray *evidence_types = NULL; + + ApplicationEvidenceDialogContext *dialog_context = + NULL; + + GError *dialog_error = NULL; + + database = + investigation_session_get_database( + application->session + ); + + if (database == NULL) + { + application_present_error( + application, + "Import impossible", + "La session ne fournit aucune connexion SQLite." + ); + + return; + } + + evidence_type_dao = + evidence_type_dao_new( + database, + &dialog_error + ); + + if (evidence_type_dao == NULL) + { + application_present_error( + application, + "Types de preuve indisponibles", + dialog_error != NULL + ? dialog_error->message + : "Impossible de créer le DAO des types de preuve." + ); + + g_clear_error( + &dialog_error + ); + + return; + } + + evidence_types = + evidence_type_dao_list_all( + evidence_type_dao, + &dialog_error + ); + + evidence_type_dao_free( + evidence_type_dao + ); + + if (evidence_types == NULL) + { + application_present_error( + application, + "Types de preuve indisponibles", + dialog_error != NULL + ? dialog_error->message + : "Impossible de lire les types de preuve." + ); + + g_clear_error( + &dialog_error + ); + + return; + } + + if (evidence_types->len == 0) + { + application_present_error( + application, + "Types de preuve indisponibles", + "Aucun type de preuve n'est enregistré dans la base." + ); + + g_ptr_array_unref( + evidence_types + ); + + return; + } + + dialog_context = + g_try_new0( + ApplicationEvidenceDialogContext, + 1 + ); + + if (dialog_context == NULL) + { + application_present_error( + application, + "Import impossible", + "Impossible d'allouer le contexte du dialogue." + ); + + g_ptr_array_unref( + evidence_types + ); + + return; + } + + dialog_context->application = + application; + + dialog_context->file_path = + g_strdup( + file_path + ); + + if (dialog_context->file_path == NULL) + { + application_present_error( + application, + "Import impossible", + "Impossible de conserver le chemin du fichier." + ); + + application_evidence_dialog_context_free( + dialog_context + ); + + g_ptr_array_unref( + evidence_types + ); + + return; + } + + if (!evidence_import_dialog_present( + main_window_get_window( + application->main_window + ), + dialog_context->file_path, + evidence_types, + application_on_evidence_metadata_completed, + dialog_context, + &dialog_error + )) + { + application_present_error( + application, + "Dialogue d'import impossible", + dialog_error != NULL + ? dialog_error->message + : "Le dialogue de métadonnées n'a pas pu être ouvert." + ); + + g_clear_error( + &dialog_error + ); + + application_evidence_dialog_context_free( + dialog_context + ); + } + + g_ptr_array_unref( + evidence_types ); } diff --git a/src/dao/evidence_type_dao.c b/src/dao/evidence_type_dao.c new file mode 100644 index 0000000..7c64ce4 --- /dev/null +++ b/src/dao/evidence_type_dao.c @@ -0,0 +1,498 @@ +/****************************************************************************** + * @file evidence_type_dao.c + * @brief Lecture des types de preuve depuis SQLite. + ******************************************************************************/ + +#include "dao/evidence_type_dao.h" + +#include "database/error.h" +#include "database/statement.h" +#include "models/evidence_type.h" + +#include + +#include + +/** + * @brief Requête listant les types de preuve. + */ +static const char *const evidence_type_dao_list_all_sql = + "SELECT " + " id, " + " code, " + " label, " + " description " + "FROM types_preuve " + "ORDER BY id ASC;"; + +/** + * @struct EvidenceTypeDao + * @brief Représentation interne du DAO. + */ +struct EvidenceTypeDao +{ + Database *database; +}; + +/** + * @brief Libère un EvidenceType contenu dans un GPtrArray. + */ +static void evidence_type_dao_destroy_type( + gpointer user_data +) +{ + EvidenceType *evidence_type = user_data; + + evidence_type_free( + evidence_type + ); +} + +/** + * @brief Détermine si un message SQLite indique un schéma incompatible. + */ +static gboolean evidence_type_dao_is_schema_error( + const char *database_message +) +{ + if (database_message == NULL) + { + return FALSE; + } + + return g_strstr_len( + database_message, + -1, + "no such table" + ) != NULL || + g_strstr_len( + database_message, + -1, + "no such column" + ) != NULL; +} + +/** + * @brief Enregistre une erreur de préparation SQL. + */ +static void evidence_type_dao_set_prepare_error( + EvidenceTypeDao *evidence_type_dao, + GError **error +) +{ + const char *database_message = NULL; + EvidenceTypeDaoError error_code = + EVIDENCE_TYPE_DAO_ERROR_PREPARE; + + if (error == NULL || + *error != NULL) + { + return; + } + + if (evidence_type_dao != NULL) + { + database_message = + database_error_get_message( + evidence_type_dao->database + ); + } + + if (evidence_type_dao_is_schema_error( + database_message + )) + { + error_code = + EVIDENCE_TYPE_DAO_ERROR_SCHEMA; + } + + g_set_error( + error, + EVIDENCE_TYPE_DAO_ERROR, + error_code, + "Impossible de préparer la lecture des types de preuve : %s", + database_message != NULL + ? database_message + : "erreur Database inconnue" + ); +} + +/** + * @brief Enregistre une erreur de lecture SQLite. + */ +static void evidence_type_dao_set_read_error( + EvidenceTypeDao *evidence_type_dao, + GError **error, + const char *context +) +{ + const char *database_message = NULL; + + if (error == NULL || + *error != NULL) + { + return; + } + + if (evidence_type_dao != NULL) + { + database_message = + database_error_get_message( + evidence_type_dao->database + ); + } + + g_set_error( + error, + EVIDENCE_TYPE_DAO_ERROR, + EVIDENCE_TYPE_DAO_ERROR_READ, + "%s : %s", + context != NULL + ? context + : "Impossible de lire un type de preuve", + database_message != NULL + ? database_message + : "donnée SQLite invalide" + ); +} + +/** + * @brief Construit un EvidenceType depuis la ligne SQLite courante. + */ +static EvidenceType *evidence_type_dao_read_current_row( + EvidenceTypeDao *evidence_type_dao, + DatabaseStatement *statement, + GError **error +) +{ + EvidenceType *evidence_type = NULL; + + int64_t identifier = 0; + + char *code = NULL; + char *label = NULL; + char *description = NULL; + + GError *model_error = NULL; + + if (evidence_type_dao == NULL || + statement == NULL) + { + g_set_error_literal( + error, + EVIDENCE_TYPE_DAO_ERROR, + EVIDENCE_TYPE_DAO_ERROR_INVALID_ARGUMENT, + "La ligne du type de preuve ne peut pas être lue." + ); + + return NULL; + } + + if (!database_statement_column_int64( + statement, + 0, + &identifier + )) + { + evidence_type_dao_set_read_error( + evidence_type_dao, + error, + "Impossible de lire l’identifiant du type de preuve" + ); + + goto cleanup; + } + + if (!database_statement_column_text( + statement, + 1, + &code + )) + { + evidence_type_dao_set_read_error( + evidence_type_dao, + error, + "Impossible de lire le code du type de preuve" + ); + + goto cleanup; + } + + if (!database_statement_column_text( + statement, + 2, + &label + )) + { + evidence_type_dao_set_read_error( + evidence_type_dao, + error, + "Impossible de lire le libellé du type de preuve" + ); + + goto cleanup; + } + + if (!database_statement_column_text( + statement, + 3, + &description + )) + { + evidence_type_dao_set_read_error( + evidence_type_dao, + error, + "Impossible de lire la description du type de preuve" + ); + + goto cleanup; + } + + evidence_type = + evidence_type_new( + (gint64) identifier, + code, + label, + description, + &model_error + ); + + if (evidence_type == NULL) + { + g_set_error( + error, + EVIDENCE_TYPE_DAO_ERROR, + EVIDENCE_TYPE_DAO_ERROR_MODEL, + "Le type de preuve lu depuis SQLite est invalide : %s", + model_error != NULL + ? model_error->message + : "erreur de modèle inconnue" + ); + } + +cleanup: + + g_clear_error( + &model_error + ); + + g_free( + description + ); + + g_free( + label + ); + + g_free( + code + ); + + return evidence_type; +} + +GQuark evidence_type_dao_error_quark(void) +{ + return g_quark_from_static_string( + "evidence-type-dao-error-quark" + ); +} + +EvidenceTypeDao *evidence_type_dao_new( + Database *database, + GError **error +) +{ + EvidenceTypeDao *evidence_type_dao = NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (database == NULL) + { + g_set_error_literal( + error, + EVIDENCE_TYPE_DAO_ERROR, + EVIDENCE_TYPE_DAO_ERROR_INVALID_ARGUMENT, + "La connexion Database est obligatoire." + ); + + return NULL; + } + + evidence_type_dao = + g_try_new0( + EvidenceTypeDao, + 1 + ); + + if (evidence_type_dao == NULL) + { + g_set_error_literal( + error, + EVIDENCE_TYPE_DAO_ERROR, + EVIDENCE_TYPE_DAO_ERROR_MEMORY, + "Impossible d’allouer le DAO des types de preuve." + ); + + return NULL; + } + + /* + * La connexion reste possédée par la session ou le code appelant. + */ + evidence_type_dao->database = + database; + + return evidence_type_dao; +} + +void evidence_type_dao_free( + EvidenceTypeDao *evidence_type_dao +) +{ + if (evidence_type_dao == NULL) + { + return; + } + + /* + * Database est empruntée et ne doit pas être fermée ici. + */ + evidence_type_dao->database = NULL; + + g_free( + evidence_type_dao + ); +} + +GPtrArray *evidence_type_dao_list_all( + EvidenceTypeDao *evidence_type_dao, + GError **error +) +{ + DatabaseStatement *statement = NULL; + + GPtrArray *evidence_types = NULL; + + DatabaseStatementStepResult step_result = + DATABASE_STATEMENT_STEP_ERROR; + + gboolean success = FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (evidence_type_dao == NULL || + evidence_type_dao->database == NULL) + { + g_set_error_literal( + error, + EVIDENCE_TYPE_DAO_ERROR, + EVIDENCE_TYPE_DAO_ERROR_INVALID_ARGUMENT, + "Le DAO des types de preuve est invalide." + ); + + return NULL; + } + + evidence_types = + g_ptr_array_new_with_free_func( + evidence_type_dao_destroy_type + ); + + if (evidence_types == NULL) + { + g_set_error_literal( + error, + EVIDENCE_TYPE_DAO_ERROR, + EVIDENCE_TYPE_DAO_ERROR_MEMORY, + "Impossible d’allouer la liste des types de preuve." + ); + + return NULL; + } + + statement = + database_statement_prepare( + evidence_type_dao->database, + evidence_type_dao_list_all_sql + ); + + if (statement == NULL) + { + evidence_type_dao_set_prepare_error( + evidence_type_dao, + error + ); + + goto cleanup; + } + + while (TRUE) + { + EvidenceType *evidence_type = NULL; + + step_result = + database_statement_step( + statement + ); + + if (step_result == + DATABASE_STATEMENT_STEP_DONE) + { + success = TRUE; + break; + } + + if (step_result == + DATABASE_STATEMENT_STEP_ERROR) + { + evidence_type_dao_set_read_error( + evidence_type_dao, + error, + "Impossible de parcourir les types de preuve" + ); + + break; + } + + evidence_type = + evidence_type_dao_read_current_row( + evidence_type_dao, + statement, + error + ); + + if (evidence_type == NULL) + { + break; + } + + g_ptr_array_add( + evidence_types, + evidence_type + ); + } + +cleanup: + + database_statement_finalize( + statement + ); + + if (!success) + { + g_ptr_array_unref( + evidence_types + ); + + return NULL; + } + + return evidence_types; +} diff --git a/src/models/evidence_type.c b/src/models/evidence_type.c new file mode 100644 index 0000000..14512b5 --- /dev/null +++ b/src/models/evidence_type.c @@ -0,0 +1,266 @@ +/****************************************************************************** + * @file evidence_type.c + * @brief Implémentation du modèle représentant un type de preuve. + ******************************************************************************/ + +#include "models/evidence_type.h" + +#include + +/** + * @struct EvidenceType + * @brief Représentation interne d’un type de preuve. + */ +struct EvidenceType +{ + gint64 identifier; + char *code; + char *label; + char *description; +}; + +/** + * @brief Copie une chaîne après suppression des espaces périphériques. + * + * @param value Chaîne à copier. + * + * @return Nouvelle chaîne normalisée, ou NULL lorsque value vaut NULL. + */ +static char *evidence_type_duplicate_trimmed( + const char *value +) +{ + char *copy = NULL; + + if (value == NULL) + { + return NULL; + } + + copy = g_strdup( + value + ); + + g_strstrip( + copy + ); + + return copy; +} + +/** + * @brief Vérifie qu’une chaîne obligatoire contient du texte. + * + * @param value Chaîne déjà normalisée. + * + * @return TRUE lorsque la chaîne contient au moins un caractère. + */ +static gboolean evidence_type_required_value_is_valid( + const char *value +) +{ + return value != NULL && + value[0] != '\0'; +} + +GQuark evidence_type_error_quark(void) +{ + return g_quark_from_static_string( + "evidence-type-error-quark" + ); +} + +EvidenceType *evidence_type_new( + gint64 identifier, + const char *code, + const char *label, + const char *description, + GError **error +) +{ + EvidenceType *evidence_type = NULL; + + char *normalized_code = NULL; + char *normalized_label = NULL; + char *normalized_description = NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (identifier <= 0) + { + g_set_error_literal( + error, + EVIDENCE_TYPE_ERROR, + EVIDENCE_TYPE_ERROR_INVALID_IDENTIFIER, + "L’identifiant du type de preuve doit être strictement positif." + ); + + return NULL; + } + + normalized_code = + evidence_type_duplicate_trimmed( + code + ); + + if (!evidence_type_required_value_is_valid( + normalized_code + )) + { + g_set_error_literal( + error, + EVIDENCE_TYPE_ERROR, + EVIDENCE_TYPE_ERROR_INVALID_CODE, + "Le code du type de preuve est obligatoire." + ); + + g_free( + normalized_code + ); + + return NULL; + } + + normalized_label = + evidence_type_duplicate_trimmed( + label + ); + + if (!evidence_type_required_value_is_valid( + normalized_label + )) + { + g_set_error_literal( + error, + EVIDENCE_TYPE_ERROR, + EVIDENCE_TYPE_ERROR_INVALID_LABEL, + "Le libellé du type de preuve est obligatoire." + ); + + g_free( + normalized_code + ); + + g_free( + normalized_label + ); + + return NULL; + } + + normalized_description = + evidence_type_duplicate_trimmed( + description + ); + + /* + * Une description vide ou composée uniquement d’espaces + * est représentée par NULL. + */ + if (normalized_description != NULL && + normalized_description[0] == '\0') + { + g_free( + normalized_description + ); + + normalized_description = NULL; + } + + evidence_type = + g_new0( + EvidenceType, + 1 + ); + + evidence_type->identifier = + identifier; + + evidence_type->code = + normalized_code; + + evidence_type->label = + normalized_label; + + evidence_type->description = + normalized_description; + + return evidence_type; +} + +void evidence_type_free( + EvidenceType *evidence_type +) +{ + if (evidence_type == NULL) + { + return; + } + + g_free( + evidence_type->code + ); + + g_free( + evidence_type->label + ); + + g_free( + evidence_type->description + ); + + g_free( + evidence_type + ); +} + +gint64 evidence_type_get_identifier( + const EvidenceType *evidence_type +) +{ + if (evidence_type == NULL) + { + return 0; + } + + return evidence_type->identifier; +} + +const char *evidence_type_get_code( + const EvidenceType *evidence_type +) +{ + if (evidence_type == NULL) + { + return NULL; + } + + return evidence_type->code; +} + +const char *evidence_type_get_label( + const EvidenceType *evidence_type +) +{ + if (evidence_type == NULL) + { + return NULL; + } + + return evidence_type->label; +} + +const char *evidence_type_get_description( + const EvidenceType *evidence_type +) +{ + if (evidence_type == NULL) + { + return NULL; + } + + return evidence_type->description; +} diff --git a/src/views/evidence_import_dialog.c b/src/views/evidence_import_dialog.c new file mode 100644 index 0000000..183bc6b --- /dev/null +++ b/src/views/evidence_import_dialog.c @@ -0,0 +1,1705 @@ +/****************************************************************************** + * @file evidence_import_dialog.c + * @brief Dialogue GTK de saisie des métadonnées d'une preuve. + ******************************************************************************/ + +#include "views/evidence_import_dialog.h" +#include "models/evidence_type.h" + +#include + +#include + +/** + * @struct EvidenceImportDialogResult + * @brief Métadonnées validées avant le lancement d'un import. + */ +struct EvidenceImportDialogResult +{ + char *type_identifier; + char *collected_at; + char *source; + char *description; +}; + +/** + * @brief État privé conservé pendant l'affichage du dialogue. + */ +typedef struct +{ + GtkWindow *window; + GtkDropDown *type_drop_down; + GtkEntry *collected_at_entry; + GtkEntry *source_entry; + GtkTextView *description_text_view; + GtkLabel *error_label; + + char *file_path; + GPtrArray *type_codes; + + EvidenceImportDialogCallback callback; + gpointer callback_data; + + gboolean completed; +} EvidenceImportDialogState; + +/** + * @brief Vérifie qu'une chaîne contient du texte non blanc. + */ +static gboolean evidence_import_dialog_value_has_text( + const char *value +) +{ + char *normalized_value = NULL; + gboolean has_text = FALSE; + + if (value == NULL) + { + return FALSE; + } + + normalized_value = + g_strdup( + value + ); + + if (normalized_value == NULL) + { + return FALSE; + } + + g_strstrip( + normalized_value + ); + + has_text = + normalized_value[0] != '\0'; + + g_free( + normalized_value + ); + + return has_text; +} + +/** + * @brief Vérifie qu'une date contient un fuseau horaire explicite. + * + * Les formats acceptés sont notamment : + * + * - 2026-07-20T08:30:00Z + * - 2026-07-20T10:30:00+02 + * - 2026-07-20T10:30:00+02:00 + */ +static gboolean evidence_import_dialog_has_explicit_timezone( + const char *value +) +{ + gsize length = 0; + const char *offset = NULL; + + if (value == NULL) + { + return FALSE; + } + + length = + strlen( + value + ); + + if (length == 0) + { + return FALSE; + } + + /* + * Fuseau UTC explicite. + */ + if (value[length - 1] == 'Z' || + value[length - 1] == 'z') + { + return TRUE; + } + + /* + * Vérifie une terminaison de la forme +02:00 ou -05:30. + */ + if (length >= 6) + { + offset = + value + length - 6; + + if ((offset[0] == '+' || + offset[0] == '-') && + g_ascii_isdigit(offset[1]) && + g_ascii_isdigit(offset[2]) && + offset[3] == ':' && + g_ascii_isdigit(offset[4]) && + g_ascii_isdigit(offset[5])) + { + return TRUE; + } + } + + /* + * Vérifie une terminaison ISO 8601 courte : + * +02 ou -05. + */ + if (length >= 3) + { + offset = + value + length - 3; + + if ((offset[0] == '+' || + offset[0] == '-') && + g_ascii_isdigit(offset[1]) && + g_ascii_isdigit(offset[2])) + { + return TRUE; + } + } + + return FALSE; +} + +/** + * @brief Convertit une date ISO 8601 en UTC au format EvidenceRecord. + * + * Exemple : + * + * 2026-07-20T09:16:01.492569+02 + * devient : + * 2026-07-20T07:16:01Z + * + * @return Nouvelle chaîne possédée par l'appelant, ou NULL. + */ +static char *evidence_import_dialog_normalize_collected_at( + const char *value, + GError **error +) +{ + char *normalized_value = NULL; + char *utc_value = NULL; + + GDateTime *date_time = NULL; + GDateTime *utc_date_time = NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (!evidence_import_dialog_value_has_text( + value + )) + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_DATA, + "La date de collecte est obligatoire." + ); + + return NULL; + } + + normalized_value = + g_strdup( + value + ); + + if (normalized_value == NULL) + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_MEMORY, + "Impossible de copier la date de collecte." + ); + + return NULL; + } + + g_strstrip( + normalized_value + ); + + date_time = + g_date_time_new_from_iso8601( + normalized_value, + NULL + ); + + g_free( + normalized_value + ); + + if (date_time == NULL) + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_DATA, + "La date de collecte n'est pas une date ISO 8601 valide." + ); + + return NULL; + } + + utc_date_time = + g_date_time_to_utc( + date_time + ); + + g_date_time_unref( + date_time + ); + + if (utc_date_time == NULL) + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_MEMORY, + "Impossible de convertir la date de collecte en UTC." + ); + + return NULL; + } + + utc_value = + g_date_time_format( + utc_date_time, + "%Y-%m-%dT%H:%M:%SZ" + ); + + g_date_time_unref( + utc_date_time + ); + + if (utc_value == NULL) + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_MEMORY, + "Impossible de formater la date de collecte." + ); + + return NULL; + } + + return utc_value; +} + +GQuark evidence_import_dialog_error_quark(void) +{ + return g_quark_from_static_string( + "evidence-import-dialog-error-quark" + ); +} + +/** + * @brief Retourne une copie nettoyée ou NULL pour une valeur vide. + */ +static char *evidence_import_dialog_duplicate_optional_text( + const char *value +) +{ + char *normalized_value = NULL; + + if (value == NULL) + { + return NULL; + } + + normalized_value = + g_strdup( + value + ); + + if (normalized_value == NULL) + { + return NULL; + } + + g_strstrip( + normalized_value + ); + + if (normalized_value[0] == '\0') + { + g_free( + normalized_value + ); + + return NULL; + } + + return normalized_value; +} + +/** + * @brief Affiche un message de validation dans le dialogue. + */ +static void evidence_import_dialog_show_error( + EvidenceImportDialogState *state, + const char *message +) +{ + if (state == NULL || + state->error_label == NULL) + { + return; + } + + gtk_label_set_text( + state->error_label, + message != NULL + ? message + : "Les métadonnées sont invalides." + ); + + gtk_widget_set_visible( + GTK_WIDGET( + state->error_label + ), + TRUE + ); +} + +/** + * @brief Libère l'état privé du dialogue. + */ +static void evidence_import_dialog_state_free( + gpointer user_data +) +{ + EvidenceImportDialogState *state = + user_data; + + if (state == NULL) + { + return; + } + + g_free( + state->file_path + ); + + g_ptr_array_unref( + state->type_codes + ); + + g_free( + state + ); +} + +/** + * @brief Retourne le contenu complet de la description. + */ +static char *evidence_import_dialog_get_description_text( + EvidenceImportDialogState *state +) +{ + GtkTextBuffer *buffer = NULL; + + GtkTextIter start; + GtkTextIter end; + + if (state == NULL || + state->description_text_view == NULL) + { + return NULL; + } + + buffer = + gtk_text_view_get_buffer( + state->description_text_view + ); + + gtk_text_buffer_get_bounds( + buffer, + &start, + &end + ); + + return gtk_text_buffer_get_text( + buffer, + &start, + &end, + FALSE + ); +} + +/** + * @brief Termine le dialogue avec une annulation. + */ +static void evidence_import_dialog_cancel( + EvidenceImportDialogState *state +) +{ + if (state == NULL || + state->completed) + { + return; + } + + state->completed = + TRUE; + + if (state->callback != NULL) + { + state->callback( + NULL, + state->callback_data + ); + } +} + +/** + * @brief Traite la fermeture native de la fenêtre. + */ +static gboolean evidence_import_dialog_on_close_requested( + GtkWindow *window, + gpointer user_data +) +{ + EvidenceImportDialogState *state = + user_data; + + (void) window; + + evidence_import_dialog_cancel( + state + ); + + /* + * FALSE laisse GTK exécuter son comportement normal de fermeture. + */ + return FALSE; +} + +/** + * @brief Traite le bouton Annuler. + */ +static void evidence_import_dialog_on_cancel_clicked( + GtkButton *button, + gpointer user_data +) +{ + EvidenceImportDialogState *state = + user_data; + + (void) button; + + if (state == NULL) + { + return; + } + + evidence_import_dialog_cancel( + state + ); + + gtk_window_close( + state->window + ); +} + +/** + * @brief Traite la validation du formulaire. + */ +static void evidence_import_dialog_on_import_clicked( + GtkButton *button, + gpointer user_data +) +{ + EvidenceImportDialogState *state = + user_data; + + EvidenceImportDialogResult *result = NULL; + + guint selected_index = + GTK_INVALID_LIST_POSITION; + + const char *type_identifier = NULL; + const char *collected_at = NULL; + const char *source = NULL; + + char *description = NULL; + char *normalized_collected_at = NULL; + + GError *normalization_error = NULL; + GError *validation_error = NULL; + + (void) button; + + if (state == NULL || + state->completed) + { + return; + } + + selected_index = + gtk_drop_down_get_selected( + state->type_drop_down + ); + + if (selected_index == + GTK_INVALID_LIST_POSITION || + selected_index >= state->type_codes->len) + { + evidence_import_dialog_show_error( + state, + "Sélectionnez un type de preuve." + ); + + return; + } + + type_identifier = + g_ptr_array_index( + state->type_codes, + selected_index + ); + + collected_at = + gtk_editable_get_text( + GTK_EDITABLE( + state->collected_at_entry + ) + ); + + source = + gtk_editable_get_text( + GTK_EDITABLE( + state->source_entry + ) + ); + + description = + evidence_import_dialog_get_description_text( + state + ); + + if (!evidence_import_dialog_validate_values( + state->file_path, + type_identifier, + collected_at, + &validation_error + )) + { + evidence_import_dialog_show_error( + state, + validation_error != NULL + ? validation_error->message + : "Les métadonnées sont invalides." + ); + + g_clear_error( + &validation_error + ); + + g_free( + description + ); + + return; + } + + normalized_collected_at = + evidence_import_dialog_normalize_collected_at( + collected_at, + &normalization_error + ); + + if (normalized_collected_at == NULL) + { + evidence_import_dialog_show_error( + state, + normalization_error != NULL + ? normalization_error->message + : "Impossible de normaliser la date de collecte." + ); + + g_clear_error( + &normalization_error + ); + + g_free( + description + ); + + return; + } + + result = + g_try_new0( + EvidenceImportDialogResult, + 1 + ); + + if (result == NULL) + { + g_free( + normalized_collected_at + ); + + g_free( + description + ); + + evidence_import_dialog_show_error( + state, + "Impossible d'allouer le résultat du formulaire." + ); + + return; + } + + g_strstrip( + normalized_collected_at + ); + + result->type_identifier = + g_strdup( + type_identifier + ); + + result->collected_at = + normalized_collected_at; + + result->source = + evidence_import_dialog_duplicate_optional_text( + source + ); + + result->description = + evidence_import_dialog_duplicate_optional_text( + description + ); + + g_free( + description + ); + + if (result->type_identifier == NULL) + { + evidence_import_dialog_result_free( + result + ); + + evidence_import_dialog_show_error( + state, + "Impossible de conserver le type de preuve." + ); + + return; + } + + state->completed = + TRUE; + + if (state->callback != NULL) + { + state->callback( + result, + state->callback_data + ); + } + else + { + evidence_import_dialog_result_free( + result + ); + } + + gtk_window_close( + state->window + ); +} + +gboolean evidence_import_dialog_present( + GtkWindow *parent, + const char *file_path, + const GPtrArray *evidence_types, + EvidenceImportDialogCallback callback, + gpointer user_data, + GError **error +) +{ + EvidenceImportDialogState *state = NULL; + + GtkStringList *type_labels = NULL; + + GtkWidget *root_box = NULL; + GtkWidget *title_label = NULL; + GtkWidget *path_title_label = NULL; + GtkWidget *path_label = NULL; + + GtkWidget *grid = NULL; + + GtkWidget *type_label = NULL; + GtkWidget *collected_at_label = NULL; + GtkWidget *source_label = NULL; + GtkWidget *description_label = NULL; + + GtkWidget *description_scrolled_window = NULL; + + GtkWidget *button_box = NULL; + GtkWidget *cancel_button = NULL; + GtkWidget *import_button = NULL; + + GDateTime *current_date_time = NULL; + char *current_date_time_iso8601 = NULL; + + guint default_type_index = 0; + guint type_index = 0; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (parent == NULL || + !GTK_IS_WINDOW(parent) || + file_path == NULL || + file_path[0] == '\0' || + evidence_types == NULL || + callback == NULL) + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_ARGUMENT, + "Les arguments du dialogue d'import sont invalides." + ); + + return FALSE; + } + + if (evidence_types->len == 0) + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_EMPTY_TYPES, + "Aucun type de preuve n'est disponible." + ); + + return FALSE; + } + + state = + g_try_new0( + EvidenceImportDialogState, + 1 + ); + + if (state == NULL) + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_MEMORY, + "Impossible d'allouer l'état du dialogue." + ); + + return FALSE; + } + + state->file_path = + g_strdup( + file_path + ); + + state->type_codes = + g_ptr_array_new_with_free_func( + g_free + ); + + state->callback = + callback; + + state->callback_data = + user_data; + + if (state->file_path == NULL || + state->type_codes == NULL) + { + evidence_import_dialog_state_free( + state + ); + + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_MEMORY, + "Impossible de préparer les données du dialogue." + ); + + return FALSE; + } + + type_labels = + gtk_string_list_new( + NULL + ); + + if (type_labels == NULL) + { + evidence_import_dialog_state_free( + state + ); + + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_MEMORY, + "Impossible de créer la liste des types de preuve." + ); + + return FALSE; + } + + for (type_index = 0; + type_index < evidence_types->len; + type_index++) + { + const EvidenceType *evidence_type = + g_ptr_array_index( + evidence_types, + type_index + ); + + const char *code = + evidence_type_get_code( + evidence_type + ); + + const char *label = + evidence_type_get_label( + evidence_type + ); + + char *code_copy = NULL; + + if (evidence_type == NULL || + code == NULL || + code[0] == '\0' || + label == NULL || + label[0] == '\0') + { + g_object_unref( + type_labels + ); + + evidence_import_dialog_state_free( + state + ); + + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_ARGUMENT, + "La liste contient un type de preuve invalide." + ); + + return FALSE; + } + + code_copy = + g_strdup( + code + ); + + if (code_copy == NULL) + { + g_object_unref( + type_labels + ); + + evidence_import_dialog_state_free( + state + ); + + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_MEMORY, + "Impossible de copier un type de preuve." + ); + + return FALSE; + } + + g_ptr_array_add( + state->type_codes, + code_copy + ); + + gtk_string_list_append( + type_labels, + label + ); + + if (g_strcmp0( + code, + "document" + ) == 0) + { + default_type_index = + type_index; + } + } + + state->window = + GTK_WINDOW( + gtk_window_new() + ); + + gtk_window_set_title( + state->window, + "Importer une preuve" + ); + + gtk_window_set_transient_for( + state->window, + parent + ); + + gtk_window_set_modal( + state->window, + TRUE + ); + + gtk_window_set_destroy_with_parent( + state->window, + TRUE + ); + + gtk_window_set_default_size( + state->window, + 620, + 520 + ); + + root_box = + gtk_box_new( + GTK_ORIENTATION_VERTICAL, + 12 + ); + + gtk_widget_set_margin_top( + root_box, + 18 + ); + + gtk_widget_set_margin_bottom( + root_box, + 18 + ); + + gtk_widget_set_margin_start( + root_box, + 18 + ); + + gtk_widget_set_margin_end( + root_box, + 18 + ); + + title_label = + gtk_label_new( + NULL + ); + + gtk_label_set_markup( + GTK_LABEL(title_label), + "Saisir les métadonnées de la preuve" + ); + + gtk_label_set_xalign( + GTK_LABEL(title_label), + 0.0F + ); + + gtk_box_append( + GTK_BOX(root_box), + title_label + ); + + path_title_label = + gtk_label_new( + "Fichier sélectionné" + ); + + gtk_label_set_xalign( + GTK_LABEL(path_title_label), + 0.0F + ); + + gtk_box_append( + GTK_BOX(root_box), + path_title_label + ); + + path_label = + gtk_label_new( + state->file_path + ); + + gtk_label_set_xalign( + GTK_LABEL(path_label), + 0.0F + ); + + gtk_label_set_wrap( + GTK_LABEL(path_label), + TRUE + ); + + gtk_label_set_selectable( + GTK_LABEL(path_label), + TRUE + ); + + gtk_box_append( + GTK_BOX(root_box), + path_label + ); + + grid = + gtk_grid_new(); + + gtk_grid_set_row_spacing( + GTK_GRID(grid), + 10 + ); + + gtk_grid_set_column_spacing( + GTK_GRID(grid), + 12 + ); + + gtk_widget_set_vexpand( + grid, + TRUE + ); + + type_label = + gtk_label_new( + "Type de preuve" + ); + + gtk_label_set_xalign( + GTK_LABEL(type_label), + 0.0F + ); + + state->type_drop_down = + GTK_DROP_DOWN( + gtk_drop_down_new( + G_LIST_MODEL(type_labels), + NULL + ) + ); + + /* + * gtk_drop_down_new() prend possession de la référence + * de type_labels. + */ + type_labels = NULL; + + gtk_drop_down_set_selected( + state->type_drop_down, + default_type_index + ); + + gtk_widget_set_hexpand( + GTK_WIDGET( + state->type_drop_down + ), + TRUE + ); + + collected_at_label = + gtk_label_new( + "Date de collecte" + ); + + gtk_label_set_xalign( + GTK_LABEL(collected_at_label), + 0.0F + ); + + state->collected_at_entry = + GTK_ENTRY( + gtk_entry_new() + ); + + gtk_entry_set_placeholder_text( + state->collected_at_entry, + "2026-07-20T10:30:00+02:00" + ); + + current_date_time = + g_date_time_new_now_local(); + + if (current_date_time != NULL) + { + current_date_time_iso8601 = + g_date_time_format_iso8601( + current_date_time + ); + + g_date_time_unref( + current_date_time + ); + } + + if (current_date_time_iso8601 != NULL) + { + gtk_editable_set_text( + GTK_EDITABLE( + state->collected_at_entry + ), + current_date_time_iso8601 + ); + + g_free( + current_date_time_iso8601 + ); + } + + source_label = + gtk_label_new( + "Source" + ); + + gtk_label_set_xalign( + GTK_LABEL(source_label), + 0.0F + ); + + state->source_entry = + GTK_ENTRY( + gtk_entry_new() + ); + + gtk_entry_set_placeholder_text( + state->source_entry, + "Ex. : conversation Instagram, email reçu..." + ); + + description_label = + gtk_label_new( + "Description" + ); + + gtk_label_set_xalign( + GTK_LABEL(description_label), + 0.0F + ); + + state->description_text_view = + GTK_TEXT_VIEW( + gtk_text_view_new() + ); + + gtk_text_view_set_wrap_mode( + state->description_text_view, + GTK_WRAP_WORD_CHAR + ); + + description_scrolled_window = + gtk_scrolled_window_new(); + + gtk_scrolled_window_set_child( + GTK_SCROLLED_WINDOW( + description_scrolled_window + ), + GTK_WIDGET( + state->description_text_view + ) + ); + + gtk_widget_set_size_request( + description_scrolled_window, + -1, + 130 + ); + + gtk_grid_attach( + GTK_GRID(grid), + type_label, + 0, + 0, + 1, + 1 + ); + + gtk_grid_attach( + GTK_GRID(grid), + GTK_WIDGET( + state->type_drop_down + ), + 1, + 0, + 1, + 1 + ); + + gtk_grid_attach( + GTK_GRID(grid), + collected_at_label, + 0, + 1, + 1, + 1 + ); + + gtk_grid_attach( + GTK_GRID(grid), + GTK_WIDGET( + state->collected_at_entry + ), + 1, + 1, + 1, + 1 + ); + + gtk_grid_attach( + GTK_GRID(grid), + source_label, + 0, + 2, + 1, + 1 + ); + + gtk_grid_attach( + GTK_GRID(grid), + GTK_WIDGET( + state->source_entry + ), + 1, + 2, + 1, + 1 + ); + + gtk_grid_attach( + GTK_GRID(grid), + description_label, + 0, + 3, + 1, + 1 + ); + + gtk_grid_attach( + GTK_GRID(grid), + description_scrolled_window, + 1, + 3, + 1, + 1 + ); + + gtk_box_append( + GTK_BOX(root_box), + grid + ); + + state->error_label = + GTK_LABEL( + gtk_label_new( + NULL + ) + ); + + gtk_label_set_xalign( + state->error_label, + 0.0F + ); + + gtk_label_set_wrap( + state->error_label, + TRUE + ); + + gtk_widget_add_css_class( + GTK_WIDGET( + state->error_label + ), + "error" + ); + + gtk_widget_set_visible( + GTK_WIDGET( + state->error_label + ), + FALSE + ); + + gtk_box_append( + GTK_BOX(root_box), + GTK_WIDGET( + state->error_label + ) + ); + + button_box = + gtk_box_new( + GTK_ORIENTATION_HORIZONTAL, + 8 + ); + + gtk_widget_set_halign( + button_box, + GTK_ALIGN_END + ); + + cancel_button = + gtk_button_new_with_label( + "Annuler" + ); + + import_button = + gtk_button_new_with_label( + "Importer" + ); + + gtk_widget_add_css_class( + import_button, + "suggested-action" + ); + + gtk_box_append( + GTK_BOX(button_box), + cancel_button + ); + + gtk_box_append( + GTK_BOX(button_box), + import_button + ); + + gtk_box_append( + GTK_BOX(root_box), + button_box + ); + + gtk_window_set_child( + state->window, + root_box + ); + + g_object_set_data_full( + G_OBJECT( + state->window + ), + "labfy-evidence-import-dialog-state", + state, + evidence_import_dialog_state_free + ); + + g_signal_connect( + state->window, + "close-request", + G_CALLBACK( + evidence_import_dialog_on_close_requested + ), + state + ); + + g_signal_connect( + cancel_button, + "clicked", + G_CALLBACK( + evidence_import_dialog_on_cancel_clicked + ), + state + ); + + g_signal_connect( + import_button, + "clicked", + G_CALLBACK( + evidence_import_dialog_on_import_clicked + ), + state + ); + + gtk_window_set_default_widget( + state->window, + import_button + ); + + gtk_window_present( + state->window + ); + + return TRUE; +} + +gboolean evidence_import_dialog_validate_values( + const char *file_path, + const char *type_identifier, + const char *collected_at, + GError **error +) +{ + char *normalized_type_identifier = NULL; + char *normalized_collected_at = NULL; + + GDateTime *collected_date_time = NULL; + + gboolean valid = FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (file_path == NULL || + file_path[0] == '\0') + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_DATA, + "Le chemin du fichier est obligatoire." + ); + + return FALSE; + } + + /* + * Le chemin n'est pas normalisé avec g_strstrip(), car les espaces + * peuvent faire partie d'un nom de fichier Unix valide. + */ + if (!g_file_test( + file_path, + G_FILE_TEST_IS_REGULAR + )) + { + g_set_error( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_DATA, + "Le fichier sélectionné est absent ou n'est pas un fichier " + "ordinaire : %s", + file_path + ); + + return FALSE; + } + + if (!evidence_import_dialog_value_has_text( + type_identifier + )) + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_DATA, + "Le type de preuve est obligatoire." + ); + + return FALSE; + } + + normalized_type_identifier = + g_strdup( + type_identifier + ); + + if (normalized_type_identifier == NULL) + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_MEMORY, + "Impossible de normaliser le type de preuve." + ); + + return FALSE; + } + + g_strstrip( + normalized_type_identifier + ); + + if (!evidence_import_dialog_value_has_text( + collected_at + )) + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_DATA, + "La date de collecte est obligatoire." + ); + + goto cleanup; + } + + normalized_collected_at = + g_strdup( + collected_at + ); + + if (normalized_collected_at == NULL) + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_MEMORY, + "Impossible de normaliser la date de collecte." + ); + + goto cleanup; + } + + g_strstrip( + normalized_collected_at + ); + + if (!evidence_import_dialog_has_explicit_timezone( + normalized_collected_at + )) + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_DATA, + "La date de collecte doit contenir un fuseau horaire." + ); + + goto cleanup; + } + + collected_date_time = + g_date_time_new_from_iso8601( + normalized_collected_at, + NULL + ); + + if (collected_date_time == NULL) + { + g_set_error_literal( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_DATA, + "La date de collecte n'est pas une date ISO 8601 valide." + ); + + goto cleanup; + } + + valid = TRUE; + +cleanup: + + if (collected_date_time != NULL) + { + g_date_time_unref( + collected_date_time + ); + } + + g_free( + normalized_collected_at + ); + + g_free( + normalized_type_identifier + ); + + return valid; +} + +void evidence_import_dialog_result_free( + EvidenceImportDialogResult *result +) +{ + if (result == NULL) + { + return; + } + + g_free( + result->type_identifier + ); + + g_free( + result->collected_at + ); + + g_free( + result->source + ); + + g_free( + result->description + ); + + g_free( + result + ); +} + +const char *evidence_import_dialog_result_get_type_identifier( + const EvidenceImportDialogResult *result +) +{ + if (result == NULL) + { + return NULL; + } + + return result->type_identifier; +} + +const char *evidence_import_dialog_result_get_collected_at( + const EvidenceImportDialogResult *result +) +{ + if (result == NULL) + { + return NULL; + } + + return result->collected_at; +} + +const char *evidence_import_dialog_result_get_source( + const EvidenceImportDialogResult *result +) +{ + if (result == NULL) + { + return NULL; + } + + return result->source; +} + +const char *evidence_import_dialog_result_get_description( + const EvidenceImportDialogResult *result +) +{ + if (result == NULL) + { + return NULL; + } + + return result->description; +} diff --git a/tests/test_evidence_import_dialog.c b/tests/test_evidence_import_dialog.c new file mode 100644 index 0000000..5050f7b --- /dev/null +++ b/tests/test_evidence_import_dialog.c @@ -0,0 +1,369 @@ +/****************************************************************************** + * @file test_evidence_import_dialog.c + * @brief Tests de validation du dialogue d'import d'une preuve. + ******************************************************************************/ + +#include "views/evidence_import_dialog.h" + +#include +#include + +/** + * @brief Fichier temporaire utilisé par les tests. + */ +typedef struct +{ + char *directory_path; + char *file_path; +} TestEvidenceImportDialogFixture; + +/** + * @brief Crée un fichier ordinaire temporaire. + */ +static TestEvidenceImportDialogFixture +test_evidence_import_dialog_fixture_create(void) +{ + TestEvidenceImportDialogFixture fixture = {0}; + GError *error = NULL; + + fixture.directory_path = + g_dir_make_tmp( + "labfy-evidence-import-dialog-test-XXXXXX", + &error + ); + + g_assert_no_error(error); + g_assert_nonnull(fixture.directory_path); + + fixture.file_path = + g_build_filename( + fixture.directory_path, + "preuve.txt", + NULL + ); + + g_assert_nonnull(fixture.file_path); + + g_file_set_contents( + fixture.file_path, + "preuve de test\n", + -1, + &error + ); + + g_assert_no_error(error); + + return fixture; +} + +/** + * @brief Supprime les ressources temporaires. + */ +static void test_evidence_import_dialog_fixture_clear( + TestEvidenceImportDialogFixture *fixture +) +{ + g_assert_nonnull(fixture); + + g_assert_cmpint( + g_remove(fixture->file_path), + ==, + 0 + ); + + g_assert_cmpint( + g_rmdir(fixture->directory_path), + ==, + 0 + ); + + g_free(fixture->file_path); + g_free(fixture->directory_path); + + fixture->file_path = NULL; + fixture->directory_path = NULL; +} + +/** + * @brief Vérifie une date UTC valide. + */ +static void test_evidence_import_dialog_validate_utc(void) +{ + TestEvidenceImportDialogFixture fixture = + test_evidence_import_dialog_fixture_create(); + + GError *error = NULL; + + g_assert_true( + evidence_import_dialog_validate_values( + fixture.file_path, + "document", + "2026-07-20T08:30:00Z", + &error + ) + ); + + g_assert_no_error(error); + + test_evidence_import_dialog_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie une date avec décalage horaire valide. + */ +static void test_evidence_import_dialog_validate_offset(void) +{ + TestEvidenceImportDialogFixture fixture = + test_evidence_import_dialog_fixture_create(); + + GError *error = NULL; + + g_assert_true( + evidence_import_dialog_validate_values( + fixture.file_path, + "photo", + "2026-07-20T10:30:00+02:00", + &error + ) + ); + + g_assert_no_error(error); + + test_evidence_import_dialog_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d'un chemin absent. + */ +static void test_evidence_import_dialog_missing_path(void) +{ + GError *error = NULL; + + g_assert_false( + evidence_import_dialog_validate_values( + NULL, + "document", + "2026-07-20T08:30:00Z", + &error + ) + ); + + g_assert_error( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_DATA + ); + + g_clear_error(&error); +} + +/** + * @brief Vérifie le refus d'un fichier inexistant. + */ +static void test_evidence_import_dialog_missing_file(void) +{ + GError *error = NULL; + + g_assert_false( + evidence_import_dialog_validate_values( + "/tmp/labfy-file-that-does-not-exist", + "document", + "2026-07-20T08:30:00Z", + &error + ) + ); + + g_assert_error( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_DATA + ); + + g_clear_error(&error); +} + +/** + * @brief Vérifie le refus d'un type vide. + */ +static void test_evidence_import_dialog_missing_type(void) +{ + TestEvidenceImportDialogFixture fixture = + test_evidence_import_dialog_fixture_create(); + + GError *error = NULL; + + g_assert_false( + evidence_import_dialog_validate_values( + fixture.file_path, + " ", + "2026-07-20T08:30:00Z", + &error + ) + ); + + g_assert_error( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_DATA + ); + + g_clear_error(&error); + + test_evidence_import_dialog_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d'une date absente. + */ +static void test_evidence_import_dialog_missing_date(void) +{ + TestEvidenceImportDialogFixture fixture = + test_evidence_import_dialog_fixture_create(); + + GError *error = NULL; + + g_assert_false( + evidence_import_dialog_validate_values( + fixture.file_path, + "document", + NULL, + &error + ) + ); + + g_assert_error( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_DATA + ); + + g_clear_error(&error); + + test_evidence_import_dialog_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d'une date sans fuseau horaire. + */ +static void test_evidence_import_dialog_missing_timezone(void) +{ + TestEvidenceImportDialogFixture fixture = + test_evidence_import_dialog_fixture_create(); + + GError *error = NULL; + + g_assert_false( + evidence_import_dialog_validate_values( + fixture.file_path, + "document", + "2026-07-20T08:30:00", + &error + ) + ); + + g_assert_error( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_DATA + ); + + g_clear_error(&error); + + test_evidence_import_dialog_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d'une date civile impossible. + */ +static void test_evidence_import_dialog_invalid_date(void) +{ + TestEvidenceImportDialogFixture fixture = + test_evidence_import_dialog_fixture_create(); + + GError *error = NULL; + + g_assert_false( + evidence_import_dialog_validate_values( + fixture.file_path, + "document", + "2026-02-30T08:30:00+01:00", + &error + ) + ); + + g_assert_error( + error, + EVIDENCE_IMPORT_DIALOG_ERROR, + EVIDENCE_IMPORT_DIALOG_ERROR_INVALID_DATA + ); + + g_clear_error(&error); + + test_evidence_import_dialog_fixture_clear( + &fixture + ); +} + +int main( + int argc, + char **argv +) +{ + g_test_init( + &argc, + &argv, + NULL + ); + + g_test_add_func( + "/evidence-import-dialog/validate-utc", + test_evidence_import_dialog_validate_utc + ); + + g_test_add_func( + "/evidence-import-dialog/validate-offset", + test_evidence_import_dialog_validate_offset + ); + + g_test_add_func( + "/evidence-import-dialog/missing-path", + test_evidence_import_dialog_missing_path + ); + + g_test_add_func( + "/evidence-import-dialog/missing-file", + test_evidence_import_dialog_missing_file + ); + + g_test_add_func( + "/evidence-import-dialog/missing-type", + test_evidence_import_dialog_missing_type + ); + + g_test_add_func( + "/evidence-import-dialog/missing-date", + test_evidence_import_dialog_missing_date + ); + + g_test_add_func( + "/evidence-import-dialog/missing-timezone", + test_evidence_import_dialog_missing_timezone + ); + + g_test_add_func( + "/evidence-import-dialog/invalid-date", + test_evidence_import_dialog_invalid_date + ); + + return g_test_run(); +} diff --git a/tests/test_evidence_type.c b/tests/test_evidence_type.c new file mode 100644 index 0000000..5d8e166 --- /dev/null +++ b/tests/test_evidence_type.c @@ -0,0 +1,339 @@ +/****************************************************************************** + * @file test_evidence_type.c + * @brief Tests unitaires du modèle EvidenceType. + ******************************************************************************/ + +#include "models/evidence_type.h" + +#include + +/** + * @brief Vérifie la création d’un type valide. + */ +static void test_evidence_type_new_valid(void) +{ + EvidenceType *evidence_type = NULL; + GError *error = NULL; + + evidence_type = evidence_type_new( + 4, + "document", + "Document", + "Document numérique ou numérisé.", + &error + ); + + g_assert_no_error(error); + g_assert_nonnull(evidence_type); + + g_assert_cmpint( + evidence_type_get_identifier(evidence_type), + ==, + 4 + ); + + g_assert_cmpstr( + evidence_type_get_code(evidence_type), + ==, + "document" + ); + + g_assert_cmpstr( + evidence_type_get_label(evidence_type), + ==, + "Document" + ); + + g_assert_cmpstr( + evidence_type_get_description(evidence_type), + ==, + "Document numérique ou numérisé." + ); + + evidence_type_free(evidence_type); +} + +/** + * @brief Vérifie la suppression des espaces périphériques. + */ +static void test_evidence_type_values_are_trimmed(void) +{ + EvidenceType *evidence_type = NULL; + GError *error = NULL; + + evidence_type = evidence_type_new( + 1, + " screenshot ", + " Capture d’écran ", + " Image capturée depuis un écran. ", + &error + ); + + g_assert_no_error(error); + g_assert_nonnull(evidence_type); + + g_assert_cmpstr( + evidence_type_get_code(evidence_type), + ==, + "screenshot" + ); + + g_assert_cmpstr( + evidence_type_get_label(evidence_type), + ==, + "Capture d’écran" + ); + + g_assert_cmpstr( + evidence_type_get_description(evidence_type), + ==, + "Image capturée depuis un écran." + ); + + evidence_type_free(evidence_type); +} + +/** + * @brief Vérifie qu’une description absente reste facultative. + */ +static void test_evidence_type_description_is_optional(void) +{ + EvidenceType *evidence_type = NULL; + GError *error = NULL; + + evidence_type = evidence_type_new( + 9, + "other", + "Autre", + NULL, + &error + ); + + g_assert_no_error(error); + g_assert_nonnull(evidence_type); + + g_assert_null( + evidence_type_get_description(evidence_type) + ); + + evidence_type_free(evidence_type); +} + +/** + * @brief Vérifie qu’une description vide est normalisée vers NULL. + */ +static void test_evidence_type_empty_description_becomes_null(void) +{ + EvidenceType *evidence_type = NULL; + GError *error = NULL; + + evidence_type = evidence_type_new( + 8, + "text", + "Texte", + " ", + &error + ); + + g_assert_no_error(error); + g_assert_nonnull(evidence_type); + + g_assert_null( + evidence_type_get_description(evidence_type) + ); + + evidence_type_free(evidence_type); +} + +/** + * @brief Vérifie le refus d’un identifiant invalide. + */ +static void test_evidence_type_invalid_identifier(void) +{ + EvidenceType *evidence_type = NULL; + GError *error = NULL; + + evidence_type = evidence_type_new( + 0, + "document", + "Document", + NULL, + &error + ); + + g_assert_null(evidence_type); + + g_assert_error( + error, + EVIDENCE_TYPE_ERROR, + EVIDENCE_TYPE_ERROR_INVALID_IDENTIFIER + ); + + g_clear_error(&error); +} + +/** + * @brief Vérifie le refus d’un code absent. + */ +static void test_evidence_type_missing_code(void) +{ + EvidenceType *evidence_type = NULL; + GError *error = NULL; + + evidence_type = evidence_type_new( + 4, + NULL, + "Document", + NULL, + &error + ); + + g_assert_null(evidence_type); + + g_assert_error( + error, + EVIDENCE_TYPE_ERROR, + EVIDENCE_TYPE_ERROR_INVALID_CODE + ); + + g_clear_error(&error); +} + +/** + * @brief Vérifie le refus d’un code vide. + */ +static void test_evidence_type_empty_code(void) +{ + EvidenceType *evidence_type = NULL; + GError *error = NULL; + + evidence_type = evidence_type_new( + 4, + " ", + "Document", + NULL, + &error + ); + + g_assert_null(evidence_type); + + g_assert_error( + error, + EVIDENCE_TYPE_ERROR, + EVIDENCE_TYPE_ERROR_INVALID_CODE + ); + + g_clear_error(&error); +} + +/** + * @brief Vérifie le refus d’un libellé absent. + */ +static void test_evidence_type_missing_label(void) +{ + EvidenceType *evidence_type = NULL; + GError *error = NULL; + + evidence_type = evidence_type_new( + 4, + "document", + NULL, + NULL, + &error + ); + + g_assert_null(evidence_type); + + g_assert_error( + error, + EVIDENCE_TYPE_ERROR, + EVIDENCE_TYPE_ERROR_INVALID_LABEL + ); + + g_clear_error(&error); +} + +/** + * @brief Vérifie le comportement des getters avec NULL. + */ +static void test_evidence_type_null_getters(void) +{ + g_assert_cmpint( + evidence_type_get_identifier(NULL), + ==, + 0 + ); + + g_assert_null( + evidence_type_get_code(NULL) + ); + + g_assert_null( + evidence_type_get_label(NULL) + ); + + g_assert_null( + evidence_type_get_description(NULL) + ); + + evidence_type_free(NULL); +} + +int main( + int argc, + char **argv +) +{ + g_test_init( + &argc, + &argv, + NULL + ); + + g_test_add_func( + "/evidence-type/new-valid", + test_evidence_type_new_valid + ); + + g_test_add_func( + "/evidence-type/values-are-trimmed", + test_evidence_type_values_are_trimmed + ); + + g_test_add_func( + "/evidence-type/description-is-optional", + test_evidence_type_description_is_optional + ); + + g_test_add_func( + "/evidence-type/empty-description-becomes-null", + test_evidence_type_empty_description_becomes_null + ); + + g_test_add_func( + "/evidence-type/invalid-identifier", + test_evidence_type_invalid_identifier + ); + + g_test_add_func( + "/evidence-type/missing-code", + test_evidence_type_missing_code + ); + + g_test_add_func( + "/evidence-type/empty-code", + test_evidence_type_empty_code + ); + + g_test_add_func( + "/evidence-type/missing-label", + test_evidence_type_missing_label + ); + + g_test_add_func( + "/evidence-type/null-getters", + test_evidence_type_null_getters + ); + + return g_test_run(); +} diff --git a/tests/test_evidence_type_dao.c b/tests/test_evidence_type_dao.c new file mode 100644 index 0000000..849ca18 --- /dev/null +++ b/tests/test_evidence_type_dao.c @@ -0,0 +1,495 @@ +/****************************************************************************** + * @file test_evidence_type_dao.c + * @brief Tests du DAO des types de preuve. + ******************************************************************************/ + +#include "dao/evidence_type_dao.h" + +#include "database/database.h" +#include "database/statement.h" +#include "models/evidence_type.h" + +#include +#include + +/** + * @brief Ressources utilisées par un test EvidenceTypeDao. + */ +typedef struct +{ + char *temporary_directory; + char *database_path; + + Database *database; + EvidenceTypeDao *evidence_type_dao; +} TestEvidenceTypeDaoFixture; + +/** + * @brief Exécute une requête SQL ne retournant aucune ligne. + */ +static void test_evidence_type_dao_execute_sql( + Database *database, + const char *sql +) +{ + DatabaseStatement *statement = NULL; + + g_assert_nonnull(database); + g_assert_nonnull(sql); + + statement = + database_statement_prepare( + database, + sql + ); + + g_assert_nonnull(statement); + + g_assert_cmpint( + database_statement_step(statement), + ==, + DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize( + statement + ); +} + +/** + * @brief Crée une base temporaire contenant le schéma de l’application. + */ +static TestEvidenceTypeDaoFixture +test_evidence_type_dao_fixture_create(void) +{ + TestEvidenceTypeDaoFixture fixture = {0}; + + GError *error = NULL; + + fixture.temporary_directory = + g_dir_make_tmp( + "labfy-evidence-type-dao-test-XXXXXX", + &error + ); + + g_assert_no_error(error); + g_assert_nonnull( + fixture.temporary_directory + ); + + fixture.database_path = + g_build_filename( + fixture.temporary_directory, + "Enquete.sqlite", + NULL + ); + + g_assert_nonnull( + fixture.database_path + ); + + g_assert_true( + database_initialize( + fixture.database_path, + "Enquete_EvidenceTypeDao", + fixture.temporary_directory + ) + ); + + fixture.database = + database_open( + fixture.database_path + ); + + g_assert_nonnull( + fixture.database + ); + + fixture.evidence_type_dao = + evidence_type_dao_new( + fixture.database, + &error + ); + + g_assert_no_error(error); + + g_assert_nonnull( + fixture.evidence_type_dao + ); + + return fixture; +} + +/** + * @brief Libère la fixture et supprime la base temporaire. + */ +static void test_evidence_type_dao_fixture_clear( + TestEvidenceTypeDaoFixture *fixture +) +{ + g_assert_nonnull(fixture); + + evidence_type_dao_free( + fixture->evidence_type_dao + ); + + database_close( + fixture->database + ); + + g_assert_cmpint( + g_remove(fixture->database_path), + ==, + 0 + ); + + g_assert_cmpint( + g_rmdir(fixture->temporary_directory), + ==, + 0 + ); + + g_free( + fixture->database_path + ); + + g_free( + fixture->temporary_directory + ); + + fixture->evidence_type_dao = NULL; + fixture->database = NULL; + fixture->database_path = NULL; + fixture->temporary_directory = NULL; +} + +/** + * @brief Vérifie le refus d’une connexion absente. + */ +static void test_evidence_type_dao_new_invalid_database(void) +{ + EvidenceTypeDao *evidence_type_dao = NULL; + + GError *error = NULL; + + evidence_type_dao = + evidence_type_dao_new( + NULL, + &error + ); + + g_assert_null( + evidence_type_dao + ); + + g_assert_error( + error, + EVIDENCE_TYPE_DAO_ERROR, + EVIDENCE_TYPE_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); +} + +/** + * @brief Vérifie le chargement du catalogue initial. + */ +static void test_evidence_type_dao_list_all(void) +{ + static const char *const expected_codes[] = + { + "screenshot", + "photo", + "video", + "document", + "email", + "archive", + "audio", + "text", + "other" + }; + + static const char *const expected_labels[] = + { + "Capture d'écran", + "Photographie", + "Vidéo", + "Document", + "Courrier électronique", + "Archive", + "Audio", + "Texte", + "Autre" + }; + + TestEvidenceTypeDaoFixture fixture = + test_evidence_type_dao_fixture_create(); + + GPtrArray *evidence_types = NULL; + + GError *error = NULL; + + guint index = 0; + + evidence_types = + evidence_type_dao_list_all( + fixture.evidence_type_dao, + &error + ); + + g_assert_no_error(error); + g_assert_nonnull(evidence_types); + + g_assert_cmpuint( + evidence_types->len, + ==, + G_N_ELEMENTS(expected_codes) + ); + + for (index = 0; + index < evidence_types->len; + index++) + { + const EvidenceType *evidence_type = + g_ptr_array_index( + evidence_types, + index + ); + + g_assert_nonnull( + evidence_type + ); + + /* + * La requête utilise ORDER BY id ASC. + */ + g_assert_cmpint( + evidence_type_get_identifier( + evidence_type + ), + ==, + (gint64) index + 1 + ); + + g_assert_cmpstr( + evidence_type_get_code( + evidence_type + ), + ==, + expected_codes[index] + ); + + g_assert_cmpstr( + evidence_type_get_label( + evidence_type + ), + ==, + expected_labels[index] + ); + } + + g_ptr_array_unref( + evidence_types + ); + + test_evidence_type_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu’une table vide retourne un tableau vide. + */ +static void test_evidence_type_dao_list_empty(void) +{ + TestEvidenceTypeDaoFixture fixture = + test_evidence_type_dao_fixture_create(); + + GPtrArray *evidence_types = NULL; + + GError *error = NULL; + + test_evidence_type_dao_execute_sql( + fixture.database, + "DELETE FROM types_preuve;" + ); + + evidence_types = + evidence_type_dao_list_all( + fixture.evidence_type_dao, + &error + ); + + g_assert_no_error(error); + g_assert_nonnull(evidence_types); + + g_assert_cmpuint( + evidence_types->len, + ==, + 0 + ); + + g_ptr_array_unref( + evidence_types + ); + + test_evidence_type_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le signalement d’une table absente. + */ +static void test_evidence_type_dao_missing_table(void) +{ + Database *database = NULL; + + EvidenceTypeDao *evidence_type_dao = NULL; + + GPtrArray *evidence_types = NULL; + + GError *error = NULL; + + database = + database_open( + ":memory:" + ); + + g_assert_nonnull(database); + + evidence_type_dao = + evidence_type_dao_new( + database, + &error + ); + + g_assert_no_error(error); + g_assert_nonnull(evidence_type_dao); + + /* + * DatabaseStatement journalise l’échec de préparation SQLite. + */ + g_test_expect_message( + NULL, + G_LOG_LEVEL_WARNING, + "*Impossible de préparer la requête SQL*" + ); + + evidence_types = + evidence_type_dao_list_all( + evidence_type_dao, + &error + ); + + g_test_assert_expected_messages(); + + g_assert_null(evidence_types); + + g_assert_error( + error, + EVIDENCE_TYPE_DAO_ERROR, + EVIDENCE_TYPE_DAO_ERROR_SCHEMA + ); + + g_clear_error( + &error + ); + + evidence_type_dao_free( + evidence_type_dao + ); + + database_close( + database + ); +} + +/** + * @brief Vérifie le refus d’une ligne produisant un modèle invalide. + */ +static void test_evidence_type_dao_invalid_model(void) +{ + TestEvidenceTypeDaoFixture fixture = + test_evidence_type_dao_fixture_create(); + + GPtrArray *evidence_types = NULL; + + GError *error = NULL; + + /* + * La contrainte SQLite interdit NULL mais pas une chaîne + * uniquement composée d’espaces. + * + * EvidenceType doit normaliser puis refuser cette valeur. + */ + test_evidence_type_dao_execute_sql( + fixture.database, + "UPDATE types_preuve " + "SET code = ' ' " + "WHERE id = 1;" + ); + + evidence_types = + evidence_type_dao_list_all( + fixture.evidence_type_dao, + &error + ); + + g_assert_null(evidence_types); + + g_assert_error( + error, + EVIDENCE_TYPE_DAO_ERROR, + EVIDENCE_TYPE_DAO_ERROR_MODEL + ); + + g_clear_error( + &error + ); + + test_evidence_type_dao_fixture_clear( + &fixture + ); +} + +int main( + int argc, + char **argv +) +{ + g_test_init( + &argc, + &argv, + NULL + ); + + g_test_add_func( + "/evidence-type-dao/new-invalid-database", + test_evidence_type_dao_new_invalid_database + ); + + g_test_add_func( + "/evidence-type-dao/list-all", + test_evidence_type_dao_list_all + ); + + g_test_add_func( + "/evidence-type-dao/list-empty", + test_evidence_type_dao_list_empty + ); + + g_test_add_func( + "/evidence-type-dao/missing-table", + test_evidence_type_dao_missing_table + ); + + g_test_add_func( + "/evidence-type-dao/invalid-model", + test_evidence_type_dao_invalid_model + ); + + return g_test_run(); +}