diff --git a/Makefile b/Makefile index 0c073bd..47c9ac0 100644 --- a/Makefile +++ b/Makefile @@ -53,6 +53,7 @@ TEST_TOOL_CATALOG := tests/test_tool_catalog TEST_TOOL_INITIALIZER := tests/test_tool_initializer TEST_FILE_HASH := tests/test_file_hash TEST_EVIDENCE_COPY := tests/test_evidence_copy +TEST_EVIDENCE_IMPORTER := tests/test_evidence_importer all: $(TARGET) @@ -228,6 +229,22 @@ $(TEST_EVIDENCE_COPY): \ -DEVIDENCE_COPY_ENABLE_TEST_HOOKS \ $^ -o $@ $(TEST_LDFLAGS) +$(TEST_EVIDENCE_IMPORTER): \ + tests/test_evidence_importer.c \ + src/core/evidence_importer.c \ + src/core/evidence_copy.c \ + src/core/file_hash.c \ + src/dao/evidence_dao.c \ + src/models/evidence_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) \ + -DEVIDENCE_IMPORTER_ENABLE_TEST_HOOKS \ + $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 + test: \ $(TEST_NODE) \ $(TEST_TREE_MODEL) \ @@ -250,7 +267,8 @@ test: \ $(TEST_TOOL_CATALOG) \ $(TEST_TOOL_INITIALIZER) \ $(TEST_FILE_HASH) \ - $(TEST_EVIDENCE_COPY) + $(TEST_EVIDENCE_COPY) \ + $(TEST_EVIDENCE_IMPORTER) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -274,6 +292,7 @@ test: \ @$(TEST_TOOL_INITIALIZER) @$(TEST_FILE_HASH) @$(TEST_EVIDENCE_COPY) + @$(TEST_EVIDENCE_IMPORTER) @echo "Tous les tests sont valides." %.o: %.c @@ -305,7 +324,8 @@ clean: $(TEST_TOOL_CATALOG) \ $(TEST_TOOL_INITIALIZER) \ $(TEST_FILE_HASH) \ - $(TEST_EVIDENCE_COPY) + $(TEST_EVIDENCE_COPY) \ + $(TEST_EVIDENCE_IMPORTER) .PHONY: clean run test diff --git a/include/core/evidence_importer.h b/include/core/evidence_importer.h new file mode 100644 index 0000000..0507860 --- /dev/null +++ b/include/core/evidence_importer.h @@ -0,0 +1,132 @@ +/****************************************************************************** + * @file evidence_importer.h + * @brief Orchestration de l’import transactionnel des preuves. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_EVIDENCE_IMPORTER_H +#define LABFY_INVESTIGATION_EVIDENCE_IMPORTER_H + +#include "database/database.h" +#include "models/evidence_record.h" + +#include +#include + +G_BEGIN_DECLS + +/** + * @brief Catégories d’erreurs du module EvidenceImporter. + */ +typedef enum +{ + EVIDENCE_IMPORTER_ERROR_INVALID_ARGUMENT, + EVIDENCE_IMPORTER_ERROR_MEMORY, + EVIDENCE_IMPORTER_ERROR_CANCELLED, + EVIDENCE_IMPORTER_ERROR_DESTINATION, + EVIDENCE_IMPORTER_ERROR_COPY, + EVIDENCE_IMPORTER_ERROR_MODEL, + EVIDENCE_IMPORTER_ERROR_DATABASE, + EVIDENCE_IMPORTER_ERROR_ROLLBACK +} EvidenceImporterError; + +/** + * @brief Domaine d’erreurs du module EvidenceImporter. + */ +#define EVIDENCE_IMPORTER_ERROR \ + evidence_importer_error_quark() + +/** + * @brief Retourne le domaine d’erreurs du module. + */ +GQuark evidence_importer_error_quark(void); + +/** + * @brief Service opaque orchestrant l’import des preuves. + * + * L’objet emprunte la connexion Database reçue à sa création. + */ +typedef struct EvidenceImporter EvidenceImporter; + +/** + * @brief Paramètres empruntés d’un import de preuve. + * + * Les chaînes restent la propriété de l’appelant. + * + * source_path, destination_directory, relative_directory et + * type_identifier sont obligatoires. + * + * collected_at, source et description sont facultatifs. + */ +typedef struct +{ + const char *source_path; + const char *destination_directory; + const char *relative_directory; + const char *type_identifier; + + const char *collected_at; + const char *source; + const char *description; +} EvidenceImportRequest; + +/** + * @brief Crée un service d’import utilisant une connexion existante. + * + * La connexion est empruntée et doit rester valide pendant toute la durée + * de vie de l’importeur. + * + * @param database Connexion ouverte et migrée. + * @param error Adresse recevant une éventuelle erreur. + * + * @return Nouvel importeur, ou NULL. + */ +EvidenceImporter *evidence_importer_new( + Database *database, + GError **error +); + +/** + * @brief Libère un importeur. + * + * La connexion Database empruntée n’est pas fermée. + * Cette fonction accepte NULL. + * + * @param evidence_importer Importeur à libérer. + */ +void evidence_importer_free( + EvidenceImporter *evidence_importer +); + +/** + * @brief Importe une preuve de manière transactionnelle. + * + * Après succès : + * + * - le fichier est présent dans l’enquête ; + * - le modèle est enregistré dans SQLite ; + * - le EvidenceRecord retourné appartient à l’appelant. + * + * Après échec : + * + * - aucun EvidenceRecord n’est retourné ; + * - aucune transaction démarrée par l’importeur ne reste active ; + * - toute copie créée par l’opération est supprimée lorsque cela est + * encore possible. + * + * @param evidence_importer Service d’import. + * @param request Paramètres empruntés de l’import. + * @param cancellable Objet d’annulation facultatif. + * @param error Adresse recevant une éventuelle erreur. + * + * @return Nouveau EvidenceRecord, ou NULL. + */ +EvidenceRecord *evidence_importer_import( + EvidenceImporter *evidence_importer, + const EvidenceImportRequest *request, + GCancellable *cancellable, + GError **error +); + +G_END_DECLS + +#endif diff --git a/include/core/evidence_importer_test.h b/include/core/evidence_importer_test.h new file mode 100644 index 0000000..759c419 --- /dev/null +++ b/include/core/evidence_importer_test.h @@ -0,0 +1,29 @@ +/****************************************************************************** + * @file evidence_importer_test.h + * @brief Crochets réservés aux tests de EvidenceImporter. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_EVIDENCE_IMPORTER_TEST_H +#define LABFY_INVESTIGATION_EVIDENCE_IMPORTER_TEST_H + +#include + +G_BEGIN_DECLS + +void evidence_importer_test_reset_hooks(void); + +void evidence_importer_test_set_cancel_after_copy( + gboolean enabled +); + +void evidence_importer_test_set_commit_failure( + gboolean enabled +); + +void evidence_importer_test_set_cleanup_failure( + gboolean enabled +); + +G_END_DECLS + +#endif diff --git a/labfy-investigation b/labfy-investigation new file mode 100755 index 0000000..0b270dd Binary files /dev/null and b/labfy-investigation differ diff --git a/src/core/evidence_importer.c b/src/core/evidence_importer.c new file mode 100644 index 0000000..b832b01 --- /dev/null +++ b/src/core/evidence_importer.c @@ -0,0 +1,1476 @@ +/****************************************************************************** + * @file evidence_importer.c + * @brief Orchestration de l’import transactionnel des preuves. + ******************************************************************************/ + +#include +#include +#include + +#include "core/evidence_importer.h" +#include "core/evidence_copy.h" +#include "dao/evidence_dao.h" +#include "database/transaction.h" +#include "database/error.h" + +/** + * @brief Longueur maximale conservée pour une extension. + */ +#define EVIDENCE_IMPORTER_MAX_EXTENSION_LENGTH 16U + +#ifdef EVIDENCE_IMPORTER_ENABLE_TEST_HOOKS +#include "core/evidence_importer_test.h" +#endif + +/** + * @brief Représentation privée du service d’import. + */ +struct EvidenceImporter +{ + Database *database; + EvidenceDao *evidence_dao; +}; + +#ifdef EVIDENCE_IMPORTER_ENABLE_TEST_HOOKS + +static gboolean evidence_importer_test_cancel_after_copy = FALSE; +static gboolean evidence_importer_test_commit_failure = FALSE; +static gboolean evidence_importer_test_cleanup_failure = FALSE; + +/** + * @brief Consomme un hook à usage unique. + */ +static gboolean evidence_importer_test_take_hook( + gboolean *hook +) +{ + gboolean enabled = FALSE; + + if (hook == NULL) + { + return FALSE; + } + + enabled = + *hook; + + *hook = FALSE; + + return enabled; +} + +void evidence_importer_test_reset_hooks(void) +{ + evidence_importer_test_cancel_after_copy = FALSE; + evidence_importer_test_commit_failure = FALSE; + evidence_importer_test_cleanup_failure = FALSE; +} + +void evidence_importer_test_set_cancel_after_copy( + gboolean enabled +) +{ + evidence_importer_test_cancel_after_copy = + enabled; +} + +void evidence_importer_test_set_commit_failure( + gboolean enabled +) +{ + evidence_importer_test_commit_failure = + enabled; +} + +void evidence_importer_test_set_cleanup_failure( + gboolean enabled +) +{ + evidence_importer_test_cleanup_failure = + enabled; +} + +#endif + +/** + * @brief Informations générées avant la copie d’une preuve. + */ +typedef struct +{ + char *identifier; + char *original_name; + char *internal_name; + char *relative_path; +} EvidenceImportStorage; + +/** + * @brief Libère les informations temporaires de stockage. + */ +static void evidence_importer_storage_clear( + EvidenceImportStorage *storage +) +{ + if (storage == NULL) + { + return; + } + + g_free( + storage->relative_path + ); + + g_free( + storage->internal_name + ); + + g_free( + storage->original_name + ); + + g_free( + storage->identifier + ); + + storage->relative_path = NULL; + storage->internal_name = NULL; + storage->original_name = NULL; + storage->identifier = NULL; +} + +/** + * @brief Enregistre une erreur littérale EvidenceImporter. + */ +static void evidence_importer_set_error_literal( + GError **error, + EvidenceImporterError error_code, + const char *error_message +) +{ + if (error == NULL) + { + return; + } + + g_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR, + error_code, + error_message + ); +} + +/** + * @brief Vérifie si une annulation a été demandée. + */ +static gboolean evidence_importer_check_cancelled( + GCancellable *cancellable, + GError **error +) +{ + if (cancellable == NULL || + !g_cancellable_is_cancelled(cancellable)) + { + return FALSE; + } + + evidence_importer_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR_CANCELLED, + "L’import de la preuve a été annulé." + ); + + return TRUE; +} + +/** + * @brief Vérifie un chemin de dossier relatif à l’enquête. + */ +static gboolean evidence_importer_relative_directory_is_valid( + const char *relative_directory +) +{ + const char *component_start = NULL; + const char *cursor = NULL; + + gsize component_length = 0; + + if (relative_directory == NULL || + relative_directory[0] == '\0' || + g_path_is_absolute(relative_directory)) + { + return FALSE; + } + + component_start = + relative_directory; + + for (cursor = relative_directory; + ; + cursor++) + { + /* + * Les antislashs sont refusés pour éviter toute ambiguïté + * entre systèmes. + */ + if (*cursor == '\\') + { + return FALSE; + } + + if (*cursor != '/' && + *cursor != '\0') + { + continue; + } + + component_length = + (gsize) (cursor - component_start); + + if (component_length == 0) + { + return FALSE; + } + + if (component_length == 1 && + component_start[0] == '.') + { + return FALSE; + } + + if (component_length == 2 && + component_start[0] == '.' && + component_start[1] == '.') + { + return FALSE; + } + + if (*cursor == '\0') + { + break; + } + + component_start = + cursor + 1; + } + + return TRUE; +} + +/** + * @brief Vérifie les champs obligatoires d’une requête. + */ +static gboolean evidence_importer_request_is_valid( + const EvidenceImportRequest *request +) +{ + if (request == NULL) + { + return FALSE; + } + + if (request->source_path == NULL || + request->source_path[0] == '\0') + { + return FALSE; + } + + if (request->destination_directory == NULL || + request->destination_directory[0] == '\0') + { + return FALSE; + } + + if (!evidence_importer_relative_directory_is_valid( + request->relative_directory + )) + { + return FALSE; + } + + if (request->type_identifier == NULL || + request->type_identifier[0] == '\0') + { + return FALSE; + } + + return TRUE; +} + +GQuark evidence_importer_error_quark(void) +{ + return g_quark_from_static_string( + "evidence-importer-error-quark" + ); +} + +EvidenceImporter *evidence_importer_new( + Database *database, + GError **error +) +{ + EvidenceImporter *evidence_importer = NULL; + EvidenceDao *evidence_dao = NULL; + + GError *dao_error = NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (database == NULL) + { + evidence_importer_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR_INVALID_ARGUMENT, + "La connexion Database est invalide." + ); + + return NULL; + } + + evidence_dao = + evidence_dao_new( + database, + &dao_error + ); + + if (evidence_dao == NULL) + { + if (error != NULL) + { + g_set_error( + error, + EVIDENCE_IMPORTER_ERROR, + EVIDENCE_IMPORTER_ERROR_DATABASE, + "Impossible de créer le DAO des preuves : %s", + dao_error != NULL + ? dao_error->message + : "erreur inconnue" + ); + } + + g_clear_error( + &dao_error + ); + + return NULL; + } + + evidence_importer = + g_try_new0( + EvidenceImporter, + 1 + ); + + if (evidence_importer == NULL) + { + evidence_dao_free( + evidence_dao + ); + + evidence_importer_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR_MEMORY, + "Impossible d’allouer le service d’import." + ); + + return NULL; + } + + evidence_importer->database = + database; + + evidence_importer->evidence_dao = + evidence_dao; + + return evidence_importer; +} + +/** + * @brief Vérifie le nom extrait du chemin source. + */ +static gboolean evidence_importer_original_name_is_valid( + const char *original_name +) +{ + if (original_name == NULL || + original_name[0] == '\0') + { + return FALSE; + } + + if (strcmp( + original_name, + "." + ) == 0 || + strcmp( + original_name, + ".." + ) == 0) + { + return FALSE; + } + + if (strchr( + original_name, + '/' + ) != NULL || + strchr( + original_name, + '\\' + ) != NULL) + { + return FALSE; + } + + return TRUE; +} + +/** + * @brief Génère le nom interne à partir de l’UUID et de l’extension. + * + * Une extension invalide est simplement ignorée. + */ +static gboolean evidence_importer_build_internal_name( + const char *identifier, + const char *original_name, + char **out_internal_name, + GError **error +) +{ + const char *extension = NULL; + + char *internal_name = NULL; + + gsize identifier_length = 0; + gsize extension_length = 0; + gsize allocation_size = 0; + gsize extension_index = 0; + + gboolean extension_is_valid = FALSE; + + if (identifier == NULL || + original_name == NULL || + out_internal_name == NULL || + *out_internal_name != NULL) + { + evidence_importer_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR_INVALID_ARGUMENT, + "Les paramètres du nom interne sont invalides." + ); + + return FALSE; + } + + identifier_length = + strlen( + identifier + ); + + extension = + strrchr( + original_name, + '.' + ); + + /* + * Un fichier caché tel que ".profile" ne possède pas ici + * d’extension exploitable. + */ + if (extension != NULL && + extension != original_name && + extension[1] != '\0') + { + extension++; + + extension_length = + strlen( + extension + ); + + extension_is_valid = + extension_length > 0 && + extension_length <= + EVIDENCE_IMPORTER_MAX_EXTENSION_LENGTH; + + for (extension_index = 0; + extension_is_valid && + extension_index < extension_length; + extension_index++) + { + if (!g_ascii_isalnum( + extension[extension_index] + )) + { + extension_is_valid = FALSE; + } + } + } + + allocation_size = + identifier_length + 1U; + + if (extension_is_valid) + { + allocation_size += + 1U + extension_length; + } + + internal_name = + g_try_malloc( + allocation_size + ); + + if (internal_name == NULL) + { + evidence_importer_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR_MEMORY, + "Impossible d’allouer le nom interne de la preuve." + ); + + return FALSE; + } + + memcpy( + internal_name, + identifier, + identifier_length + ); + + if (!extension_is_valid) + { + internal_name[identifier_length] = '\0'; + + *out_internal_name = + internal_name; + + return TRUE; + } + + internal_name[identifier_length] = '.'; + + for (extension_index = 0; + extension_index < extension_length; + extension_index++) + { + internal_name[ + identifier_length + + 1U + + extension_index + ] = + (char) g_ascii_tolower( + extension[extension_index] + ); + } + + internal_name[ + identifier_length + + 1U + + extension_length + ] = '\0'; + + *out_internal_name = + internal_name; + + return TRUE; +} + +/** + * @brief Génère les informations nécessaires à la future copie. + */ +static gboolean evidence_importer_prepare_storage( + const EvidenceImportRequest *request, + EvidenceImportStorage *storage, + GError **error +) +{ + if (request == NULL || + storage == NULL) + { + evidence_importer_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR_INVALID_ARGUMENT, + "Les paramètres de préparation du stockage sont invalides." + ); + + return FALSE; + } + + storage->identifier = + g_uuid_string_random(); + + if (storage->identifier == NULL) + { + evidence_importer_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR_MEMORY, + "Impossible de générer l’identifiant de la preuve." + ); + + goto failure; + } + + if (!g_uuid_string_is_valid( + storage->identifier + )) + { + evidence_importer_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR_DESTINATION, + "L’identifiant généré pour la preuve est invalide." + ); + + goto failure; + } + + storage->original_name = + g_path_get_basename( + request->source_path + ); + + if (!evidence_importer_original_name_is_valid( + storage->original_name + )) + { + evidence_importer_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR_INVALID_ARGUMENT, + "Le nom original du fichier source est invalide." + ); + + goto failure; + } + + if (!evidence_importer_build_internal_name( + storage->identifier, + storage->original_name, + &storage->internal_name, + error + )) + { + goto failure; + } + + storage->relative_path = + g_build_filename( + request->relative_directory, + storage->internal_name, + NULL + ); + + if (storage->relative_path == NULL) + { + evidence_importer_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR_MEMORY, + "Impossible de construire le chemin relatif de la preuve." + ); + + goto failure; + } + + return TRUE; + +failure: + + evidence_importer_storage_clear( + storage + ); + + return FALSE; +} + +void evidence_importer_free( + EvidenceImporter *evidence_importer +) +{ + if (evidence_importer == NULL) + { + return; + } + + evidence_dao_free( + evidence_importer->evidence_dao + ); + + evidence_importer->evidence_dao = NULL; + evidence_importer->database = NULL; + + g_free( + evidence_importer + ); +} + +/** + * @brief Traduit une erreur EvidenceCopy en erreur EvidenceImporter. + */ +static EvidenceImporterError evidence_importer_map_copy_error( + const GError *copy_error +) +{ + if (copy_error == NULL) + { + return EVIDENCE_IMPORTER_ERROR_COPY; + } + + if (g_error_matches( + copy_error, + EVIDENCE_COPY_ERROR, + EVIDENCE_COPY_ERROR_CANCELLED + )) + { + return EVIDENCE_IMPORTER_ERROR_CANCELLED; + } + + if (g_error_matches( + copy_error, + EVIDENCE_COPY_ERROR, + EVIDENCE_COPY_ERROR_DESTINATION_INVALID + ) || + g_error_matches( + copy_error, + EVIDENCE_COPY_ERROR, + EVIDENCE_COPY_ERROR_DESTINATION_EXISTS + )) + { + return EVIDENCE_IMPORTER_ERROR_DESTINATION; + } + + if (g_error_matches( + copy_error, + EVIDENCE_COPY_ERROR, + EVIDENCE_COPY_ERROR_CLEANUP + )) + { + return EVIDENCE_IMPORTER_ERROR_ROLLBACK; + } + + return EVIDENCE_IMPORTER_ERROR_COPY; +} + +/** + * @brief Conserve le contexte d’une erreur EvidenceCopy. + */ +static void evidence_importer_set_copy_error( + GError **error, + const GError *copy_error +) +{ + EvidenceImporterError error_code = + evidence_importer_map_copy_error( + copy_error + ); + + if (error == NULL) + { + return; + } + + g_set_error( + error, + EVIDENCE_IMPORTER_ERROR, + error_code, + "Impossible de copier la preuve : %s", + copy_error != NULL + ? copy_error->message + : "erreur de copie inconnue" + ); +} + +/** + * @brief Exécute le hook situé immédiatement après la copie. + */ +static void evidence_importer_after_copy( + GCancellable *cancellable +) +{ +#ifdef EVIDENCE_IMPORTER_ENABLE_TEST_HOOKS + + if (cancellable != NULL && + evidence_importer_test_take_hook( + &evidence_importer_test_cancel_after_copy + )) + { + g_cancellable_cancel( + cancellable + ); + } + +#else + + (void) cancellable; + +#endif +} + +/** + * @brief Valide la transaction, sauf panne simulée par un test. + */ +static gboolean evidence_importer_commit_transaction( + Database *database +) +{ +#ifdef EVIDENCE_IMPORTER_ENABLE_TEST_HOOKS + + if (evidence_importer_test_take_hook( + &evidence_importer_test_commit_failure + )) + { + database_error_set( + database, + DATABASE_ERROR_SQLITE, + "Échec de COMMIT simulé par EvidenceImporter." + ); + + return FALSE; + } + +#endif + + return database_transaction_commit( + database + ); +} + +/** + * @brief Supprime un chemin, sauf panne simulée par un test. + */ +static int evidence_importer_remove_path( + const char *path +) +{ +#ifdef EVIDENCE_IMPORTER_ENABLE_TEST_HOOKS + + if (evidence_importer_test_take_hook( + &evidence_importer_test_cleanup_failure + )) + { + errno = EACCES; + + return -1; + } + +#endif + + return g_remove( + path + ); +} + +/** + * @brief Supprime une copie qui ne peut pas être conservée. + */ +static gboolean evidence_importer_remove_copy( + const EvidenceCopyResult *copy_result, + GError **error +) +{ + const char *destination_path = NULL; + + char *primary_error_message = NULL; + + int saved_errno = 0; + + if (copy_result == NULL) + { + evidence_importer_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR_INVALID_ARGUMENT, + "Le résultat de copie à supprimer est invalide." + ); + + return FALSE; + } + + destination_path = + evidence_copy_result_get_destination_path( + copy_result + ); + + if (destination_path == NULL || + destination_path[0] == '\0') + { + evidence_importer_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR_ROLLBACK, + "Le chemin de la copie à supprimer est invalide." + ); + + return FALSE; + } + + if (evidence_importer_remove_path( + destination_path + ) == 0) + { + return TRUE; + } + + saved_errno = errno; + + if (error == NULL) + { + g_warning( + "Impossible de supprimer la copie '%s' : %s", + destination_path, + g_strerror(saved_errno) + ); + + return FALSE; + } + + if (*error != NULL) + { + primary_error_message = + g_strdup( + (*error)->message + ); + + g_clear_error( + error + ); + } + + g_set_error( + error, + EVIDENCE_IMPORTER_ERROR, + EVIDENCE_IMPORTER_ERROR_ROLLBACK, + "La copie '%s' n’a pas pu être supprimée : %s. " + "Cause initiale : %s", + destination_path, + g_strerror(saved_errno), + primary_error_message != NULL + ? primary_error_message + : "erreur inconnue" + ); + + g_free( + primary_error_message + ); + + return FALSE; +} + +/** + * @brief Génère la date UTC actuelle au format EvidenceRecord. + */ +static char *evidence_importer_create_imported_at( + GError **error +) +{ + GDateTime *current_time = NULL; + + char *imported_at = NULL; + + current_time = + g_date_time_new_now_utc(); + + if (current_time == NULL) + { + evidence_importer_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR_MEMORY, + "Impossible de créer la date UTC d’importation." + ); + + return NULL; + } + + imported_at = + g_date_time_format( + current_time, + "%Y-%m-%dT%H:%M:%SZ" + ); + + g_date_time_unref( + current_time + ); + + if (imported_at == NULL) + { + evidence_importer_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR_MEMORY, + "Impossible de formater la date UTC d’importation." + ); + + return NULL; + } + + return imported_at; +} + +/** + * @brief Conserve le contexte d’une erreur EvidenceRecord. + */ +static void evidence_importer_set_model_error( + GError **error, + const GError *model_error +) +{ + if (error == NULL) + { + return; + } + + g_set_error( + error, + EVIDENCE_IMPORTER_ERROR, + EVIDENCE_IMPORTER_ERROR_MODEL, + "Impossible de créer le modèle de preuve : %s", + model_error != NULL + ? model_error->message + : "erreur de modèle inconnue" + ); +} + +/** + * @brief Conserve le contexte d’une erreur EvidenceDao. + */ +static void evidence_importer_set_dao_error( + GError **error, + const GError *dao_error +) +{ + if (error == NULL || + *error != NULL) + { + return; + } + + g_set_error( + error, + EVIDENCE_IMPORTER_ERROR, + EVIDENCE_IMPORTER_ERROR_DATABASE, + "Impossible d’enregistrer la preuve : %s", + dao_error != NULL + ? dao_error->message + : "erreur DAO inconnue" + ); +} + +/** + * @brief Conserve le contexte d’une erreur Database. + */ +static void evidence_importer_set_database_error( + GError **error, + const char *context, + const Database *database +) +{ + const char *database_message = NULL; + + if (error == NULL || + *error != NULL) + { + return; + } + + database_message = + database_error_get_message( + database + ); + + g_set_error( + error, + EVIDENCE_IMPORTER_ERROR, + EVIDENCE_IMPORTER_ERROR_DATABASE, + "%s : %s", + context, + database_message != NULL + ? database_message + : "erreur Database inconnue" + ); +} + +/** + * @brief Annule la transaction démarrée par l’importeur. + */ +static gboolean evidence_importer_rollback_transaction( + Database *database, + GError **error +) +{ + const char *database_message = NULL; + + char *primary_error_message = NULL; + + if (database == NULL) + { + return FALSE; + } + + if (!database_transaction_is_active( + database + )) + { + return TRUE; + } + + if (database_transaction_rollback( + database + )) + { + return TRUE; + } + + database_message = + database_error_get_message( + database + ); + + if (error == NULL) + { + g_warning( + "Impossible d’annuler la transaction SQLite : %s", + database_message != NULL + ? database_message + : "erreur inconnue" + ); + + return FALSE; + } + + if (*error != NULL) + { + primary_error_message = + g_strdup( + (*error)->message + ); + + g_clear_error( + error + ); + } + + g_set_error( + error, + EVIDENCE_IMPORTER_ERROR, + EVIDENCE_IMPORTER_ERROR_ROLLBACK, + "Impossible d’annuler la transaction SQLite : %s. " + "Cause initiale : %s", + database_message != NULL + ? database_message + : "erreur inconnue", + primary_error_message != NULL + ? primary_error_message + : "erreur inconnue" + ); + + g_free( + primary_error_message + ); + + return FALSE; +} + +EvidenceRecord *evidence_importer_import( + EvidenceImporter *evidence_importer, + const EvidenceImportRequest *request, + GCancellable *cancellable, + GError **error +) +{ + EvidenceImportStorage storage = {0}; + + EvidenceCopyResult *copy_result = NULL; + EvidenceRecord *evidence_record = NULL; + + GError *copy_error = NULL; + GError *model_error = NULL; + GError *dao_error = NULL; + + char *imported_at = NULL; + + gboolean transaction_started = FALSE; + gboolean success = FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (evidence_importer == NULL || + evidence_importer->database == NULL || + evidence_importer->evidence_dao == NULL) + { + evidence_importer_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR_INVALID_ARGUMENT, + "Le service d’import est invalide." + ); + + return NULL; + } + + if (!evidence_importer_request_is_valid( + request + )) + { + evidence_importer_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR_INVALID_ARGUMENT, + "La requête d’import est invalide." + ); + + return NULL; + } + + if (evidence_importer_check_cancelled( + cancellable, + error + )) + { + return NULL; + } + + /* + * Le refus doit survenir avant toute copie. + */ + if (database_transaction_is_active( + evidence_importer->database + )) + { + evidence_importer_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR_DATABASE, + "Une transaction Database est déjà active." + ); + + return NULL; + } + + if (!evidence_importer_prepare_storage( + request, + &storage, + error + )) + { + goto cleanup; + } + + copy_result = + evidence_copy_file( + request->source_path, + request->destination_directory, + storage.internal_name, + cancellable, + ©_error + ); + + if (copy_result == NULL) + { + evidence_importer_set_copy_error( + error, + copy_error + ); + + goto cleanup; + } + + evidence_importer_after_copy( + cancellable + ); + + if (g_strcmp0( + evidence_copy_result_get_destination_name( + copy_result + ), + storage.internal_name + ) != 0) + { + evidence_importer_set_error_literal( + error, + EVIDENCE_IMPORTER_ERROR_COPY, + "Le nom retourné par EvidenceCopy ne correspond pas " + "au nom interne généré." + ); + + goto cleanup; + } + + if (evidence_importer_check_cancelled( + cancellable, + error + )) + { + goto cleanup; + } + + imported_at = + evidence_importer_create_imported_at( + error + ); + + if (imported_at == NULL) + { + goto cleanup; + } + + evidence_record = + evidence_record_new( + storage.identifier, + storage.original_name, + storage.internal_name, + storage.relative_path, + request->type_identifier, + evidence_copy_result_get_size_bytes( + copy_result + ), + evidence_copy_result_get_sha256( + copy_result + ), + imported_at, + request->collected_at, + request->source, + request->description, + EVIDENCE_INTEGRITY_STATUS_VALID, + &model_error + ); + + if (evidence_record == NULL) + { + evidence_importer_set_model_error( + error, + model_error + ); + + goto cleanup; + } + + if (evidence_importer_check_cancelled( + cancellable, + error + )) + { + goto cleanup; + } + + if (!database_transaction_begin( + evidence_importer->database + )) + { + evidence_importer_set_database_error( + error, + "Impossible de démarrer la transaction SQLite", + evidence_importer->database + ); + + goto cleanup; + } + + transaction_started = TRUE; + + if (evidence_importer_check_cancelled( + cancellable, + error + )) + { + goto cleanup; + } + + if (!evidence_dao_insert( + evidence_importer->evidence_dao, + evidence_record, + &dao_error + )) + { + evidence_importer_set_dao_error( + error, + dao_error + ); + + goto cleanup; + } + + if (evidence_importer_check_cancelled( + cancellable, + error + )) + { + goto cleanup; + } + + if (!evidence_importer_commit_transaction( + evidence_importer->database + )) + { + evidence_importer_set_database_error( + error, + "Impossible de valider la transaction SQLite", + evidence_importer->database + ); + + goto cleanup; + } + + transaction_started = FALSE; + success = TRUE; + +cleanup: + + g_clear_error( + &dao_error + ); + + g_clear_error( + &model_error + ); + + g_clear_error( + ©_error + ); + + if (!success && + transaction_started) + { + evidence_importer_rollback_transaction( + evidence_importer->database, + error + ); + + transaction_started = FALSE; + } + + /* + * Une copie n’est conservée que si le commit SQLite a réussi. + */ + if (!success && + copy_result != NULL) + { + evidence_importer_remove_copy( + copy_result, + error + ); + } + + evidence_copy_result_free( + copy_result + ); + + g_free( + imported_at + ); + + evidence_importer_storage_clear( + &storage + ); + + if (!success) + { + evidence_record_free( + evidence_record + ); + + return NULL; + } + + /* + * Le fichier et la ligne SQLite sont maintenant validés. + * Le modèle appartient à l’appelant. + */ + return evidence_record; +} diff --git a/tests/test_background_task b/tests/test_background_task new file mode 100755 index 0000000..0c76ca0 Binary files /dev/null and b/tests/test_background_task differ diff --git a/tests/test_database b/tests/test_database new file mode 100755 index 0000000..7599e29 Binary files /dev/null and b/tests/test_database differ diff --git a/tests/test_error b/tests/test_error new file mode 100755 index 0000000..5ad3581 Binary files /dev/null and b/tests/test_error differ diff --git a/tests/test_evidence_copy b/tests/test_evidence_copy new file mode 100755 index 0000000..402f688 Binary files /dev/null and b/tests/test_evidence_copy differ diff --git a/tests/test_evidence_dao b/tests/test_evidence_dao new file mode 100755 index 0000000..b90e043 Binary files /dev/null and b/tests/test_evidence_dao differ diff --git a/tests/test_evidence_importer b/tests/test_evidence_importer new file mode 100755 index 0000000..5c8dba5 Binary files /dev/null and b/tests/test_evidence_importer differ diff --git a/tests/test_evidence_importer.c b/tests/test_evidence_importer.c new file mode 100644 index 0000000..b80f1c9 --- /dev/null +++ b/tests/test_evidence_importer.c @@ -0,0 +1,2665 @@ +/****************************************************************************** + * @file test_evidence_importer.c + * @brief Tests de l’import transactionnel des preuves. + ******************************************************************************/ + +#include "core/evidence_importer.h" + +#include "dao/evidence_dao.h" +#include "database/database.h" +#include "database/transaction.h" +#include "models/evidence_record.h" +#include "core/evidence_importer_test.h" + +#include +#include +#include + +#include +#include + +/** + * @brief Dossier relatif utilisé par les tests. + */ +#define TEST_EVIDENCE_RELATIVE_DIRECTORY \ + "01_Preuves_Originales/Documents" + +/** + * @brief Environnement temporaire d’un test EvidenceImporter. + */ +typedef struct +{ + char *root_directory; + char *database_path; + + char *source_directory; + char *evidence_directory; + char *destination_directory; + + Database *database; + EvidenceDao *evidence_dao; + EvidenceImporter *evidence_importer; +} TestEvidenceImporterFixture; + +/** + * @brief Vérifie grossièrement le format UTC strict du modèle. + */ +static void test_evidence_importer_assert_utc_timestamp( + const char *timestamp +) +{ + assert(timestamp != NULL); + + assert( + strlen( + timestamp + ) == 20 + ); + + assert(timestamp[4] == '-'); + assert(timestamp[7] == '-'); + assert(timestamp[10] == 'T'); + assert(timestamp[13] == ':'); + assert(timestamp[16] == ':'); + assert(timestamp[19] == 'Z'); +} + +/** + * @brief Crée une enquête temporaire complète. + */ +static TestEvidenceImporterFixture +test_evidence_importer_fixture_create(void) +{ + TestEvidenceImporterFixture fixture = {0}; + + GError *error = NULL; + + fixture.root_directory = + g_dir_make_tmp( + "labfy-evidence-importer-test-XXXXXX", + &error + ); + + assert(fixture.root_directory != NULL); + assert(error == NULL); + + fixture.database_path = + g_build_filename( + fixture.root_directory, + "Enquete.sqlite", + NULL + ); + + fixture.source_directory = + g_build_filename( + fixture.root_directory, + "sources", + NULL + ); + + fixture.evidence_directory = + g_build_filename( + fixture.root_directory, + "01_Preuves_Originales", + NULL + ); + + fixture.destination_directory = + g_build_filename( + fixture.evidence_directory, + "Documents", + NULL + ); + + assert(fixture.database_path != NULL); + assert(fixture.source_directory != NULL); + assert(fixture.evidence_directory != NULL); + assert(fixture.destination_directory != NULL); + + assert( + g_mkdir( + fixture.source_directory, + 0700 + ) == 0 + ); + + assert( + g_mkdir( + fixture.evidence_directory, + 0700 + ) == 0 + ); + + assert( + g_mkdir( + fixture.destination_directory, + 0700 + ) == 0 + ); + + assert( + database_initialize( + fixture.database_path, + "Enquete_EvidenceImporter", + fixture.root_directory + ) + ); + + fixture.database = + database_open( + fixture.database_path + ); + + assert(fixture.database != NULL); + + fixture.evidence_dao = + evidence_dao_new( + fixture.database, + &error + ); + + assert(fixture.evidence_dao != NULL); + assert(error == NULL); + + fixture.evidence_importer = + evidence_importer_new( + fixture.database, + &error + ); + + assert(fixture.evidence_importer != NULL); + assert(error == NULL); + + return fixture; +} + +/** + * @brief Détruit une fixture dont les preuves ont déjà été supprimées. + */ +static void test_evidence_importer_fixture_clear( + TestEvidenceImporterFixture *fixture +) +{ + assert(fixture != NULL); + + evidence_importer_free( + fixture->evidence_importer + ); + + evidence_dao_free( + fixture->evidence_dao + ); + + database_close( + fixture->database + ); + + assert( + g_remove( + fixture->database_path + ) == 0 + ); + + assert( + g_rmdir( + fixture->destination_directory + ) == 0 + ); + + assert( + g_rmdir( + fixture->evidence_directory + ) == 0 + ); + + assert( + g_rmdir( + fixture->source_directory + ) == 0 + ); + + assert( + g_rmdir( + fixture->root_directory + ) == 0 + ); + + g_free( + fixture->destination_directory + ); + + g_free( + fixture->evidence_directory + ); + + g_free( + fixture->source_directory + ); + + g_free( + fixture->database_path + ); + + g_free( + fixture->root_directory + ); + + fixture->evidence_importer = NULL; + fixture->evidence_dao = NULL; + fixture->database = NULL; + + fixture->destination_directory = NULL; + fixture->evidence_directory = NULL; + fixture->source_directory = NULL; + fixture->database_path = NULL; + fixture->root_directory = NULL; +} + +/** + * @brief Vérifie le refus d’une connexion absente. + */ +static void test_evidence_importer_new_null_database(void) +{ + EvidenceImporter *evidence_importer = NULL; + + GError *error = NULL; + + evidence_importer = + evidence_importer_new( + NULL, + &error + ); + + assert(evidence_importer == NULL); + assert(error != NULL); + + assert( + error->domain == + EVIDENCE_IMPORTER_ERROR + ); + + assert( + error->code == + (gint) EVIDENCE_IMPORTER_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + evidence_importer_free( + NULL + ); +} + +/** + * @brief Vérifie la création et la libération d’un importeur valide. + */ +static void test_evidence_importer_new_valid(void) +{ + TestEvidenceImporterFixture fixture = + test_evidence_importer_fixture_create(); + + /* + * La création de la fixture prouve que l’importeur et le DAO + * peuvent emprunter simultanément la même connexion. + */ + assert(fixture.evidence_importer != NULL); + assert(fixture.evidence_dao != NULL); + assert(fixture.database != NULL); + + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + test_evidence_importer_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie un import complet : fichier, modèle et SQLite. + */ +static void test_evidence_importer_import_valid_text_file(void) +{ + TestEvidenceImporterFixture fixture = + test_evidence_importer_fixture_create(); + + EvidenceImportRequest request = {0}; + + EvidenceRecord *evidence_record = NULL; + EvidenceRecord *loaded_record = NULL; + + char *source_path = NULL; + char *destination_path = NULL; + char *expected_relative_path = NULL; + + char *source_contents = NULL; + char *destination_contents = NULL; + + const char *identifier = NULL; + const char *internal_name = NULL; + + gsize source_contents_size = 0; + gsize destination_contents_size = 0; + + guint64 evidence_count = 0; + + GError *error = NULL; + + source_path = + g_build_filename( + fixture.source_directory, + "Preuve Originale.TXT", + NULL + ); + + assert(source_path != NULL); + + assert( + g_file_set_contents( + source_path, + "abc", + 3, + &error + ) + ); + + assert(error == NULL); + + request.source_path = + source_path; + + request.destination_directory = + fixture.destination_directory; + + request.relative_directory = + TEST_EVIDENCE_RELATIVE_DIRECTORY; + + request.type_identifier = + "document"; + + request.collected_at = + "2026-07-18T20:30:00Z"; + + request.source = + "Fichier communiqué directement par la victime."; + + request.description = + "Première preuve importée par le service transactionnel."; + + evidence_record = + evidence_importer_import( + fixture.evidence_importer, + &request, + NULL, + &error + ); + + assert(error == NULL); + assert(evidence_record != NULL); + + /* + * Le commit doit être terminé avant le retour. + */ + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + identifier = + evidence_record_get_identifier( + evidence_record + ); + + internal_name = + evidence_record_get_internal_name( + evidence_record + ); + + assert(identifier != NULL); + assert(internal_name != NULL); + + assert( + g_uuid_string_is_valid( + identifier + ) + ); + + assert( + strcmp( + evidence_record_get_original_name( + evidence_record + ), + "Preuve Originale.TXT" + ) == 0 + ); + + /* + * L’extension est normalisée en minuscules. + */ + assert( + g_str_has_prefix( + internal_name, + identifier + ) + ); + + assert( + g_str_has_suffix( + internal_name, + ".txt" + ) + ); + + destination_path = + g_build_filename( + fixture.destination_directory, + internal_name, + NULL + ); + + expected_relative_path = + g_build_filename( + TEST_EVIDENCE_RELATIVE_DIRECTORY, + internal_name, + NULL + ); + + assert(destination_path != NULL); + assert(expected_relative_path != NULL); + + assert( + strcmp( + evidence_record_get_relative_path( + evidence_record + ), + expected_relative_path + ) == 0 + ); + + assert( + strcmp( + evidence_record_get_type_identifier( + evidence_record + ), + "document" + ) == 0 + ); + + assert( + evidence_record_get_size_bytes( + evidence_record + ) == 3 + ); + + assert( + strcmp( + evidence_record_get_sha256( + evidence_record + ), + "ba7816bf8f01cfea414140de5dae2223" + "b00361a396177a9cb410ff61f20015ad" + ) == 0 + ); + + test_evidence_importer_assert_utc_timestamp( + evidence_record_get_imported_at( + evidence_record + ) + ); + + assert( + strcmp( + evidence_record_get_collected_at( + evidence_record + ), + request.collected_at + ) == 0 + ); + + assert( + strcmp( + evidence_record_get_source( + evidence_record + ), + request.source + ) == 0 + ); + + assert( + strcmp( + evidence_record_get_description( + evidence_record + ), + request.description + ) == 0 + ); + + assert( + evidence_record_get_integrity_status( + evidence_record + ) == + EVIDENCE_INTEGRITY_STATUS_VALID + ); + + /* + * La copie validée doit exister après le commit. + */ + assert( + g_file_test( + destination_path, + G_FILE_TEST_IS_REGULAR + ) + ); + + assert( + g_file_get_contents( + source_path, + &source_contents, + &source_contents_size, + &error + ) + ); + + assert(error == NULL); + + assert( + g_file_get_contents( + destination_path, + &destination_contents, + &destination_contents_size, + &error + ) + ); + + assert(error == NULL); + + assert(source_contents_size == 3); + assert(destination_contents_size == 3); + + assert( + memcmp( + source_contents, + "abc", + 3 + ) == 0 + ); + + assert( + memcmp( + destination_contents, + source_contents, + 3 + ) == 0 + ); + + /* + * La ligne SQLite doit exister exactement une fois. + */ + assert( + evidence_dao_count( + fixture.evidence_dao, + &evidence_count, + &error + ) + ); + + assert(error == NULL); + assert(evidence_count == 1); + + loaded_record = + evidence_dao_find_by_identifier( + fixture.evidence_dao, + identifier, + &error + ); + + assert(error == NULL); + assert(loaded_record != NULL); + + assert( + strcmp( + evidence_record_get_identifier( + loaded_record + ), + identifier + ) == 0 + ); + + assert( + strcmp( + evidence_record_get_original_name( + loaded_record + ), + evidence_record_get_original_name( + evidence_record + ) + ) == 0 + ); + + assert( + strcmp( + evidence_record_get_internal_name( + loaded_record + ), + internal_name + ) == 0 + ); + + assert( + strcmp( + evidence_record_get_relative_path( + loaded_record + ), + expected_relative_path + ) == 0 + ); + + assert( + evidence_record_get_size_bytes( + loaded_record + ) == + evidence_record_get_size_bytes( + evidence_record + ) + ); + + assert( + strcmp( + evidence_record_get_sha256( + loaded_record + ), + evidence_record_get_sha256( + evidence_record + ) + ) == 0 + ); + + assert( + strcmp( + evidence_record_get_imported_at( + loaded_record + ), + evidence_record_get_imported_at( + evidence_record + ) + ) == 0 + ); + + assert( + evidence_record_get_integrity_status( + loaded_record + ) == + EVIDENCE_INTEGRITY_STATUS_VALID + ); + + evidence_record_free( + loaded_record + ); + + evidence_record_free( + evidence_record + ); + + g_free( + destination_contents + ); + + g_free( + source_contents + ); + + assert( + g_remove( + destination_path + ) == 0 + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + expected_relative_path + ); + + g_free( + destination_path + ); + + g_free( + source_path + ); + + test_evidence_importer_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie l’import d’un fichier vide sans métadonnées facultatives. + */ +static void test_evidence_importer_import_empty_optional_null(void) +{ + TestEvidenceImporterFixture fixture = + test_evidence_importer_fixture_create(); + + EvidenceImportRequest request = {0}; + + EvidenceRecord *evidence_record = NULL; + EvidenceRecord *loaded_record = NULL; + + char *source_path = NULL; + char *destination_path = NULL; + + const char *identifier = NULL; + const char *internal_name = NULL; + + guint64 evidence_count = 0; + + GError *error = NULL; + + source_path = + g_build_filename( + fixture.source_directory, + "preuve_vide", + NULL + ); + + assert(source_path != NULL); + + assert( + g_file_set_contents( + source_path, + "", + 0, + &error + ) + ); + + assert(error == NULL); + + request.source_path = + source_path; + + request.destination_directory = + fixture.destination_directory; + + request.relative_directory = + TEST_EVIDENCE_RELATIVE_DIRECTORY; + + request.type_identifier = + "document"; + + request.collected_at = NULL; + request.source = NULL; + request.description = NULL; + + evidence_record = + evidence_importer_import( + fixture.evidence_importer, + &request, + NULL, + &error + ); + + assert(error == NULL); + assert(evidence_record != NULL); + + identifier = + evidence_record_get_identifier( + evidence_record + ); + + internal_name = + evidence_record_get_internal_name( + evidence_record + ); + + assert(identifier != NULL); + assert(internal_name != NULL); + + /* + * Le fichier original ne possède pas d’extension : + * le nom interne doit être exactement l’UUID. + */ + assert( + strcmp( + internal_name, + identifier + ) == 0 + ); + + assert( + evidence_record_get_size_bytes( + evidence_record + ) == 0 + ); + + assert( + strcmp( + evidence_record_get_sha256( + evidence_record + ), + "e3b0c44298fc1c149afbf4c8996fb924" + "27ae41e4649b934ca495991b7852b855" + ) == 0 + ); + + assert( + evidence_record_get_collected_at( + evidence_record + ) == NULL + ); + + assert( + evidence_record_get_source( + evidence_record + ) == NULL + ); + + assert( + evidence_record_get_description( + evidence_record + ) == NULL + ); + + destination_path = + g_build_filename( + fixture.destination_directory, + internal_name, + NULL + ); + + assert(destination_path != NULL); + + assert( + g_file_test( + destination_path, + G_FILE_TEST_IS_REGULAR + ) + ); + + assert( + evidence_dao_count( + fixture.evidence_dao, + &evidence_count, + &error + ) + ); + + assert(error == NULL); + assert(evidence_count == 1); + + loaded_record = + evidence_dao_find_by_identifier( + fixture.evidence_dao, + identifier, + &error + ); + + assert(error == NULL); + assert(loaded_record != NULL); + + assert( + evidence_record_get_collected_at( + loaded_record + ) == NULL + ); + + assert( + evidence_record_get_source( + loaded_record + ) == NULL + ); + + assert( + evidence_record_get_description( + loaded_record + ) == NULL + ); + + evidence_record_free( + loaded_record + ); + + evidence_record_free( + evidence_record + ); + + assert( + g_remove( + destination_path + ) == 0 + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + source_path + ); + + test_evidence_importer_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie l’import exact d’un fichier binaire. + */ +static void test_evidence_importer_import_binary_file(void) +{ + TestEvidenceImporterFixture fixture = + test_evidence_importer_fixture_create(); + + static const guint8 binary_data[] = + { + 0x00, + 0xFF, + 0x80, + 0x41, + 0x00, + 0x7F, + 0x10 + }; + + EvidenceImportRequest request = {0}; + + EvidenceRecord *evidence_record = NULL; + + char *source_path = NULL; + char *destination_path = NULL; + char *destination_contents = NULL; + + const char *internal_name = NULL; + + gsize destination_contents_size = 0; + + GError *error = NULL; + + source_path = + g_build_filename( + fixture.source_directory, + "donnees.BIN", + NULL + ); + + assert(source_path != NULL); + + assert( + g_file_set_contents( + source_path, + (const char *) binary_data, + (gssize) sizeof(binary_data), + &error + ) + ); + + assert(error == NULL); + + request.source_path = + source_path; + + request.destination_directory = + fixture.destination_directory; + + request.relative_directory = + TEST_EVIDENCE_RELATIVE_DIRECTORY; + + request.type_identifier = + "document"; + + evidence_record = + evidence_importer_import( + fixture.evidence_importer, + &request, + NULL, + &error + ); + + assert(error == NULL); + assert(evidence_record != NULL); + + internal_name = + evidence_record_get_internal_name( + evidence_record + ); + + assert(internal_name != NULL); + + assert( + g_str_has_suffix( + internal_name, + ".bin" + ) + ); + + assert( + evidence_record_get_size_bytes( + evidence_record + ) == + sizeof(binary_data) + ); + + assert( + strcmp( + evidence_record_get_sha256( + evidence_record + ), + "c90d3a7754f84abc867e780e5a0c0f8d" + "a5c663bd1766ad89e538536a3763f367" + ) == 0 + ); + + destination_path = + g_build_filename( + fixture.destination_directory, + internal_name, + NULL + ); + + assert(destination_path != NULL); + + assert( + g_file_get_contents( + destination_path, + &destination_contents, + &destination_contents_size, + &error + ) + ); + + assert(error == NULL); + + assert( + destination_contents_size == + sizeof(binary_data) + ); + + assert( + memcmp( + destination_contents, + binary_data, + sizeof(binary_data) + ) == 0 + ); + + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + evidence_record_free( + evidence_record + ); + + g_free( + destination_contents + ); + + assert( + g_remove( + destination_path + ) == 0 + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + source_path + ); + + test_evidence_importer_fixture_clear( + &fixture + ); +} + +/** + * @brief Compte les entrées présentes dans le dossier de destination. + */ +static guint test_evidence_importer_count_destination_entries( + const TestEvidenceImporterFixture *fixture +) +{ + GDir *directory = NULL; + + const char *entry_name = NULL; + + guint entry_count = 0; + + GError *error = NULL; + + assert(fixture != NULL); + assert(fixture->destination_directory != NULL); + + directory = + g_dir_open( + fixture->destination_directory, + 0, + &error + ); + + assert(directory != NULL); + assert(error == NULL); + + while ((entry_name = g_dir_read_name(directory)) != NULL) + { + assert(entry_name[0] != '\0'); + + entry_count++; + } + + g_dir_close( + directory + ); + + return entry_count; +} + +/** + * @brief Vérifie qu’une annulation préalable ne crée aucune preuve. + */ +static void test_evidence_importer_cancelled_before_copy(void) +{ + TestEvidenceImporterFixture fixture = + test_evidence_importer_fixture_create(); + + EvidenceImportRequest request = {0}; + + EvidenceRecord *evidence_record = NULL; + + GCancellable *cancellable = NULL; + + char *source_path = NULL; + + guint64 evidence_count = 42; + + GError *error = NULL; + + source_path = + g_build_filename( + fixture.source_directory, + "preuve_annulee.txt", + NULL + ); + + assert(source_path != NULL); + + assert( + g_file_set_contents( + source_path, + "preuve annulee", + -1, + &error + ) + ); + + assert(error == NULL); + + request.source_path = + source_path; + + request.destination_directory = + fixture.destination_directory; + + request.relative_directory = + TEST_EVIDENCE_RELATIVE_DIRECTORY; + + request.type_identifier = + "document"; + + cancellable = + g_cancellable_new(); + + assert(cancellable != NULL); + + g_cancellable_cancel( + cancellable + ); + + evidence_record = + evidence_importer_import( + fixture.evidence_importer, + &request, + cancellable, + &error + ); + + assert(evidence_record == NULL); + assert(error != NULL); + + assert( + error->domain == + EVIDENCE_IMPORTER_ERROR + ); + + assert( + error->code == + (gint) EVIDENCE_IMPORTER_ERROR_CANCELLED + ); + + assert( + test_evidence_importer_count_destination_entries( + &fixture + ) == 0 + ); + + assert( + evidence_dao_count( + fixture.evidence_dao, + &evidence_count, + NULL + ) + ); + + assert(evidence_count == 0); + + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + g_clear_error( + &error + ); + + g_object_unref( + cancellable + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + source_path + ); + + test_evidence_importer_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le rollback et le nettoyage pour un type inconnu. + */ +static void test_evidence_importer_unknown_type_cleanup(void) +{ + TestEvidenceImporterFixture fixture = + test_evidence_importer_fixture_create(); + + EvidenceImportRequest request = {0}; + + EvidenceRecord *evidence_record = NULL; + + char *source_path = NULL; + + guint64 evidence_count = 42; + + GError *error = NULL; + + source_path = + g_build_filename( + fixture.source_directory, + "preuve_type_inconnu.txt", + NULL + ); + + assert(source_path != NULL); + + assert( + g_file_set_contents( + source_path, + "contenu de preuve", + -1, + &error + ) + ); + + assert(error == NULL); + + request.source_path = + source_path; + + request.destination_directory = + fixture.destination_directory; + + request.relative_directory = + TEST_EVIDENCE_RELATIVE_DIRECTORY; + + request.type_identifier = + "type_totalement_inconnu"; + + evidence_record = + evidence_importer_import( + fixture.evidence_importer, + &request, + NULL, + &error + ); + + assert(evidence_record == NULL); + assert(error != NULL); + + assert( + error->domain == + EVIDENCE_IMPORTER_ERROR + ); + + assert( + error->code == + (gint) EVIDENCE_IMPORTER_ERROR_DATABASE + ); + + assert( + strstr( + error->message, + "type de preuve" + ) != NULL + ); + + /* + * La copie créée avant l’insertion doit avoir été supprimée. + */ + assert( + test_evidence_importer_count_destination_entries( + &fixture + ) == 0 + ); + + assert( + evidence_dao_count( + fixture.evidence_dao, + &evidence_count, + NULL + ) + ); + + assert(evidence_count == 0); + + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + /* + * Le fichier source ne doit jamais être supprimé ou modifié. + */ + assert( + g_file_test( + source_path, + G_FILE_TEST_IS_REGULAR + ) + ); + + g_clear_error( + &error + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + source_path + ); + + test_evidence_importer_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie que l’importeur respecte une transaction appelante. + */ +static void test_evidence_importer_transaction_already_active(void) +{ + TestEvidenceImporterFixture fixture = + test_evidence_importer_fixture_create(); + + EvidenceImportRequest request = {0}; + + EvidenceRecord *evidence_record = NULL; + + char *source_path = NULL; + + guint64 evidence_count = 42; + + GError *error = NULL; + + source_path = + g_build_filename( + fixture.source_directory, + "preuve_transaction.txt", + NULL + ); + + assert(source_path != NULL); + + assert( + g_file_set_contents( + source_path, + "transaction externe", + -1, + &error + ) + ); + + assert(error == NULL); + + request.source_path = + source_path; + + request.destination_directory = + fixture.destination_directory; + + request.relative_directory = + TEST_EVIDENCE_RELATIVE_DIRECTORY; + + request.type_identifier = + "document"; + + assert( + database_transaction_begin( + fixture.database + ) + ); + + assert( + database_transaction_is_active( + fixture.database + ) + ); + + evidence_record = + evidence_importer_import( + fixture.evidence_importer, + &request, + NULL, + &error + ); + + assert(evidence_record == NULL); + assert(error != NULL); + + assert( + error->domain == + EVIDENCE_IMPORTER_ERROR + ); + + assert( + error->code == + (gint) EVIDENCE_IMPORTER_ERROR_DATABASE + ); + + /* + * L’importeur ne possède pas cette transaction : + * il ne doit donc pas l’annuler. + */ + assert( + database_transaction_is_active( + fixture.database + ) + ); + + assert( + test_evidence_importer_count_destination_entries( + &fixture + ) == 0 + ); + + assert( + evidence_dao_count( + fixture.evidence_dao, + &evidence_count, + NULL + ) + ); + + assert(evidence_count == 0); + + g_clear_error( + &error + ); + + assert( + database_transaction_rollback( + fixture.database + ) + ); + + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + source_path + ); + + test_evidence_importer_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le nettoyage lorsque le modèle refuse les métadonnées. + */ +static void test_evidence_importer_invalid_collected_at_cleanup(void) +{ + TestEvidenceImporterFixture fixture = + test_evidence_importer_fixture_create(); + + EvidenceImportRequest request = {0}; + + EvidenceRecord *evidence_record = NULL; + + char *source_path = NULL; + + guint64 evidence_count = 42; + + GError *error = NULL; + + source_path = + g_build_filename( + fixture.source_directory, + "preuve_date_invalide.txt", + NULL + ); + + assert(source_path != NULL); + + assert( + g_file_set_contents( + source_path, + "preuve avec date invalide", + -1, + &error + ) + ); + + assert(error == NULL); + + request.source_path = + source_path; + + request.destination_directory = + fixture.destination_directory; + + request.relative_directory = + TEST_EVIDENCE_RELATIVE_DIRECTORY; + + request.type_identifier = + "document"; + + /* + * EvidenceRecord exige une date UTC stricte. + */ + request.collected_at = + "18/07/2026 20:30"; + + request.source = + "Fichier de test."; + + request.description = + "La création du modèle doit échouer."; + + evidence_record = + evidence_importer_import( + fixture.evidence_importer, + &request, + NULL, + &error + ); + + assert(evidence_record == NULL); + assert(error != NULL); + + assert( + error->domain == + EVIDENCE_IMPORTER_ERROR + ); + + assert( + error->code == + (gint) EVIDENCE_IMPORTER_ERROR_MODEL + ); + + /* + * La copie a eu lieu avant la création du modèle, + * mais elle doit avoir été supprimée. + */ + assert( + test_evidence_importer_count_destination_entries( + &fixture + ) == 0 + ); + + assert( + evidence_dao_count( + fixture.evidence_dao, + &evidence_count, + NULL + ) + ); + + assert(evidence_count == 0); + + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + /* + * Le fichier source reste intact. + */ + assert( + g_file_test( + source_path, + G_FILE_TEST_IS_REGULAR + ) + ); + + g_clear_error( + &error + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + source_path + ); + + test_evidence_importer_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie la conservation d’un nom original complexe. + */ +static void test_evidence_importer_complex_original_name(void) +{ + TestEvidenceImporterFixture fixture = + test_evidence_importer_fixture_create(); + + EvidenceImportRequest request = {0}; + + EvidenceRecord *evidence_record = NULL; + + char *source_path = NULL; + char *destination_path = NULL; + + const char *identifier = NULL; + const char *internal_name = NULL; + + guint64 evidence_count = 0; + + GError *error = NULL; + + source_path = + g_build_filename( + fixture.source_directory, + "Capture été.Version Finale.PDF", + NULL + ); + + assert(source_path != NULL); + + assert( + g_file_set_contents( + source_path, + "contenu du document", + -1, + &error + ) + ); + + assert(error == NULL); + + request.source_path = + source_path; + + request.destination_directory = + fixture.destination_directory; + + request.relative_directory = + TEST_EVIDENCE_RELATIVE_DIRECTORY; + + request.type_identifier = + "document"; + + evidence_record = + evidence_importer_import( + fixture.evidence_importer, + &request, + NULL, + &error + ); + + assert(error == NULL); + assert(evidence_record != NULL); + + identifier = + evidence_record_get_identifier( + evidence_record + ); + + internal_name = + evidence_record_get_internal_name( + evidence_record + ); + + assert(identifier != NULL); + assert(internal_name != NULL); + + /* + * Le nom fourni par l’utilisateur doit être conservé + * exactement dans le modèle. + */ + assert( + strcmp( + evidence_record_get_original_name( + evidence_record + ), + "Capture été.Version Finale.PDF" + ) == 0 + ); + + /* + * Le nom interne ne conserve que l’UUID et + * l’extension finale normalisée. + */ + assert( + g_str_has_prefix( + internal_name, + identifier + ) + ); + + assert( + g_str_has_suffix( + internal_name, + ".pdf" + ) + ); + + assert( + strlen( + internal_name + ) == + strlen(identifier) + strlen(".pdf") + ); + + assert( + strchr( + internal_name, + ' ' + ) == NULL + ); + + assert( + strchr( + internal_name, + '/' + ) == NULL + ); + + assert( + strchr( + internal_name, + '\\' + ) == NULL + ); + + destination_path = + g_build_filename( + fixture.destination_directory, + internal_name, + NULL + ); + + assert(destination_path != NULL); + + assert( + g_file_test( + destination_path, + G_FILE_TEST_IS_REGULAR + ) + ); + + assert( + evidence_dao_count( + fixture.evidence_dao, + &evidence_count, + &error + ) + ); + + assert(error == NULL); + assert(evidence_count == 1); + + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + evidence_record_free( + evidence_record + ); + + assert( + g_remove( + destination_path + ) == 0 + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + source_path + ); + + test_evidence_importer_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d’un chemin relatif sortant de l’enquête. + */ +static void test_evidence_importer_invalid_relative_directory(void) +{ + TestEvidenceImporterFixture fixture = + test_evidence_importer_fixture_create(); + + EvidenceImportRequest request = {0}; + + EvidenceRecord *evidence_record = NULL; + + char *source_path = NULL; + + guint64 evidence_count = 42; + + GError *error = NULL; + + source_path = + g_build_filename( + fixture.source_directory, + "preuve.txt", + NULL + ); + + assert(source_path != NULL); + + assert( + g_file_set_contents( + source_path, + "contenu", + -1, + &error + ) + ); + + assert(error == NULL); + + request.source_path = + source_path; + + request.destination_directory = + fixture.destination_directory; + + request.relative_directory = + "../../hors_enquete"; + + request.type_identifier = + "document"; + + evidence_record = + evidence_importer_import( + fixture.evidence_importer, + &request, + NULL, + &error + ); + + assert(evidence_record == NULL); + assert(error != NULL); + + assert( + error->domain == + EVIDENCE_IMPORTER_ERROR + ); + + assert( + error->code == + (gint) EVIDENCE_IMPORTER_ERROR_INVALID_ARGUMENT + ); + + assert( + test_evidence_importer_count_destination_entries( + &fixture + ) == 0 + ); + + assert( + evidence_dao_count( + fixture.evidence_dao, + &evidence_count, + NULL + ) + ); + + assert(evidence_count == 0); + + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + g_clear_error( + &error + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + source_path + ); + + test_evidence_importer_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d’un fichier source inexistant. + */ +static void test_evidence_importer_missing_source(void) +{ + TestEvidenceImporterFixture fixture = + test_evidence_importer_fixture_create(); + + EvidenceImportRequest request = {0}; + + EvidenceRecord *evidence_record = NULL; + + char *source_path = NULL; + + guint64 evidence_count = 42; + + GError *error = NULL; + + source_path = + g_build_filename( + fixture.source_directory, + "preuve_absente.txt", + NULL + ); + + assert(source_path != NULL); + + assert( + !g_file_test( + source_path, + G_FILE_TEST_EXISTS + ) + ); + + request.source_path = + source_path; + + request.destination_directory = + fixture.destination_directory; + + request.relative_directory = + TEST_EVIDENCE_RELATIVE_DIRECTORY; + + request.type_identifier = + "document"; + + evidence_record = + evidence_importer_import( + fixture.evidence_importer, + &request, + NULL, + &error + ); + + assert(evidence_record == NULL); + assert(error != NULL); + + assert( + error->domain == + EVIDENCE_IMPORTER_ERROR + ); + + assert( + error->code == + (gint) EVIDENCE_IMPORTER_ERROR_COPY + ); + + assert( + strstr( + error->message, + "Impossible de copier" + ) != NULL + ); + + assert( + test_evidence_importer_count_destination_entries( + &fixture + ) == 0 + ); + + assert( + evidence_dao_count( + fixture.evidence_dao, + &evidence_count, + NULL + ) + ); + + assert(evidence_count == 0); + + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + g_clear_error( + &error + ); + + g_free( + source_path + ); + + test_evidence_importer_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie l’annulation après la création de la copie. + */ +static void test_evidence_importer_cancelled_after_copy_cleanup(void) +{ + TestEvidenceImporterFixture fixture = + test_evidence_importer_fixture_create(); + + EvidenceImportRequest request = {0}; + + EvidenceRecord *evidence_record = NULL; + + GCancellable *cancellable = NULL; + + char *source_path = NULL; + + guint64 evidence_count = 42; + + GError *error = NULL; + + evidence_importer_test_reset_hooks(); + + source_path = + g_build_filename( + fixture.source_directory, + "preuve_annulee_apres_copie.txt", + NULL + ); + + assert(source_path != NULL); + + assert( + g_file_set_contents( + source_path, + "annulation après copie", + -1, + &error + ) + ); + + assert(error == NULL); + + request.source_path = + source_path; + + request.destination_directory = + fixture.destination_directory; + + request.relative_directory = + TEST_EVIDENCE_RELATIVE_DIRECTORY; + + request.type_identifier = + "document"; + + cancellable = + g_cancellable_new(); + + assert(cancellable != NULL); + + evidence_importer_test_set_cancel_after_copy( + TRUE + ); + + evidence_record = + evidence_importer_import( + fixture.evidence_importer, + &request, + cancellable, + &error + ); + + assert(evidence_record == NULL); + assert(error != NULL); + + assert( + error->domain == + EVIDENCE_IMPORTER_ERROR + ); + + assert( + error->code == + (gint) EVIDENCE_IMPORTER_ERROR_CANCELLED + ); + + /* + * Le fichier a été copié avant l’annulation, + * puis supprimé par la compensation. + */ + assert( + test_evidence_importer_count_destination_entries( + &fixture + ) == 0 + ); + + assert( + evidence_dao_count( + fixture.evidence_dao, + &evidence_count, + NULL + ) + ); + + assert(evidence_count == 0); + + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + assert( + g_file_test( + source_path, + G_FILE_TEST_IS_REGULAR + ) + ); + + evidence_importer_test_reset_hooks(); + + g_clear_error( + &error + ); + + g_object_unref( + cancellable + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + source_path + ); + + test_evidence_importer_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le rollback lorsque le COMMIT SQLite échoue. + */ +static void test_evidence_importer_commit_failure_cleanup(void) +{ + TestEvidenceImporterFixture fixture = + test_evidence_importer_fixture_create(); + + EvidenceImportRequest request = {0}; + + EvidenceRecord *evidence_record = NULL; + + char *source_path = NULL; + + guint64 evidence_count = 42; + + GError *error = NULL; + + evidence_importer_test_reset_hooks(); + + source_path = + g_build_filename( + fixture.source_directory, + "preuve_commit_echec.txt", + NULL + ); + + assert(source_path != NULL); + + assert( + g_file_set_contents( + source_path, + "échec artificiel du commit", + -1, + &error + ) + ); + + assert(error == NULL); + + request.source_path = + source_path; + + request.destination_directory = + fixture.destination_directory; + + request.relative_directory = + TEST_EVIDENCE_RELATIVE_DIRECTORY; + + request.type_identifier = + "document"; + + request.source = + "Test du rollback après échec du commit."; + + evidence_importer_test_set_commit_failure( + TRUE + ); + + evidence_record = + evidence_importer_import( + fixture.evidence_importer, + &request, + NULL, + &error + ); + + assert(evidence_record == NULL); + assert(error != NULL); + + assert( + error->domain == + EVIDENCE_IMPORTER_ERROR + ); + + assert( + error->code == + (gint) EVIDENCE_IMPORTER_ERROR_DATABASE + ); + + assert( + strstr( + error->message, + "COMMIT simulé" + ) != NULL + ); + + /* + * L’insertion a eu lieu dans la transaction, + * mais le rollback doit l’avoir annulée. + */ + assert( + evidence_dao_count( + fixture.evidence_dao, + &evidence_count, + NULL + ) + ); + + assert(evidence_count == 0); + + /* + * La copie doit également avoir été supprimée. + */ + assert( + test_evidence_importer_count_destination_entries( + &fixture + ) == 0 + ); + + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + /* + * Le fichier source reste intact. + */ + assert( + g_file_test( + source_path, + G_FILE_TEST_IS_REGULAR + ) + ); + + evidence_importer_test_reset_hooks(); + + g_clear_error( + &error + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + source_path + ); + + test_evidence_importer_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le signalement d’une copie orpheline. + */ +static void test_evidence_importer_cleanup_failure_reports_orphan(void) +{ + TestEvidenceImporterFixture fixture = + test_evidence_importer_fixture_create(); + + EvidenceImportRequest request = {0}; + + EvidenceRecord *evidence_record = NULL; + + GDir *destination_directory = NULL; + + const char *orphan_name = NULL; + + char *source_path = NULL; + char *orphan_path = NULL; + + guint64 evidence_count = 42; + + GError *error = NULL; + GError *directory_error = NULL; + + evidence_importer_test_reset_hooks(); + + source_path = + g_build_filename( + fixture.source_directory, + "preuve_nettoyage_echec.txt", + NULL + ); + + assert(source_path != NULL); + + assert( + g_file_set_contents( + source_path, + "échec artificiel du nettoyage", + -1, + &error + ) + ); + + assert(error == NULL); + + request.source_path = + source_path; + + request.destination_directory = + fixture.destination_directory; + + request.relative_directory = + TEST_EVIDENCE_RELATIVE_DIRECTORY; + + /* + * Le type inconnu provoque l’échec après la copie. + */ + request.type_identifier = + "type_inconnu_pour_nettoyage"; + + evidence_importer_test_set_cleanup_failure( + TRUE + ); + + evidence_record = + evidence_importer_import( + fixture.evidence_importer, + &request, + NULL, + &error + ); + + assert(evidence_record == NULL); + assert(error != NULL); + + assert( + error->domain == + EVIDENCE_IMPORTER_ERROR + ); + + assert( + error->code == + (gint) EVIDENCE_IMPORTER_ERROR_ROLLBACK + ); + + assert( + strstr( + error->message, + "n’a pas pu être supprimée" + ) != NULL + ); + + assert( + strstr( + error->message, + "Cause initiale" + ) != NULL + ); + + /* + * L’insertion SQLite a été annulée. + */ + assert( + evidence_dao_count( + fixture.evidence_dao, + &evidence_count, + NULL + ) + ); + + assert(evidence_count == 0); + + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + /* + * Le hook a empêché la suppression : + * une copie orpheline doit donc être présente. + */ + assert( + test_evidence_importer_count_destination_entries( + &fixture + ) == 1 + ); + + destination_directory = + g_dir_open( + fixture.destination_directory, + 0, + &directory_error + ); + + assert(destination_directory != NULL); + assert(directory_error == NULL); + + orphan_name = + g_dir_read_name( + destination_directory + ); + + assert(orphan_name != NULL); + assert(orphan_name[0] != '\0'); + + orphan_path = + g_build_filename( + fixture.destination_directory, + orphan_name, + NULL + ); + + assert(orphan_path != NULL); + + assert( + g_file_test( + orphan_path, + G_FILE_TEST_IS_REGULAR + ) + ); + + /* + * Le message doit permettre de localiser le fichier orphelin. + */ + assert( + strstr( + error->message, + orphan_path + ) != NULL + ); + + g_dir_close( + destination_directory + ); + + destination_directory = NULL; + + /* + * On désactive le hook avant de nettoyer manuellement + * l’environnement du test. + */ + evidence_importer_test_reset_hooks(); + + g_clear_error( + &error + ); + + assert( + g_remove( + orphan_path + ) == 0 + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + orphan_path + ); + + g_free( + source_path + ); + + test_evidence_importer_fixture_clear( + &fixture + ); +} + +int main(void) +{ + test_evidence_importer_new_null_database(); + test_evidence_importer_new_valid(); + test_evidence_importer_import_valid_text_file(); + test_evidence_importer_import_empty_optional_null(); + test_evidence_importer_import_binary_file(); + test_evidence_importer_cancelled_before_copy(); + test_evidence_importer_unknown_type_cleanup(); + test_evidence_importer_transaction_already_active(); + test_evidence_importer_invalid_collected_at_cleanup(); + test_evidence_importer_complex_original_name(); + test_evidence_importer_invalid_relative_directory(); + test_evidence_importer_missing_source(); + test_evidence_importer_cancelled_after_copy_cleanup(); + test_evidence_importer_commit_failure_cleanup(); + test_evidence_importer_cleanup_failure_reports_orphan(); + + printf( + "EvidenceImporter : premiers tests transactionnels valides.\n" + ); + + return 0; +} diff --git a/tests/test_evidence_record b/tests/test_evidence_record new file mode 100755 index 0000000..9f4a34c Binary files /dev/null and b/tests/test_evidence_record differ diff --git a/tests/test_file_hash b/tests/test_file_hash new file mode 100755 index 0000000..27ec998 Binary files /dev/null and b/tests/test_file_hash differ diff --git a/tests/test_investigation_dao b/tests/test_investigation_dao new file mode 100755 index 0000000..0559b8e Binary files /dev/null and b/tests/test_investigation_dao differ diff --git a/tests/test_investigation_node b/tests/test_investigation_node new file mode 100755 index 0000000..b7614ef Binary files /dev/null and b/tests/test_investigation_node differ diff --git a/tests/test_investigation_project b/tests/test_investigation_project new file mode 100755 index 0000000..e10c1b2 Binary files /dev/null and b/tests/test_investigation_project differ diff --git a/tests/test_investigation_record b/tests/test_investigation_record new file mode 100755 index 0000000..ba2880b Binary files /dev/null and b/tests/test_investigation_record differ diff --git a/tests/test_investigation_session b/tests/test_investigation_session new file mode 100755 index 0000000..4d68d86 Binary files /dev/null and b/tests/test_investigation_session differ diff --git a/tests/test_investigation_tree_builder b/tests/test_investigation_tree_builder new file mode 100755 index 0000000..d6cd484 Binary files /dev/null and b/tests/test_investigation_tree_builder differ diff --git a/tests/test_investigation_tree_model b/tests/test_investigation_tree_model new file mode 100755 index 0000000..2c823e1 Binary files /dev/null and b/tests/test_investigation_tree_model differ diff --git a/tests/test_statement b/tests/test_statement new file mode 100755 index 0000000..4bc6b75 Binary files /dev/null and b/tests/test_statement differ diff --git a/tests/test_task_manager b/tests/test_task_manager new file mode 100755 index 0000000..9c71a73 Binary files /dev/null and b/tests/test_task_manager differ diff --git a/tests/test_tool_catalog b/tests/test_tool_catalog new file mode 100755 index 0000000..6d1d2d4 Binary files /dev/null and b/tests/test_tool_catalog differ diff --git a/tests/test_tool_initializer b/tests/test_tool_initializer new file mode 100755 index 0000000..b9ce449 Binary files /dev/null and b/tests/test_tool_initializer differ diff --git a/tests/test_tool_process b/tests/test_tool_process new file mode 100755 index 0000000..fdb4d27 Binary files /dev/null and b/tests/test_tool_process differ diff --git a/tests/test_tool_registry b/tests/test_tool_registry new file mode 100755 index 0000000..75486bf Binary files /dev/null and b/tests/test_tool_registry differ diff --git a/tests/test_tool_task b/tests/test_tool_task new file mode 100755 index 0000000..ad9a91a Binary files /dev/null and b/tests/test_tool_task differ diff --git a/tests/test_transaction b/tests/test_transaction new file mode 100755 index 0000000..dac2078 Binary files /dev/null and b/tests/test_transaction differ