/****************************************************************************** * @file test_database.c * @brief Tests d'intégration du module Database. ******************************************************************************/ #include "database/database.h" #include "database/error.h" #include #include #include #include #include #include /** * @brief Lit une valeur unique retournée par une requête SQL. * * @param database Base SQLite ouverte. * @param sql Requête retournant une seule colonne et une seule ligne. * * @return Une nouvelle chaîne à libérer avec g_free(), ou NULL. */ static char *test_database_read_single_text( sqlite3 *database, const char *sql ) { sqlite3_stmt *statement = NULL; const unsigned char *text = NULL; char *result_text = NULL; int result = SQLITE_ERROR; assert(database != NULL); assert(sql != NULL); result = sqlite3_prepare_v2( database, sql, -1, &statement, NULL ); assert(result == SQLITE_OK); assert(statement != NULL); result = sqlite3_step(statement); assert(result == SQLITE_ROW); text = sqlite3_column_text( statement, 0 ); assert(text != NULL); result_text = g_strdup( (const char *)text ); assert(result_text != NULL); result = sqlite3_finalize(statement); assert(result == SQLITE_OK); 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. */ static void test_database_assert_table_exists( sqlite3 *database, const char *table_name ) { sqlite3_stmt *statement = NULL; int result = SQLITE_ERROR; assert(database != NULL); assert(table_name != NULL); result = sqlite3_prepare_v2( database, "SELECT COUNT(*) " "FROM sqlite_master " "WHERE type = 'table' AND 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_step(statement); assert(result == SQLITE_ROW); assert(sqlite3_column_int(statement, 0) == 1); result = sqlite3_finalize(statement); 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. */ static void test_database_initialize_valid_database(void) { char *temporary_directory = NULL; char *database_path = NULL; char *schema_version = NULL; char *application_name = NULL; char *created_at = NULL; char *investigation_uuid = NULL; char *investigation_name = NULL; char *investigation_root_path = NULL; char *investigation_created_at = NULL; char *investigation_id = NULL; char *investigation_count = NULL; char *investigation_updated_at = NULL; sqlite3 *database = NULL; GError *error = NULL; int result = SQLITE_ERROR; temporary_directory = g_dir_make_tmp( "labfy-database-test-XXXXXX", &error ); assert(temporary_directory != NULL); assert(error == NULL); database_path = g_build_filename( temporary_directory, "Enquete.sqlite", NULL ); assert(database_path != NULL); assert( database_initialize( database_path, "Enquete_Test", temporary_directory ) ); assert( g_file_test( database_path, G_FILE_TEST_IS_REGULAR ) ); result = sqlite3_open_v2( database_path, &database, SQLITE_OPEN_READONLY, NULL ); assert(result == SQLITE_OK); assert(database != NULL); test_database_assert_table_exists( database, "metadata" ); test_database_assert_table_exists( 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, "SELECT value FROM metadata " "WHERE key = 'schema_version';" ); application_name = test_database_read_single_text( database, "SELECT value FROM metadata " "WHERE key = 'application';" ); created_at = test_database_read_single_text( database, "SELECT value FROM metadata " "WHERE key = 'created_at';" ); investigation_uuid = test_database_read_single_text( database, "SELECT value FROM metadata " "WHERE key = 'investigation_uuid';" ); investigation_name = test_database_read_single_text( database, "SELECT name FROM investigation " "LIMIT 1;" ); investigation_root_path = test_database_read_single_text( database, "SELECT root_path FROM investigation " "LIMIT 1;" ); investigation_created_at = test_database_read_single_text( database, "SELECT created_at FROM investigation " "LIMIT 1;" ); investigation_updated_at = test_database_read_single_text( database, "SELECT updated_at FROM investigation " "LIMIT 1;" ); investigation_id = test_database_read_single_text( database, "SELECT id FROM investigation " "LIMIT 1;" ); investigation_count = test_database_read_single_text( database, "SELECT CAST(COUNT(*) AS TEXT) " "FROM investigation;" ); assert(strcmp(schema_version, "2") == 0); assert(strcmp(application_name, "Labfy Investigation") == 0); assert(created_at[0] != '\0'); assert(investigation_uuid[0] != '\0'); assert(strcmp(investigation_name, "Enquete_Test") == 0); assert(strcmp(investigation_root_path, temporary_directory) == 0); assert(strcmp(investigation_created_at, created_at) == 0); assert(investigation_updated_at[0] != '\0'); assert(strcmp(investigation_updated_at,created_at) == 0); assert(strcmp(investigation_id,investigation_uuid) == 0 ); result = sqlite3_close(database); assert(result == SQLITE_OK); assert(g_remove(database_path) == 0); assert(g_rmdir(temporary_directory) == 0); assert(investigation_id[0] != '\0'); assert(g_uuid_string_is_valid(investigation_id)); assert(strcmp(investigation_count, "1") == 0); g_free(investigation_created_at); g_free(investigation_root_path); g_free(investigation_name); g_free(investigation_uuid); g_free(created_at); g_free(application_name); g_free(schema_version); g_free(database_path); g_free(temporary_directory); g_free(investigation_id); g_free(investigation_count); } /** * @brief Vérifie le refus des paramètres invalides. */ static void test_database_initialize_invalid_parameters(void) { assert( !database_initialize( NULL, "Enquete", "/tmp/Enquete" ) ); assert( !database_initialize( "", "Enquete", "/tmp/Enquete" ) ); assert( !database_initialize( "/tmp/Enquete.sqlite", NULL, "/tmp/Enquete" ) ); assert( !database_initialize( "/tmp/Enquete.sqlite", "", "/tmp/Enquete" ) ); assert( !database_initialize( "/tmp/Enquete.sqlite", "Enquete", NULL ) ); assert( !database_initialize( "/tmp/Enquete.sqlite", "Enquete", "" ) ); } /** * @brief Vérifie l'échec sur un chemin dont le parent n'existe pas. */ static void test_database_initialize_missing_parent(void) { assert( !database_initialize( "/tmp/labfy-missing-parent/database/Enquete.sqlite", "Enquete", "/tmp/labfy-missing-parent" ) ); } /** * @brief Vérifie qu'une seconde initialisation échouée est annulée. * * La première initialisation crée une base valide. * La seconde tentative doit échouer sans modifier les données existantes. */ static void test_database_initialize_rollback(void) { char *temporary_directory = NULL; char *database_path = NULL; char *investigation_count = NULL; char *metadata_count = NULL; char *investigation_name = NULL; char *investigation_root_path = NULL; char *integrity_result = NULL; sqlite3 *database = NULL; GError *error = NULL; int result = SQLITE_ERROR; temporary_directory = g_dir_make_tmp( "labfy-database-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); /* * Première initialisation valide. */ assert( database_initialize( database_path, "Enquete_Initiale", temporary_directory ) ); /* * La seconde initialisation doit échouer. * * Elle provoquera une erreur pendant l'installation du schéma * ou lors de l'insertion des métadonnées déjà existantes. */ assert( !database_initialize( database_path, "Enquete_Seconde", "/tmp/racine-seconde" ) ); result = sqlite3_open_v2( database_path, &database, SQLITE_OPEN_READONLY, NULL ); assert(result == SQLITE_OK); assert(database != NULL); investigation_count = test_database_read_single_text( database, "SELECT CAST(COUNT(*) AS TEXT) " "FROM investigation;" ); metadata_count = test_database_read_single_text( database, "SELECT CAST(COUNT(*) AS TEXT) " "FROM metadata " "WHERE key IN " "(" " 'schema_version'," " 'application'," " 'created_at'," " 'investigation_uuid'" ");" ); investigation_name = test_database_read_single_text( database, "SELECT name " "FROM investigation " "LIMIT 1;" ); investigation_root_path = test_database_read_single_text( database, "SELECT root_path " "FROM investigation " "LIMIT 1;" ); integrity_result = test_database_read_single_text( database, "PRAGMA integrity_check;" ); /* * Aucune seconde enquête ne doit avoir été ajoutée. */ assert(strcmp(investigation_count, "1") == 0); /* * Les quatre métadonnées initiales doivent toujours être présentes * sans duplication. */ assert(strcmp(metadata_count, "4") == 0); /* * Les données de la première initialisation doivent être intactes. */ assert( strcmp( investigation_name, "Enquete_Initiale" ) == 0 ); assert( strcmp( investigation_root_path, temporary_directory ) == 0 ); /* * La base doit rester cohérente après le rollback. */ 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(investigation_root_path); g_free(investigation_name); g_free(metadata_count); g_free(investigation_count); g_free(database_path); 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(); printf( "Database : tous les tests sont valides.\n" ); return 0; }