diff --git a/Makefile b/Makefile index a8e0ce1..809146d 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,7 @@ TEST_INVESTIGATION_RECORD = tests/test_investigation_record TEST_EVIDENCE_RECORD := tests/test_evidence_record TEST_EVIDENCE_RECORD := tests/test_evidence_record TEST_INVESTIGATION_DAO := tests/test_investigation_dao +TEST_EVIDENCE_DAO := tests/test_evidence_dao TEST_INVESTIGATION_SESSION := tests/test_investigation_session TEST_BACKGROUND_TASK := tests/test_background_task TEST_TASK_MANAGER := tests/test_task_manager @@ -90,7 +91,8 @@ $(TEST_DATABASE): \ src/database/database.c \ src/database/transaction.c \ src/database/statement.c \ - src/database/schema.c + src/database/schema.c \ + src/database/error.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 $(TEST_STATEMENT): \ @@ -140,6 +142,17 @@ $(TEST_INVESTIGATION_DAO): \ src/database/error.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 +$(TEST_EVIDENCE_DAO): \ + tests/test_evidence_dao.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) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 + $(TEST_INVESTIGATION_SESSION): \ tests/test_investigation_session.c \ src/core/investigation_session.c \ @@ -211,6 +224,7 @@ test: \ $(TEST_INVESTIGATION_RECORD) \ $(TEST_EVIDENCE_RECORD) \ $(TEST_INVESTIGATION_DAO) \ + $(TEST_EVIDENCE_DAO) \ $(TEST_INVESTIGATION_SESSION) \ $(TEST_BACKGROUND_TASK) \ $(TEST_TASK_MANAGER) \ @@ -231,6 +245,7 @@ test: \ @$(TEST_INVESTIGATION_RECORD) @$(TEST_EVIDENCE_RECORD) @$(TEST_INVESTIGATION_DAO) + @$(TEST_EVIDENCE_DAO) @$(TEST_INVESTIGATION_SESSION) @$(TEST_BACKGROUND_TASK) @$(TEST_TASK_MANAGER) @@ -260,6 +275,7 @@ clean: $(TEST_INVESTIGATION_RECORD) \ $(TEST_EVIDENCE_RECORD) \ $(TEST_INVESTIGATION_DAO) \ + $(TEST_EVIDENCE_DAO) \ $(TEST_INVESTIGATION_SESSION) \ $(TEST_BACKGROUND_TASK) \ $(TEST_TASK_MANAGER) \ diff --git a/database/schema_v2.sql b/database/schema_v2.sql new file mode 100644 index 0000000..f579833 --- /dev/null +++ b/database/schema_v2.sql @@ -0,0 +1,105 @@ +/****************************************************************************** + * Labfy Investigation + * + * Migration du schéma SQLite V1 vers V2 + ******************************************************************************/ + +/* + * Nom du fichier tel qu’il existait avant son import. + * + * La colonne name existante conserve le nom interne utilisé dans + * l’arborescence de l’enquête. + */ +ALTER TABLE preuves +ADD COLUMN original_name TEXT; + +/* + * Date déclarée de collecte de la preuve. + * + * Elle est distincte de : + * + * - file_created_at : date technique connue du fichier ; + * - imported_at : date d’import dans l’enquête. + */ +ALTER TABLE preuves +ADD COLUMN collected_at TEXT; + +/* + * Description textuelle initiale de la provenance. + * + * Une future évolution pourra relier une preuve à la table sources + * avec un identifiant métier. + */ +ALTER TABLE preuves +ADD COLUMN source TEXT; + +/* + * Correspondance avec EvidenceIntegrityStatus : + * + * 0 = UNKNOWN + * 1 = VALID + * 2 = MISSING + * 3 = MODIFIED + * 4 = ERROR + */ +ALTER TABLE preuves +ADD COLUMN integrity_status INTEGER NOT NULL DEFAULT 0 +CHECK ( + integrity_status BETWEEN 0 AND 4 +); + +/* + * Les éventuelles preuves V1 utilisent leur ancien nom visible comme + * nom original afin de rester lisibles après migration. + */ +UPDATE preuves +SET original_name = name +WHERE original_name IS NULL + OR length(trim(original_name)) = 0; + +CREATE INDEX idx_preuves_imported_at +ON preuves(imported_at); + +/* + * SQLite ne permet pas d’ajouter directement une contrainte NOT NULL + * à une colonne ajoutée lorsque des lignes peuvent déjà exister. + * + * Ces triggers renforcent donc les insertions et modifications V2. + */ +CREATE TRIGGER preuves_v2_validate_insert +BEFORE INSERT ON preuves +FOR EACH ROW +WHEN + NEW.original_name IS NULL + OR length(trim(NEW.original_name)) = 0 + OR NEW.size_bytes IS NULL + OR NEW.size_bytes < 0 + OR NEW.sha256 IS NULL + OR length(NEW.sha256) != 64 + OR NEW.sha256 != lower(NEW.sha256) + OR NEW.integrity_status NOT BETWEEN 0 AND 4 +BEGIN + SELECT RAISE( + ABORT, + 'La preuve ne respecte pas les contraintes du schéma V2.' + ); +END; + +CREATE TRIGGER preuves_v2_validate_update +BEFORE UPDATE ON preuves +FOR EACH ROW +WHEN + NEW.original_name IS NULL + OR length(trim(NEW.original_name)) = 0 + OR NEW.size_bytes IS NULL + OR NEW.size_bytes < 0 + OR NEW.sha256 IS NULL + OR length(NEW.sha256) != 64 + OR NEW.sha256 != lower(NEW.sha256) + OR NEW.integrity_status NOT BETWEEN 0 AND 4 +BEGIN + SELECT RAISE( + ABORT, + 'La preuve ne respecte pas les contraintes du schéma V2.' + ); +END; diff --git a/include/dao/evidence_dao.h b/include/dao/evidence_dao.h new file mode 100644 index 0000000..6e8ac78 --- /dev/null +++ b/include/dao/evidence_dao.h @@ -0,0 +1,142 @@ +/****************************************************************************** + * @file evidence_dao.h + * @brief Persistance des preuves numériques dans SQLite. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_EVIDENCE_DAO_H +#define LABFY_INVESTIGATION_EVIDENCE_DAO_H + +#include "database/database.h" +#include "models/evidence_record.h" + +#include + +/** + * @brief Catégories d’erreurs du DAO des preuves. + */ +typedef enum +{ + EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + EVIDENCE_DAO_ERROR_MEMORY, + EVIDENCE_DAO_ERROR_PREPARE, + EVIDENCE_DAO_ERROR_BIND, + EVIDENCE_DAO_ERROR_EXECUTE, + EVIDENCE_DAO_ERROR_CONSTRAINT, + EVIDENCE_DAO_ERROR_READ, + EVIDENCE_DAO_ERROR_MODEL, + EVIDENCE_DAO_ERROR_SCHEMA, + EVIDENCE_DAO_ERROR_RANGE +} EvidenceDaoError; + +/** + * @brief Domaine d’erreurs du DAO des preuves. + */ +#define EVIDENCE_DAO_ERROR \ + evidence_dao_error_quark() + +/** + * @brief Retourne le domaine d’erreurs du DAO des preuves. + */ +GQuark evidence_dao_error_quark(void); + +/** + * @brief DAO opaque donnant accès aux preuves persistées. + * + * L’objet emprunte la connexion Database reçue lors de sa création. + */ +typedef struct EvidenceDao EvidenceDao; + +/** + * @brief Crée un DAO utilisant une connexion Database existante. + * + * La connexion est empruntée et doit rester valide pendant toute la durée + * de vie du DAO. + * + * @param database Connexion ouverte. + * @param error Adresse recevant une éventuelle erreur. + * + * @return Nouveau DAO, ou NULL en cas d’échec. + */ +EvidenceDao *evidence_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_dao DAO à libérer. + */ +void evidence_dao_free( + EvidenceDao *evidence_dao +); + +/** + * @brief Insère une nouvelle preuve. + * + * Aucun enregistrement existant n’est remplacé ou modifié. + * + * @param evidence_dao DAO valide. + * @param evidence_record Preuve empruntée. + * @param error Adresse recevant une éventuelle erreur. + * + * @return TRUE si l’insertion réussit, sinon FALSE. + */ +gboolean evidence_dao_insert( + EvidenceDao *evidence_dao, + const EvidenceRecord *evidence_record, + GError **error +); + +/** + * @brief Recherche une preuve par son identifiant. + * + * Une preuve absente retourne NULL sans produire d’erreur. + * + * @param evidence_dao DAO valide. + * @param identifier Identifiant UUID recherché. + * @param error Adresse recevant une éventuelle erreur. + * + * @return Nouveau modèle possédé par l’appelant, ou NULL. + */ +EvidenceRecord *evidence_dao_find_by_identifier( + EvidenceDao *evidence_dao, + const char *identifier, + GError **error +); + +/** + * @brief Charge toutes les preuves dans un ordre déterministe. + * + * Le tableau retourné utilise evidence_record_free() comme fonction + * de destruction. + * + * @param evidence_dao DAO valide. + * @param error Adresse recevant une éventuelle erreur. + * + * @return Nouveau GPtrArray, ou NULL en cas d’échec. + */ +GPtrArray *evidence_dao_list_all( + EvidenceDao *evidence_dao, + GError **error +); + +/** + * @brief Compte les preuves persistées. + * + * @param evidence_dao DAO valide. + * @param out_count Destination du nombre de preuves. + * @param error Adresse recevant une éventuelle erreur. + * + * @return TRUE si le comptage réussit, sinon FALSE. + */ +gboolean evidence_dao_count( + EvidenceDao *evidence_dao, + guint64 *out_count, + GError **error +); + +#endif diff --git a/include/database/database.h b/include/database/database.h index d80e87c..221aab6 100644 --- a/include/database/database.h +++ b/include/database/database.h @@ -42,6 +42,26 @@ void database_close( Database *database ); +/** + * @brief Met à jour une base ouverte vers la dernière version du schéma. + * + * La fonction : + * + * - lit metadata.schema_version ; + * - applique chaque migration manquante dans une transaction ; + * - met à jour la version uniquement après une migration réussie ; + * - ne modifie rien lorsque la base est déjà à jour. + * + * La fonction refuse une migration lorsqu’une transaction est déjà active. + * + * @param database Connexion Database ouverte. + * + * @return true si la base est à jour, sinon false. + */ +bool database_migrate_to_latest( + Database *database +); + /** * @brief Initialise la base SQLite d'une nouvelle enquête. * diff --git a/include/database/schema.h b/include/database/schema.h index 276765d..5f8998a 100644 --- a/include/database/schema.h +++ b/include/database/schema.h @@ -31,4 +31,27 @@ bool schema_install_v1( Database *database ); +/** + * @brief Installe les modifications du schéma SQLite V2. + * + * La connexion doit être valide et une transaction doit déjà être active. + * + * Cette fonction : + * + * - étend la table preuves ; + * - conserve les éventuelles lignes V1 ; + * - ajoute les contraintes nécessaires aux nouvelles preuves ; + * - ajoute les index V2. + * + * Elle ne modifie pas elle-même la version enregistrée dans metadata. + * Elle ne réalise ni COMMIT ni ROLLBACK. + * + * @param database Connexion Database ouverte. + * + * @return true si la migration V2 a été appliquée, sinon false. + */ +bool schema_install_v2( + Database *database +); + #endif diff --git a/include/database/statement.h b/include/database/statement.h index 6ed5ae0..e8e4024 100644 --- a/include/database/statement.h +++ b/include/database/statement.h @@ -186,23 +186,6 @@ bool database_statement_bind_null( int index ); -/** - * @brief Lie une chaîne de caractères à un paramètre SQL. - * - * Les indices des paramètres SQLite commencent à 1. - * - * @param statement Requête préparée. - * @param index Indice du paramètre SQL. - * @param value Chaîne de caractères à lier. - * - * @return true en cas de succès, sinon false. - */ -bool database_statement_bind_text( - DatabaseStatement *statement, - int index, - const char *value -); - /** * @brief Finalise une requête préparée et libère ses ressources. * diff --git a/labfy-investigation b/labfy-investigation index ec5ef1d..18572e4 100755 Binary files a/labfy-investigation and b/labfy-investigation differ diff --git a/src/core/investigation_session.c b/src/core/investigation_session.c index ebc5e67..9adcdea 100644 --- a/src/core/investigation_session.c +++ b/src/core/investigation_session.c @@ -192,6 +192,33 @@ InvestigationSession *investigation_session_open( goto cleanup; } + if (!database_migrate_to_latest( + database + )) + { + database_error_message = + database_error_get_message( + database + ); + + if (database_error_message == NULL || + database_error_message[0] == '\0') + { + database_error_message = + "La base de données n’a pas pu être mise à jour."; + } + + g_set_error( + error, + INVESTIGATION_SESSION_ERROR, + INVESTIGATION_SESSION_ERROR_DATABASE, + "Impossible de mettre à jour le schéma SQLite : %s", + database_error_message + ); + + goto cleanup; + } + record = investigation_dao_load( database ); diff --git a/src/dao/evidence_dao.c b/src/dao/evidence_dao.c new file mode 100644 index 0000000..004c846 --- /dev/null +++ b/src/dao/evidence_dao.c @@ -0,0 +1,1482 @@ +/****************************************************************************** + * @file evidence_dao.c + * @brief Persistance des preuves numériques dans SQLite. + ******************************************************************************/ + +#include "dao/evidence_dao.h" + +#include "database/error.h" +#include "database/statement.h" + +#include +#include +#include + +/** + * @brief Représentation privée du DAO des preuves. + * + * La connexion Database est empruntée. + */ +struct EvidenceDao +{ + Database *database; +}; + +/** + * @brief Requête d’insertion d’une preuve. + * + * updated_at reçoit initialement la même date que imported_at. + * + * Les colonnes historiques non représentées par EvidenceRecord utilisent + * leurs valeurs par défaut ou SQL NULL. + */ +static const char *const evidence_dao_insert_sql = + "INSERT INTO preuves" + "(" + " id," + " original_name," + " name," + " relative_path," + " type_id," + " size_bytes," + " sha256," + " description," + " imported_at," + " updated_at," + " source," + " collected_at," + " integrity_status" + ")" + "VALUES" + "(" + " ?," + " ?," + " ?," + " ?," + " (" + " SELECT id " + " FROM types_preuve " + " WHERE code = ?" + " )," + " ?," + " ?," + " ?," + " ?," + " ?," + " ?," + " ?," + " ?" + ");"; + +/** + * @brief Requête de recherche d’une preuve par son identifiant. + */ +static const char *const evidence_dao_find_by_identifier_sql = + "SELECT " + " preuves.id," + " preuves.original_name," + " preuves.name," + " preuves.relative_path," + " types_preuve.code," + " preuves.size_bytes," + " preuves.sha256," + " preuves.imported_at," + " preuves.collected_at," + " preuves.source," + " preuves.description," + " preuves.integrity_status " + "FROM preuves " + "LEFT JOIN types_preuve " + "ON types_preuve.id = preuves.type_id " + "WHERE preuves.id = ?;"; + +/** + * @brief Requête de détection d’un identifiant existant. + */ +static const char *const evidence_dao_identifier_exists_sql = + "SELECT 1 " + "FROM preuves " + "WHERE id = ? " + "LIMIT 1;"; + +/** + * @brief Requête de détection d’un chemin existant. + */ +static const char *const evidence_dao_relative_path_exists_sql = + "SELECT 1 " + "FROM preuves " + "WHERE relative_path = ? " + "LIMIT 1;"; + +/** + * @brief Requête de détection d’un type de preuve. + */ +static const char *const evidence_dao_type_exists_sql = + "SELECT 1 " + "FROM types_preuve " + "WHERE code = ? " + "LIMIT 1;"; + + /** + * @brief Requête de comptage des preuves. + */ +static const char *const evidence_dao_count_sql = + "SELECT COUNT(*) " + "FROM preuves;"; + + /** + * @brief Requête de chargement de toutes les preuves. + */ +static const char *const evidence_dao_list_all_sql = + "SELECT " + " preuves.id," + " preuves.original_name," + " preuves.name," + " preuves.relative_path," + " types_preuve.code," + " preuves.size_bytes," + " preuves.sha256," + " preuves.imported_at," + " preuves.collected_at," + " preuves.source," + " preuves.description," + " preuves.integrity_status " + "FROM preuves " + "LEFT JOIN types_preuve " + "ON types_preuve.id = preuves.type_id " + "ORDER BY " + " preuves.imported_at ASC," + " preuves.id ASC;"; + +/** + * @brief Enregistre une erreur littérale. + */ +static void evidence_dao_set_error_literal( + GError **error, + EvidenceDaoError error_code, + const char *error_message +) +{ + if (error == NULL) + { + return; + } + + g_set_error_literal( + error, + EVIDENCE_DAO_ERROR, + error_code, + error_message + ); +} + +/** + * @brief Transforme la dernière erreur Database en GError du DAO. + */ +static void evidence_dao_set_database_error( + EvidenceDao *evidence_dao, + GError **error, + EvidenceDaoError error_code, + const char *context +) +{ + const char *database_error_message = NULL; + + if (error == NULL) + { + return; + } + + if (evidence_dao != NULL && + evidence_dao->database != NULL) + { + database_error_message = + database_error_get_message( + evidence_dao->database + ); + } + + if (database_error_message != NULL && + database_error_message[0] != '\0') + { + g_set_error( + error, + EVIDENCE_DAO_ERROR, + error_code, + "%s : %s", + context, + database_error_message + ); + + return; + } + + g_set_error_literal( + error, + EVIDENCE_DAO_ERROR, + error_code, + context + ); +} + +/** + * @brief Exécute une requête SELECT d’existence. + */ +static gboolean evidence_dao_value_exists( + EvidenceDao *evidence_dao, + const char *sql, + const char *value, + gboolean *out_exists, + GError **error +) +{ + DatabaseStatement *statement = NULL; + + DatabaseStatementStepResult step_result = + DATABASE_STATEMENT_STEP_ERROR; + + gboolean success = FALSE; + + if (evidence_dao == NULL || + evidence_dao->database == NULL || + sql == NULL || + value == NULL || + out_exists == NULL) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + "Les paramètres de la recherche d’existence sont invalides." + ); + + return FALSE; + } + + *out_exists = FALSE; + + statement = + database_statement_prepare( + evidence_dao->database, + sql + ); + + if (statement == NULL) + { + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_PREPARE, + "Impossible de préparer la recherche d’existence" + ); + + goto cleanup; + } + + if (!database_statement_bind_text( + statement, + 1, + value + )) + { + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_BIND, + "Impossible de lier la valeur recherchée" + ); + + goto cleanup; + } + + step_result = + database_statement_step( + statement + ); + + if (step_result == DATABASE_STATEMENT_STEP_ERROR) + { + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_EXECUTE, + "Impossible d’exécuter la recherche d’existence" + ); + + goto cleanup; + } + + *out_exists = + step_result == DATABASE_STATEMENT_STEP_ROW; + + success = TRUE; + +cleanup: + + database_statement_finalize( + statement + ); + + return success; +} + +/** + * @brief Lie un texte facultatif ou SQL NULL. + */ +static gboolean evidence_dao_bind_optional_text( + DatabaseStatement *statement, + int parameter_index, + const char *value +) +{ + if (value == NULL) + { + return database_statement_bind_null( + statement, + parameter_index + ); + } + + return database_statement_bind_text( + statement, + parameter_index, + value + ); +} + +/** + * @brief Convertit une erreur d’insertion SQLite en erreur DAO. + */ +static void evidence_dao_set_insert_error( + EvidenceDao *evidence_dao, + GError **error +) +{ + const char *database_error_message = NULL; + + if (evidence_dao != NULL && + evidence_dao->database != NULL) + { + database_error_message = + database_error_get_message( + evidence_dao->database + ); + } + + if (database_error_message != NULL && + strstr( + database_error_message, + "preuves.id" + ) != NULL) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_CONSTRAINT, + "Une preuve possède déjà cet identifiant." + ); + + return; + } + + if (database_error_message != NULL && + strstr( + database_error_message, + "preuves.relative_path" + ) != NULL) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_CONSTRAINT, + "Une preuve utilise déjà ce chemin relatif." + ); + + return; + } + + if (database_error_message != NULL && + ( + strstr( + database_error_message, + "constraint" + ) != NULL || + strstr( + database_error_message, + "CONSTRAINT" + ) != NULL + )) + { + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_CONSTRAINT, + "La preuve viole une contrainte SQLite" + ); + + return; + } + + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_EXECUTE, + "Impossible d’insérer la preuve" + ); +} + +/** + * @brief Construit un EvidenceRecord depuis la ligne SQLite courante. + */ +static EvidenceRecord *evidence_dao_read_current_record( + EvidenceDao *evidence_dao, + DatabaseStatement *statement, + GError **error +) +{ + EvidenceRecord *evidence_record = NULL; + + char *identifier = NULL; + char *original_name = NULL; + char *internal_name = NULL; + char *relative_path = NULL; + char *type_identifier = NULL; + char *sha256 = NULL; + char *imported_at = NULL; + char *collected_at = NULL; + char *source = NULL; + char *description = NULL; + + int64_t size_bytes = -1; + int64_t integrity_status_value = -1; + + GError *model_error = NULL; + + if (evidence_dao == NULL || + evidence_dao->database == NULL || + statement == NULL) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + "Impossible de lire une preuve avec des paramètres invalides." + ); + + return NULL; + } + + if (!database_statement_column_text( + statement, + 0, + &identifier + ) || + !database_statement_column_text( + statement, + 1, + &original_name + ) || + !database_statement_column_text( + statement, + 2, + &internal_name + ) || + !database_statement_column_text( + statement, + 3, + &relative_path + ) || + !database_statement_column_text( + statement, + 4, + &type_identifier + ) || + !database_statement_column_int64( + statement, + 5, + &size_bytes + ) || + !database_statement_column_text( + statement, + 6, + &sha256 + ) || + !database_statement_column_text( + statement, + 7, + &imported_at + ) || + !database_statement_column_text( + statement, + 8, + &collected_at + ) || + !database_statement_column_text( + statement, + 9, + &source + ) || + !database_statement_column_text( + statement, + 10, + &description + ) || + !database_statement_column_int64( + statement, + 11, + &integrity_status_value + )) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_READ, + "Impossible de lire les colonnes de la preuve." + ); + + goto cleanup; + } + + if (size_bytes < 0) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_RANGE, + "La preuve persistée possède une taille négative." + ); + + goto cleanup; + } + + if (integrity_status_value < + EVIDENCE_INTEGRITY_STATUS_UNKNOWN || + integrity_status_value > + EVIDENCE_INTEGRITY_STATUS_ERROR) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_MODEL, + "La preuve persistée possède un état d’intégrité invalide." + ); + + goto cleanup; + } + + evidence_record = + evidence_record_new( + identifier, + original_name, + internal_name, + relative_path, + type_identifier, + (guint64) size_bytes, + sha256, + imported_at, + collected_at, + source, + description, + (EvidenceIntegrityStatus) integrity_status_value, + &model_error + ); + + if (evidence_record == NULL) + { + if (model_error != NULL) + { + g_set_error( + error, + EVIDENCE_DAO_ERROR, + EVIDENCE_DAO_ERROR_MODEL, + "La preuve persistée est invalide : %s", + model_error->message + ); + } + else + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_MODEL, + "La preuve persistée ne peut pas être reconstruite." + ); + } + } + +cleanup: + + g_clear_error( + &model_error + ); + + g_free( + description + ); + + g_free( + source + ); + + g_free( + collected_at + ); + + g_free( + imported_at + ); + + g_free( + sha256 + ); + + g_free( + type_identifier + ); + + g_free( + relative_path + ); + + g_free( + internal_name + ); + + g_free( + original_name + ); + + g_free( + identifier + ); + + return evidence_record; +} + +/** + * @brief Libère un EvidenceRecord contenu dans un GPtrArray. + */ +static void evidence_dao_record_destroy( + gpointer data +) +{ + evidence_record_free( + data + ); +} + +GQuark evidence_dao_error_quark(void) +{ + return g_quark_from_static_string( + "evidence-dao-error-quark" + ); +} + +EvidenceDao *evidence_dao_new( + Database *database, + GError **error +) +{ + EvidenceDao *evidence_dao = NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (database == NULL) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + "La connexion Database est absente." + ); + + return NULL; + } + + evidence_dao = + g_try_new0( + EvidenceDao, + 1 + ); + + if (evidence_dao == NULL) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_MEMORY, + "Impossible d’allouer le DAO des preuves." + ); + + return NULL; + } + + evidence_dao->database = database; + + return evidence_dao; +} + +void evidence_dao_free( + EvidenceDao *evidence_dao +) +{ + if (evidence_dao == NULL) + { + return; + } + + evidence_dao->database = NULL; + + g_free( + evidence_dao + ); +} + +gboolean evidence_dao_insert( + EvidenceDao *evidence_dao, + const EvidenceRecord *evidence_record, + GError **error +) +{ + DatabaseStatement *statement = NULL; + + const char *identifier = NULL; + const char *original_name = NULL; + const char *internal_name = NULL; + const char *relative_path = NULL; + const char *type_identifier = NULL; + const char *sha256 = NULL; + const char *imported_at = NULL; + const char *collected_at = NULL; + const char *source = NULL; + const char *description = NULL; + + guint64 size_bytes = 0; + + EvidenceIntegrityStatus integrity_status = + EVIDENCE_INTEGRITY_STATUS_UNKNOWN; + + gboolean value_exists = FALSE; + gboolean success = FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (evidence_dao == NULL || + evidence_dao->database == NULL || + evidence_record == NULL) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + "Le DAO ou la preuve est absent." + ); + + return FALSE; + } + + identifier = + evidence_record_get_identifier( + evidence_record + ); + + original_name = + evidence_record_get_original_name( + evidence_record + ); + + internal_name = + evidence_record_get_internal_name( + evidence_record + ); + + relative_path = + evidence_record_get_relative_path( + evidence_record + ); + + type_identifier = + evidence_record_get_type_identifier( + evidence_record + ); + + size_bytes = + evidence_record_get_size_bytes( + evidence_record + ); + + sha256 = + evidence_record_get_sha256( + evidence_record + ); + + imported_at = + evidence_record_get_imported_at( + evidence_record + ); + + collected_at = + evidence_record_get_collected_at( + evidence_record + ); + + source = + evidence_record_get_source( + evidence_record + ); + + description = + evidence_record_get_description( + evidence_record + ); + + integrity_status = + evidence_record_get_integrity_status( + evidence_record + ); + + if (identifier == NULL || + original_name == NULL || + internal_name == NULL || + relative_path == NULL || + type_identifier == NULL || + sha256 == NULL || + imported_at == NULL) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + "La preuve contient une valeur obligatoire absente." + ); + + return FALSE; + } + + /* + * SQLite stocke les INTEGER dans un entier signé sur 64 bits. + */ + if (size_bytes > (guint64) G_MAXINT64) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_RANGE, + "La taille de la preuve dépasse la capacité de SQLite." + ); + + return FALSE; + } + + if (!evidence_dao_value_exists( + evidence_dao, + evidence_dao_identifier_exists_sql, + identifier, + &value_exists, + error + )) + { + return FALSE; + } + + if (value_exists) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_CONSTRAINT, + "Une preuve possède déjà cet identifiant." + ); + + return FALSE; + } + + if (!evidence_dao_value_exists( + evidence_dao, + evidence_dao_relative_path_exists_sql, + relative_path, + &value_exists, + error + )) + { + return FALSE; + } + + if (value_exists) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_CONSTRAINT, + "Une preuve utilise déjà ce chemin relatif." + ); + + return FALSE; + } + + if (!evidence_dao_value_exists( + evidence_dao, + evidence_dao_type_exists_sql, + type_identifier, + &value_exists, + error + )) + { + return FALSE; + } + + if (!value_exists) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_CONSTRAINT, + "Le type de preuve n’existe pas dans le catalogue SQLite." + ); + + return FALSE; + } + + statement = + database_statement_prepare( + evidence_dao->database, + evidence_dao_insert_sql + ); + + if (statement == NULL) + { + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_PREPARE, + "Impossible de préparer l’insertion de la preuve" + ); + + goto cleanup; + } + + if (!database_statement_bind_text( + statement, + 1, + identifier + ) || + !database_statement_bind_text( + statement, + 2, + original_name + ) || + !database_statement_bind_text( + statement, + 3, + internal_name + ) || + !database_statement_bind_text( + statement, + 4, + relative_path + ) || + !database_statement_bind_text( + statement, + 5, + type_identifier + ) || + !database_statement_bind_int64( + statement, + 6, + (int64_t) size_bytes + ) || + !database_statement_bind_text( + statement, + 7, + sha256 + ) || + !evidence_dao_bind_optional_text( + statement, + 8, + description + ) || + !database_statement_bind_text( + statement, + 9, + imported_at + ) || + !database_statement_bind_text( + statement, + 10, + imported_at + ) || + !evidence_dao_bind_optional_text( + statement, + 11, + source + ) || + !evidence_dao_bind_optional_text( + statement, + 12, + collected_at + ) || + !database_statement_bind_int64( + statement, + 13, + (int64_t) integrity_status + )) + { + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_BIND, + "Impossible de lier les données de la preuve" + ); + + goto cleanup; + } + + if (database_statement_step( + statement + ) != DATABASE_STATEMENT_STEP_DONE) + { + evidence_dao_set_insert_error( + evidence_dao, + error + ); + + goto cleanup; + } + + success = TRUE; + +cleanup: + + database_statement_finalize( + statement + ); + + return success; +} + +EvidenceRecord *evidence_dao_find_by_identifier( + EvidenceDao *evidence_dao, + const char *identifier, + GError **error +) +{ + DatabaseStatement *statement = NULL; + EvidenceRecord *evidence_record = NULL; + + DatabaseStatementStepResult step_result = + DATABASE_STATEMENT_STEP_ERROR; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (evidence_dao == NULL || + evidence_dao->database == NULL || + identifier == NULL || + !g_uuid_string_is_valid(identifier)) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + "L’identifiant de preuve est invalide." + ); + + return NULL; + } + + statement = + database_statement_prepare( + evidence_dao->database, + evidence_dao_find_by_identifier_sql + ); + + if (statement == NULL) + { + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_PREPARE, + "Impossible de préparer la recherche de la preuve" + ); + + goto cleanup; + } + + if (!database_statement_bind_text( + statement, + 1, + identifier + )) + { + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_BIND, + "Impossible de lier l’identifiant de la preuve" + ); + + goto cleanup; + } + + step_result = + database_statement_step( + statement + ); + + if (step_result == DATABASE_STATEMENT_STEP_ERROR) + { + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_EXECUTE, + "Impossible d’exécuter la recherche de la preuve" + ); + + goto cleanup; + } + + /* + * Une preuve absente n’est pas une erreur. + */ + if (step_result == DATABASE_STATEMENT_STEP_DONE) + { + goto cleanup; + } + + evidence_record = + evidence_dao_read_current_record( + evidence_dao, + statement, + error + ); + + if (evidence_record == NULL) + { + goto cleanup; + } + + /* + * L’identifiant est une clé primaire, mais on vérifie tout de même + * que la requête ne fournit aucune seconde ligne. + */ + step_result = + database_statement_step( + statement + ); + + if (step_result != DATABASE_STATEMENT_STEP_DONE) + { + if (step_result == DATABASE_STATEMENT_STEP_ROW) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_SCHEMA, + "Plusieurs preuves possèdent le même identifiant." + ); + } + else + { + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_EXECUTE, + "Impossible de terminer la lecture de la preuve" + ); + } + + evidence_record_free( + evidence_record + ); + + evidence_record = NULL; + } + +cleanup: + + database_statement_finalize( + statement + ); + + return evidence_record; +} + +gboolean evidence_dao_count( + EvidenceDao *evidence_dao, + guint64 *out_count, + GError **error +) +{ + DatabaseStatement *statement = NULL; + + DatabaseStatementStepResult step_result = + DATABASE_STATEMENT_STEP_ERROR; + + int64_t count_value = -1; + + gboolean success = FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (evidence_dao == NULL || + evidence_dao->database == NULL || + out_count == NULL) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + "Les paramètres du comptage des preuves sont invalides." + ); + + return FALSE; + } + + /* + * Une erreur ne doit jamais laisser une ancienne valeur dans + * la destination fournie par l’appelant. + */ + *out_count = 0; + + statement = + database_statement_prepare( + evidence_dao->database, + evidence_dao_count_sql + ); + + if (statement == NULL) + { + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_PREPARE, + "Impossible de préparer le comptage des preuves" + ); + + goto cleanup; + } + + step_result = + database_statement_step( + statement + ); + + if (step_result == DATABASE_STATEMENT_STEP_ERROR) + { + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_EXECUTE, + "Impossible d’exécuter le comptage des preuves" + ); + + goto cleanup; + } + + /* + * COUNT(*) doit toujours produire exactement une ligne. + */ + if (step_result != DATABASE_STATEMENT_STEP_ROW) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_READ, + "Le comptage des preuves n’a retourné aucune ligne." + ); + + goto cleanup; + } + + if (!database_statement_column_int64( + statement, + 0, + &count_value + )) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_READ, + "Impossible de lire le nombre de preuves." + ); + + goto cleanup; + } + + if (count_value < 0) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_RANGE, + "Le nombre de preuves retourné par SQLite est invalide." + ); + + goto cleanup; + } + + step_result = + database_statement_step( + statement + ); + + if (step_result != DATABASE_STATEMENT_STEP_DONE) + { + if (step_result == DATABASE_STATEMENT_STEP_ROW) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_SCHEMA, + "Le comptage des preuves a retourné plusieurs lignes." + ); + } + else + { + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_EXECUTE, + "Impossible de terminer le comptage des preuves" + ); + } + + goto cleanup; + } + + *out_count = + (guint64) count_value; + + success = TRUE; + +cleanup: + + database_statement_finalize( + statement + ); + + return success; +} + +GPtrArray *evidence_dao_list_all( + EvidenceDao *evidence_dao, + GError **error +) +{ + DatabaseStatement *statement = NULL; + + GPtrArray *evidence_records = NULL; + EvidenceRecord *evidence_record = NULL; + + DatabaseStatementStepResult step_result = + DATABASE_STATEMENT_STEP_ERROR; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (evidence_dao == NULL || + evidence_dao->database == NULL) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + "Le DAO des preuves est invalide." + ); + + return NULL; + } + + evidence_records = + g_ptr_array_new_with_free_func( + evidence_dao_record_destroy + ); + + if (evidence_records == NULL) + { + evidence_dao_set_error_literal( + error, + EVIDENCE_DAO_ERROR_MEMORY, + "Impossible d’allouer la liste des preuves." + ); + + return NULL; + } + + statement = + database_statement_prepare( + evidence_dao->database, + evidence_dao_list_all_sql + ); + + if (statement == NULL) + { + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_PREPARE, + "Impossible de préparer le chargement des preuves" + ); + + goto error; + } + + while (true) + { + step_result = + database_statement_step( + statement + ); + + if (step_result == DATABASE_STATEMENT_STEP_DONE) + { + break; + } + + if (step_result == DATABASE_STATEMENT_STEP_ERROR) + { + evidence_dao_set_database_error( + evidence_dao, + error, + EVIDENCE_DAO_ERROR_EXECUTE, + "Impossible d’exécuter le chargement des preuves" + ); + + goto error; + } + + evidence_record = + evidence_dao_read_current_record( + evidence_dao, + statement, + error + ); + + if (evidence_record == NULL) + { + goto error; + } + + g_ptr_array_add( + evidence_records, + evidence_record + ); + + /* + * Le tableau devient propriétaire du modèle. + */ + evidence_record = NULL; + } + + database_statement_finalize( + statement + ); + + return evidence_records; + +error: + + evidence_record_free( + evidence_record + ); + + database_statement_finalize( + statement + ); + + g_ptr_array_unref( + evidence_records + ); + + return NULL; +} diff --git a/src/database/database.c b/src/database/database.c index c0179e7..6ed7217 100644 --- a/src/database/database.c +++ b/src/database/database.c @@ -16,7 +16,12 @@ /** * @brief Version actuelle du schéma SQLite. */ -#define DATABASE_SCHEMA_VERSION "1" +#define DATABASE_SCHEMA_VERSION_CURRENT 2 + +/** + * @brief Version actuelle sous forme textuelle pour metadata. + */ +#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "2" /** * @brief Nom de l'application enregistré dans les métadonnées. @@ -225,7 +230,7 @@ static bool database_insert_all_metadata( database_insert_metadata( statement, "schema_version", - DATABASE_SCHEMA_VERSION + DATABASE_SCHEMA_VERSION_CURRENT_TEXT ) && database_insert_metadata( statement, @@ -366,6 +371,22 @@ bool database_get_transaction_active( return database->transaction_active; } +/** + * @brief Requête de lecture de la version du schéma. + */ +static const char *const database_select_schema_version_sql = + "SELECT value " + "FROM metadata " + "WHERE key = ?;"; + +/** + * @brief Requête de mise à jour de la version du schéma. + */ +static const char *const database_update_schema_version_sql = + "UPDATE metadata " + "SET value = ? " + "WHERE key = ?;"; + void database_set_transaction_active( Database *database, bool transaction_active @@ -379,6 +400,274 @@ void database_set_transaction_active( database->transaction_active = transaction_active; } +/** + * @brief Lit et valide la version enregistrée dans metadata. + */ +static bool database_read_schema_version( + Database *database, + int *out_schema_version +) +{ + DatabaseStatement *statement = NULL; + DatabaseStatementStepResult step_result; + + char *version_text = NULL; + char *end_pointer = NULL; + + gint64 parsed_version = 0; + + bool success = false; + + if (database == NULL || + out_schema_version == NULL) + { + return false; + } + + *out_schema_version = 0; + + statement = + database_statement_prepare( + database, + database_select_schema_version_sql + ); + + if (statement == NULL) + { + return false; + } + + if (!database_statement_bind_text( + statement, + 1, + "schema_version" + )) + { + goto cleanup; + } + + step_result = + database_statement_step( + statement + ); + + if (step_result == DATABASE_STATEMENT_STEP_ERROR) + { + goto cleanup; + } + + if (step_result == DATABASE_STATEMENT_STEP_DONE) + { + database_set_error( + database, + DATABASE_ERROR_INVALID_STATE, + "La version du schéma est absente des métadonnées." + ); + + goto cleanup; + } + + if (!database_statement_column_text( + statement, + 0, + &version_text + ) || + version_text == NULL || + version_text[0] == '\0') + { + database_set_error( + database, + DATABASE_ERROR_INVALID_STATE, + "La version du schéma est invalide." + ); + + goto cleanup; + } + + parsed_version = + g_ascii_strtoll( + version_text, + &end_pointer, + 10 + ); + + if (end_pointer == version_text || + end_pointer == NULL || + end_pointer[0] != '\0' || + parsed_version < 1 || + parsed_version > G_MAXINT) + { + database_set_error( + database, + DATABASE_ERROR_INVALID_STATE, + "La version du schéma n’est pas un entier valide." + ); + + goto cleanup; + } + + /* + * La clé metadata est primaire, mais on vérifie malgré tout + * que la requête ne retourne aucune seconde ligne. + */ + step_result = + database_statement_step( + statement + ); + + if (step_result != DATABASE_STATEMENT_STEP_DONE) + { + if (step_result == DATABASE_STATEMENT_STEP_ROW) + { + database_set_error( + database, + DATABASE_ERROR_INVALID_STATE, + "Plusieurs versions du schéma sont enregistrées." + ); + } + + goto cleanup; + } + + *out_schema_version = + (int) parsed_version; + + database_clear_error_internal( + database + ); + + success = true; + +cleanup: + + g_free( + version_text + ); + + database_statement_finalize( + statement + ); + + return success; +} + +/** + * @brief Met à jour la version enregistrée dans metadata. + */ +static bool database_update_schema_version( + Database *database, + const char *version_text +) +{ + DatabaseStatement *statement = NULL; + bool success = false; + + if (database == NULL || + version_text == NULL || + version_text[0] == '\0') + { + return false; + } + + statement = + database_statement_prepare( + database, + database_update_schema_version_sql + ); + + if (statement == NULL) + { + return false; + } + + success = + database_statement_bind_text( + statement, + 1, + version_text + ) && + database_statement_bind_text( + statement, + 2, + "schema_version" + ) && + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_DONE; + + database_statement_finalize( + statement + ); + + return success; +} + +/** + * @brief Applique atomiquement la migration du schéma V1 vers V2. + */ +static bool database_migrate_v1_to_v2( + Database *database +) +{ + bool transaction_started = false; + + if (database == NULL) + { + return false; + } + + if (!database_transaction_begin( + database + )) + { + return false; + } + + transaction_started = true; + + if (!schema_install_v2( + database + )) + { + goto rollback; + } + + if (!database_update_schema_version( + database, + "2" + )) + { + goto rollback; + } + + if (!database_transaction_commit( + database + )) + { + goto rollback; + } + + transaction_started = false; + + return true; + +rollback: + + if (transaction_started) + { + if (!database_transaction_rollback( + database + )) + { + g_warning( + "Impossible d’annuler la migration SQLite V1 vers V2." + ); + } + } + + return false; +} + Database *database_open( const char *database_path ) @@ -445,6 +734,89 @@ Database *database_open( return database; } +bool database_migrate_to_latest( + Database *database +) +{ + int schema_version = 0; + + if (database == NULL) + { + return false; + } + + if (database_get_transaction_active( + database + )) + { + database_set_error( + database, + DATABASE_ERROR_INVALID_STATE, + "La migration est impossible pendant une transaction active." + ); + + return false; + } + + if (!database_read_schema_version( + database, + &schema_version + )) + { + return false; + } + + if (schema_version > + DATABASE_SCHEMA_VERSION_CURRENT) + { + database_set_error( + database, + DATABASE_ERROR_INVALID_STATE, + "La base utilise une version de schéma plus récente " + "que cette version de Labfy Investigation." + ); + + return false; + } + + while (schema_version < + DATABASE_SCHEMA_VERSION_CURRENT) + { + switch (schema_version) + { + case 1: + if (!database_migrate_v1_to_v2( + database + )) + { + return false; + } + + schema_version = 2; + break; + + default: + database_set_error( + database, + DATABASE_ERROR_INVALID_STATE, + "La version du schéma ne possède aucune " + "migration prise en charge." + ); + + return false; + } + } + + database->schema_version = + schema_version; + + database_clear_error_internal( + database + ); + + return true; +} + bool database_initialize( const char *database_path, const char *investigation_name, @@ -514,6 +886,17 @@ bool database_initialize( goto rollback; } + /* + * Une nouvelle base reçoit immédiatement toutes les versions du schéma + * dans la transaction initiale. + */ + if (!schema_install_v2( + database + )) + { + goto rollback; + } + if (!database_insert_all_metadata( database, created_at, diff --git a/src/database/schema.c b/src/database/schema.c index 79c5e8f..77b2e20 100644 --- a/src/database/schema.c +++ b/src/database/schema.c @@ -1,6 +1,6 @@ /****************************************************************************** * @file schema.c - * @brief Installation du schéma SQLite de Labfy Investigation. + * @brief Installation des versions du schéma SQLite. ******************************************************************************/ #include "database/schema.h" @@ -11,22 +11,34 @@ #include /** - * @brief Charge le schéma SQL V1 depuis le fichier du projet. + * @brief Charge un fichier SQL du projet. * - * @param database Connexion utilisée pour enregistrer une éventuelle erreur. + * @param database Connexion recevant les erreurs. + * @param schema_path Chemin du fichier SQL. + * @param schema_name Nom utilisé dans les diagnostics. * - * @return Une nouvelle chaîne terminée par zéro, à libérer avec g_free(), - * ou NULL en cas d'échec. + * @return Nouvelle chaîne SQL, ou NULL. */ -static char *schema_load_v1_sql( - Database *database +static char *schema_load_sql( + Database *database, + const char *schema_path, + const char *schema_name ) { char *schema_sql = NULL; GError *error = NULL; + if (database == NULL || + schema_path == NULL || + schema_path[0] == '\0' || + schema_name == NULL || + schema_name[0] == '\0') + { + return NULL; + } + if (!g_file_get_contents( - "database/schema_v1.sql", + schema_path, &schema_sql, NULL, &error @@ -37,17 +49,21 @@ static char *schema_load_v1_sql( DATABASE_ERROR_INVALID_STATE, error != NULL ? error->message - : "Impossible de charger le schéma SQLite V1." + : "Impossible de charger le schéma SQLite." ); g_warning( - "Impossible de charger database/schema_v1.sql : %s", + "Impossible de charger %s depuis '%s' : %s", + schema_name, + schema_path, error != NULL ? error->message : "erreur inconnue" ); - g_clear_error(&error); + g_clear_error( + &error + ); return NULL; } @@ -55,11 +71,19 @@ static char *schema_load_v1_sql( return schema_sql; } -bool schema_install_v1( - Database *database +/** + * @brief Exécute le contenu d’un fichier de schéma SQL. + * + * La transaction reste sous la responsabilité du code appelant. + */ +static bool schema_execute_file( + Database *database, + const char *schema_path, + const char *schema_name ) { sqlite3 *database_handle = NULL; + char *schema_sql = NULL; char *error_message = NULL; @@ -70,9 +94,10 @@ bool schema_install_v1( return false; } - database_handle = database_get_handle( - database - ); + database_handle = + database_get_handle( + database + ); if (database_handle == NULL) { @@ -85,24 +110,30 @@ bool schema_install_v1( return false; } - schema_sql = schema_load_v1_sql( - database - ); + schema_sql = + schema_load_sql( + database, + schema_path, + schema_name + ); if (schema_sql == NULL) { return false; } - result = sqlite3_exec( - database_handle, - schema_sql, - NULL, - NULL, - &error_message - ); + result = + sqlite3_exec( + database_handle, + schema_sql, + NULL, + NULL, + &error_message + ); - g_free(schema_sql); + g_free( + schema_sql + ); if (result != SQLITE_OK) { @@ -115,18 +146,23 @@ bool schema_install_v1( ); g_warning( - "Impossible d'installer le schéma SQLite V1 : %s", + "Impossible d’installer %s : %s", + schema_name, error_message != NULL ? error_message : sqlite3_errmsg(database_handle) ); - sqlite3_free(error_message); + sqlite3_free( + error_message + ); return false; } - sqlite3_free(error_message); + sqlite3_free( + error_message + ); database_clear_error_internal( database @@ -134,3 +170,25 @@ bool schema_install_v1( return true; } + +bool schema_install_v1( + Database *database +) +{ + return schema_execute_file( + database, + "database/schema_v1.sql", + "le schéma SQLite V1" + ); +} + +bool schema_install_v2( + Database *database +) +{ + return schema_execute_file( + database, + "database/schema_v2.sql", + "la migration SQLite V2" + ); +} diff --git a/tests/test_database b/tests/test_database index 2a1ae25..a93ca32 100755 Binary files a/tests/test_database and b/tests/test_database differ diff --git a/tests/test_database.c b/tests/test_database.c index 1741900..edaa7f8 100644 --- a/tests/test_database.c +++ b/tests/test_database.c @@ -4,6 +4,7 @@ ******************************************************************************/ #include "database/database.h" +#include "database/error.h" #include #include @@ -69,6 +70,183 @@ static char *test_database_read_single_text( return result_text; } +/** + * @brief Exécute une ou plusieurs instructions SQL de test. + */ +static void test_database_execute_sql( + sqlite3 *database, + const char *sql +) +{ + char *error_message = NULL; + int result = SQLITE_ERROR; + + assert(database != NULL); + assert(sql != NULL); + assert(sql[0] != '\0'); + + result = sqlite3_exec( + database, + sql, + NULL, + NULL, + &error_message + ); + + if (result != SQLITE_OK) + { + fprintf( + stderr, + "Erreur SQL de test : %s\n", + error_message != NULL + ? error_message + : sqlite3_errmsg(database) + ); + } + + assert(result == SQLITE_OK); + + sqlite3_free( + error_message + ); +} + +/** + * @brief Crée une base V1 contenant une ancienne preuve. + * + * La base est construite directement avec schema_v1.sql afin de tester + * le véritable chemin de migration, sans passer par database_initialize() + * qui crée désormais directement une base V2. + */ +static void test_database_create_v1_database( + const char *database_path +) +{ + sqlite3 *database = NULL; + + char *schema_sql = NULL; + + GError *error = NULL; + + int result = SQLITE_ERROR; + + assert(database_path != NULL); + assert(database_path[0] != '\0'); + + result = sqlite3_open_v2( + database_path, + &database, + SQLITE_OPEN_READWRITE | + SQLITE_OPEN_CREATE, + NULL + ); + + assert(result == SQLITE_OK); + assert(database != NULL); + + assert( + g_file_get_contents( + "database/schema_v1.sql", + &schema_sql, + NULL, + &error + ) + ); + + assert(error == NULL); + assert(schema_sql != NULL); + + test_database_execute_sql( + database, + "BEGIN IMMEDIATE;" + ); + + test_database_execute_sql( + database, + schema_sql + ); + + test_database_execute_sql( + database, + "INSERT INTO metadata" + "(" + " key," + " value" + ")" + "VALUES" + " ('schema_version', '1')," + " ('application', 'Labfy Investigation')," + " ('created_at', '2026-07-18T10:00:00Z')," + " (" + " 'investigation_uuid'," + " '11111111-1111-4111-8111-111111111111'" + " );" + ); + + /* + * Cette ligne utilise uniquement les colonnes du schéma V1. + * + * La migration devra conserver la ligne et copier name dans + * original_name. + */ + test_database_execute_sql( + database, + "INSERT INTO preuves" + "(" + " id," + " name," + " relative_path," + " type_id," + " size_bytes," + " sha256," + " mime_type," + " description," + " commentaire," + " categorie_id," + " file_created_at," + " imported_at," + " updated_at," + " status," + " locked" + ")" + "VALUES" + "(" + " '22222222-2222-4222-8222-222222222222'," + " 'ancienne_capture.png'," + " '01_Preuves_Originales/ancienne_capture.png'," + " (SELECT id FROM types_preuve " + " WHERE code = 'screenshot')," + " 128," + " '0123456789abcdef0123456789abcdef" + "0123456789abcdef0123456789abcdef'," + " 'image/png'," + " 'Preuve créée avec le schéma V1.'," + " NULL," + " NULL," + " NULL," + " '2026-07-18T10:00:00Z'," + " '2026-07-18T10:00:00Z'," + " 'active'," + " 1" + ");" + ); + + test_database_execute_sql( + database, + "COMMIT;" + ); + + g_free( + schema_sql + ); + + result = sqlite3_close( + database + ); + + assert(result == SQLITE_OK); +} + /** * @brief Vérifie qu'une table existe. */ @@ -116,6 +294,75 @@ static void test_database_assert_table_exists( assert(result == SQLITE_OK); } +/** + * @brief Vérifie qu’une colonne existe dans une table. + */ +static void test_database_assert_column_exists( + sqlite3 *database, + const char *table_name, + const char *column_name +) +{ + sqlite3_stmt *statement = NULL; + int result = SQLITE_ERROR; + + assert(database != NULL); + assert(table_name != NULL); + assert(column_name != NULL); + + result = sqlite3_prepare_v2( + database, + "SELECT COUNT(*) " + "FROM pragma_table_info(?) " + "WHERE name = ?;", + -1, + &statement, + NULL + ); + + assert(result == SQLITE_OK); + assert(statement != NULL); + + result = sqlite3_bind_text( + statement, + 1, + table_name, + -1, + SQLITE_TRANSIENT + ); + + assert(result == SQLITE_OK); + + result = sqlite3_bind_text( + statement, + 2, + column_name, + -1, + SQLITE_TRANSIENT + ); + + assert(result == SQLITE_OK); + + result = sqlite3_step( + statement + ); + + assert(result == SQLITE_ROW); + + assert( + sqlite3_column_int( + statement, + 0 + ) == 1 + ); + + result = sqlite3_finalize( + statement + ); + + assert(result == SQLITE_OK); +} + /** * @brief Vérifie l'initialisation complète d'une base. */ @@ -189,6 +436,34 @@ static void test_database_initialize_valid_database(void) database, "investigation" ); + test_database_assert_table_exists( + database, + "preuves" + ); + + test_database_assert_column_exists( + database, + "preuves", + "original_name" + ); + + test_database_assert_column_exists( + database, + "preuves", + "collected_at" + ); + + test_database_assert_column_exists( + database, + "preuves", + "source" + ); + + test_database_assert_column_exists( + database, + "preuves", + "integrity_status" + ); schema_version = test_database_read_single_text( database, @@ -250,7 +525,7 @@ static void test_database_initialize_valid_database(void) "FROM investigation;" ); - assert(strcmp(schema_version, "1") == 0); + assert(strcmp(schema_version, "2") == 0); assert(strcmp(application_name, "Labfy Investigation") == 0); assert(created_at[0] != '\0'); @@ -519,10 +794,611 @@ static void test_database_initialize_rollback(void) g_free(temporary_directory); } +/** + * @brief Vérifie la migration V1 vers V2 et sa répétition sans effet. + */ +static void test_database_migrate_v1_to_v2(void) +{ + char *temporary_directory = NULL; + char *database_path = NULL; + + char *schema_version = NULL; + char *original_name = NULL; + char *internal_name = NULL; + char *relative_path = NULL; + char *integrity_status = NULL; + char *evidence_count = NULL; + char *v2_column_count = NULL; + char *migration_index_count = NULL; + char *migration_trigger_count = NULL; + + Database *database_context = NULL; + sqlite3 *database = NULL; + + GError *error = NULL; + + int result = SQLITE_ERROR; + + temporary_directory = + g_dir_make_tmp( + "labfy-database-migration-test-XXXXXX", + &error + ); + + assert(temporary_directory != NULL); + assert(error == NULL); + + database_path = + g_build_filename( + temporary_directory, + "Enquete.sqlite", + NULL + ); + + assert(database_path != NULL); + + test_database_create_v1_database( + database_path + ); + + database_context = + database_open( + database_path + ); + + assert(database_context != NULL); + + /* + * Premier appel : migration réelle de V1 vers V2. + */ + assert( + database_migrate_to_latest( + database_context + ) + ); + + assert( + database_error_get_code( + database_context + ) == DATABASE_ERROR_NONE + ); + + /* + * Second appel : la base est déjà en V2. + * + * Aucun ALTER TABLE, index ou trigger ne doit être rejoué. + */ + assert( + database_migrate_to_latest( + database_context + ) + ); + + assert( + database_error_get_code( + database_context + ) == DATABASE_ERROR_NONE + ); + + database_close( + database_context + ); + + result = sqlite3_open_v2( + database_path, + &database, + SQLITE_OPEN_READONLY, + NULL + ); + + assert(result == SQLITE_OK); + assert(database != NULL); + + schema_version = + test_database_read_single_text( + database, + "SELECT value " + "FROM metadata " + "WHERE key = 'schema_version';" + ); + + evidence_count = + test_database_read_single_text( + database, + "SELECT CAST(COUNT(*) AS TEXT) " + "FROM preuves;" + ); + + internal_name = + test_database_read_single_text( + database, + "SELECT name " + "FROM preuves " + "WHERE id = " + "'22222222-2222-4222-8222-222222222222';" + ); + + original_name = + test_database_read_single_text( + database, + "SELECT original_name " + "FROM preuves " + "WHERE id = " + "'22222222-2222-4222-8222-222222222222';" + ); + + relative_path = + test_database_read_single_text( + database, + "SELECT relative_path " + "FROM preuves " + "WHERE id = " + "'22222222-2222-4222-8222-222222222222';" + ); + + integrity_status = + test_database_read_single_text( + database, + "SELECT CAST(integrity_status AS TEXT) " + "FROM preuves " + "WHERE id = " + "'22222222-2222-4222-8222-222222222222';" + ); + + v2_column_count = + test_database_read_single_text( + database, + "SELECT CAST(COUNT(*) AS TEXT) " + "FROM pragma_table_info('preuves') " + "WHERE name IN" + "(" + " 'original_name'," + " 'collected_at'," + " 'source'," + " 'integrity_status'" + ");" + ); + + migration_index_count = + test_database_read_single_text( + database, + "SELECT CAST(COUNT(*) AS TEXT) " + "FROM sqlite_master " + "WHERE type = 'index' " + "AND name = 'idx_preuves_imported_at';" + ); + + migration_trigger_count = + test_database_read_single_text( + database, + "SELECT CAST(COUNT(*) AS TEXT) " + "FROM sqlite_master " + "WHERE type = 'trigger' " + "AND name IN" + "(" + " 'preuves_v2_validate_insert'," + " 'preuves_v2_validate_update'" + ");" + ); + + assert( + strcmp( + schema_version, + "2" + ) == 0 + ); + + /* + * La ligne V1 doit être conservée. + */ + assert( + strcmp( + evidence_count, + "1" + ) == 0 + ); + + assert( + strcmp( + internal_name, + "ancienne_capture.png" + ) == 0 + ); + + /* + * Le backfill V2 doit recopier name dans original_name. + */ + assert( + strcmp( + original_name, + "ancienne_capture.png" + ) == 0 + ); + + assert( + strcmp( + relative_path, + "01_Preuves_Originales/ancienne_capture.png" + ) == 0 + ); + + /* + * Une ancienne preuve n’ayant jamais été contrôlée reçoit UNKNOWN. + */ + assert( + strcmp( + integrity_status, + "0" + ) == 0 + ); + + /* + * Les quatre colonnes ne doivent exister qu’une fois chacune. + */ + assert( + strcmp( + v2_column_count, + "4" + ) == 0 + ); + + assert( + strcmp( + migration_index_count, + "1" + ) == 0 + ); + + assert( + strcmp( + migration_trigger_count, + "2" + ) == 0 + ); + + result = sqlite3_close( + database + ); + + assert(result == SQLITE_OK); + + assert( + g_remove( + database_path + ) == 0 + ); + + assert( + g_rmdir( + temporary_directory + ) == 0 + ); + + g_free( + migration_trigger_count + ); + + g_free( + migration_index_count + ); + + g_free( + v2_column_count + ); + + g_free( + evidence_count + ); + + g_free( + integrity_status + ); + + g_free( + relative_path + ); + + g_free( + internal_name + ); + + g_free( + original_name + ); + + g_free( + schema_version + ); + + g_free( + database_path + ); + + g_free( + temporary_directory + ); +} + +/** + * @brief Vérifie qu’une migration V2 échouée est entièrement annulée. + */ +static void test_database_migration_rollback(void) +{ + char *temporary_directory = NULL; + char *database_path = NULL; + + char *schema_version = NULL; + char *original_name_column_count = NULL; + char *collected_at_column_count = NULL; + char *source_column_count = NULL; + char *integrity_status_column_count = NULL; + char *evidence_count = NULL; + char *integrity_result = NULL; + + Database *database_context = NULL; + sqlite3 *database = NULL; + + GError *error = NULL; + + int result = SQLITE_ERROR; + + temporary_directory = + g_dir_make_tmp( + "labfy-database-migration-rollback-test-XXXXXX", + &error + ); + + assert(temporary_directory != NULL); + assert(error == NULL); + + database_path = + g_build_filename( + temporary_directory, + "Enquete.sqlite", + NULL + ); + + assert(database_path != NULL); + + test_database_create_v1_database( + database_path + ); + + result = sqlite3_open_v2( + database_path, + &database, + SQLITE_OPEN_READWRITE, + NULL + ); + + assert(result == SQLITE_OK); + assert(database != NULL); + + /* + * La migration V2 essaiera de créer un index portant ce nom. + * Le conflit doit provoquer un échec après les ALTER TABLE. + */ + test_database_execute_sql( + database, + "CREATE INDEX idx_preuves_imported_at " + "ON preuves(name);" + ); + + result = sqlite3_close( + database + ); + + assert(result == SQLITE_OK); + + database = NULL; + + database_context = + database_open( + database_path + ); + + assert(database_context != NULL); + + assert( + !database_migrate_to_latest( + database_context + ) + ); + + assert( + database_error_get_code( + database_context + ) != DATABASE_ERROR_NONE + ); + + database_close( + database_context + ); + + result = sqlite3_open_v2( + database_path, + &database, + SQLITE_OPEN_READONLY, + NULL + ); + + assert(result == SQLITE_OK); + assert(database != NULL); + + schema_version = + test_database_read_single_text( + database, + "SELECT value " + "FROM metadata " + "WHERE key = 'schema_version';" + ); + + original_name_column_count = + test_database_read_single_text( + database, + "SELECT CAST(COUNT(*) AS TEXT) " + "FROM pragma_table_info('preuves') " + "WHERE name = 'original_name';" + ); + + collected_at_column_count = + test_database_read_single_text( + database, + "SELECT CAST(COUNT(*) AS TEXT) " + "FROM pragma_table_info('preuves') " + "WHERE name = 'collected_at';" + ); + + source_column_count = + test_database_read_single_text( + database, + "SELECT CAST(COUNT(*) AS TEXT) " + "FROM pragma_table_info('preuves') " + "WHERE name = 'source';" + ); + + integrity_status_column_count = + test_database_read_single_text( + database, + "SELECT CAST(COUNT(*) AS TEXT) " + "FROM pragma_table_info('preuves') " + "WHERE name = 'integrity_status';" + ); + + evidence_count = + test_database_read_single_text( + database, + "SELECT CAST(COUNT(*) AS TEXT) " + "FROM preuves;" + ); + + integrity_result = + test_database_read_single_text( + database, + "PRAGMA integrity_check;" + ); + + /* + * La version ne doit pas avancer lorsque le SQL V2 échoue. + */ + assert( + strcmp( + schema_version, + "1" + ) == 0 + ); + + /* + * Les ALTER TABLE précédant l’erreur doivent avoir été annulés. + */ + assert( + strcmp( + original_name_column_count, + "0" + ) == 0 + ); + + assert( + strcmp( + collected_at_column_count, + "0" + ) == 0 + ); + + assert( + strcmp( + source_column_count, + "0" + ) == 0 + ); + + assert( + strcmp( + integrity_status_column_count, + "0" + ) == 0 + ); + + /* + * La preuve V1 doit rester intacte. + */ + assert( + strcmp( + evidence_count, + "1" + ) == 0 + ); + + assert( + strcmp( + integrity_result, + "ok" + ) == 0 + ); + + result = sqlite3_close( + database + ); + + assert(result == SQLITE_OK); + + assert( + g_remove( + database_path + ) == 0 + ); + + assert( + g_rmdir( + temporary_directory + ) == 0 + ); + + g_free( + integrity_result + ); + + g_free( + evidence_count + ); + + g_free( + integrity_status_column_count + ); + + g_free( + source_column_count + ); + + g_free( + collected_at_column_count + ); + + g_free( + original_name_column_count + ); + + g_free( + schema_version + ); + + g_free( + database_path + ); + + g_free( + temporary_directory + ); +} + int main(void) { test_database_initialize_valid_database(); test_database_initialize_rollback(); + + test_database_migrate_v1_to_v2(); + test_database_migration_rollback(); + test_database_initialize_invalid_parameters(); test_database_initialize_missing_parent(); diff --git a/tests/test_error b/tests/test_error index 1185635..674f2f5 100755 Binary files a/tests/test_error and b/tests/test_error differ diff --git a/tests/test_evidence_dao b/tests/test_evidence_dao new file mode 100755 index 0000000..33d02a4 Binary files /dev/null and b/tests/test_evidence_dao differ diff --git a/tests/test_evidence_dao.c b/tests/test_evidence_dao.c new file mode 100644 index 0000000..98bd754 --- /dev/null +++ b/tests/test_evidence_dao.c @@ -0,0 +1,1773 @@ +/****************************************************************************** + * @file test_evidence_dao.c + * @brief Tests du DAO des preuves numériques. + ******************************************************************************/ + +#include "dao/evidence_dao.h" + +#include "database/database.h" +#include "database/statement.h" +#include "models/evidence_record.h" + +#include +#include + +#include +#include + +/** + * @brief Vérifie le refus d’une connexion absente. + */ +static void test_evidence_dao_new_null_database(void) +{ + EvidenceDao *evidence_dao = NULL; + GError *error = NULL; + + evidence_dao = evidence_dao_new( + NULL, + &error + ); + + assert(evidence_dao == NULL); + assert(error != NULL); + + assert( + error->domain == + EVIDENCE_DAO_ERROR + ); + + assert( + error->code == + (gint) EVIDENCE_DAO_ERROR_INVALID_ARGUMENT + ); + + assert(error->message != NULL); + assert(error->message[0] != '\0'); + + g_clear_error( + &error + ); +} + +/** + * @brief Environnement temporaire d’un test EvidenceDao. + */ +typedef struct +{ + char *temporary_directory; + char *database_path; + + Database *database; + EvidenceDao *evidence_dao; +} TestEvidenceDaoFixture; + +static TestEvidenceDaoFixture test_evidence_dao_fixture_create(void); + +static void test_evidence_dao_fixture_clear( + TestEvidenceDaoFixture *fixture +); + +/** + * @brief Retourne le nombre de preuves présentes dans la base de test. + */ +static int64_t test_evidence_dao_count_rows( + Database *database +) +{ + DatabaseStatement *statement = NULL; + int64_t evidence_count = -1; + + assert(database != NULL); + + statement = + database_statement_prepare( + database, + "SELECT COUNT(*) FROM preuves;" + ); + + assert(statement != NULL); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_ROW + ); + + assert( + database_statement_column_int64( + statement, + 0, + &evidence_count + ) + ); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize( + statement + ); + + return evidence_count; +} + +/** + * @brief Vérifie le refus d’un identifiant déjà utilisé. + */ +static void test_evidence_dao_insert_duplicate_identifier(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + EvidenceRecord *first_record = NULL; + EvidenceRecord *second_record = NULL; + + GError *error = NULL; + + first_record = + evidence_record_new( + "55555555-5555-4555-8555-555555555555", + "capture_1.png", + "capture_001.png", + "01_Preuves_Originales/Captures_Ecran/capture_001.png", + "screenshot", + 100, + "dddddddddddddddddddddddddddddddd" + "dddddddddddddddddddddddddddddddd", + "2026-07-18T18:00:00Z", + NULL, + NULL, + NULL, + EVIDENCE_INTEGRITY_STATUS_UNKNOWN, + &error + ); + + assert(first_record != NULL); + assert(error == NULL); + + second_record = + evidence_record_new( + "55555555-5555-4555-8555-555555555555", + "capture_2.png", + "capture_002.png", + "01_Preuves_Originales/Captures_Ecran/capture_002.png", + "screenshot", + 200, + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "2026-07-18T18:05:00Z", + NULL, + NULL, + NULL, + EVIDENCE_INTEGRITY_STATUS_UNKNOWN, + &error + ); + + assert(second_record != NULL); + assert(error == NULL); + + assert( + evidence_dao_insert( + fixture.evidence_dao, + first_record, + &error + ) + ); + + assert(error == NULL); + + assert( + !evidence_dao_insert( + fixture.evidence_dao, + second_record, + &error + ) + ); + + assert(error != NULL); + assert(error->domain == EVIDENCE_DAO_ERROR); + + assert( + error->code == + (gint) EVIDENCE_DAO_ERROR_CONSTRAINT + ); + + assert( + strstr( + error->message, + "identifiant" + ) != NULL + ); + + assert( + test_evidence_dao_count_rows( + fixture.database + ) == 1 + ); + + g_clear_error( + &error + ); + + evidence_record_free( + second_record + ); + + evidence_record_free( + first_record + ); + + test_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d’un chemin relatif déjà utilisé. + */ +static void test_evidence_dao_insert_duplicate_relative_path(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + EvidenceRecord *first_record = NULL; + EvidenceRecord *second_record = NULL; + + GError *error = NULL; + + const char *shared_relative_path = + "01_Preuves_Originales/Documents/document_commun.pdf"; + + first_record = + evidence_record_new( + "66666666-6666-4666-8666-666666666666", + "document_1.pdf", + "document_commun.pdf", + shared_relative_path, + "document", + 300, + "ffffffffffffffffffffffffffffffff" + "ffffffffffffffffffffffffffffffff", + "2026-07-18T18:10:00Z", + NULL, + NULL, + NULL, + EVIDENCE_INTEGRITY_STATUS_UNKNOWN, + &error + ); + + assert(first_record != NULL); + assert(error == NULL); + + second_record = + evidence_record_new( + "77777777-7777-4777-8777-777777777777", + "document_2.pdf", + "document_commun.pdf", + shared_relative_path, + "document", + 400, + "11111111111111111111111111111111" + "11111111111111111111111111111111", + "2026-07-18T18:15:00Z", + NULL, + NULL, + NULL, + EVIDENCE_INTEGRITY_STATUS_UNKNOWN, + &error + ); + + assert(second_record != NULL); + assert(error == NULL); + + assert( + evidence_dao_insert( + fixture.evidence_dao, + first_record, + &error + ) + ); + + assert(error == NULL); + + assert( + !evidence_dao_insert( + fixture.evidence_dao, + second_record, + &error + ) + ); + + assert(error != NULL); + assert(error->domain == EVIDENCE_DAO_ERROR); + + assert( + error->code == + (gint) EVIDENCE_DAO_ERROR_CONSTRAINT + ); + + assert( + strstr( + error->message, + "chemin relatif" + ) != NULL + ); + + assert( + test_evidence_dao_count_rows( + fixture.database + ) == 1 + ); + + g_clear_error( + &error + ); + + evidence_record_free( + second_record + ); + + evidence_record_free( + first_record + ); + + test_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d’un type absent du catalogue SQLite. + */ +static void test_evidence_dao_insert_unknown_type(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + EvidenceRecord *evidence_record = NULL; + + GError *error = NULL; + + evidence_record = + evidence_record_new( + "88888888-8888-4888-8888-888888888888", + "preuve.bin", + "preuve_001.bin", + "01_Preuves_Originales/Autres/preuve_001.bin", + "type_inconnu", + 500, + "22222222222222222222222222222222" + "22222222222222222222222222222222", + "2026-07-18T18:20:00Z", + NULL, + NULL, + NULL, + EVIDENCE_INTEGRITY_STATUS_UNKNOWN, + &error + ); + + assert(evidence_record != NULL); + assert(error == NULL); + + assert( + !evidence_dao_insert( + fixture.evidence_dao, + evidence_record, + &error + ) + ); + + assert(error != NULL); + assert(error->domain == EVIDENCE_DAO_ERROR); + + assert( + error->code == + (gint) EVIDENCE_DAO_ERROR_CONSTRAINT + ); + + assert( + strstr( + error->message, + "type de preuve" + ) != NULL + ); + + assert( + test_evidence_dao_count_rows( + fixture.database + ) == 0 + ); + + g_clear_error( + &error + ); + + evidence_record_free( + evidence_record + ); + + test_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Crée une base V2 temporaire et son DAO. + */ +static TestEvidenceDaoFixture test_evidence_dao_fixture_create(void) +{ + TestEvidenceDaoFixture fixture = {0}; + + GError *error = NULL; + + fixture.temporary_directory = + g_dir_make_tmp( + "labfy-evidence-dao-test-XXXXXX", + &error + ); + + assert(fixture.temporary_directory != NULL); + assert(error == NULL); + + fixture.database_path = + g_build_filename( + fixture.temporary_directory, + "Enquete.sqlite", + NULL + ); + + assert(fixture.database_path != NULL); + + assert( + database_initialize( + fixture.database_path, + "Enquete_EvidenceDao", + fixture.temporary_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); + + return fixture; +} + +/** + * @brief Détruit une fixture et ses fichiers temporaires. + */ +static void test_evidence_dao_fixture_clear( + TestEvidenceDaoFixture *fixture +) +{ + assert(fixture != NULL); + + evidence_dao_free( + fixture->evidence_dao + ); + + database_close( + fixture->database + ); + + assert( + g_remove( + fixture->database_path + ) == 0 + ); + + assert( + g_rmdir( + fixture->temporary_directory + ) == 0 + ); + + g_free( + fixture->database_path + ); + + g_free( + fixture->temporary_directory + ); + + fixture->evidence_dao = NULL; + fixture->database = NULL; + fixture->database_path = NULL; + fixture->temporary_directory = NULL; +} + +/** + * @brief Vérifie la création et la libération d’un DAO valide. + */ +static void test_evidence_dao_new_valid(void) +{ + Database *database = NULL; + EvidenceDao *evidence_dao = NULL; + GError *error = NULL; + + database = database_open( + ":memory:" + ); + + assert(database != NULL); + + evidence_dao = evidence_dao_new( + database, + &error + ); + + assert(evidence_dao != NULL); + assert(error == NULL); + + /* + * La libération du DAO ne doit pas fermer Database. + */ + evidence_dao_free( + evidence_dao + ); + + database_close( + database + ); +} + +/** + * @brief Vérifie que la destruction accepte NULL. + */ +static void test_evidence_dao_free_null(void) +{ + evidence_dao_free( + NULL + ); +} + +/** + * @brief Vérifie l’insertion complète d’une preuve valide. + */ +static void test_evidence_dao_insert_valid_full(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + EvidenceRecord *evidence_record = NULL; + DatabaseStatement *statement = NULL; + + char *identifier = NULL; + char *original_name = NULL; + char *internal_name = NULL; + char *relative_path = NULL; + char *type_identifier = NULL; + char *sha256 = NULL; + char *imported_at = NULL; + char *collected_at = NULL; + char *source = NULL; + char *description = NULL; + + int64_t size_bytes = -1; + int64_t integrity_status = -1; + + GError *error = NULL; + + evidence_record = + evidence_record_new( + "11111111-1111-4111-8111-111111111111", + "Capture Instagram.png", + "capture_instagram_001.png", + "01_Preuves_Originales/Captures_Ecran/" + "capture_instagram_001.png", + "screenshot", + 4096, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "2026-07-18T16:00:00Z", + "2026-07-17T20:30:00Z", + "Compte Instagram communiqué par la victime.", + "Capture originale de la conversation.", + EVIDENCE_INTEGRITY_STATUS_VALID, + &error + ); + + assert(evidence_record != NULL); + assert(error == NULL); + + assert( + evidence_dao_insert( + fixture.evidence_dao, + evidence_record, + &error + ) + ); + + assert(error == NULL); + + statement = + database_statement_prepare( + fixture.database, + "SELECT " + " preuves.id," + " preuves.original_name," + " preuves.name," + " preuves.relative_path," + " types_preuve.code," + " preuves.size_bytes," + " preuves.sha256," + " preuves.imported_at," + " preuves.collected_at," + " preuves.source," + " preuves.description," + " preuves.integrity_status " + "FROM preuves " + "INNER JOIN types_preuve " + "ON types_preuve.id = preuves.type_id " + "WHERE preuves.id = ?;" + ); + + assert(statement != NULL); + + assert( + database_statement_bind_text( + statement, + 1, + "11111111-1111-4111-8111-111111111111" + ) + ); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_ROW + ); + + assert( + database_statement_column_text( + statement, + 0, + &identifier + ) + ); + + assert( + database_statement_column_text( + statement, + 1, + &original_name + ) + ); + + assert( + database_statement_column_text( + statement, + 2, + &internal_name + ) + ); + + assert( + database_statement_column_text( + statement, + 3, + &relative_path + ) + ); + + assert( + database_statement_column_text( + statement, + 4, + &type_identifier + ) + ); + + assert( + database_statement_column_int64( + statement, + 5, + &size_bytes + ) + ); + + assert( + database_statement_column_text( + statement, + 6, + &sha256 + ) + ); + + assert( + database_statement_column_text( + statement, + 7, + &imported_at + ) + ); + + assert( + database_statement_column_text( + statement, + 8, + &collected_at + ) + ); + + assert( + database_statement_column_text( + statement, + 9, + &source + ) + ); + + assert( + database_statement_column_text( + statement, + 10, + &description + ) + ); + + assert( + database_statement_column_int64( + statement, + 11, + &integrity_status + ) + ); + + /* + * L’identifiant est unique : aucune seconde ligne ne doit exister. + */ + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_DONE + ); + + assert( + strcmp( + identifier, + "11111111-1111-4111-8111-111111111111" + ) == 0 + ); + + assert( + strcmp( + original_name, + "Capture Instagram.png" + ) == 0 + ); + + assert( + strcmp( + internal_name, + "capture_instagram_001.png" + ) == 0 + ); + + assert( + strcmp( + relative_path, + "01_Preuves_Originales/Captures_Ecran/" + "capture_instagram_001.png" + ) == 0 + ); + + assert( + strcmp( + type_identifier, + "screenshot" + ) == 0 + ); + + assert(size_bytes == 4096); + + assert( + strcmp( + sha256, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ) == 0 + ); + + assert( + strcmp( + imported_at, + "2026-07-18T16:00:00Z" + ) == 0 + ); + + assert( + strcmp( + collected_at, + "2026-07-17T20:30:00Z" + ) == 0 + ); + + assert( + strcmp( + source, + "Compte Instagram communiqué par la victime." + ) == 0 + ); + + assert( + strcmp( + description, + "Capture originale de la conversation." + ) == 0 + ); + + assert( + integrity_status == + EVIDENCE_INTEGRITY_STATUS_VALID + ); + + database_statement_finalize( + statement + ); + + g_free(description); + g_free(source); + g_free(collected_at); + g_free(imported_at); + g_free(sha256); + g_free(type_identifier); + g_free(relative_path); + g_free(internal_name); + g_free(original_name); + g_free(identifier); + + evidence_record_free( + evidence_record + ); + + test_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie l’insertion d’une preuve dont les champs facultatifs + * sont absents. + */ +static void test_evidence_dao_insert_optional_null(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + EvidenceRecord *evidence_record = NULL; + DatabaseStatement *statement = NULL; + + bool collected_at_is_null = false; + bool source_is_null = false; + bool description_is_null = false; + + GError *error = NULL; + + evidence_record = + evidence_record_new( + "22222222-2222-4222-8222-222222222222", + "document.pdf", + "document_001.pdf", + "01_Preuves_Originales/Documents/document_001.pdf", + "document", + 2048, + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "2026-07-18T16:30:00Z", + NULL, + NULL, + NULL, + EVIDENCE_INTEGRITY_STATUS_UNKNOWN, + &error + ); + + assert(evidence_record != NULL); + assert(error == NULL); + + assert( + evidence_dao_insert( + fixture.evidence_dao, + evidence_record, + &error + ) + ); + + assert(error == NULL); + + statement = + database_statement_prepare( + fixture.database, + "SELECT " + " collected_at," + " source," + " description " + "FROM preuves " + "WHERE id = ?;" + ); + + assert(statement != NULL); + + assert( + database_statement_bind_text( + statement, + 1, + "22222222-2222-4222-8222-222222222222" + ) + ); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_ROW + ); + + assert( + database_statement_column_is_null( + statement, + 0, + &collected_at_is_null + ) + ); + + assert( + database_statement_column_is_null( + statement, + 1, + &source_is_null + ) + ); + + assert( + database_statement_column_is_null( + statement, + 2, + &description_is_null + ) + ); + + assert(collected_at_is_null); + assert(source_is_null); + assert(description_is_null); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize( + statement + ); + + evidence_record_free( + evidence_record + ); + + test_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie que deux preuves distinctes peuvent partager le même hash. + * + * Un même fichier peut être présent à plusieurs emplacements ou provenir + * de plusieurs collectes. Le SHA-256 ne doit donc pas être unique. + */ +static void test_evidence_dao_insert_duplicate_sha256(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + EvidenceRecord *first_record = NULL; + EvidenceRecord *second_record = NULL; + + DatabaseStatement *statement = NULL; + + int64_t evidence_count = -1; + + GError *error = NULL; + + const char *shared_sha256 = + "cccccccccccccccccccccccccccccccc" + "cccccccccccccccccccccccccccccccc"; + + first_record = + evidence_record_new( + "33333333-3333-4333-8333-333333333333", + "capture_1.png", + "capture_001.png", + "01_Preuves_Originales/Captures_Ecran/capture_001.png", + "screenshot", + 1024, + shared_sha256, + "2026-07-18T17:00:00Z", + NULL, + NULL, + NULL, + EVIDENCE_INTEGRITY_STATUS_UNKNOWN, + &error + ); + + assert(first_record != NULL); + assert(error == NULL); + + second_record = + evidence_record_new( + "44444444-4444-4444-8444-444444444444", + "capture_2.png", + "capture_002.png", + "01_Preuves_Originales/Captures_Ecran/capture_002.png", + "screenshot", + 1024, + shared_sha256, + "2026-07-18T17:05:00Z", + NULL, + NULL, + NULL, + EVIDENCE_INTEGRITY_STATUS_UNKNOWN, + &error + ); + + assert(second_record != NULL); + assert(error == NULL); + + assert( + evidence_dao_insert( + fixture.evidence_dao, + first_record, + &error + ) + ); + + assert(error == NULL); + + assert( + evidence_dao_insert( + fixture.evidence_dao, + second_record, + &error + ) + ); + + assert(error == NULL); + + statement = + database_statement_prepare( + fixture.database, + "SELECT COUNT(*) " + "FROM preuves " + "WHERE sha256 = ?;" + ); + + assert(statement != NULL); + + assert( + database_statement_bind_text( + statement, + 1, + shared_sha256 + ) + ); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_ROW + ); + + assert( + database_statement_column_int64( + statement, + 0, + &evidence_count + ) + ); + + assert(evidence_count == 2); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize( + statement + ); + + evidence_record_free( + second_record + ); + + evidence_record_free( + first_record + ); + + test_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie la recherche d’une preuve existante. + */ +static void test_evidence_dao_find_existing(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + EvidenceRecord *inserted_record = NULL; + EvidenceRecord *loaded_record = NULL; + + GError *error = NULL; + + inserted_record = + evidence_record_new( + "99999999-9999-4999-8999-999999999999", + "conversation.png", + "conversation_001.png", + "01_Preuves_Originales/Captures_Ecran/" + "conversation_001.png", + "screenshot", + 8192, + "33333333333333333333333333333333" + "33333333333333333333333333333333", + "2026-07-18T19:00:00Z", + "2026-07-17T21:00:00Z", + "Conversation Instagram.", + "Capture conservée sans modification.", + EVIDENCE_INTEGRITY_STATUS_VALID, + &error + ); + + assert(inserted_record != NULL); + assert(error == NULL); + + assert( + evidence_dao_insert( + fixture.evidence_dao, + inserted_record, + &error + ) + ); + + assert(error == NULL); + + loaded_record = + evidence_dao_find_by_identifier( + fixture.evidence_dao, + "99999999-9999-4999-8999-999999999999", + &error + ); + + assert(loaded_record != NULL); + assert(error == NULL); + + assert( + strcmp( + evidence_record_get_identifier( + loaded_record + ), + "99999999-9999-4999-8999-999999999999" + ) == 0 + ); + + assert( + strcmp( + evidence_record_get_original_name( + loaded_record + ), + "conversation.png" + ) == 0 + ); + + assert( + strcmp( + evidence_record_get_type_identifier( + loaded_record + ), + "screenshot" + ) == 0 + ); + + assert( + evidence_record_get_size_bytes( + loaded_record + ) == 8192 + ); + + assert( + evidence_record_get_integrity_status( + loaded_record + ) == EVIDENCE_INTEGRITY_STATUS_VALID + ); + + evidence_record_free( + loaded_record + ); + + evidence_record_free( + inserted_record + ); + + test_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu’une preuve absente retourne NULL sans erreur. + */ +static void test_evidence_dao_find_missing(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + EvidenceRecord *evidence_record = NULL; + GError *error = NULL; + + evidence_record = + evidence_dao_find_by_identifier( + fixture.evidence_dao, + "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + &error + ); + + assert(evidence_record == NULL); + assert(error == NULL); + + test_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d’un identifiant de recherche invalide. + */ +static void test_evidence_dao_find_invalid_identifier(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + EvidenceRecord *evidence_record = NULL; + GError *error = NULL; + + evidence_record = + evidence_dao_find_by_identifier( + fixture.evidence_dao, + "identifiant-invalide", + &error + ); + + assert(evidence_record == NULL); + assert(error != NULL); + assert(error->domain == EVIDENCE_DAO_ERROR); + + assert( + error->code == + (gint) EVIDENCE_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + test_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le comptage d’une table de preuves vide. + */ +static void test_evidence_dao_count_empty(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + guint64 evidence_count = 42; + + GError *error = NULL; + + assert( + evidence_dao_count( + fixture.evidence_dao, + &evidence_count, + &error + ) + ); + + assert(error == NULL); + assert(evidence_count == 0); + + test_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le comptage de plusieurs preuves. + */ +static void test_evidence_dao_count_records(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + EvidenceRecord *first_record = NULL; + EvidenceRecord *second_record = NULL; + + guint64 evidence_count = 0; + + GError *error = NULL; + + first_record = + evidence_record_new( + "aaaaaaaa-1111-4111-8111-111111111111", + "preuve_1.png", + "preuve_001.png", + "01_Preuves_Originales/Captures_Ecran/preuve_001.png", + "screenshot", + 1000, + "44444444444444444444444444444444" + "44444444444444444444444444444444", + "2026-07-18T19:30:00Z", + NULL, + NULL, + NULL, + EVIDENCE_INTEGRITY_STATUS_UNKNOWN, + &error + ); + + assert(first_record != NULL); + assert(error == NULL); + + second_record = + evidence_record_new( + "bbbbbbbb-2222-4222-8222-222222222222", + "preuve_2.pdf", + "preuve_002.pdf", + "01_Preuves_Originales/Documents/preuve_002.pdf", + "document", + 2000, + "55555555555555555555555555555555" + "55555555555555555555555555555555", + "2026-07-18T19:35:00Z", + NULL, + NULL, + NULL, + EVIDENCE_INTEGRITY_STATUS_UNKNOWN, + &error + ); + + assert(second_record != NULL); + assert(error == NULL); + + assert( + evidence_dao_insert( + fixture.evidence_dao, + first_record, + &error + ) + ); + + assert(error == NULL); + + assert( + evidence_dao_insert( + fixture.evidence_dao, + second_record, + &error + ) + ); + + assert(error == NULL); + + assert( + evidence_dao_count( + fixture.evidence_dao, + &evidence_count, + &error + ) + ); + + assert(error == NULL); + assert(evidence_count == 2); + + evidence_record_free( + second_record + ); + + evidence_record_free( + first_record + ); + + test_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus des paramètres de comptage invalides. + */ +static void test_evidence_dao_count_invalid_arguments(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + guint64 evidence_count = 123; + + GError *error = NULL; + + assert( + !evidence_dao_count( + NULL, + &evidence_count, + &error + ) + ); + + assert(error != NULL); + assert(error->domain == EVIDENCE_DAO_ERROR); + + assert( + error->code == + (gint) EVIDENCE_DAO_ERROR_INVALID_ARGUMENT + ); + + /* + * Le DAO absent empêche la fonction d’accéder à la destination. + */ + assert(evidence_count == 123); + + g_clear_error( + &error + ); + + assert( + !evidence_dao_count( + fixture.evidence_dao, + NULL, + &error + ) + ); + + assert(error != NULL); + + assert( + error->code == + (gint) EVIDENCE_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + test_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le chargement d’une liste vide. + */ +static void test_evidence_dao_list_empty(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + GPtrArray *evidence_records = NULL; + GError *error = NULL; + + evidence_records = + evidence_dao_list_all( + fixture.evidence_dao, + &error + ); + + assert(evidence_records != NULL); + assert(error == NULL); + assert(evidence_records->len == 0); + + g_ptr_array_unref( + evidence_records + ); + + test_evidence_dao_fixture_clear( + &fixture + ); +} +/** + * @brief Vérifie le chargement de plusieurs preuves dans un ordre stable. + */ +static void test_evidence_dao_list_order(void) +{ + TestEvidenceDaoFixture fixture = + test_evidence_dao_fixture_create(); + + EvidenceRecord *first_inserted_record = NULL; + EvidenceRecord *second_inserted_record = NULL; + EvidenceRecord *third_inserted_record = NULL; + + const EvidenceRecord *first_loaded_record = NULL; + const EvidenceRecord *second_loaded_record = NULL; + const EvidenceRecord *third_loaded_record = NULL; + + GPtrArray *evidence_records = NULL; + GError *error = NULL; + + /* + * Cette preuve est insérée en premier, mais sa date est la plus tardive. + */ + third_inserted_record = + evidence_record_new( + "30000000-0000-4000-8000-000000000003", + "preuve_tardive.pdf", + "preuve_tardive.pdf", + "01_Preuves_Originales/Documents/preuve_tardive.pdf", + "document", + 3000, + "66666666666666666666666666666666" + "66666666666666666666666666666666", + "2026-07-18T21:00:00Z", + NULL, + NULL, + NULL, + EVIDENCE_INTEGRITY_STATUS_UNKNOWN, + &error + ); + + assert(third_inserted_record != NULL); + assert(error == NULL); + + second_inserted_record = + evidence_record_new( + "20000000-0000-4000-8000-000000000002", + "preuve_b.png", + "preuve_b.png", + "01_Preuves_Originales/Captures_Ecran/preuve_b.png", + "screenshot", + 2000, + "77777777777777777777777777777777" + "77777777777777777777777777777777", + "2026-07-18T20:00:00Z", + NULL, + NULL, + NULL, + EVIDENCE_INTEGRITY_STATUS_UNKNOWN, + &error + ); + + assert(second_inserted_record != NULL); + assert(error == NULL); + + first_inserted_record = + evidence_record_new( + "10000000-0000-4000-8000-000000000001", + "preuve_a.png", + "preuve_a.png", + "01_Preuves_Originales/Captures_Ecran/preuve_a.png", + "screenshot", + 1000, + "88888888888888888888888888888888" + "88888888888888888888888888888888", + "2026-07-18T20:00:00Z", + NULL, + NULL, + NULL, + EVIDENCE_INTEGRITY_STATUS_UNKNOWN, + &error + ); + + assert(first_inserted_record != NULL); + assert(error == NULL); + + assert( + evidence_dao_insert( + fixture.evidence_dao, + third_inserted_record, + &error + ) + ); + + assert(error == NULL); + + assert( + evidence_dao_insert( + fixture.evidence_dao, + second_inserted_record, + &error + ) + ); + + assert(error == NULL); + + assert( + evidence_dao_insert( + fixture.evidence_dao, + first_inserted_record, + &error + ) + ); + + assert(error == NULL); + + evidence_records = + evidence_dao_list_all( + fixture.evidence_dao, + &error + ); + + assert(evidence_records != NULL); + assert(error == NULL); + assert(evidence_records->len == 3); + + first_loaded_record = + g_ptr_array_index( + evidence_records, + 0 + ); + + second_loaded_record = + g_ptr_array_index( + evidence_records, + 1 + ); + + third_loaded_record = + g_ptr_array_index( + evidence_records, + 2 + ); + + assert(first_loaded_record != NULL); + assert(second_loaded_record != NULL); + assert(third_loaded_record != NULL); + + /* + * Les deux premières preuves partagent la même date. + * Leur identifiant sert donc de second critère de tri. + */ + assert( + strcmp( + evidence_record_get_identifier( + first_loaded_record + ), + "10000000-0000-4000-8000-000000000001" + ) == 0 + ); + + assert( + strcmp( + evidence_record_get_identifier( + second_loaded_record + ), + "20000000-0000-4000-8000-000000000002" + ) == 0 + ); + + assert( + strcmp( + evidence_record_get_identifier( + third_loaded_record + ), + "30000000-0000-4000-8000-000000000003" + ) == 0 + ); + + /* + * Le tableau libère lui-même les trois modèles chargés. + */ + g_ptr_array_unref( + evidence_records + ); + + evidence_record_free( + first_inserted_record + ); + + evidence_record_free( + second_inserted_record + ); + + evidence_record_free( + third_inserted_record + ); + + test_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d’un DAO absent. + */ +static void test_evidence_dao_list_invalid_arguments(void) +{ + GPtrArray *evidence_records = NULL; + GError *error = NULL; + + evidence_records = + evidence_dao_list_all( + NULL, + &error + ); + + assert(evidence_records == NULL); + assert(error != NULL); + assert(error->domain == EVIDENCE_DAO_ERROR); + + assert( + error->code == + (gint) EVIDENCE_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); +} + +int main(void) +{ + test_evidence_dao_new_null_database(); + test_evidence_dao_new_valid(); + test_evidence_dao_free_null(); + test_evidence_dao_insert_valid_full(); + test_evidence_dao_insert_valid_full(); + test_evidence_dao_insert_optional_null(); + test_evidence_dao_insert_duplicate_sha256(); + test_evidence_dao_insert_duplicate_identifier(); + test_evidence_dao_insert_duplicate_relative_path(); + test_evidence_dao_insert_unknown_type(); + test_evidence_dao_find_existing(); + test_evidence_dao_find_missing(); + test_evidence_dao_find_invalid_identifier(); + test_evidence_dao_count_empty(); + test_evidence_dao_count_records(); + test_evidence_dao_count_invalid_arguments(); + test_evidence_dao_list_empty(); + test_evidence_dao_list_order(); + test_evidence_dao_list_invalid_arguments(); + + printf( + "EvidenceDao : tests de construction valides.\n" + ); + + return 0; +} diff --git a/tests/test_evidence_record b/tests/test_evidence_record index f0e6983..9f4a34c 100755 Binary files a/tests/test_evidence_record and b/tests/test_evidence_record differ diff --git a/tests/test_evidence_record.c b/tests/test_evidence_record.c index 7419802..893e3bd 100644 --- a/tests/test_evidence_record.c +++ b/tests/test_evidence_record.c @@ -6,7 +6,6 @@ #include "models/evidence_record.h" #include -#include #define TEST_EVIDENCE_IDENTIFIER \ "6e62b9af-2046-4efd-b3b9-29869f816951" diff --git a/tests/test_investigation_dao b/tests/test_investigation_dao index 0234ddd..368bb9c 100755 Binary files a/tests/test_investigation_dao and b/tests/test_investigation_dao differ diff --git a/tests/test_investigation_project b/tests/test_investigation_project index 3bd6369..a5ef20f 100755 Binary files a/tests/test_investigation_project and b/tests/test_investigation_project differ diff --git a/tests/test_investigation_session b/tests/test_investigation_session index 04f8d66..a1e875f 100755 Binary files a/tests/test_investigation_session and b/tests/test_investigation_session differ diff --git a/tests/test_investigation_session.c b/tests/test_investigation_session.c index c82f368..55e7f50 100644 --- a/tests/test_investigation_session.c +++ b/tests/test_investigation_session.c @@ -548,7 +548,7 @@ static void test_open_invalid_database(void) test_assert_session_error( error, - INVESTIGATION_SESSION_ERROR_RECORD + INVESTIGATION_SESSION_ERROR_DATABASE ); g_clear_error(&error); diff --git a/tests/test_statement b/tests/test_statement index 79b937b..5d67d15 100755 Binary files a/tests/test_statement and b/tests/test_statement differ diff --git a/tests/test_transaction b/tests/test_transaction index 3533f19..d434f54 100755 Binary files a/tests/test_transaction and b/tests/test_transaction differ