From 00dff97b15152d742d66986b553c4ecc27e09d2f Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Tue, 21 Jul 2026 08:26:09 +0200 Subject: [PATCH] feat: add investigation graph model --- Makefile | 17 +- include/models/investigation_graph_model.h | 274 ++++ src/models/investigation_graph_model.c | 1142 ++++++++++++++ tests/test_investigation_graph_model.c | 1596 ++++++++++++++++++++ 4 files changed, 3027 insertions(+), 2 deletions(-) create mode 100644 include/models/investigation_graph_model.h create mode 100644 src/models/investigation_graph_model.c create mode 100644 tests/test_investigation_graph_model.c diff --git a/Makefile b/Makefile index c7ba565..fa93a0f 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,7 @@ RELATION_SERVICE_TEST_CFLAGS := \ $(TEST_CFLAGS) \ -Wpedantic \ -DRELATION_SERVICE_ENABLE_TEST_HOOKS +INVESTIGATION_GRAPH_MODEL_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic SRC := $(shell find src -name "*.c") @@ -98,6 +99,7 @@ TEST_RELATION_RECORD := tests/test_relation_record 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 all: $(TARGET) @@ -477,6 +479,14 @@ $(TEST_RELATION_SERVICE): \ $(CC) $(RELATION_SERVICE_TEST_CFLAGS) $^ -o $@ \ $(TEST_LDFLAGS) -lsqlite3 +$(TEST_INVESTIGATION_GRAPH_MODEL): \ + tests/test_investigation_graph_model.c \ + src/models/investigation_graph_model.c \ + src/models/entity_record.c \ + src/models/relation_record.c + $(CC) $(INVESTIGATION_GRAPH_MODEL_TEST_CFLAGS) $^ -o $@ \ + $(TEST_LDFLAGS) + test: \ $(TEST_NODE) \ $(TEST_TREE_MODEL) \ @@ -519,7 +529,8 @@ test: \ $(TEST_RELATION_RECORD) \ $(TEST_RELATION_DAO) \ $(TEST_RELATION_EVIDENCE_DAO) \ - $(TEST_RELATION_SERVICE) + $(TEST_RELATION_SERVICE) \ + $(TEST_INVESTIGATION_GRAPH_MODEL) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -563,6 +574,7 @@ test: \ @$(TEST_RELATION_DAO) @$(TEST_RELATION_EVIDENCE_DAO) @$(TEST_RELATION_SERVICE) + @$(TEST_INVESTIGATION_GRAPH_MODEL) @echo "Tous les tests sont valides." %.o: %.c @@ -612,6 +624,7 @@ clean: $(TEST_RELATION_RECORD) \ $(TEST_RELATION_DAO) \ $(TEST_RELATION_EVIDENCE_DAO) \ - $(TEST_RELATION_SERVICE) + $(TEST_RELATION_SERVICE) \ + $(TEST_INVESTIGATION_GRAPH_MODEL) .PHONY: clean run test diff --git a/include/models/investigation_graph_model.h b/include/models/investigation_graph_model.h new file mode 100644 index 0000000..fe6e9da --- /dev/null +++ b/include/models/investigation_graph_model.h @@ -0,0 +1,274 @@ +/****************************************************************************** + * @file investigation_graph_model.h + * @brief Graphe métier en mémoire d'une enquête. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_INVESTIGATION_GRAPH_MODEL_H +#define LABFY_INVESTIGATION_INVESTIGATION_GRAPH_MODEL_H + +#include "models/entity_record.h" +#include "models/relation_record.h" + +#include + +G_BEGIN_DECLS + +/** + * @brief Graphe métier opaque d'une enquête. + * + * Le graphe possède les EntityRecord et RelationRecord ajoutés avec succès. + * Il ne contient aucune donnée d'affichage ou de disposition graphique. + */ +typedef struct InvestigationGraphModel InvestigationGraphModel; + +/** + * @brief Catégories d'erreurs produites par InvestigationGraphModel. + */ +typedef enum +{ + /** + * Un argument public est invalide. + */ + INVESTIGATION_GRAPH_MODEL_ERROR_INVALID_ARGUMENT, + + /** + * Une allocation mémoire a échoué. + */ + INVESTIGATION_GRAPH_MODEL_ERROR_MEMORY, + + /** + * Une entité possède un UUID déjà présent. + */ + INVESTIGATION_GRAPH_MODEL_ERROR_DUPLICATE_ENTITY, + + /** + * Une relation possède un UUID déjà présent. + */ + INVESTIGATION_GRAPH_MODEL_ERROR_DUPLICATE_RELATION, + + /** + * L'entité source d'une relation est absente. + */ + INVESTIGATION_GRAPH_MODEL_ERROR_SOURCE_NOT_FOUND, + + /** + * L'entité cible d'une relation est absente. + */ + INVESTIGATION_GRAPH_MODEL_ERROR_TARGET_NOT_FOUND, + + /** + * L'entité demandée est absente. + */ + INVESTIGATION_GRAPH_MODEL_ERROR_ENTITY_NOT_FOUND +} InvestigationGraphModelError; + +/** + * @brief Domaine d'erreur du modèle de graphe. + */ +#define INVESTIGATION_GRAPH_MODEL_ERROR \ + investigation_graph_model_error_quark() + +/** + * @brief Retourne le domaine d'erreur du modèle. + */ +GQuark investigation_graph_model_error_quark(void); + +/** + * @brief Crée un graphe métier vide. + * + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau graphe, ou NULL en cas d'échec. + */ +InvestigationGraphModel *investigation_graph_model_new( + GError **error +); + +/** + * @brief Libère le graphe et tous les modèles qu'il possède. + * + * Cette fonction accepte NULL. + * + * @param graph_model Graphe à libérer. + */ +void investigation_graph_model_free( + InvestigationGraphModel *graph_model +); + +/** + * @brief Ajoute une entité au graphe. + * + * En cas de succès, le graphe prend possession de entity_record. + * En cas d'échec, l'appelant conserve la propriété du modèle. + * + * @param graph_model Graphe valide. + * @param entity_record Entité à transférer. + * @param error Emplacement facultatif recevant une erreur. + * + * @return TRUE si l'entité est ajoutée. + */ +gboolean investigation_graph_model_add_entity( + InvestigationGraphModel *graph_model, + EntityRecord *entity_record, + GError **error +); + +/** + * @brief Ajoute une relation orientée au graphe. + * + * Les entités source et cible doivent déjà exister. + * + * En cas de succès, le graphe prend possession de relation_record. + * En cas d'échec, l'appelant conserve la propriété du modèle. + * + * @param graph_model Graphe valide. + * @param relation_record Relation à transférer. + * @param error Emplacement facultatif recevant une erreur. + * + * @return TRUE si la relation est ajoutée. + */ +gboolean investigation_graph_model_add_relation( + InvestigationGraphModel *graph_model, + RelationRecord *relation_record, + GError **error +); + +/** + * @brief Recherche une entité par UUID. + * + * Le pointeur retourné est emprunté au graphe. + * + * @param graph_model Graphe à consulter. + * @param entity_identifier UUID recherché. + * + * @return Entité empruntée, ou NULL. + */ +const EntityRecord *investigation_graph_model_find_entity( + const InvestigationGraphModel *graph_model, + const char *entity_identifier +); + +/** + * @brief Recherche une relation par UUID. + * + * Le pointeur retourné est emprunté au graphe. + * + * @param graph_model Graphe à consulter. + * @param relation_identifier UUID recherché. + * + * @return Relation empruntée, ou NULL. + */ +const RelationRecord *investigation_graph_model_find_relation( + const InvestigationGraphModel *graph_model, + const char *relation_identifier +); + +/** + * @brief Retourne le nombre d'entités du graphe. + * + * @return Nombre d'entités, ou 0 pour un graphe NULL. + */ +guint64 investigation_graph_model_get_entity_count( + const InvestigationGraphModel *graph_model +); + +/** + * @brief Retourne le nombre de relations du graphe. + * + * @return Nombre de relations, ou 0 pour un graphe NULL. + */ +guint64 investigation_graph_model_get_relation_count( + const InvestigationGraphModel *graph_model +); + +/** + * @brief Liste toutes les entités par UUID croissant. + * + * Le tableau appartient à l'appelant. Ses éléments sont empruntés au graphe + * et ne doivent pas être libérés. + * + * @param graph_model Graphe valide. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau tableau de const EntityRecord *, ou NULL en cas d'échec. + */ +GPtrArray *investigation_graph_model_list_entities( + const InvestigationGraphModel *graph_model, + GError **error +); + +/** + * @brief Liste toutes les relations par UUID croissant. + * + * Le tableau appartient à l'appelant. Ses éléments sont empruntés au graphe + * et ne doivent pas être libérés. + * + * @param graph_model Graphe valide. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau tableau de const RelationRecord *, ou NULL en cas d'échec. + */ +GPtrArray *investigation_graph_model_list_relations( + const InvestigationGraphModel *graph_model, + GError **error +); + +/** + * @brief Liste les relations sortantes d'une entité. + * + * Le tableau appartient à l'appelant. Les relations restent la propriété + * du graphe. + * + * @param graph_model Graphe valide. + * @param entity_identifier UUID de l'entité source. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau tableau trié, ou NULL en cas d'échec. + */ +GPtrArray *investigation_graph_model_list_outgoing_relations( + const InvestigationGraphModel *graph_model, + const char *entity_identifier, + GError **error +); + +/** + * @brief Liste les relations entrantes d'une entité. + * + * Le tableau appartient à l'appelant. Les relations restent la propriété + * du graphe. + * + * @param graph_model Graphe valide. + * @param entity_identifier UUID de l'entité cible. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau tableau trié, ou NULL en cas d'échec. + */ +GPtrArray *investigation_graph_model_list_incoming_relations( + const InvestigationGraphModel *graph_model, + const char *entity_identifier, + GError **error +); + +/** + * @brief Liste toutes les relations incidentes d'une entité. + * + * Une relation ne peut apparaître qu'une seule fois dans le tableau. + * + * Le tableau appartient à l'appelant. Les relations restent la propriété + * du graphe. + * + * @param graph_model Graphe valide. + * @param entity_identifier UUID de l'entité. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau tableau trié, ou NULL en cas d'échec. + */ +GPtrArray *investigation_graph_model_list_incident_relations( + const InvestigationGraphModel *graph_model, + const char *entity_identifier, + GError **error +); + +G_END_DECLS + +#endif diff --git a/src/models/investigation_graph_model.c b/src/models/investigation_graph_model.c new file mode 100644 index 0000000..766d120 --- /dev/null +++ b/src/models/investigation_graph_model.c @@ -0,0 +1,1142 @@ +/****************************************************************************** + * @file investigation_graph_model.c + * @brief Graphe métier en mémoire d'une enquête. + ******************************************************************************/ + +#include "models/investigation_graph_model.h" + +#include + +/** + * @brief Représentation privée du graphe métier. + * + * Les tables principales possèdent les modèles. + * Les index entrants et sortants ne possèdent que leurs tableaux. + * Les relations contenues dans ces tableaux sont empruntées à + * relations_by_identifier. + */ +struct InvestigationGraphModel +{ + GHashTable *entities_by_identifier; + GHashTable *relations_by_identifier; + + GHashTable *outgoing_relations_by_entity; + GHashTable *incoming_relations_by_entity; +}; + +/** + * @brief Enregistre une erreur littérale du modèle. + */ +static void investigation_graph_model_set_error_literal( + GError **error, + InvestigationGraphModelError error_code, + const char *message +) +{ + if (error == NULL || + *error != NULL) + { + return; + } + + g_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR, + error_code, + message + ); +} + +/** + * @brief Libère une entité possédée par une GHashTable. + */ +static void investigation_graph_model_destroy_entity( + gpointer data +) +{ + entity_record_free( + data + ); +} + +/** + * @brief Libère une relation possédée par une GHashTable. + */ +static void investigation_graph_model_destroy_relation( + gpointer data +) +{ + relation_record_free( + data + ); +} + +/** + * @brief Libère un tableau d'index. + * + * Les éléments du tableau sont empruntés et ne sont pas libérés. + */ +static void investigation_graph_model_destroy_relation_array( + gpointer data +) +{ + if (data == NULL) + { + return; + } + + g_ptr_array_unref( + data + ); +} + +/** + * @brief Compare deux pointeurs vers EntityRecord par UUID. + */ +static gint investigation_graph_model_compare_entities( + gconstpointer first_data, + gconstpointer second_data +) +{ + const EntityRecord *const *first_entity = + first_data; + + const EntityRecord *const *second_entity = + second_data; + + return g_strcmp0( + entity_record_get_identifier( + *first_entity + ), + entity_record_get_identifier( + *second_entity + ) + ); +} + +/** + * @brief Compare deux pointeurs vers RelationRecord par UUID. + */ +static gint investigation_graph_model_compare_relations( + gconstpointer first_data, + gconstpointer second_data +) +{ + const RelationRecord *const *first_relation = + first_data; + + const RelationRecord *const *second_relation = + second_data; + + return g_strcmp0( + relation_record_get_identifier( + *first_relation + ), + relation_record_get_identifier( + *second_relation + ) + ); +} + +/** + * @brief Vérifie qu'un UUID d'entité est exploitable. + */ +static gboolean investigation_graph_model_entity_identifier_is_valid( + const char *entity_identifier +) +{ + return entity_identifier != NULL && + g_uuid_string_is_valid( + entity_identifier + ); +} + +/** + * @brief Vérifie qu'un UUID de relation est exploitable. + */ +static gboolean investigation_graph_model_relation_identifier_is_valid( + const char *relation_identifier +) +{ + return relation_identifier != NULL && + g_uuid_string_is_valid( + relation_identifier + ); +} + +/** + * @brief Valide une demande de navigation depuis une entité. + */ +static gboolean investigation_graph_model_validate_entity_request( + const InvestigationGraphModel *graph_model, + const char *entity_identifier, + GError **error +) +{ + if (graph_model == NULL) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_INVALID_ARGUMENT, + "Le graphe d'enquête est absent." + ); + + return FALSE; + } + + if (!investigation_graph_model_entity_identifier_is_valid( + entity_identifier + )) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_INVALID_ARGUMENT, + "L'identifiant d'entité est invalide." + ); + + return FALSE; + } + + if (!g_hash_table_contains( + graph_model->entities_by_identifier, + entity_identifier + )) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_ENTITY_NOT_FOUND, + "L'entité demandée n'existe pas dans le graphe." + ); + + return FALSE; + } + + return TRUE; +} + +/** + * @brief Copie les pointeurs empruntés d'un tableau d'index. + */ +static GPtrArray *investigation_graph_model_copy_relation_array( + const GPtrArray *source_array, + GError **error +) +{ + GPtrArray *copied_array = + NULL; + + guint relation_index = + 0; + + copied_array = + g_ptr_array_new(); + + if (copied_array == NULL) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_MEMORY, + "Impossible d'allouer la liste des relations." + ); + + return NULL; + } + + if (source_array != NULL) + { + for (relation_index = 0; + relation_index < source_array->len; + relation_index++) + { + g_ptr_array_add( + copied_array, + g_ptr_array_index( + source_array, + relation_index + ) + ); + } + } + + g_ptr_array_sort( + copied_array, + investigation_graph_model_compare_relations + ); + + return copied_array; +} + +GQuark investigation_graph_model_error_quark(void) +{ + return g_quark_from_static_string( + "investigation-graph-model-error-quark" + ); +} + +InvestigationGraphModel *investigation_graph_model_new( + GError **error +) +{ + InvestigationGraphModel *graph_model = + NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + graph_model = + g_try_new0( + InvestigationGraphModel, + 1 + ); + + if (graph_model == NULL) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_MEMORY, + "Impossible d'allouer le graphe d'enquête." + ); + + return NULL; + } + + graph_model->entities_by_identifier = + g_hash_table_new_full( + g_str_hash, + g_str_equal, + g_free, + investigation_graph_model_destroy_entity + ); + + graph_model->relations_by_identifier = + g_hash_table_new_full( + g_str_hash, + g_str_equal, + g_free, + investigation_graph_model_destroy_relation + ); + + graph_model->outgoing_relations_by_entity = + g_hash_table_new_full( + g_str_hash, + g_str_equal, + g_free, + investigation_graph_model_destroy_relation_array + ); + + graph_model->incoming_relations_by_entity = + g_hash_table_new_full( + g_str_hash, + g_str_equal, + g_free, + investigation_graph_model_destroy_relation_array + ); + + if (graph_model->entities_by_identifier == NULL || + graph_model->relations_by_identifier == NULL || + graph_model->outgoing_relations_by_entity == NULL || + graph_model->incoming_relations_by_entity == NULL) + { + investigation_graph_model_free( + graph_model + ); + + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_MEMORY, + "Impossible d'allouer les index du graphe d'enquête." + ); + + return NULL; + } + + return graph_model; +} + +void investigation_graph_model_free( + InvestigationGraphModel *graph_model +) +{ + if (graph_model == NULL) + { + return; + } + + /* + * Les index ne possèdent pas les RelationRecord. + * Ils sont donc détruits avant la table propriétaire des relations. + */ + g_clear_pointer( + &graph_model->incoming_relations_by_entity, + g_hash_table_unref + ); + + g_clear_pointer( + &graph_model->outgoing_relations_by_entity, + g_hash_table_unref + ); + + g_clear_pointer( + &graph_model->relations_by_identifier, + g_hash_table_unref + ); + + g_clear_pointer( + &graph_model->entities_by_identifier, + g_hash_table_unref + ); + + g_free( + graph_model + ); +} + +gboolean investigation_graph_model_add_entity( + InvestigationGraphModel *graph_model, + EntityRecord *entity_record, + GError **error +) +{ + const char *entity_identifier = + NULL; + + char *entity_key = + NULL; + + char *outgoing_key = + NULL; + + char *incoming_key = + NULL; + + GPtrArray *outgoing_relations = + NULL; + + GPtrArray *incoming_relations = + NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (graph_model == NULL || + entity_record == NULL) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_INVALID_ARGUMENT, + "Le graphe ou l'entité est absent." + ); + + return FALSE; + } + + entity_identifier = + entity_record_get_identifier( + entity_record + ); + + if (!investigation_graph_model_entity_identifier_is_valid( + entity_identifier + )) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_INVALID_ARGUMENT, + "L'entité possède un identifiant invalide." + ); + + return FALSE; + } + + if (g_hash_table_contains( + graph_model->entities_by_identifier, + entity_identifier + )) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_DUPLICATE_ENTITY, + "Une entité possède déjà cet identifiant." + ); + + return FALSE; + } + + entity_key = + g_strdup( + entity_identifier + ); + + outgoing_key = + g_strdup( + entity_identifier + ); + + incoming_key = + g_strdup( + entity_identifier + ); + + outgoing_relations = + g_ptr_array_new(); + + incoming_relations = + g_ptr_array_new(); + + if (entity_key == NULL || + outgoing_key == NULL || + incoming_key == NULL || + outgoing_relations == NULL || + incoming_relations == NULL) + { + g_free( + incoming_key + ); + + g_free( + outgoing_key + ); + + g_free( + entity_key + ); + + if (incoming_relations != NULL) + { + g_ptr_array_unref( + incoming_relations + ); + } + + if (outgoing_relations != NULL) + { + g_ptr_array_unref( + outgoing_relations + ); + } + + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_MEMORY, + "Impossible d'allouer les index de l'entité." + ); + + return FALSE; + } + + g_hash_table_insert( + graph_model->entities_by_identifier, + entity_key, + entity_record + ); + + g_hash_table_insert( + graph_model->outgoing_relations_by_entity, + outgoing_key, + outgoing_relations + ); + + g_hash_table_insert( + graph_model->incoming_relations_by_entity, + incoming_key, + incoming_relations + ); + + return TRUE; +} + +gboolean investigation_graph_model_add_relation( + InvestigationGraphModel *graph_model, + RelationRecord *relation_record, + GError **error +) +{ + const char *relation_identifier = + NULL; + + const char *source_identifier = + NULL; + + const char *target_identifier = + NULL; + + char *relation_key = + NULL; + + GPtrArray *outgoing_relations = + NULL; + + GPtrArray *incoming_relations = + NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (graph_model == NULL || + relation_record == NULL) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_INVALID_ARGUMENT, + "Le graphe ou la relation est absent." + ); + + return FALSE; + } + + relation_identifier = + relation_record_get_identifier( + relation_record + ); + + source_identifier = + relation_record_get_source_entity_identifier( + relation_record + ); + + target_identifier = + relation_record_get_target_entity_identifier( + relation_record + ); + + if (!investigation_graph_model_relation_identifier_is_valid( + relation_identifier + ) || + !investigation_graph_model_entity_identifier_is_valid( + source_identifier + ) || + !investigation_graph_model_entity_identifier_is_valid( + target_identifier + )) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_INVALID_ARGUMENT, + "La relation contient un identifiant invalide." + ); + + return FALSE; + } + + if (g_hash_table_contains( + graph_model->relations_by_identifier, + relation_identifier + )) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_DUPLICATE_RELATION, + "Une relation possède déjà cet identifiant." + ); + + return FALSE; + } + + if (!g_hash_table_contains( + graph_model->entities_by_identifier, + source_identifier + )) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_SOURCE_NOT_FOUND, + "L'entité source de la relation est absente du graphe." + ); + + return FALSE; + } + + if (!g_hash_table_contains( + graph_model->entities_by_identifier, + target_identifier + )) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_TARGET_NOT_FOUND, + "L'entité cible de la relation est absente du graphe." + ); + + return FALSE; + } + + outgoing_relations = + g_hash_table_lookup( + graph_model->outgoing_relations_by_entity, + source_identifier + ); + + incoming_relations = + g_hash_table_lookup( + graph_model->incoming_relations_by_entity, + target_identifier + ); + + if (outgoing_relations == NULL || + incoming_relations == NULL) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_INVALID_ARGUMENT, + "Les index internes du graphe sont incohérents." + ); + + return FALSE; + } + + relation_key = + g_strdup( + relation_identifier + ); + + if (relation_key == NULL) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_MEMORY, + "Impossible d'allouer l'index de la relation." + ); + + return FALSE; + } + + g_hash_table_insert( + graph_model->relations_by_identifier, + relation_key, + relation_record + ); + + g_ptr_array_add( + outgoing_relations, + relation_record + ); + + g_ptr_array_add( + incoming_relations, + relation_record + ); + + return TRUE; +} + +const EntityRecord *investigation_graph_model_find_entity( + const InvestigationGraphModel *graph_model, + const char *entity_identifier +) +{ + if (graph_model == NULL || + !investigation_graph_model_entity_identifier_is_valid( + entity_identifier + )) + { + return NULL; + } + + return g_hash_table_lookup( + graph_model->entities_by_identifier, + entity_identifier + ); +} + +const RelationRecord *investigation_graph_model_find_relation( + const InvestigationGraphModel *graph_model, + const char *relation_identifier +) +{ + if (graph_model == NULL || + !investigation_graph_model_relation_identifier_is_valid( + relation_identifier + )) + { + return NULL; + } + + return g_hash_table_lookup( + graph_model->relations_by_identifier, + relation_identifier + ); +} + +guint64 investigation_graph_model_get_entity_count( + const InvestigationGraphModel *graph_model +) +{ + if (graph_model == NULL) + { + return 0; + } + + return (guint64) g_hash_table_size( + graph_model->entities_by_identifier + ); +} + +guint64 investigation_graph_model_get_relation_count( + const InvestigationGraphModel *graph_model +) +{ + if (graph_model == NULL) + { + return 0; + } + + return (guint64) g_hash_table_size( + graph_model->relations_by_identifier + ); +} + +GPtrArray *investigation_graph_model_list_entities( + const InvestigationGraphModel *graph_model, + GError **error +) +{ + GPtrArray *entities = + NULL; + + GHashTableIter iterator; + + gpointer entity_record = + NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (graph_model == NULL) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_INVALID_ARGUMENT, + "Le graphe d'enquête est absent." + ); + + return NULL; + } + + entities = + g_ptr_array_new(); + + if (entities == NULL) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_MEMORY, + "Impossible d'allouer la liste des entités." + ); + + return NULL; + } + + g_hash_table_iter_init( + &iterator, + graph_model->entities_by_identifier + ); + + while (g_hash_table_iter_next( + &iterator, + NULL, + &entity_record + )) + { + g_ptr_array_add( + entities, + entity_record + ); + } + + g_ptr_array_sort( + entities, + investigation_graph_model_compare_entities + ); + + return entities; +} + +GPtrArray *investigation_graph_model_list_relations( + const InvestigationGraphModel *graph_model, + GError **error +) +{ + GPtrArray *relations = + NULL; + + GHashTableIter iterator; + + gpointer relation_record = + NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (graph_model == NULL) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_INVALID_ARGUMENT, + "Le graphe d'enquête est absent." + ); + + return NULL; + } + + relations = + g_ptr_array_new(); + + if (relations == NULL) + { + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_MEMORY, + "Impossible d'allouer la liste des relations." + ); + + return NULL; + } + + g_hash_table_iter_init( + &iterator, + graph_model->relations_by_identifier + ); + + while (g_hash_table_iter_next( + &iterator, + NULL, + &relation_record + )) + { + g_ptr_array_add( + relations, + relation_record + ); + } + + g_ptr_array_sort( + relations, + investigation_graph_model_compare_relations + ); + + return relations; +} + +GPtrArray *investigation_graph_model_list_outgoing_relations( + const InvestigationGraphModel *graph_model, + const char *entity_identifier, + GError **error +) +{ + const GPtrArray *outgoing_relations = + NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (!investigation_graph_model_validate_entity_request( + graph_model, + entity_identifier, + error + )) + { + return NULL; + } + + outgoing_relations = + g_hash_table_lookup( + graph_model->outgoing_relations_by_entity, + entity_identifier + ); + + return investigation_graph_model_copy_relation_array( + outgoing_relations, + error + ); +} + +GPtrArray *investigation_graph_model_list_incoming_relations( + const InvestigationGraphModel *graph_model, + const char *entity_identifier, + GError **error +) +{ + const GPtrArray *incoming_relations = + NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (!investigation_graph_model_validate_entity_request( + graph_model, + entity_identifier, + error + )) + { + return NULL; + } + + incoming_relations = + g_hash_table_lookup( + graph_model->incoming_relations_by_entity, + entity_identifier + ); + + return investigation_graph_model_copy_relation_array( + incoming_relations, + error + ); +} + +GPtrArray *investigation_graph_model_list_incident_relations( + const InvestigationGraphModel *graph_model, + const char *entity_identifier, + GError **error +) +{ + const GPtrArray *outgoing_relations = + NULL; + + const GPtrArray *incoming_relations = + NULL; + + GPtrArray *incident_relations = + NULL; + + GHashTable *seen_relations = + NULL; + + guint relation_index = + 0; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (!investigation_graph_model_validate_entity_request( + graph_model, + entity_identifier, + error + )) + { + return NULL; + } + + outgoing_relations = + g_hash_table_lookup( + graph_model->outgoing_relations_by_entity, + entity_identifier + ); + + incoming_relations = + g_hash_table_lookup( + graph_model->incoming_relations_by_entity, + entity_identifier + ); + + incident_relations = + g_ptr_array_new(); + + seen_relations = + g_hash_table_new( + g_direct_hash, + g_direct_equal + ); + + if (incident_relations == NULL || + seen_relations == NULL) + { + if (incident_relations != NULL) + { + g_ptr_array_unref( + incident_relations + ); + } + + if (seen_relations != NULL) + { + g_hash_table_unref( + seen_relations + ); + } + + investigation_graph_model_set_error_literal( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_MEMORY, + "Impossible d'allouer la liste des relations incidentes." + ); + + return NULL; + } + + if (outgoing_relations != NULL) + { + for (relation_index = 0; + relation_index < outgoing_relations->len; + relation_index++) + { + gpointer relation_record = + g_ptr_array_index( + outgoing_relations, + relation_index + ); + + if (g_hash_table_add( + seen_relations, + relation_record + )) + { + g_ptr_array_add( + incident_relations, + relation_record + ); + } + } + } + + if (incoming_relations != NULL) + { + for (relation_index = 0; + relation_index < incoming_relations->len; + relation_index++) + { + gpointer relation_record = + g_ptr_array_index( + incoming_relations, + relation_index + ); + + if (g_hash_table_add( + seen_relations, + relation_record + )) + { + g_ptr_array_add( + incident_relations, + relation_record + ); + } + } + } + + g_hash_table_unref( + seen_relations + ); + + g_ptr_array_sort( + incident_relations, + investigation_graph_model_compare_relations + ); + + return incident_relations; +} diff --git a/tests/test_investigation_graph_model.c b/tests/test_investigation_graph_model.c new file mode 100644 index 0000000..8b92e4f --- /dev/null +++ b/tests/test_investigation_graph_model.c @@ -0,0 +1,1596 @@ +/****************************************************************************** + * @file test_investigation_graph_model.c + * @brief Tests unitaires du graphe métier d'une enquête. + ******************************************************************************/ + +#include "models/investigation_graph_model.h" + +#include "models/entity_record.h" +#include "models/relation_record.h" + +#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 Vérifie le domaine et le code d'une erreur du graphe. + */ +static void test_graph_assert_error( + const GError *error, + InvestigationGraphModelError expected_code +) +{ + assert(error != NULL); + + assert( + error->domain == + INVESTIGATION_GRAPH_MODEL_ERROR + ); + + assert( + error->code == + (gint) expected_code + ); + + assert(error->message != NULL); + assert(error->message[0] != '\0'); +} + +/** + * @brief Crée une entité valide. + */ +static EntityRecord *test_graph_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-21T20:00:00Z", + "2026-07-21T20:00:00Z", + ENTITY_STATUS_ACTIVE, + &error + ); + + assert(entity_record != NULL); + assert(error == NULL); + + return entity_record; +} + +/** + * @brief Crée une relation valide. + */ +static RelationRecord *test_graph_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, + NULL, + 75, + "2026-07-21T20:10:00Z", + "2026-07-21T20:10:00Z", + RELATION_STATUS_ACTIVE, + &error + ); + + assert(relation_record != NULL); + assert(error == NULL); + + return relation_record; +} + +/** + * @brief Ajoute une entité et vérifie le succès. + */ +static void test_graph_add_entity_or_abort( + InvestigationGraphModel *graph_model, + EntityRecord *entity_record +) +{ + GError *error = + NULL; + + assert( + investigation_graph_model_add_entity( + graph_model, + entity_record, + &error + ) + ); + + assert(error == NULL); +} + +/** + * @brief Ajoute une relation et vérifie le succès. + */ +static void test_graph_add_relation_or_abort( + InvestigationGraphModel *graph_model, + RelationRecord *relation_record +) +{ + GError *error = + NULL; + + assert( + investigation_graph_model_add_relation( + graph_model, + relation_record, + &error + ) + ); + + assert(error == NULL); +} + +/** + * @brief Ajoute les trois entités courantes. + */ +static void test_graph_add_standard_entities( + InvestigationGraphModel *graph_model +) +{ + test_graph_add_entity_or_abort( + graph_model, + test_graph_create_entity( + TEST_ENTITY_A_IDENTIFIER, + "Personne A" + ) + ); + + test_graph_add_entity_or_abort( + graph_model, + test_graph_create_entity( + TEST_ENTITY_B_IDENTIFIER, + "Personne B" + ) + ); + + test_graph_add_entity_or_abort( + graph_model, + test_graph_create_entity( + TEST_ENTITY_C_IDENTIFIER, + "Personne C" + ) + ); +} + +/** + * @brief Vérifie la création, le comptage et la destruction d'un graphe vide. + */ +static void test_graph_empty_lifecycle(void) +{ + InvestigationGraphModel *graph_model = + NULL; + + GPtrArray *entities = + NULL; + + GPtrArray *relations = + NULL; + + GError *error = + NULL; + + graph_model = + investigation_graph_model_new( + &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 + ); + + entities = + investigation_graph_model_list_entities( + graph_model, + &error + ); + + assert(entities != NULL); + assert(error == NULL); + assert(entities->len == 0); + + relations = + investigation_graph_model_list_relations( + graph_model, + &error + ); + + assert(relations != NULL); + assert(error == NULL); + assert(relations->len == 0); + + g_ptr_array_unref( + relations + ); + + g_ptr_array_unref( + entities + ); + + investigation_graph_model_free( + graph_model + ); + + investigation_graph_model_free( + NULL + ); +} + +/** + * @brief Vérifie l'ajout et la recherche d'une entité. + */ +static void test_graph_add_and_find_entity(void) +{ + InvestigationGraphModel *graph_model = + NULL; + + EntityRecord *entity_record = + NULL; + + const EntityRecord *loaded_entity = + NULL; + + GError *error = + NULL; + + graph_model = + investigation_graph_model_new( + &error + ); + + assert(graph_model != NULL); + assert(error == NULL); + + entity_record = + test_graph_create_entity( + TEST_ENTITY_A_IDENTIFIER, + "Personne A" + ); + + test_graph_add_entity_or_abort( + graph_model, + entity_record + ); + + /* + * Le pointeur retourné doit être le même objet possédé par le graphe. + */ + loaded_entity = + investigation_graph_model_find_entity( + graph_model, + TEST_ENTITY_A_IDENTIFIER + ); + + assert(loaded_entity == entity_record); + + assert( + strcmp( + entity_record_get_identifier( + loaded_entity + ), + TEST_ENTITY_A_IDENTIFIER + ) == 0 + ); + + assert( + investigation_graph_model_get_entity_count( + graph_model + ) == 1 + ); + + assert( + investigation_graph_model_find_entity( + graph_model, + TEST_ENTITY_B_IDENTIFIER + ) == NULL + ); + + assert( + investigation_graph_model_find_entity( + graph_model, + "identifiant-invalide" + ) == NULL + ); + + investigation_graph_model_free( + graph_model + ); +} + +/** + * @brief Vérifie le refus d'une entité NULL. + */ +static void test_graph_reject_null_entity(void) +{ + InvestigationGraphModel *graph_model = + NULL; + + GError *error = + NULL; + + graph_model = + investigation_graph_model_new( + &error + ); + + assert(graph_model != NULL); + assert(error == NULL); + + assert( + !investigation_graph_model_add_entity( + graph_model, + NULL, + &error + ) + ); + + test_graph_assert_error( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_INVALID_ARGUMENT + ); + + assert( + investigation_graph_model_get_entity_count( + graph_model + ) == 0 + ); + + g_clear_error( + &error + ); + + investigation_graph_model_free( + graph_model + ); +} + +/** + * @brief Vérifie le refus d'un UUID d'entité dupliqué. + */ +static void test_graph_reject_duplicate_entity(void) +{ + InvestigationGraphModel *graph_model = + NULL; + + EntityRecord *first_entity = + NULL; + + EntityRecord *duplicate_entity = + NULL; + + GError *error = + NULL; + + graph_model = + investigation_graph_model_new( + &error + ); + + assert(graph_model != NULL); + assert(error == NULL); + + first_entity = + test_graph_create_entity( + TEST_ENTITY_A_IDENTIFIER, + "Personne A" + ); + + duplicate_entity = + test_graph_create_entity( + TEST_ENTITY_A_IDENTIFIER, + "Autre valeur" + ); + + test_graph_add_entity_or_abort( + graph_model, + first_entity + ); + + assert( + !investigation_graph_model_add_entity( + graph_model, + duplicate_entity, + &error + ) + ); + + test_graph_assert_error( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_DUPLICATE_ENTITY + ); + + assert( + investigation_graph_model_get_entity_count( + graph_model + ) == 1 + ); + + /* + * Après l'échec, le modèle appartient encore à l'appelant. + */ + entity_record_free( + duplicate_entity + ); + + g_clear_error( + &error + ); + + investigation_graph_model_free( + graph_model + ); +} + +/** + * @brief Vérifie l'ordre stable de la liste globale des entités. + */ +static void test_graph_list_entities_sorted(void) +{ + InvestigationGraphModel *graph_model = + NULL; + + GPtrArray *entities = + NULL; + + const EntityRecord *first_entity = + NULL; + + const EntityRecord *second_entity = + NULL; + + const EntityRecord *third_entity = + NULL; + + GError *error = + NULL; + + graph_model = + investigation_graph_model_new( + &error + ); + + assert(graph_model != NULL); + assert(error == NULL); + + test_graph_add_entity_or_abort( + graph_model, + test_graph_create_entity( + TEST_ENTITY_C_IDENTIFIER, + "Personne C" + ) + ); + + test_graph_add_entity_or_abort( + graph_model, + test_graph_create_entity( + TEST_ENTITY_A_IDENTIFIER, + "Personne A" + ) + ); + + test_graph_add_entity_or_abort( + graph_model, + test_graph_create_entity( + TEST_ENTITY_B_IDENTIFIER, + "Personne B" + ) + ); + + entities = + investigation_graph_model_list_entities( + graph_model, + &error + ); + + assert(entities != NULL); + assert(error == NULL); + assert(entities->len == 3); + + first_entity = + g_ptr_array_index( + entities, + 0 + ); + + second_entity = + g_ptr_array_index( + entities, + 1 + ); + + third_entity = + g_ptr_array_index( + entities, + 2 + ); + + assert( + strcmp( + entity_record_get_identifier( + first_entity + ), + TEST_ENTITY_A_IDENTIFIER + ) == 0 + ); + + assert( + strcmp( + entity_record_get_identifier( + second_entity + ), + TEST_ENTITY_B_IDENTIFIER + ) == 0 + ); + + assert( + strcmp( + entity_record_get_identifier( + third_entity + ), + TEST_ENTITY_C_IDENTIFIER + ) == 0 + ); + + /* + * Le conteneur ne doit pas posséder les modèles. + */ + g_ptr_array_unref( + entities + ); + + assert( + investigation_graph_model_find_entity( + graph_model, + TEST_ENTITY_A_IDENTIFIER + ) != NULL + ); + + investigation_graph_model_free( + graph_model + ); +} + +/** + * @brief Vérifie l'ajout, la recherche et l'orientation d'une relation. + */ +static void test_graph_add_and_find_relation(void) +{ + InvestigationGraphModel *graph_model = + NULL; + + RelationRecord *relation_record = + NULL; + + const RelationRecord *loaded_relation = + NULL; + + GPtrArray *outgoing_relations = + NULL; + + GPtrArray *incoming_relations = + NULL; + + GError *error = + NULL; + + graph_model = + investigation_graph_model_new( + &error + ); + + assert(graph_model != NULL); + assert(error == NULL); + + test_graph_add_standard_entities( + graph_model + ); + + relation_record = + test_graph_create_relation( + TEST_RELATION_AB_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + test_graph_add_relation_or_abort( + graph_model, + relation_record + ); + + loaded_relation = + investigation_graph_model_find_relation( + graph_model, + TEST_RELATION_AB_IDENTIFIER + ); + + assert(loaded_relation == relation_record); + + assert( + investigation_graph_model_get_relation_count( + graph_model + ) == 1 + ); + + outgoing_relations = + investigation_graph_model_list_outgoing_relations( + graph_model, + TEST_ENTITY_A_IDENTIFIER, + &error + ); + + assert(outgoing_relations != NULL); + assert(error == NULL); + assert(outgoing_relations->len == 1); + + assert( + g_ptr_array_index( + outgoing_relations, + 0 + ) == relation_record + ); + + incoming_relations = + investigation_graph_model_list_incoming_relations( + graph_model, + TEST_ENTITY_B_IDENTIFIER, + &error + ); + + assert(incoming_relations != NULL); + assert(error == NULL); + assert(incoming_relations->len == 1); + + assert( + g_ptr_array_index( + incoming_relations, + 0 + ) == relation_record + ); + + g_ptr_array_unref( + incoming_relations + ); + + g_ptr_array_unref( + outgoing_relations + ); + + investigation_graph_model_free( + graph_model + ); +} + +/** + * @brief Vérifie le refus d'une relation NULL. + */ +static void test_graph_reject_null_relation(void) +{ + InvestigationGraphModel *graph_model = + NULL; + + GError *error = + NULL; + + graph_model = + investigation_graph_model_new( + &error + ); + + assert(graph_model != NULL); + assert(error == NULL); + + assert( + !investigation_graph_model_add_relation( + graph_model, + NULL, + &error + ) + ); + + test_graph_assert_error( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_INVALID_ARGUMENT + ); + + assert( + investigation_graph_model_get_relation_count( + graph_model + ) == 0 + ); + + g_clear_error( + &error + ); + + investigation_graph_model_free( + graph_model + ); +} + +/** + * @brief Vérifie le refus d'un UUID de relation dupliqué. + */ +static void test_graph_reject_duplicate_relation(void) +{ + InvestigationGraphModel *graph_model = + NULL; + + RelationRecord *first_relation = + NULL; + + RelationRecord *duplicate_relation = + NULL; + + GError *error = + NULL; + + graph_model = + investigation_graph_model_new( + &error + ); + + assert(graph_model != NULL); + assert(error == NULL); + + test_graph_add_standard_entities( + graph_model + ); + + first_relation = + test_graph_create_relation( + TEST_RELATION_AB_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + duplicate_relation = + test_graph_create_relation( + TEST_RELATION_AB_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_C_IDENTIFIER, + "controls" + ); + + test_graph_add_relation_or_abort( + graph_model, + first_relation + ); + + assert( + !investigation_graph_model_add_relation( + graph_model, + duplicate_relation, + &error + ) + ); + + test_graph_assert_error( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_DUPLICATE_RELATION + ); + + assert( + investigation_graph_model_get_relation_count( + graph_model + ) == 1 + ); + + /* + * Après l'échec, la relation appartient encore à l'appelant. + */ + relation_record_free( + duplicate_relation + ); + + g_clear_error( + &error + ); + + investigation_graph_model_free( + graph_model + ); +} + +/** + * @brief Vérifie le refus d'une relation dont la source est absente. + */ +static void test_graph_reject_missing_source(void) +{ + InvestigationGraphModel *graph_model = + NULL; + + RelationRecord *relation_record = + NULL; + + GPtrArray *incoming_relations = + NULL; + + GError *error = + NULL; + + graph_model = + investigation_graph_model_new( + &error + ); + + assert(graph_model != NULL); + assert(error == NULL); + + test_graph_add_entity_or_abort( + graph_model, + test_graph_create_entity( + TEST_ENTITY_B_IDENTIFIER, + "Personne B" + ) + ); + + relation_record = + test_graph_create_relation( + TEST_RELATION_AB_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + assert( + !investigation_graph_model_add_relation( + graph_model, + relation_record, + &error + ) + ); + + test_graph_assert_error( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_SOURCE_NOT_FOUND + ); + + assert( + investigation_graph_model_get_relation_count( + graph_model + ) == 0 + ); + + g_clear_error( + &error + ); + + incoming_relations = + investigation_graph_model_list_incoming_relations( + graph_model, + TEST_ENTITY_B_IDENTIFIER, + &error + ); + + assert(incoming_relations != NULL); + assert(error == NULL); + assert(incoming_relations->len == 0); + + g_ptr_array_unref( + incoming_relations + ); + + relation_record_free( + relation_record + ); + + investigation_graph_model_free( + graph_model + ); +} + +/** + * @brief Vérifie le refus d'une relation dont la cible est absente. + */ +static void test_graph_reject_missing_target(void) +{ + InvestigationGraphModel *graph_model = + NULL; + + RelationRecord *relation_record = + NULL; + + GPtrArray *outgoing_relations = + NULL; + + GError *error = + NULL; + + graph_model = + investigation_graph_model_new( + &error + ); + + assert(graph_model != NULL); + assert(error == NULL); + + test_graph_add_entity_or_abort( + graph_model, + test_graph_create_entity( + TEST_ENTITY_A_IDENTIFIER, + "Personne A" + ) + ); + + relation_record = + test_graph_create_relation( + TEST_RELATION_AB_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + assert( + !investigation_graph_model_add_relation( + graph_model, + relation_record, + &error + ) + ); + + test_graph_assert_error( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_TARGET_NOT_FOUND + ); + + assert( + investigation_graph_model_get_relation_count( + graph_model + ) == 0 + ); + + g_clear_error( + &error + ); + + outgoing_relations = + investigation_graph_model_list_outgoing_relations( + graph_model, + TEST_ENTITY_A_IDENTIFIER, + &error + ); + + assert(outgoing_relations != NULL); + assert(error == NULL); + assert(outgoing_relations->len == 0); + + g_ptr_array_unref( + outgoing_relations + ); + + relation_record_free( + relation_record + ); + + investigation_graph_model_free( + graph_model + ); +} + +/** + * @brief Vérifie l'ordre global des relations. + */ +static void test_graph_list_relations_sorted(void) +{ + InvestigationGraphModel *graph_model = + NULL; + + GPtrArray *relations = + NULL; + + const RelationRecord *first_relation = + NULL; + + const RelationRecord *second_relation = + NULL; + + const RelationRecord *third_relation = + NULL; + + GError *error = + NULL; + + graph_model = + investigation_graph_model_new( + &error + ); + + assert(graph_model != NULL); + assert(error == NULL); + + test_graph_add_standard_entities( + graph_model + ); + + test_graph_add_relation_or_abort( + graph_model, + test_graph_create_relation( + TEST_RELATION_CA_IDENTIFIER, + TEST_ENTITY_C_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + "knows" + ) + ); + + test_graph_add_relation_or_abort( + graph_model, + test_graph_create_relation( + TEST_RELATION_AB_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ) + ); + + test_graph_add_relation_or_abort( + graph_model, + test_graph_create_relation( + TEST_RELATION_AC_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_C_IDENTIFIER, + "controls" + ) + ); + + relations = + investigation_graph_model_list_relations( + graph_model, + &error + ); + + assert(relations != NULL); + assert(error == NULL); + assert(relations->len == 3); + + first_relation = + g_ptr_array_index( + relations, + 0 + ); + + second_relation = + g_ptr_array_index( + relations, + 1 + ); + + third_relation = + g_ptr_array_index( + relations, + 2 + ); + + assert( + strcmp( + relation_record_get_identifier( + first_relation + ), + TEST_RELATION_AB_IDENTIFIER + ) == 0 + ); + + assert( + strcmp( + relation_record_get_identifier( + second_relation + ), + TEST_RELATION_AC_IDENTIFIER + ) == 0 + ); + + assert( + strcmp( + relation_record_get_identifier( + third_relation + ), + TEST_RELATION_CA_IDENTIFIER + ) == 0 + ); + + g_ptr_array_unref( + relations + ); + + assert( + investigation_graph_model_find_relation( + graph_model, + TEST_RELATION_AB_IDENTIFIER + ) != NULL + ); + + investigation_graph_model_free( + graph_model + ); +} + +/** + * @brief Vérifie les listes entrantes, sortantes et incidentes. + */ +static void test_graph_navigation_indexes(void) +{ + InvestigationGraphModel *graph_model = + NULL; + + RelationRecord *relation_ab = + NULL; + + RelationRecord *relation_ac = + NULL; + + RelationRecord *relation_ca = + NULL; + + GPtrArray *outgoing_a = + NULL; + + GPtrArray *incoming_a = + NULL; + + GPtrArray *incident_a = + NULL; + + GPtrArray *outgoing_b = + NULL; + + GError *error = + NULL; + + graph_model = + investigation_graph_model_new( + &error + ); + + assert(graph_model != NULL); + assert(error == NULL); + + test_graph_add_standard_entities( + graph_model + ); + + relation_ab = + test_graph_create_relation( + TEST_RELATION_AB_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + relation_ac = + test_graph_create_relation( + TEST_RELATION_AC_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_C_IDENTIFIER, + "controls" + ); + + relation_ca = + test_graph_create_relation( + TEST_RELATION_CA_IDENTIFIER, + TEST_ENTITY_C_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + "knows" + ); + + test_graph_add_relation_or_abort( + graph_model, + relation_ca + ); + + test_graph_add_relation_or_abort( + graph_model, + relation_ac + ); + + test_graph_add_relation_or_abort( + graph_model, + relation_ab + ); + + 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); + + assert( + g_ptr_array_index( + outgoing_a, + 0 + ) == relation_ab + ); + + assert( + g_ptr_array_index( + outgoing_a, + 1 + ) == relation_ac + ); + + 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); + + assert( + g_ptr_array_index( + incoming_a, + 0 + ) == relation_ca + ); + + 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); + + assert( + g_ptr_array_index( + incident_a, + 0 + ) == relation_ab + ); + + assert( + g_ptr_array_index( + incident_a, + 1 + ) == relation_ac + ); + + assert( + g_ptr_array_index( + incident_a, + 2 + ) == relation_ca + ); + + /* + * B ne possède aucune relation sortante. + */ + outgoing_b = + investigation_graph_model_list_outgoing_relations( + graph_model, + TEST_ENTITY_B_IDENTIFIER, + &error + ); + + assert(outgoing_b != NULL); + assert(error == NULL); + assert(outgoing_b->len == 0); + + g_ptr_array_unref( + outgoing_b + ); + + g_ptr_array_unref( + incident_a + ); + + g_ptr_array_unref( + incoming_a + ); + + g_ptr_array_unref( + outgoing_a + ); + + investigation_graph_model_free( + graph_model + ); +} + +/** + * @brief Vérifie les erreurs de navigation par entité. + */ +static void test_graph_navigation_errors(void) +{ + InvestigationGraphModel *graph_model = + NULL; + + GPtrArray *relations = + NULL; + + GError *error = + NULL; + + graph_model = + investigation_graph_model_new( + &error + ); + + assert(graph_model != NULL); + assert(error == NULL); + + relations = + investigation_graph_model_list_outgoing_relations( + graph_model, + "identifiant-invalide", + &error + ); + + assert(relations == NULL); + + test_graph_assert_error( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + relations = + investigation_graph_model_list_incoming_relations( + graph_model, + TEST_ENTITY_A_IDENTIFIER, + &error + ); + + assert(relations == NULL); + + test_graph_assert_error( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_ENTITY_NOT_FOUND + ); + + g_clear_error( + &error + ); + + relations = + investigation_graph_model_list_incident_relations( + NULL, + TEST_ENTITY_A_IDENTIFIER, + &error + ); + + assert(relations == NULL); + + test_graph_assert_error( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + investigation_graph_model_free( + graph_model + ); +} + +/** + * @brief Vérifie les valeurs défensives pour un graphe NULL. + */ +static void test_graph_null_accessors(void) +{ + GPtrArray *items = + NULL; + + GError *error = + NULL; + + assert( + investigation_graph_model_get_entity_count( + NULL + ) == 0 + ); + + assert( + investigation_graph_model_get_relation_count( + NULL + ) == 0 + ); + + assert( + investigation_graph_model_find_entity( + NULL, + TEST_ENTITY_A_IDENTIFIER + ) == NULL + ); + + assert( + investigation_graph_model_find_relation( + NULL, + TEST_RELATION_AB_IDENTIFIER + ) == NULL + ); + + items = + investigation_graph_model_list_entities( + NULL, + &error + ); + + assert(items == NULL); + + test_graph_assert_error( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + items = + investigation_graph_model_list_relations( + NULL, + &error + ); + + assert(items == NULL); + + test_graph_assert_error( + error, + INVESTIGATION_GRAPH_MODEL_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); +} + +/** + * @brief Vérifie que GError est facultatif. + */ +static void test_graph_optional_error(void) +{ + InvestigationGraphModel *graph_model = + NULL; + + EntityRecord *duplicate_entity = + NULL; + + graph_model = + investigation_graph_model_new( + NULL + ); + + assert(graph_model != NULL); + + test_graph_add_entity_or_abort( + graph_model, + test_graph_create_entity( + TEST_ENTITY_A_IDENTIFIER, + "Personne A" + ) + ); + + duplicate_entity = + test_graph_create_entity( + TEST_ENTITY_A_IDENTIFIER, + "Duplicata" + ); + + assert( + !investigation_graph_model_add_entity( + graph_model, + duplicate_entity, + NULL + ) + ); + + entity_record_free( + duplicate_entity + ); + + investigation_graph_model_free( + graph_model + ); +} + +int main( + int argc, + char **argv +) +{ + g_test_init( + &argc, + &argv, + NULL + ); + + test_graph_empty_lifecycle(); + test_graph_add_and_find_entity(); + test_graph_reject_null_entity(); + test_graph_reject_duplicate_entity(); + test_graph_list_entities_sorted(); + test_graph_add_and_find_relation(); + test_graph_reject_null_relation(); + test_graph_reject_duplicate_relation(); + test_graph_reject_missing_source(); + test_graph_reject_missing_target(); + test_graph_list_relations_sorted(); + test_graph_navigation_indexes(); + test_graph_navigation_errors(); + test_graph_null_accessors(); + test_graph_optional_error(); + + printf( + "InvestigationGraphModel : tous les tests sont valides.\n" + ); + + return 0; +}