From c389dba0d9c45d3eee8b327e629822e9af159bfb Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Tue, 21 Jul 2026 08:55:36 +0200 Subject: [PATCH] feat: load investigation graph from SQLite --- Makefile | 25 +- include/core/investigation_graph_loader.h | 121 ++ src/core/investigation_graph_loader.c | 562 +++++++++ tests/test_investigation_graph_loader.c | 1370 +++++++++++++++++++++ 4 files changed, 2076 insertions(+), 2 deletions(-) create mode 100644 include/core/investigation_graph_loader.h create mode 100644 src/core/investigation_graph_loader.c create mode 100644 tests/test_investigation_graph_loader.c diff --git a/Makefile b/Makefile index fa93a0f..0c2a248 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,7 @@ RELATION_SERVICE_TEST_CFLAGS := \ -Wpedantic \ -DRELATION_SERVICE_ENABLE_TEST_HOOKS INVESTIGATION_GRAPH_MODEL_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic +INVESTIGATION_GRAPH_LOADER_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic SRC := $(shell find src -name "*.c") @@ -100,6 +101,7 @@ TEST_RELATION_DAO := tests/test_relation_dao TEST_RELATION_EVIDENCE_DAO := tests/test_relation_evidence_dao TEST_RELATION_SERVICE := tests/test_relation_service TEST_INVESTIGATION_GRAPH_MODEL := tests/test_investigation_graph_model +TEST_INVESTIGATION_GRAPH_LOADER := tests/test_investigation_graph_loader all: $(TARGET) @@ -487,6 +489,22 @@ $(TEST_INVESTIGATION_GRAPH_MODEL): \ $(CC) $(INVESTIGATION_GRAPH_MODEL_TEST_CFLAGS) $^ -o $@ \ $(TEST_LDFLAGS) +$(TEST_INVESTIGATION_GRAPH_LOADER): \ + tests/test_investigation_graph_loader.c \ + src/core/investigation_graph_loader.c \ + src/dao/entity_dao.c \ + src/dao/relation_dao.c \ + src/models/investigation_graph_model.c \ + src/models/entity_record.c \ + src/models/relation_record.c \ + src/database/database.c \ + src/database/schema.c \ + src/database/statement.c \ + src/database/transaction.c \ + src/database/error.c + $(CC) $(INVESTIGATION_GRAPH_LOADER_TEST_CFLAGS) $^ -o $@ \ + $(TEST_LDFLAGS) -lsqlite3 + test: \ $(TEST_NODE) \ $(TEST_TREE_MODEL) \ @@ -530,7 +548,8 @@ test: \ $(TEST_RELATION_DAO) \ $(TEST_RELATION_EVIDENCE_DAO) \ $(TEST_RELATION_SERVICE) \ - $(TEST_INVESTIGATION_GRAPH_MODEL) + $(TEST_INVESTIGATION_GRAPH_MODEL) \ + $(TEST_INVESTIGATION_GRAPH_LOADER) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -575,6 +594,7 @@ test: \ @$(TEST_RELATION_EVIDENCE_DAO) @$(TEST_RELATION_SERVICE) @$(TEST_INVESTIGATION_GRAPH_MODEL) + @$(TEST_INVESTIGATION_GRAPH_LOADER) @echo "Tous les tests sont valides." %.o: %.c @@ -625,6 +645,7 @@ clean: $(TEST_RELATION_DAO) \ $(TEST_RELATION_EVIDENCE_DAO) \ $(TEST_RELATION_SERVICE) \ - $(TEST_INVESTIGATION_GRAPH_MODEL) + $(TEST_INVESTIGATION_GRAPH_MODEL) \ + $(TEST_INVESTIGATION_GRAPH_LOADER) .PHONY: clean run test diff --git a/include/core/investigation_graph_loader.h b/include/core/investigation_graph_loader.h new file mode 100644 index 0000000..eff1c76 --- /dev/null +++ b/include/core/investigation_graph_loader.h @@ -0,0 +1,121 @@ +/****************************************************************************** + * @file investigation_graph_loader.h + * @brief Chargement du graphe métier d'une enquête depuis SQLite. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_INVESTIGATION_GRAPH_LOADER_H +#define LABFY_INVESTIGATION_INVESTIGATION_GRAPH_LOADER_H + +#include "database/database.h" +#include "models/investigation_graph_model.h" + +#include + +G_BEGIN_DECLS + +/** + * @brief Chargeur opaque du graphe métier d'une enquête. + * + * Le chargeur emprunte la connexion Database reçue lors de sa création. + * Il possède les DAO qu'il utilise. + */ +typedef struct InvestigationGraphLoader InvestigationGraphLoader; + +/** + * @brief Catégories d'erreurs produites par InvestigationGraphLoader. + */ +typedef enum +{ + /** + * Un argument public est invalide. + */ + INVESTIGATION_GRAPH_LOADER_ERROR_INVALID_ARGUMENT, + + /** + * Une allocation ou la création du graphe a échoué. + */ + INVESTIGATION_GRAPH_LOADER_ERROR_MEMORY, + + /** + * Les entités n'ont pas pu être chargées depuis SQLite. + */ + INVESTIGATION_GRAPH_LOADER_ERROR_ENTITY_LOAD, + + /** + * Une entité n'a pas pu être transférée dans le graphe. + */ + INVESTIGATION_GRAPH_LOADER_ERROR_ENTITY_TRANSFER, + + /** + * Les relations n'ont pas pu être chargées depuis SQLite. + */ + INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_LOAD, + + /** + * Une relation n'a pas pu être transférée dans le graphe. + */ + INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_TRANSFER +} InvestigationGraphLoaderError; + +/** + * @brief Domaine d'erreur du chargeur. + */ +#define INVESTIGATION_GRAPH_LOADER_ERROR \ + investigation_graph_loader_error_quark() + +/** + * @brief Retourne le domaine d'erreur du chargeur. + */ +GQuark investigation_graph_loader_error_quark(void); + +/** + * @brief Crée un chargeur utilisant une connexion Database existante. + * + * La connexion est empruntée et doit rester valide pendant toute la durée de + * vie du chargeur. + * + * @param database Connexion SQLite ouverte et initialisée. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau chargeur, ou NULL en cas d'échec. + */ +InvestigationGraphLoader *investigation_graph_loader_new( + Database *database, + GError **error +); + +/** + * @brief Libère le chargeur. + * + * Les DAO internes sont libérés, mais la connexion Database empruntée n'est + * jamais fermée. + * + * Cette fonction accepte NULL. + * + * @param graph_loader Chargeur à libérer. + */ +void investigation_graph_loader_free( + InvestigationGraphLoader *graph_loader +); + +/** + * @brief Construit un nouveau graphe depuis les données SQLite. + * + * Toutes les entités sont chargées et transférées avant les relations. + * + * Le graphe retourné appartient à l'appelant. Au moindre échec, le graphe + * partiellement construit et tous les modèles temporaires sont détruits. + * + * @param graph_loader Chargeur valide. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau graphe complet, ou NULL en cas d'échec. + */ +InvestigationGraphModel *investigation_graph_loader_load( + InvestigationGraphLoader *graph_loader, + GError **error +); + +G_END_DECLS + +#endif diff --git a/src/core/investigation_graph_loader.c b/src/core/investigation_graph_loader.c new file mode 100644 index 0000000..1c588ae --- /dev/null +++ b/src/core/investigation_graph_loader.c @@ -0,0 +1,562 @@ +/****************************************************************************** + * @file investigation_graph_loader.c + * @brief Chargement du graphe métier d'une enquête depuis SQLite. + ******************************************************************************/ + +#include "core/investigation_graph_loader.h" + +#include "dao/entity_dao.h" +#include "dao/relation_dao.h" +#include "models/entity_record.h" +#include "models/relation_record.h" + +#include + +/** + * @brief Représentation privée du chargeur. + * + * La connexion Database est empruntée. + * Les DAO sont possédés. + */ +struct InvestigationGraphLoader +{ + Database *database; + + EntityDao *entity_dao; + RelationDao *relation_dao; +}; + +/** + * @brief Enregistre une erreur littérale du chargeur. + */ +static void investigation_graph_loader_set_error_literal( + GError **error, + InvestigationGraphLoaderError error_code, + const char *message +) +{ + if (error == NULL || + *error != NULL) + { + return; + } + + g_set_error_literal( + error, + INVESTIGATION_GRAPH_LOADER_ERROR, + error_code, + message + ); +} + +/** + * @brief Conserve le contexte d'une erreur provenant d'un autre composant. + */ +static void investigation_graph_loader_set_nested_error( + GError **error, + InvestigationGraphLoaderError error_code, + const char *context, + const GError *nested_error +) +{ + if (error == NULL || + *error != NULL) + { + return; + } + + g_set_error( + error, + INVESTIGATION_GRAPH_LOADER_ERROR, + error_code, + "%s : %s", + context, + nested_error != NULL + ? nested_error->message + : "erreur inconnue" + ); +} + +/** + * @brief Transfère toutes les entités d'un tableau vers le graphe. + * + * Le tableau possède initialement tous ses EntityRecord. + * + * Chaque élément est retiré du tableau avant son ajout. En cas de succès, le + * graphe en devient propriétaire. En cas d'échec, le modèle retiré est libéré + * explicitement et les éléments restants restent possédés par le tableau. + */ +static gboolean investigation_graph_loader_transfer_entities( + InvestigationGraphModel *graph_model, + GPtrArray *entity_records, + GError **error +) +{ + EntityRecord *entity_record = + NULL; + + GError *graph_error = + NULL; + + if (graph_model == NULL || + entity_records == NULL) + { + investigation_graph_loader_set_error_literal( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_INVALID_ARGUMENT, + "Le graphe ou la liste des entités est absent." + ); + + return FALSE; + } + + while (entity_records->len > 0) + { + entity_record = + g_ptr_array_steal_index( + entity_records, + 0 + ); + + if (entity_record == NULL) + { + investigation_graph_loader_set_error_literal( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_ENTITY_TRANSFER, + "La liste des entités contient un modèle absent." + ); + + return FALSE; + } + + if (!investigation_graph_model_add_entity( + graph_model, + entity_record, + &graph_error + )) + { + investigation_graph_loader_set_nested_error( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_ENTITY_TRANSFER, + "Impossible de transférer une entité dans le graphe", + graph_error + ); + + g_clear_error( + &graph_error + ); + + entity_record_free( + entity_record + ); + + return FALSE; + } + + entity_record = + NULL; + } + + return TRUE; +} + +/** + * @brief Transfère toutes les relations d'un tableau vers le graphe. + * + * Le tableau possède initialement tous ses RelationRecord. + * + * Chaque élément est retiré du tableau avant son ajout. En cas de succès, le + * graphe en devient propriétaire. En cas d'échec, le modèle retiré est libéré + * explicitement et les éléments restants restent possédés par le tableau. + */ +static gboolean investigation_graph_loader_transfer_relations( + InvestigationGraphModel *graph_model, + GPtrArray *relation_records, + GError **error +) +{ + RelationRecord *relation_record = + NULL; + + GError *graph_error = + NULL; + + if (graph_model == NULL || + relation_records == NULL) + { + investigation_graph_loader_set_error_literal( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_INVALID_ARGUMENT, + "Le graphe ou la liste des relations est absent." + ); + + return FALSE; + } + + while (relation_records->len > 0) + { + relation_record = + g_ptr_array_steal_index( + relation_records, + 0 + ); + + if (relation_record == NULL) + { + investigation_graph_loader_set_error_literal( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_TRANSFER, + "La liste des relations contient un modèle absent." + ); + + return FALSE; + } + + if (!investigation_graph_model_add_relation( + graph_model, + relation_record, + &graph_error + )) + { + investigation_graph_loader_set_nested_error( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_TRANSFER, + "Impossible de transférer une relation dans le graphe", + graph_error + ); + + g_clear_error( + &graph_error + ); + + relation_record_free( + relation_record + ); + + return FALSE; + } + + relation_record = + NULL; + } + + return TRUE; +} + +GQuark investigation_graph_loader_error_quark(void) +{ + return g_quark_from_static_string( + "investigation-graph-loader-error-quark" + ); +} + +InvestigationGraphLoader *investigation_graph_loader_new( + Database *database, + GError **error +) +{ + InvestigationGraphLoader *graph_loader = + NULL; + + EntityDao *entity_dao = + NULL; + + RelationDao *relation_dao = + NULL; + + GError *dao_error = + NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (database == NULL) + { + investigation_graph_loader_set_error_literal( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_INVALID_ARGUMENT, + "La connexion Database est obligatoire." + ); + + return NULL; + } + + entity_dao = + entity_dao_new( + database, + &dao_error + ); + + if (entity_dao == NULL) + { + investigation_graph_loader_set_nested_error( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_ENTITY_LOAD, + "Impossible de créer le DAO des entités", + dao_error + ); + + g_clear_error( + &dao_error + ); + + return NULL; + } + + relation_dao = + relation_dao_new( + database, + &dao_error + ); + + if (relation_dao == NULL) + { + investigation_graph_loader_set_nested_error( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_LOAD, + "Impossible de créer le DAO des relations", + dao_error + ); + + g_clear_error( + &dao_error + ); + + entity_dao_free( + entity_dao + ); + + return NULL; + } + + graph_loader = + g_try_new0( + InvestigationGraphLoader, + 1 + ); + + if (graph_loader == NULL) + { + relation_dao_free( + relation_dao + ); + + entity_dao_free( + entity_dao + ); + + investigation_graph_loader_set_error_literal( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_MEMORY, + "Impossible d'allouer le chargeur du graphe d'enquête." + ); + + return NULL; + } + + graph_loader->database = + database; + + graph_loader->entity_dao = + entity_dao; + + graph_loader->relation_dao = + relation_dao; + + return graph_loader; +} + +void investigation_graph_loader_free( + InvestigationGraphLoader *graph_loader +) +{ + if (graph_loader == NULL) + { + return; + } + + relation_dao_free( + graph_loader->relation_dao + ); + + entity_dao_free( + graph_loader->entity_dao + ); + + graph_loader->relation_dao = + NULL; + + graph_loader->entity_dao = + NULL; + + graph_loader->database = + NULL; + + g_free( + graph_loader + ); +} + +InvestigationGraphModel *investigation_graph_loader_load( + InvestigationGraphLoader *graph_loader, + GError **error +) +{ + InvestigationGraphModel *graph_model = + NULL; + + GPtrArray *entity_records = + NULL; + + GPtrArray *relation_records = + NULL; + + GError *component_error = + NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (graph_loader == NULL || + graph_loader->database == NULL || + graph_loader->entity_dao == NULL || + graph_loader->relation_dao == NULL) + { + investigation_graph_loader_set_error_literal( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_INVALID_ARGUMENT, + "Le chargeur du graphe d'enquête est invalide." + ); + + return NULL; + } + + graph_model = + investigation_graph_model_new( + &component_error + ); + + if (graph_model == NULL) + { + investigation_graph_loader_set_nested_error( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_MEMORY, + "Impossible de créer le graphe d'enquête", + component_error + ); + + g_clear_error( + &component_error + ); + + return NULL; + } + + entity_records = + entity_dao_list_all( + graph_loader->entity_dao, + &component_error + ); + + if (entity_records == NULL) + { + investigation_graph_loader_set_nested_error( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_ENTITY_LOAD, + "Impossible de charger les entités depuis SQLite", + component_error + ); + + goto failure; + } + + g_clear_error( + &component_error + ); + + if (!investigation_graph_loader_transfer_entities( + graph_model, + entity_records, + error + )) + { + goto failure; + } + + g_ptr_array_unref( + entity_records + ); + + entity_records = + NULL; + + relation_records = + relation_dao_list_all( + graph_loader->relation_dao, + &component_error + ); + + if (relation_records == NULL) + { + investigation_graph_loader_set_nested_error( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_LOAD, + "Impossible de charger les relations depuis SQLite", + component_error + ); + + goto failure; + } + + g_clear_error( + &component_error + ); + + if (!investigation_graph_loader_transfer_relations( + graph_model, + relation_records, + error + )) + { + goto failure; + } + + g_ptr_array_unref( + relation_records + ); + + relation_records = + NULL; + + return graph_model; + +failure: + + g_clear_error( + &component_error + ); + + if (relation_records != NULL) + { + g_ptr_array_unref( + relation_records + ); + } + + if (entity_records != NULL) + { + g_ptr_array_unref( + entity_records + ); + } + + investigation_graph_model_free( + graph_model + ); + + return NULL; +} diff --git a/tests/test_investigation_graph_loader.c b/tests/test_investigation_graph_loader.c new file mode 100644 index 0000000..e66a0c8 --- /dev/null +++ b/tests/test_investigation_graph_loader.c @@ -0,0 +1,1370 @@ +/****************************************************************************** + * @file test_investigation_graph_loader.c + * @brief Tests du chargement du graphe d'enquête depuis SQLite. + ******************************************************************************/ + +#include "core/investigation_graph_loader.h" + +#include "dao/entity_dao.h" +#include "dao/relation_dao.h" + +#include "database/database.h" +#include "database/statement.h" + +#include "models/entity_record.h" +#include "models/investigation_graph_model.h" +#include "models/relation_record.h" + +#include +#include +#include +#include + +#include +#include + +#define TEST_ENTITY_A_IDENTIFIER \ + "10000000-0000-4000-8000-000000000001" + +#define TEST_ENTITY_B_IDENTIFIER \ + "20000000-0000-4000-8000-000000000001" + +#define TEST_ENTITY_C_IDENTIFIER \ + "30000000-0000-4000-8000-000000000001" + +#define TEST_RELATION_AB_IDENTIFIER \ + "40000000-0000-4000-8000-000000000001" + +#define TEST_RELATION_AC_IDENTIFIER \ + "40000000-0000-4000-8000-000000000002" + +#define TEST_RELATION_CA_IDENTIFIER \ + "40000000-0000-4000-8000-000000000003" + +/** + * @brief Environnement temporaire d'un test InvestigationGraphLoader. + */ +typedef struct +{ + char *temporary_directory; + char *database_path; + + Database *database; + + EntityDao *entity_dao; + RelationDao *relation_dao; + + InvestigationGraphLoader *graph_loader; +} TestInvestigationGraphLoaderFixture; + +/** + * @brief Vérifie le domaine et le code d'une erreur du chargeur. + */ +static void test_graph_loader_assert_error( + const GError *error, + InvestigationGraphLoaderError expected_code +) +{ + assert(error != NULL); + + assert( + error->domain == + INVESTIGATION_GRAPH_LOADER_ERROR + ); + + assert( + error->code == + (gint) expected_code + ); + + assert(error->message != NULL); + assert(error->message[0] != '\0'); +} + +/** + * @brief Exécute une instruction SQL ne retournant aucune ligne. + */ +static void test_graph_loader_execute_sql( + Database *database, + const char *sql +) +{ + DatabaseStatement *statement = + NULL; + + assert(database != NULL); + assert(sql != NULL); + + statement = + database_statement_prepare( + database, + sql + ); + + assert(statement != NULL); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize( + statement + ); +} + +/** + * @brief Crée une base temporaire et les composants nécessaires. + */ +static TestInvestigationGraphLoaderFixture +test_graph_loader_fixture_create(void) +{ + TestInvestigationGraphLoaderFixture fixture = + {0}; + + GError *error = + NULL; + + fixture.temporary_directory = + g_dir_make_tmp( + "labfy-graph-loader-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_GraphLoader", + fixture.temporary_directory + ) + ); + + fixture.database = + database_open( + fixture.database_path + ); + + assert(fixture.database != NULL); + + fixture.entity_dao = + entity_dao_new( + fixture.database, + &error + ); + + assert(fixture.entity_dao != NULL); + assert(error == NULL); + + fixture.relation_dao = + relation_dao_new( + fixture.database, + &error + ); + + assert(fixture.relation_dao != NULL); + assert(error == NULL); + + fixture.graph_loader = + investigation_graph_loader_new( + fixture.database, + &error + ); + + assert(fixture.graph_loader != NULL); + assert(error == NULL); + + return fixture; +} + +/** + * @brief Libère une fixture et supprime sa base temporaire. + */ +static void test_graph_loader_fixture_clear( + TestInvestigationGraphLoaderFixture *fixture +) +{ + assert(fixture != NULL); + + investigation_graph_loader_free( + fixture->graph_loader + ); + + relation_dao_free( + fixture->relation_dao + ); + + entity_dao_free( + fixture->entity_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->graph_loader = + NULL; + + fixture->relation_dao = + NULL; + + fixture->entity_dao = + NULL; + + fixture->database = + NULL; + + fixture->database_path = + NULL; + + fixture->temporary_directory = + NULL; +} + +/** + * @brief Crée une entité de test valide. + */ +static EntityRecord *test_graph_loader_create_entity( + const char *identifier, + const char *value +) +{ + EntityRecord *entity_record = + NULL; + + GError *error = + NULL; + + entity_record = + entity_record_new( + identifier, + "person", + value, + NULL, + NULL, + 80, + "2026-07-21T21:00:00Z", + "2026-07-21T21:00:00Z", + ENTITY_STATUS_ACTIVE, + &error + ); + + assert(entity_record != NULL); + assert(error == NULL); + + return entity_record; +} + +/** + * @brief Insère une entité dans SQLite. + */ +static void test_graph_loader_insert_entity( + TestInvestigationGraphLoaderFixture *fixture, + const char *identifier, + const char *value +) +{ + EntityRecord *entity_record = + NULL; + + GError *error = + NULL; + + assert(fixture != NULL); + + entity_record = + test_graph_loader_create_entity( + identifier, + value + ); + + assert( + entity_dao_insert( + fixture->entity_dao, + entity_record, + &error + ) + ); + + assert(error == NULL); + + entity_record_free( + entity_record + ); +} + +/** + * @brief Insère les trois entités standard. + */ +static void test_graph_loader_insert_standard_entities( + TestInvestigationGraphLoaderFixture *fixture +) +{ + test_graph_loader_insert_entity( + fixture, + TEST_ENTITY_A_IDENTIFIER, + "Personne A" + ); + + test_graph_loader_insert_entity( + fixture, + TEST_ENTITY_B_IDENTIFIER, + "Personne B" + ); + + test_graph_loader_insert_entity( + fixture, + TEST_ENTITY_C_IDENTIFIER, + "Personne C" + ); +} + +/** + * @brief Crée une relation de test valide. + */ +static RelationRecord *test_graph_loader_create_relation( + const char *identifier, + const char *source_identifier, + const char *target_identifier, + const char *relation_type +) +{ + RelationRecord *relation_record = + NULL; + + GError *error = + NULL; + + relation_record = + relation_record_new( + identifier, + source_identifier, + target_identifier, + relation_type, + NULL, + "Relation de test.", + 75, + "2026-07-21T21:10:00Z", + "2026-07-21T21:10:00Z", + RELATION_STATUS_ACTIVE, + &error + ); + + assert(relation_record != NULL); + assert(error == NULL); + + return relation_record; +} + +/** + * @brief Insère une relation dans SQLite. + */ +static void test_graph_loader_insert_relation( + TestInvestigationGraphLoaderFixture *fixture, + const char *identifier, + const char *source_identifier, + const char *target_identifier, + const char *relation_type +) +{ + RelationRecord *relation_record = + NULL; + + GError *error = + NULL; + + assert(fixture != NULL); + + relation_record = + test_graph_loader_create_relation( + identifier, + source_identifier, + target_identifier, + relation_type + ); + + assert( + relation_dao_insert( + fixture->relation_dao, + relation_record, + &error + ) + ); + + assert(error == NULL); + + relation_record_free( + relation_record + ); +} + +/** + * @brief Insère les trois relations standard. + */ +static void test_graph_loader_insert_standard_relations( + TestInvestigationGraphLoaderFixture *fixture +) +{ + test_graph_loader_insert_relation( + fixture, + TEST_RELATION_AB_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + test_graph_loader_insert_relation( + fixture, + TEST_RELATION_AC_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_C_IDENTIFIER, + "controls" + ); + + test_graph_loader_insert_relation( + fixture, + TEST_RELATION_CA_IDENTIFIER, + TEST_ENTITY_C_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + "knows" + ); +} + +/** + * @brief Insère directement une relation incohérente. + */ +static void test_graph_loader_insert_inconsistent_relation( + TestInvestigationGraphLoaderFixture *fixture, + const char *identifier, + const char *source_identifier, + const char *target_identifier +) +{ + DatabaseStatement *statement = + NULL; + + assert(fixture != NULL); + + test_graph_loader_execute_sql( + fixture->database, + "PRAGMA foreign_keys = OFF;" + ); + + statement = + database_statement_prepare( + fixture->database, + "INSERT INTO relations" + "(" + " id," + " entite_source_id," + " entite_cible_id," + " type_relation," + " confiance," + " created_at," + " updated_at," + " status" + ")" + "VALUES" + "(" + " ?," + " ?," + " ?," + " ?," + " ?," + " ?," + " ?," + " ?" + ");" + ); + + assert(statement != NULL); + + assert( + database_statement_bind_text( + statement, + 1, + identifier + ) + ); + + assert( + database_statement_bind_text( + statement, + 2, + source_identifier + ) + ); + + assert( + database_statement_bind_text( + statement, + 3, + target_identifier + ) + ); + + assert( + database_statement_bind_text( + statement, + 4, + "uses" + ) + ); + + assert( + database_statement_bind_int64( + statement, + 5, + 50 + ) + ); + + assert( + database_statement_bind_text( + statement, + 6, + "2026-07-21T21:20:00Z" + ) + ); + + assert( + database_statement_bind_text( + statement, + 7, + "2026-07-21T21:20:00Z" + ) + ); + + assert( + database_statement_bind_text( + statement, + 8, + "active" + ) + ); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize( + statement + ); + + test_graph_loader_execute_sql( + fixture->database, + "PRAGMA foreign_keys = ON;" + ); +} + +/** + * @brief Vérifie le refus d'une connexion NULL. + */ +static void test_graph_loader_new_null_database(void) +{ + InvestigationGraphLoader *graph_loader = + NULL; + + GError *error = + NULL; + + graph_loader = + investigation_graph_loader_new( + NULL, + &error + ); + + assert(graph_loader == NULL); + + test_graph_loader_assert_error( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); +} + +/** + * @brief Vérifie le cycle de vie et la connexion empruntée. + */ +static void test_graph_loader_lifecycle(void) +{ + TestInvestigationGraphLoaderFixture fixture = + test_graph_loader_fixture_create(); + + DatabaseStatement *statement = + NULL; + + investigation_graph_loader_free( + fixture.graph_loader + ); + + fixture.graph_loader = + NULL; + + statement = + database_statement_prepare( + fixture.database, + "SELECT 1;" + ); + + assert(statement != NULL); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_ROW + ); + + database_statement_finalize( + statement + ); + + investigation_graph_loader_free( + NULL + ); + + test_graph_loader_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le chargement d'une base vide. + */ +static void test_graph_loader_load_empty_database(void) +{ + TestInvestigationGraphLoaderFixture fixture = + test_graph_loader_fixture_create(); + + InvestigationGraphModel *graph_model = + NULL; + + GError *error = + NULL; + + graph_model = + investigation_graph_loader_load( + fixture.graph_loader, + &error + ); + + assert(graph_model != NULL); + assert(error == NULL); + + assert( + investigation_graph_model_get_entity_count( + graph_model + ) == 0 + ); + + assert( + investigation_graph_model_get_relation_count( + graph_model + ) == 0 + ); + + investigation_graph_model_free( + graph_model + ); + + test_graph_loader_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le chargement d'entités sans relation. + */ +static void test_graph_loader_load_entities_only(void) +{ + TestInvestigationGraphLoaderFixture fixture = + test_graph_loader_fixture_create(); + + InvestigationGraphModel *graph_model = + NULL; + + GPtrArray *entities = + NULL; + + GError *error = + NULL; + + test_graph_loader_insert_standard_entities( + &fixture + ); + + graph_model = + investigation_graph_loader_load( + fixture.graph_loader, + &error + ); + + assert(graph_model != NULL); + assert(error == NULL); + + assert( + investigation_graph_model_get_entity_count( + graph_model + ) == 3 + ); + + assert( + investigation_graph_model_get_relation_count( + graph_model + ) == 0 + ); + + assert( + investigation_graph_model_find_entity( + graph_model, + TEST_ENTITY_A_IDENTIFIER + ) != NULL + ); + + assert( + investigation_graph_model_find_entity( + graph_model, + TEST_ENTITY_B_IDENTIFIER + ) != NULL + ); + + entities = + investigation_graph_model_list_entities( + graph_model, + &error + ); + + assert(entities != NULL); + assert(error == NULL); + assert(entities->len == 3); + + g_ptr_array_unref( + entities + ); + + investigation_graph_model_free( + graph_model + ); + + test_graph_loader_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le chargement complet et les index du graphe. + */ +static void test_graph_loader_load_complete_graph(void) +{ + TestInvestigationGraphLoaderFixture fixture = + test_graph_loader_fixture_create(); + + InvestigationGraphModel *graph_model = + NULL; + + const RelationRecord *relation_ab = + NULL; + + GPtrArray *outgoing_a = + NULL; + + GPtrArray *incoming_a = + NULL; + + GPtrArray *incident_a = + NULL; + + GError *error = + NULL; + + test_graph_loader_insert_standard_entities( + &fixture + ); + + test_graph_loader_insert_standard_relations( + &fixture + ); + + graph_model = + investigation_graph_loader_load( + fixture.graph_loader, + &error + ); + + assert(graph_model != NULL); + assert(error == NULL); + + assert( + investigation_graph_model_get_entity_count( + graph_model + ) == 3 + ); + + assert( + investigation_graph_model_get_relation_count( + graph_model + ) == 3 + ); + + relation_ab = + investigation_graph_model_find_relation( + graph_model, + TEST_RELATION_AB_IDENTIFIER + ); + + assert(relation_ab != NULL); + + assert( + strcmp( + relation_record_get_source_entity_identifier( + relation_ab + ), + TEST_ENTITY_A_IDENTIFIER + ) == 0 + ); + + assert( + strcmp( + relation_record_get_target_entity_identifier( + relation_ab + ), + TEST_ENTITY_B_IDENTIFIER + ) == 0 + ); + + outgoing_a = + investigation_graph_model_list_outgoing_relations( + graph_model, + TEST_ENTITY_A_IDENTIFIER, + &error + ); + + assert(outgoing_a != NULL); + assert(error == NULL); + assert(outgoing_a->len == 2); + + incoming_a = + investigation_graph_model_list_incoming_relations( + graph_model, + TEST_ENTITY_A_IDENTIFIER, + &error + ); + + assert(incoming_a != NULL); + assert(error == NULL); + assert(incoming_a->len == 1); + + incident_a = + investigation_graph_model_list_incident_relations( + graph_model, + TEST_ENTITY_A_IDENTIFIER, + &error + ); + + assert(incident_a != NULL); + assert(error == NULL); + assert(incident_a->len == 3); + + g_ptr_array_unref( + incident_a + ); + + g_ptr_array_unref( + incoming_a + ); + + g_ptr_array_unref( + outgoing_a + ); + + investigation_graph_model_free( + graph_model + ); + + test_graph_loader_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie que deux chargements produisent deux graphes indépendants. + */ +static void test_graph_loader_independent_graphs(void) +{ + TestInvestigationGraphLoaderFixture fixture = + test_graph_loader_fixture_create(); + + InvestigationGraphModel *first_graph = + NULL; + + InvestigationGraphModel *second_graph = + NULL; + + const EntityRecord *first_entity = + NULL; + + const EntityRecord *second_entity = + NULL; + + const RelationRecord *first_relation = + NULL; + + const RelationRecord *second_relation = + NULL; + + GError *error = + NULL; + + test_graph_loader_insert_standard_entities( + &fixture + ); + + test_graph_loader_insert_standard_relations( + &fixture + ); + + first_graph = + investigation_graph_loader_load( + fixture.graph_loader, + &error + ); + + assert(first_graph != NULL); + assert(error == NULL); + + second_graph = + investigation_graph_loader_load( + fixture.graph_loader, + &error + ); + + assert(second_graph != NULL); + assert(error == NULL); + + assert(first_graph != second_graph); + + first_entity = + investigation_graph_model_find_entity( + first_graph, + TEST_ENTITY_A_IDENTIFIER + ); + + second_entity = + investigation_graph_model_find_entity( + second_graph, + TEST_ENTITY_A_IDENTIFIER + ); + + assert(first_entity != NULL); + assert(second_entity != NULL); + assert(first_entity != second_entity); + + first_relation = + investigation_graph_model_find_relation( + first_graph, + TEST_RELATION_AB_IDENTIFIER + ); + + second_relation = + investigation_graph_model_find_relation( + second_graph, + TEST_RELATION_AB_IDENTIFIER + ); + + assert(first_relation != NULL); + assert(second_relation != NULL); + assert(first_relation != second_relation); + + investigation_graph_model_free( + first_graph + ); + + assert( + investigation_graph_model_find_entity( + second_graph, + TEST_ENTITY_A_IDENTIFIER + ) != NULL + ); + + assert( + investigation_graph_model_find_relation( + second_graph, + TEST_RELATION_AB_IDENTIFIER + ) != NULL + ); + + investigation_graph_model_free( + second_graph + ); + + test_graph_loader_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d'un chargeur NULL et GError facultatif. + */ +static void test_graph_loader_invalid_arguments(void) +{ + InvestigationGraphModel *graph_model = + NULL; + + GError *error = + NULL; + + graph_model = + investigation_graph_loader_load( + NULL, + &error + ); + + assert(graph_model == NULL); + + test_graph_loader_assert_error( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + graph_model = + investigation_graph_loader_load( + NULL, + NULL + ); + + assert(graph_model == NULL); +} + +/** + * @brief Vérifie l'erreur lorsque la table entites est absente. + */ +static void test_graph_loader_missing_entities_table(void) +{ + Database *database = + NULL; + + InvestigationGraphLoader *graph_loader = + NULL; + + InvestigationGraphModel *graph_model = + NULL; + + GError *error = + NULL; + + database = + database_open( + ":memory:" + ); + + assert(database != NULL); + + graph_loader = + investigation_graph_loader_new( + database, + &error + ); + + assert(graph_loader != NULL); + assert(error == NULL); + + g_test_expect_message( + NULL, + G_LOG_LEVEL_WARNING, + "*Impossible de préparer la requête SQL*" + ); + + graph_model = + investigation_graph_loader_load( + graph_loader, + &error + ); + + g_test_assert_expected_messages(); + + assert(graph_model == NULL); + + test_graph_loader_assert_error( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_ENTITY_LOAD + ); + + g_clear_error( + &error + ); + + investigation_graph_loader_free( + graph_loader + ); + + database_close( + database + ); +} + +/** + * @brief Vérifie l'erreur lorsque la table relations est absente. + */ +static void test_graph_loader_missing_relations_table(void) +{ + TestInvestigationGraphLoaderFixture fixture = + test_graph_loader_fixture_create(); + + InvestigationGraphModel *graph_model = + NULL; + + GError *error = + NULL; + + test_graph_loader_insert_entity( + &fixture, + TEST_ENTITY_A_IDENTIFIER, + "Personne A" + ); + + test_graph_loader_execute_sql( + fixture.database, + "ALTER TABLE relations " + "RENAME TO relations_missing;" + ); + + g_test_expect_message( + NULL, + G_LOG_LEVEL_WARNING, + "*Impossible de préparer la requête SQL*" + ); + + graph_model = + investigation_graph_loader_load( + fixture.graph_loader, + &error + ); + + g_test_assert_expected_messages(); + + assert(graph_model == NULL); + + test_graph_loader_assert_error( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_LOAD + ); + + g_clear_error( + &error + ); + + test_graph_loader_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d'une relation dont la source est absente. + */ +static void test_graph_loader_missing_source_entity(void) +{ + TestInvestigationGraphLoaderFixture fixture = + test_graph_loader_fixture_create(); + + InvestigationGraphModel *graph_model = + NULL; + + GError *error = + NULL; + + test_graph_loader_insert_entity( + &fixture, + TEST_ENTITY_B_IDENTIFIER, + "Personne B" + ); + + test_graph_loader_insert_inconsistent_relation( + &fixture, + TEST_RELATION_AB_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER + ); + + graph_model = + investigation_graph_loader_load( + fixture.graph_loader, + &error + ); + + assert(graph_model == NULL); + + test_graph_loader_assert_error( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_TRANSFER + ); + + assert( + strstr( + error->message, + "source" + ) != NULL + ); + + g_clear_error( + &error + ); + + /* + * Après correction de l'incohérence, le même chargeur doit fonctionner. + */ + test_graph_loader_insert_entity( + &fixture, + TEST_ENTITY_A_IDENTIFIER, + "Personne A" + ); + + graph_model = + investigation_graph_loader_load( + fixture.graph_loader, + &error + ); + + assert(graph_model != NULL); + assert(error == NULL); + + assert( + investigation_graph_model_get_relation_count( + graph_model + ) == 1 + ); + + investigation_graph_model_free( + graph_model + ); + + test_graph_loader_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d'une relation dont la cible est absente. + */ +static void test_graph_loader_missing_target_entity(void) +{ + TestInvestigationGraphLoaderFixture fixture = + test_graph_loader_fixture_create(); + + InvestigationGraphModel *graph_model = + NULL; + + GError *error = + NULL; + + test_graph_loader_insert_entity( + &fixture, + TEST_ENTITY_A_IDENTIFIER, + "Personne A" + ); + + test_graph_loader_insert_inconsistent_relation( + &fixture, + TEST_RELATION_AB_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER + ); + + graph_model = + investigation_graph_loader_load( + fixture.graph_loader, + &error + ); + + assert(graph_model == NULL); + + test_graph_loader_assert_error( + error, + INVESTIGATION_GRAPH_LOADER_ERROR_RELATION_TRANSFER + ); + + assert( + strstr( + error->message, + "cible" + ) != NULL + ); + + g_clear_error( + &error + ); + + test_graph_loader_fixture_clear( + &fixture + ); +} + +int main( + int argc, + char **argv +) +{ + g_test_init( + &argc, + &argv, + NULL + ); + + test_graph_loader_new_null_database(); + test_graph_loader_lifecycle(); + test_graph_loader_load_empty_database(); + test_graph_loader_load_entities_only(); + test_graph_loader_load_complete_graph(); + test_graph_loader_independent_graphs(); + test_graph_loader_invalid_arguments(); + test_graph_loader_missing_entities_table(); + test_graph_loader_missing_relations_table(); + test_graph_loader_missing_source_entity(); + test_graph_loader_missing_target_entity(); + + printf( + "InvestigationGraphLoader : tous les tests sont valides.\n" + ); + + return 0; +}