From fbaaac583d80b59dc0b73838c8ba36278abb6bed Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Tue, 21 Jul 2026 07:27:50 +0200 Subject: [PATCH] feat: associate relations with evidence --- Makefile | 25 +- include/dao/relation_evidence_dao.h | 218 +++ src/dao/relation_evidence_dao.c | 1069 +++++++++++++++ tests/test_relation_evidence_dao.c | 1906 +++++++++++++++++++++++++++ 4 files changed, 3216 insertions(+), 2 deletions(-) create mode 100644 include/dao/relation_evidence_dao.h create mode 100644 src/dao/relation_evidence_dao.c create mode 100644 tests/test_relation_evidence_dao.c diff --git a/Makefile b/Makefile index f47e081..13a0363 100644 --- a/Makefile +++ b/Makefile @@ -47,6 +47,7 @@ ENTITY_DAO_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic EVIDENCE_ENTITY_DAO_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic RELATION_RECORD_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic RELATION_DAO_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic +RELATION_EVIDENCE_DAO_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic SRC := $(shell find src -name "*.c") @@ -94,6 +95,7 @@ TEST_ENTITY_DAO := tests/test_entity_dao TEST_EVIDENCE_ENTITY_DAO := tests/test_evidence_entity_dao TEST_RELATION_RECORD := tests/test_relation_record TEST_RELATION_DAO := tests/test_relation_dao +TEST_RELATION_EVIDENCE_DAO := tests/test_relation_evidence_dao all: $(TARGET) @@ -439,6 +441,22 @@ $(TEST_RELATION_DAO): \ src/database/error.c $(CC) $(RELATION_DAO_TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 +$(TEST_RELATION_EVIDENCE_DAO): \ + tests/test_relation_evidence_dao.c \ + src/dao/relation_evidence_dao.c \ + src/dao/relation_dao.c \ + src/dao/evidence_dao.c \ + src/dao/entity_dao.c \ + src/models/relation_record.c \ + src/models/evidence_record.c \ + src/models/entity_record.c \ + src/database/database.c \ + src/database/schema.c \ + src/database/statement.c \ + src/database/transaction.c \ + src/database/error.c + $(CC) $(RELATION_EVIDENCE_DAO_TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 + test: \ $(TEST_NODE) \ $(TEST_TREE_MODEL) \ @@ -479,7 +497,8 @@ test: \ $(TEST_ENTITY_DAO) \ $(TEST_EVIDENCE_ENTITY_DAO) \ $(TEST_RELATION_RECORD) \ - $(TEST_RELATION_DAO) + $(TEST_RELATION_DAO) \ + $(TEST_RELATION_EVIDENCE_DAO) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -521,6 +540,7 @@ test: \ @$(TEST_EVIDENCE_ENTITY_DAO) @$(TEST_RELATION_RECORD) @$(TEST_RELATION_DAO) + @$(TEST_RELATION_EVIDENCE_DAO) @echo "Tous les tests sont valides." %.o: %.c @@ -568,6 +588,7 @@ clean: $(TEST_ENTITY_DAO) \ $(TEST_EVIDENCE_ENTITY_DAO) \ $(TEST_RELATION_RECORD) \ - $(TEST_RELATION_DAO) + $(TEST_RELATION_DAO) \ + $(TEST_RELATION_EVIDENCE_DAO) .PHONY: clean run test diff --git a/include/dao/relation_evidence_dao.h b/include/dao/relation_evidence_dao.h new file mode 100644 index 0000000..ba27072 --- /dev/null +++ b/include/dao/relation_evidence_dao.h @@ -0,0 +1,218 @@ +/****************************************************************************** + * @file relation_evidence_dao.h + * @brief Persistance des associations entre relations et preuves. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_RELATION_EVIDENCE_DAO_H +#define LABFY_INVESTIGATION_RELATION_EVIDENCE_DAO_H + +#include "database/database.h" + +#include + +G_BEGIN_DECLS + +/** + * @brief DAO opaque des associations entre relations et preuves. + * + * Le DAO emprunte la connexion Database reçue lors de sa création. + */ +typedef struct RelationEvidenceDao RelationEvidenceDao; + +/** + * @brief Catégories d'erreurs produites par RelationEvidenceDao. + */ +typedef enum +{ + /** + * Un argument public est invalide. + */ + RELATION_EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + + /** + * L'allocation d'une ressource a échoué. + */ + RELATION_EVIDENCE_DAO_ERROR_MEMORY, + + /** + * La requête SQLite n'a pas pu être préparée. + */ + RELATION_EVIDENCE_DAO_ERROR_PREPARE, + + /** + * Un paramètre n'a pas pu être lié à la requête. + */ + RELATION_EVIDENCE_DAO_ERROR_BIND, + + /** + * La requête SQLite n'a pas pu être exécutée. + */ + RELATION_EVIDENCE_DAO_ERROR_EXECUTE, + + /** + * Une contrainte SQLite a été violée. + */ + RELATION_EVIDENCE_DAO_ERROR_CONSTRAINT, + + /** + * Un enregistrement ou une association demandée n'existe pas. + */ + RELATION_EVIDENCE_DAO_ERROR_NOT_FOUND, + + /** + * Une ligne SQLite n'a pas pu être lue. + */ + RELATION_EVIDENCE_DAO_ERROR_READ, + + /** + * La table ou une colonne attendue est absente. + */ + RELATION_EVIDENCE_DAO_ERROR_SCHEMA +} RelationEvidenceDaoError; + +/** + * @brief Domaine d'erreur du DAO. + */ +#define RELATION_EVIDENCE_DAO_ERROR \ + relation_evidence_dao_error_quark() + +/** + * @brief Retourne le domaine d'erreur du DAO. + */ +GQuark relation_evidence_dao_error_quark(void); + +/** + * @brief Crée un DAO utilisant une connexion Database existante. + * + * La connexion est empruntée et doit rester valide pendant toute + * la durée de vie du DAO. + * + * @param database Connexion SQLite ouverte. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau DAO, ou NULL en cas d'échec. + */ +RelationEvidenceDao *relation_evidence_dao_new( + Database *database, + GError **error +); + +/** + * @brief Libère le DAO. + * + * La connexion Database empruntée n'est pas fermée. + * Cette fonction accepte NULL. + * + * @param relation_evidence_dao DAO à libérer. + */ +void relation_evidence_dao_free( + RelationEvidenceDao *relation_evidence_dao +); + +/** + * @brief Associe une relation à une preuve. + * + * Les deux UUID doivent correspondre à des enregistrements existants. + * Une association déjà existante produit une erreur de contrainte. + * + * @param relation_evidence_dao DAO valide. + * @param relation_identifier UUID de la relation. + * @param evidence_identifier UUID de la preuve. + * @param error Emplacement facultatif recevant une erreur. + * + * @return TRUE si l'association est créée. + */ +gboolean relation_evidence_dao_link( + RelationEvidenceDao *relation_evidence_dao, + const char *relation_identifier, + const char *evidence_identifier, + GError **error +); + +/** + * @brief Supprime une association entre une relation et une preuve. + * + * Une association absente produit RELATION_EVIDENCE_DAO_ERROR_NOT_FOUND. + * + * La relation et la preuve ne sont jamais supprimées. + * + * @param relation_evidence_dao DAO valide. + * @param relation_identifier UUID de la relation. + * @param evidence_identifier UUID de la preuve. + * @param error Emplacement facultatif recevant une erreur. + * + * @return TRUE si l'association est supprimée. + */ +gboolean relation_evidence_dao_unlink( + RelationEvidenceDao *relation_evidence_dao, + const char *relation_identifier, + const char *evidence_identifier, + GError **error +); + +/** + * @brief Vérifie si une association existe. + * + * @param relation_evidence_dao DAO valide. + * @param relation_identifier UUID de la relation. + * @param evidence_identifier UUID de la preuve. + * @param out_exists Destination du résultat. + * @param error Emplacement facultatif recevant une erreur. + * + * @return TRUE si la recherche a été exécutée correctement. + */ +gboolean relation_evidence_dao_exists( + RelationEvidenceDao *relation_evidence_dao, + const char *relation_identifier, + const char *evidence_identifier, + gboolean *out_exists, + GError **error +); + +/** + * @brief Liste les UUID des preuves liées à une relation. + * + * Les identifiants sont triés par ordre lexicographique croissant. + * + * Le tableau et les chaînes qu'il contient appartiennent à l'appelant. + * Le tableau utilise g_free() comme fonction de destruction. + * + * Une relation sans association produit un tableau vide. + * + * @param relation_evidence_dao DAO valide. + * @param relation_identifier UUID de la relation. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau GPtrArray de chaînes, ou NULL en cas d'échec. + */ +GPtrArray *relation_evidence_dao_list_evidence_identifiers( + RelationEvidenceDao *relation_evidence_dao, + const char *relation_identifier, + GError **error +); + +/** + * @brief Liste les UUID des relations liées à une preuve. + * + * Les identifiants sont triés par ordre lexicographique croissant. + * + * Le tableau et les chaînes qu'il contient appartiennent à l'appelant. + * Le tableau utilise g_free() comme fonction de destruction. + * + * Une preuve sans association produit un tableau vide. + * + * @param relation_evidence_dao DAO valide. + * @param evidence_identifier UUID de la preuve. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau GPtrArray de chaînes, ou NULL en cas d'échec. + */ +GPtrArray *relation_evidence_dao_list_relation_identifiers( + RelationEvidenceDao *relation_evidence_dao, + const char *evidence_identifier, + GError **error +); + +G_END_DECLS + +#endif diff --git a/src/dao/relation_evidence_dao.c b/src/dao/relation_evidence_dao.c new file mode 100644 index 0000000..52f09d8 --- /dev/null +++ b/src/dao/relation_evidence_dao.c @@ -0,0 +1,1069 @@ +/****************************************************************************** + * @file relation_evidence_dao.c + * @brief Persistance des associations entre relations et preuves. + ******************************************************************************/ + +#include "dao/relation_evidence_dao.h" + +#include "database/error.h" +#include "database/statement.h" + +#include + +/** + * @brief Représentation privée du DAO. + * + * La connexion Database est empruntée. + */ +struct RelationEvidenceDao +{ + Database *database; +}; + +/** + * @brief Requête vérifiant l'existence d'une relation. + */ +static const char *const relation_evidence_dao_relation_exists_sql = + "SELECT 1 " + "FROM relations " + "WHERE id = ? " + "LIMIT 1;"; + +/** + * @brief Requête vérifiant l'existence d'une preuve. + */ +static const char *const relation_evidence_dao_evidence_exists_sql = + "SELECT 1 " + "FROM preuves " + "WHERE id = ? " + "LIMIT 1;"; + +/** + * @brief Requête vérifiant l'existence d'une association. + */ +static const char *const relation_evidence_dao_association_exists_sql = + "SELECT 1 " + "FROM relation_preuves " + "WHERE relation_id = ? " + "AND preuve_id = ? " + "LIMIT 1;"; + +/** + * @brief Requête créant une association. + */ +static const char *const relation_evidence_dao_link_sql = + "INSERT INTO relation_preuves" + "(" + " relation_id," + " preuve_id" + ")" + "VALUES" + "(" + " ?," + " ?" + ");"; + +/** + * @brief Requête supprimant une association. + */ +static const char *const relation_evidence_dao_unlink_sql = + "DELETE FROM relation_preuves " + "WHERE relation_id = ? " + "AND preuve_id = ?;"; + +/** + * @brief Requête listant les preuves liées à une relation. + */ +static const char *const +relation_evidence_dao_list_evidence_identifiers_sql = + "SELECT preuve_id " + "FROM relation_preuves " + "WHERE relation_id = ? " + "ORDER BY preuve_id ASC;"; + +/** + * @brief Requête listant les relations liées à une preuve. + */ +static const char *const +relation_evidence_dao_list_relation_identifiers_sql = + "SELECT relation_id " + "FROM relation_preuves " + "WHERE preuve_id = ? " + "ORDER BY relation_id ASC;"; + +/** + * @brief Enregistre une erreur littérale du DAO. + */ +static void relation_evidence_dao_set_error_literal( + GError **error, + RelationEvidenceDaoError error_code, + const char *message +) +{ + if (error == NULL || + *error != NULL) + { + return; + } + + g_set_error_literal( + error, + RELATION_EVIDENCE_DAO_ERROR, + error_code, + message + ); +} + +/** + * @brief Détermine si SQLite signale un schéma incompatible. + */ +static gboolean relation_evidence_dao_is_schema_error( + const char *database_message +) +{ + if (database_message == NULL) + { + return FALSE; + } + + return g_strstr_len( + database_message, + -1, + "no such table" + ) != NULL || + g_strstr_len( + database_message, + -1, + "no such column" + ) != NULL; +} + +/** + * @brief Détermine si SQLite signale une contrainte. + */ +static gboolean relation_evidence_dao_is_constraint_error( + const char *database_message +) +{ + if (database_message == NULL) + { + return FALSE; + } + + return g_strstr_len( + database_message, + -1, + "constraint" + ) != NULL || + g_strstr_len( + database_message, + -1, + "CONSTRAINT" + ) != NULL; +} + +/** + * @brief Transforme la dernière erreur Database en erreur du DAO. + */ +static void relation_evidence_dao_set_database_error( + RelationEvidenceDao *relation_evidence_dao, + GError **error, + RelationEvidenceDaoError error_code, + const char *context +) +{ + const char *database_message = + NULL; + + if (error == NULL || + *error != NULL) + { + return; + } + + if (relation_evidence_dao != NULL && + relation_evidence_dao->database != NULL) + { + database_message = + database_error_get_message( + relation_evidence_dao->database + ); + } + + if (error_code == + RELATION_EVIDENCE_DAO_ERROR_PREPARE && + relation_evidence_dao_is_schema_error( + database_message + )) + { + error_code = + RELATION_EVIDENCE_DAO_ERROR_SCHEMA; + } + + if (database_message != NULL && + database_message[0] != '\0') + { + g_set_error( + error, + RELATION_EVIDENCE_DAO_ERROR, + error_code, + "%s : %s", + context, + database_message + ); + + return; + } + + g_set_error_literal( + error, + RELATION_EVIDENCE_DAO_ERROR, + error_code, + context + ); +} + +/** + * @brief Vérifie le DAO et les deux UUID d'une association. + */ +static gboolean relation_evidence_dao_validate_association( + RelationEvidenceDao *relation_evidence_dao, + const char *relation_identifier, + const char *evidence_identifier, + GError **error +) +{ + if (relation_evidence_dao == NULL || + relation_evidence_dao->database == NULL) + { + relation_evidence_dao_set_error_literal( + error, + RELATION_EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + "Le DAO des associations relation-preuve est invalide." + ); + + return FALSE; + } + + if (relation_identifier == NULL || + !g_uuid_string_is_valid( + relation_identifier + )) + { + relation_evidence_dao_set_error_literal( + error, + RELATION_EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + "L'identifiant de relation est invalide." + ); + + return FALSE; + } + + if (evidence_identifier == NULL || + !g_uuid_string_is_valid( + evidence_identifier + )) + { + relation_evidence_dao_set_error_literal( + error, + RELATION_EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + "L'identifiant de preuve est invalide." + ); + + return FALSE; + } + + return TRUE; +} + +/** + * @brief Vérifie le DAO et un UUID utilisé pour une liste. + */ +static gboolean relation_evidence_dao_validate_list_request( + RelationEvidenceDao *relation_evidence_dao, + const char *identifier, + const char *invalid_identifier_message, + GError **error +) +{ + if (relation_evidence_dao == NULL || + relation_evidence_dao->database == NULL) + { + relation_evidence_dao_set_error_literal( + error, + RELATION_EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + "Le DAO des associations relation-preuve est invalide." + ); + + return FALSE; + } + + if (identifier == NULL || + !g_uuid_string_is_valid( + identifier + )) + { + relation_evidence_dao_set_error_literal( + error, + RELATION_EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + invalid_identifier_message + ); + + return FALSE; + } + + return TRUE; +} + +/** + * @brief Exécute une requête SELECT 1 avec un ou deux paramètres. + */ +static gboolean relation_evidence_dao_query_exists( + RelationEvidenceDao *relation_evidence_dao, + const char *sql, + const char *first_value, + const char *second_value, + gboolean *out_exists, + GError **error +) +{ + DatabaseStatement *statement = + NULL; + + DatabaseStatementStepResult step_result = + DATABASE_STATEMENT_STEP_ERROR; + + gboolean success = + FALSE; + + if (relation_evidence_dao == NULL || + relation_evidence_dao->database == NULL || + sql == NULL || + first_value == NULL || + out_exists == NULL) + { + relation_evidence_dao_set_error_literal( + error, + RELATION_EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + "Les paramètres de la recherche d'existence sont invalides." + ); + + return FALSE; + } + + *out_exists = + FALSE; + + statement = + database_statement_prepare( + relation_evidence_dao->database, + sql + ); + + if (statement == NULL) + { + relation_evidence_dao_set_database_error( + relation_evidence_dao, + error, + RELATION_EVIDENCE_DAO_ERROR_PREPARE, + "Impossible de préparer la recherche d'existence" + ); + + goto cleanup; + } + + if (!database_statement_bind_text( + statement, + 1, + first_value + )) + { + relation_evidence_dao_set_database_error( + relation_evidence_dao, + error, + RELATION_EVIDENCE_DAO_ERROR_BIND, + "Impossible de lier le premier identifiant" + ); + + goto cleanup; + } + + if (second_value != NULL && + !database_statement_bind_text( + statement, + 2, + second_value + )) + { + relation_evidence_dao_set_database_error( + relation_evidence_dao, + error, + RELATION_EVIDENCE_DAO_ERROR_BIND, + "Impossible de lier le second identifiant" + ); + + goto cleanup; + } + + step_result = + database_statement_step( + statement + ); + + if (step_result == + DATABASE_STATEMENT_STEP_ERROR) + { + relation_evidence_dao_set_database_error( + relation_evidence_dao, + error, + RELATION_EVIDENCE_DAO_ERROR_EXECUTE, + "Impossible d'exécuter la recherche d'existence" + ); + + goto cleanup; + } + + *out_exists = + step_result == + DATABASE_STATEMENT_STEP_ROW; + + success = + TRUE; + +cleanup: + + database_statement_finalize( + statement + ); + + return success; +} + +/** + * @brief Exécute une modification à deux UUID. + */ +static gboolean relation_evidence_dao_execute_pair_statement( + RelationEvidenceDao *relation_evidence_dao, + const char *sql, + const char *relation_identifier, + const char *evidence_identifier, + const char *prepare_context, + const char *bind_context, + const char *execute_context, + GError **error +) +{ + DatabaseStatement *statement = + NULL; + + DatabaseStatementStepResult step_result = + DATABASE_STATEMENT_STEP_ERROR; + + const char *database_message = + NULL; + + gboolean success = + FALSE; + + statement = + database_statement_prepare( + relation_evidence_dao->database, + sql + ); + + if (statement == NULL) + { + relation_evidence_dao_set_database_error( + relation_evidence_dao, + error, + RELATION_EVIDENCE_DAO_ERROR_PREPARE, + prepare_context + ); + + goto cleanup; + } + + if (!database_statement_bind_text( + statement, + 1, + relation_identifier + ) || + !database_statement_bind_text( + statement, + 2, + evidence_identifier + )) + { + relation_evidence_dao_set_database_error( + relation_evidence_dao, + error, + RELATION_EVIDENCE_DAO_ERROR_BIND, + bind_context + ); + + goto cleanup; + } + + step_result = + database_statement_step( + statement + ); + + if (step_result != + DATABASE_STATEMENT_STEP_DONE) + { + database_message = + database_error_get_message( + relation_evidence_dao->database + ); + + if (relation_evidence_dao_is_constraint_error( + database_message + )) + { + relation_evidence_dao_set_database_error( + relation_evidence_dao, + error, + RELATION_EVIDENCE_DAO_ERROR_CONSTRAINT, + execute_context + ); + } + else + { + relation_evidence_dao_set_database_error( + relation_evidence_dao, + error, + RELATION_EVIDENCE_DAO_ERROR_EXECUTE, + execute_context + ); + } + + goto cleanup; + } + + success = + TRUE; + +cleanup: + + database_statement_finalize( + statement + ); + + return success; +} + +/** + * @brief Charge une liste d'UUID depuis une requête à un paramètre. + */ +static GPtrArray *relation_evidence_dao_list_identifiers( + RelationEvidenceDao *relation_evidence_dao, + const char *sql, + const char *identifier, + const char *prepare_context, + const char *bind_context, + const char *execute_context, + GError **error +) +{ + DatabaseStatement *statement = + NULL; + + GPtrArray *identifiers = + NULL; + + DatabaseStatementStepResult step_result = + DATABASE_STATEMENT_STEP_ERROR; + + identifiers = + g_ptr_array_new_with_free_func( + g_free + ); + + if (identifiers == NULL) + { + relation_evidence_dao_set_error_literal( + error, + RELATION_EVIDENCE_DAO_ERROR_MEMORY, + "Impossible d'allouer la liste d'identifiants." + ); + + return NULL; + } + + statement = + database_statement_prepare( + relation_evidence_dao->database, + sql + ); + + if (statement == NULL) + { + relation_evidence_dao_set_database_error( + relation_evidence_dao, + error, + RELATION_EVIDENCE_DAO_ERROR_PREPARE, + prepare_context + ); + + goto failure; + } + + if (!database_statement_bind_text( + statement, + 1, + identifier + )) + { + relation_evidence_dao_set_database_error( + relation_evidence_dao, + error, + RELATION_EVIDENCE_DAO_ERROR_BIND, + bind_context + ); + + goto failure; + } + + while (TRUE) + { + char *loaded_identifier = + NULL; + + step_result = + database_statement_step( + statement + ); + + if (step_result == + DATABASE_STATEMENT_STEP_DONE) + { + break; + } + + if (step_result == + DATABASE_STATEMENT_STEP_ERROR) + { + relation_evidence_dao_set_database_error( + relation_evidence_dao, + error, + RELATION_EVIDENCE_DAO_ERROR_EXECUTE, + execute_context + ); + + goto failure; + } + + if (!database_statement_column_text( + statement, + 0, + &loaded_identifier + )) + { + relation_evidence_dao_set_database_error( + relation_evidence_dao, + error, + RELATION_EVIDENCE_DAO_ERROR_READ, + "Impossible de lire un identifiant associé" + ); + + g_free( + loaded_identifier + ); + + goto failure; + } + + if (loaded_identifier == NULL || + !g_uuid_string_is_valid( + loaded_identifier + )) + { + relation_evidence_dao_set_error_literal( + error, + RELATION_EVIDENCE_DAO_ERROR_READ, + "La base contient un identifiant associé invalide." + ); + + g_free( + loaded_identifier + ); + + goto failure; + } + + g_ptr_array_add( + identifiers, + loaded_identifier + ); + } + + database_statement_finalize( + statement + ); + + return identifiers; + +failure: + + database_statement_finalize( + statement + ); + + g_ptr_array_unref( + identifiers + ); + + return NULL; +} + +GQuark relation_evidence_dao_error_quark(void) +{ + return g_quark_from_static_string( + "relation-evidence-dao-error-quark" + ); +} + +RelationEvidenceDao *relation_evidence_dao_new( + Database *database, + GError **error +) +{ + RelationEvidenceDao *relation_evidence_dao = + NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (database == NULL) + { + relation_evidence_dao_set_error_literal( + error, + RELATION_EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + "La connexion Database est obligatoire." + ); + + return NULL; + } + + relation_evidence_dao = + g_try_new0( + RelationEvidenceDao, + 1 + ); + + if (relation_evidence_dao == NULL) + { + relation_evidence_dao_set_error_literal( + error, + RELATION_EVIDENCE_DAO_ERROR_MEMORY, + "Impossible d'allouer le DAO des associations relation-preuve." + ); + + return NULL; + } + + relation_evidence_dao->database = + database; + + return relation_evidence_dao; +} + +void relation_evidence_dao_free( + RelationEvidenceDao *relation_evidence_dao +) +{ + if (relation_evidence_dao == NULL) + { + return; + } + + relation_evidence_dao->database = + NULL; + + g_free( + relation_evidence_dao + ); +} + +gboolean relation_evidence_dao_link( + RelationEvidenceDao *relation_evidence_dao, + const char *relation_identifier, + const char *evidence_identifier, + GError **error +) +{ + gboolean relation_exists = + FALSE; + + gboolean evidence_exists = + FALSE; + + gboolean association_exists = + FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (!relation_evidence_dao_validate_association( + relation_evidence_dao, + relation_identifier, + evidence_identifier, + error + )) + { + return FALSE; + } + + if (!relation_evidence_dao_query_exists( + relation_evidence_dao, + relation_evidence_dao_relation_exists_sql, + relation_identifier, + NULL, + &relation_exists, + error + )) + { + return FALSE; + } + + if (!relation_exists) + { + relation_evidence_dao_set_error_literal( + error, + RELATION_EVIDENCE_DAO_ERROR_NOT_FOUND, + "La relation à associer n'existe pas." + ); + + return FALSE; + } + + if (!relation_evidence_dao_query_exists( + relation_evidence_dao, + relation_evidence_dao_evidence_exists_sql, + evidence_identifier, + NULL, + &evidence_exists, + error + )) + { + return FALSE; + } + + if (!evidence_exists) + { + relation_evidence_dao_set_error_literal( + error, + RELATION_EVIDENCE_DAO_ERROR_NOT_FOUND, + "La preuve à associer n'existe pas." + ); + + return FALSE; + } + + if (!relation_evidence_dao_query_exists( + relation_evidence_dao, + relation_evidence_dao_association_exists_sql, + relation_identifier, + evidence_identifier, + &association_exists, + error + )) + { + return FALSE; + } + + if (association_exists) + { + relation_evidence_dao_set_error_literal( + error, + RELATION_EVIDENCE_DAO_ERROR_CONSTRAINT, + "Cette relation est déjà associée à cette preuve." + ); + + return FALSE; + } + + return relation_evidence_dao_execute_pair_statement( + relation_evidence_dao, + relation_evidence_dao_link_sql, + relation_identifier, + evidence_identifier, + "Impossible de préparer la création de l'association", + "Impossible de lier les identifiants de l'association", + "Impossible de créer l'association", + error + ); +} + +gboolean relation_evidence_dao_unlink( + RelationEvidenceDao *relation_evidence_dao, + const char *relation_identifier, + const char *evidence_identifier, + GError **error +) +{ + gboolean association_exists = + FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (!relation_evidence_dao_validate_association( + relation_evidence_dao, + relation_identifier, + evidence_identifier, + error + )) + { + return FALSE; + } + + if (!relation_evidence_dao_query_exists( + relation_evidence_dao, + relation_evidence_dao_association_exists_sql, + relation_identifier, + evidence_identifier, + &association_exists, + error + )) + { + return FALSE; + } + + if (!association_exists) + { + relation_evidence_dao_set_error_literal( + error, + RELATION_EVIDENCE_DAO_ERROR_NOT_FOUND, + "L'association entre la relation et la preuve n'existe pas." + ); + + return FALSE; + } + + return relation_evidence_dao_execute_pair_statement( + relation_evidence_dao, + relation_evidence_dao_unlink_sql, + relation_identifier, + evidence_identifier, + "Impossible de préparer la suppression de l'association", + "Impossible de lier les identifiants de l'association", + "Impossible de supprimer l'association", + error + ); +} + +gboolean relation_evidence_dao_exists( + RelationEvidenceDao *relation_evidence_dao, + const char *relation_identifier, + const char *evidence_identifier, + gboolean *out_exists, + GError **error +) +{ + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (out_exists == NULL) + { + relation_evidence_dao_set_error_literal( + error, + RELATION_EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, + "La destination du résultat d'existence est obligatoire." + ); + + return FALSE; + } + + if (!relation_evidence_dao_validate_association( + relation_evidence_dao, + relation_identifier, + evidence_identifier, + error + )) + { + return FALSE; + } + + return relation_evidence_dao_query_exists( + relation_evidence_dao, + relation_evidence_dao_association_exists_sql, + relation_identifier, + evidence_identifier, + out_exists, + error + ); +} + +GPtrArray *relation_evidence_dao_list_evidence_identifiers( + RelationEvidenceDao *relation_evidence_dao, + const char *relation_identifier, + GError **error +) +{ + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (!relation_evidence_dao_validate_list_request( + relation_evidence_dao, + relation_identifier, + "L'identifiant de relation est invalide.", + error + )) + { + return NULL; + } + + return relation_evidence_dao_list_identifiers( + relation_evidence_dao, + relation_evidence_dao_list_evidence_identifiers_sql, + relation_identifier, + "Impossible de préparer la liste des preuves associées", + "Impossible de lier l'identifiant de relation", + "Impossible de charger les preuves associées", + error + ); +} + +GPtrArray *relation_evidence_dao_list_relation_identifiers( + RelationEvidenceDao *relation_evidence_dao, + const char *evidence_identifier, + GError **error +) +{ + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (!relation_evidence_dao_validate_list_request( + relation_evidence_dao, + evidence_identifier, + "L'identifiant de preuve est invalide.", + error + )) + { + return NULL; + } + + return relation_evidence_dao_list_identifiers( + relation_evidence_dao, + relation_evidence_dao_list_relation_identifiers_sql, + evidence_identifier, + "Impossible de préparer la liste des relations associées", + "Impossible de lier l'identifiant de preuve", + "Impossible de charger les relations associées", + error + ); +} diff --git a/tests/test_relation_evidence_dao.c b/tests/test_relation_evidence_dao.c new file mode 100644 index 0000000..da2914f --- /dev/null +++ b/tests/test_relation_evidence_dao.c @@ -0,0 +1,1906 @@ +/****************************************************************************** + * @file test_relation_evidence_dao.c + * @brief Tests du DAO des associations entre relations et preuves. + ******************************************************************************/ + +#include "dao/entity_dao.h" +#include "dao/evidence_dao.h" +#include "dao/relation_dao.h" +#include "dao/relation_evidence_dao.h" + +#include "database/database.h" +#include "database/statement.h" +#include "models/entity_record.h" +#include "models/evidence_record.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" + +/** + * @brief Environnement temporaire d'un test RelationEvidenceDao. + */ +typedef struct +{ + char *temporary_directory; + char *database_path; + + Database *database; + + EvidenceDao *evidence_dao; + EntityDao *entity_dao; + RelationDao *relation_dao; + RelationEvidenceDao *relation_evidence_dao; +} TestRelationEvidenceDaoFixture; + +/** + * @brief Vérifie le domaine et le code d'une erreur. + */ +static void test_relation_evidence_dao_assert_error( + const GError *error, + RelationEvidenceDaoError expected_code +) +{ + assert(error != NULL); + + assert( + error->domain == + RELATION_EVIDENCE_DAO_ERROR + ); + + assert( + error->code == + (gint) expected_code + ); + + assert(error->message != NULL); + assert(error->message[0] != '\0'); +} + +/** + * @brief Crée une base temporaire et les DAO nécessaires. + */ +static TestRelationEvidenceDaoFixture +test_relation_evidence_dao_fixture_create(void) +{ + TestRelationEvidenceDaoFixture fixture = + {0}; + + GError *error = + NULL; + + fixture.temporary_directory = + g_dir_make_tmp( + "labfy-relation-evidence-dao-test-XXXXXX", + &error + ); + + assert(fixture.temporary_directory != NULL); + assert(error == NULL); + + fixture.database_path = + g_build_filename( + fixture.temporary_directory, + "Enquete.sqlite", + NULL + ); + + assert(fixture.database_path != NULL); + + assert( + database_initialize( + fixture.database_path, + "Enquete_RelationEvidenceDao", + fixture.temporary_directory + ) + ); + + fixture.database = + database_open( + fixture.database_path + ); + + assert(fixture.database != NULL); + + fixture.evidence_dao = + evidence_dao_new( + fixture.database, + &error + ); + + assert(fixture.evidence_dao != NULL); + assert(error == NULL); + + 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.relation_evidence_dao = + relation_evidence_dao_new( + fixture.database, + &error + ); + + assert(fixture.relation_evidence_dao != NULL); + assert(error == NULL); + + return fixture; +} + +/** + * @brief Libère une fixture et supprime sa base temporaire. + */ +static void test_relation_evidence_dao_fixture_clear( + TestRelationEvidenceDaoFixture *fixture +) +{ + assert(fixture != NULL); + + relation_evidence_dao_free( + fixture->relation_evidence_dao + ); + + relation_dao_free( + fixture->relation_dao + ); + + entity_dao_free( + fixture->entity_dao + ); + + evidence_dao_free( + fixture->evidence_dao + ); + + database_close( + fixture->database + ); + + assert( + g_remove( + fixture->database_path + ) == 0 + ); + + assert( + g_rmdir( + fixture->temporary_directory + ) == 0 + ); + + g_free( + fixture->database_path + ); + + g_free( + fixture->temporary_directory + ); + + fixture->relation_evidence_dao = + NULL; + + fixture->relation_dao = + NULL; + + fixture->entity_dao = + NULL; + + fixture->evidence_dao = + NULL; + + fixture->database = + NULL; + + fixture->database_path = + NULL; + + fixture->temporary_directory = + NULL; +} + +/** + * @brief Crée une entité de test. + */ +static EntityRecord *test_relation_evidence_dao_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-20T20:00:00Z", + "2026-07-20T20:00:00Z", + ENTITY_STATUS_ACTIVE, + &error + ); + + assert(entity_record != NULL); + assert(error == NULL); + + return entity_record; +} + +/** + * @brief Insère une entité de test. + */ +static void test_relation_evidence_dao_insert_entity( + TestRelationEvidenceDaoFixture *fixture, + const char *identifier, + const char *value +) +{ + EntityRecord *entity_record = + NULL; + + GError *error = + NULL; + + entity_record = + test_relation_evidence_dao_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 entités standard des tests. + */ +static void test_relation_evidence_dao_insert_standard_entities( + TestRelationEvidenceDaoFixture *fixture +) +{ + test_relation_evidence_dao_insert_entity( + fixture, + TEST_ENTITY_A_IDENTIFIER, + "Personne A" + ); + + test_relation_evidence_dao_insert_entity( + fixture, + TEST_ENTITY_B_IDENTIFIER, + "Personne B" + ); + + test_relation_evidence_dao_insert_entity( + fixture, + TEST_ENTITY_C_IDENTIFIER, + "Personne C" + ); +} + +/** + * @brief Crée une relation de test. + */ +static RelationRecord *test_relation_evidence_dao_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-20T20:10:00Z", + "2026-07-20T20:10:00Z", + RELATION_STATUS_ACTIVE, + &error + ); + + assert(relation_record != NULL); + assert(error == NULL); + + return relation_record; +} + +/** + * @brief Insère une relation de test. + */ +static void test_relation_evidence_dao_insert_relation( + TestRelationEvidenceDaoFixture *fixture, + const char *identifier, + const char *source_identifier, + const char *target_identifier, + const char *relation_type +) +{ + RelationRecord *relation_record = + NULL; + + GError *error = + NULL; + + relation_record = + test_relation_evidence_dao_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 Crée une preuve de test. + */ +static EvidenceRecord *test_relation_evidence_dao_create_evidence( + const char *identifier +) +{ + EvidenceRecord *evidence_record = + NULL; + + char *internal_name = + NULL; + + char *relative_path = + NULL; + + GError *error = + NULL; + + internal_name = + g_strdup_printf( + "%s.bin", + identifier + ); + + relative_path = + g_strdup_printf( + "01_Preuves_Originales/Documents/%s.bin", + identifier + ); + + assert(internal_name != NULL); + assert(relative_path != NULL); + + evidence_record = + evidence_record_new( + identifier, + "preuve.bin", + internal_name, + relative_path, + "document", + 128, + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "2026-07-20T20:20:00Z", + NULL, + NULL, + NULL, + EVIDENCE_INTEGRITY_STATUS_UNKNOWN, + &error + ); + + assert(evidence_record != NULL); + assert(error == NULL); + + g_free( + relative_path + ); + + g_free( + internal_name + ); + + return evidence_record; +} + +/** + * @brief Insère une preuve de test. + */ +static void test_relation_evidence_dao_insert_evidence( + TestRelationEvidenceDaoFixture *fixture, + const char *identifier +) +{ + EvidenceRecord *evidence_record = + NULL; + + GError *error = + NULL; + + evidence_record = + test_relation_evidence_dao_create_evidence( + identifier + ); + + assert( + evidence_dao_insert( + fixture->evidence_dao, + evidence_record, + &error + ) + ); + + assert(error == NULL); + + evidence_record_free( + evidence_record + ); +} + +/** + * @brief Retourne le nombre d'associations persistées. + */ +static int64_t test_relation_evidence_dao_count_rows( + Database *database +) +{ + DatabaseStatement *statement = + NULL; + + int64_t association_count = + -1; + + statement = + database_statement_prepare( + database, + "SELECT COUNT(*) " + "FROM relation_preuves;" + ); + + assert(statement != NULL); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_ROW + ); + + assert( + database_statement_column_int64( + statement, + 0, + &association_count + ) + ); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize( + statement + ); + + return association_count; +} + +/** + * @brief Vérifie qu'une relation existe encore. + */ +static void test_relation_evidence_dao_assert_relation_exists( + TestRelationEvidenceDaoFixture *fixture, + const char *identifier +) +{ + RelationRecord *relation_record = + NULL; + + GError *error = + NULL; + + relation_record = + relation_dao_find_by_identifier( + fixture->relation_dao, + identifier, + &error + ); + + assert(relation_record != NULL); + assert(error == NULL); + + relation_record_free( + relation_record + ); +} + +/** + * @brief Vérifie qu'une preuve existe encore. + */ +static void test_relation_evidence_dao_assert_evidence_exists( + TestRelationEvidenceDaoFixture *fixture, + const char *identifier +) +{ + EvidenceRecord *evidence_record = + NULL; + + GError *error = + NULL; + + evidence_record = + evidence_dao_find_by_identifier( + fixture->evidence_dao, + identifier, + &error + ); + + assert(evidence_record != NULL); + assert(error == NULL); + + evidence_record_free( + evidence_record + ); +} + +/** + * @brief Vérifie le refus d'une connexion absente. + */ +static void test_relation_evidence_dao_new_null_database(void) +{ + RelationEvidenceDao *relation_evidence_dao = + NULL; + + GError *error = + NULL; + + relation_evidence_dao = + relation_evidence_dao_new( + NULL, + &error + ); + + assert(relation_evidence_dao == NULL); + + test_relation_evidence_dao_assert_error( + error, + RELATION_EVIDENCE_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); +} + +/** + * @brief Vérifie la création et la destruction du DAO. + */ +static void test_relation_evidence_dao_lifecycle(void) +{ + Database *database = + NULL; + + RelationEvidenceDao *relation_evidence_dao = + NULL; + + GError *error = + NULL; + + database = + database_open( + ":memory:" + ); + + assert(database != NULL); + + relation_evidence_dao = + relation_evidence_dao_new( + database, + &error + ); + + assert(relation_evidence_dao != NULL); + assert(error == NULL); + + relation_evidence_dao_free( + relation_evidence_dao + ); + + relation_evidence_dao_free( + NULL + ); + + database_close( + database + ); +} + +/** + * @brief Vérifie la création et la détection d'une association. + */ +static void test_relation_evidence_dao_link_and_exists(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000001"; + + static const char *const evidence_identifier = + "50000000-0000-4000-8000-000000000001"; + + TestRelationEvidenceDaoFixture fixture = + test_relation_evidence_dao_fixture_create(); + + gboolean association_exists = + TRUE; + + GError *error = + NULL; + + test_relation_evidence_dao_insert_standard_entities( + &fixture + ); + + test_relation_evidence_dao_insert_relation( + &fixture, + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + test_relation_evidence_dao_insert_evidence( + &fixture, + evidence_identifier + ); + + assert( + relation_evidence_dao_exists( + fixture.relation_evidence_dao, + relation_identifier, + evidence_identifier, + &association_exists, + &error + ) + ); + + assert(error == NULL); + assert(!association_exists); + + assert( + relation_evidence_dao_link( + fixture.relation_evidence_dao, + relation_identifier, + evidence_identifier, + &error + ) + ); + + assert(error == NULL); + + assert( + relation_evidence_dao_exists( + fixture.relation_evidence_dao, + relation_identifier, + evidence_identifier, + &association_exists, + &error + ) + ); + + assert(error == NULL); + assert(association_exists); + + assert( + test_relation_evidence_dao_count_rows( + fixture.database + ) == 1 + ); + + test_relation_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d'une association dupliquée. + */ +static void test_relation_evidence_dao_link_duplicate(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000002"; + + static const char *const evidence_identifier = + "50000000-0000-4000-8000-000000000002"; + + TestRelationEvidenceDaoFixture fixture = + test_relation_evidence_dao_fixture_create(); + + GError *error = + NULL; + + test_relation_evidence_dao_insert_standard_entities( + &fixture + ); + + test_relation_evidence_dao_insert_relation( + &fixture, + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "controls" + ); + + test_relation_evidence_dao_insert_evidence( + &fixture, + evidence_identifier + ); + + assert( + relation_evidence_dao_link( + fixture.relation_evidence_dao, + relation_identifier, + evidence_identifier, + &error + ) + ); + + assert(error == NULL); + + assert( + !relation_evidence_dao_link( + fixture.relation_evidence_dao, + relation_identifier, + evidence_identifier, + &error + ) + ); + + test_relation_evidence_dao_assert_error( + error, + RELATION_EVIDENCE_DAO_ERROR_CONSTRAINT + ); + + assert( + test_relation_evidence_dao_count_rows( + fixture.database + ) == 1 + ); + + g_clear_error( + &error + ); + + test_relation_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie la clé primaire composée directement dans SQLite. + */ +static void test_relation_evidence_dao_composite_primary_key(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000003"; + + static const char *const evidence_identifier = + "50000000-0000-4000-8000-000000000003"; + + TestRelationEvidenceDaoFixture fixture = + test_relation_evidence_dao_fixture_create(); + + DatabaseStatement *statement = + NULL; + + GError *error = + NULL; + + test_relation_evidence_dao_insert_standard_entities( + &fixture + ); + + test_relation_evidence_dao_insert_relation( + &fixture, + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "knows" + ); + + test_relation_evidence_dao_insert_evidence( + &fixture, + evidence_identifier + ); + + assert( + relation_evidence_dao_link( + fixture.relation_evidence_dao, + relation_identifier, + evidence_identifier, + &error + ) + ); + + assert(error == NULL); + + statement = + database_statement_prepare( + fixture.database, + "INSERT INTO relation_preuves" + "(" + " relation_id," + " preuve_id" + ")" + "VALUES" + "(" + " ?," + " ?" + ");" + ); + + assert(statement != NULL); + + assert( + database_statement_bind_text( + statement, + 1, + relation_identifier + ) + ); + + assert( + database_statement_bind_text( + statement, + 2, + evidence_identifier + ) + ); + + g_test_expect_message( + NULL, + G_LOG_LEVEL_WARNING, + "*UNIQUE constraint failed*" + ); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_ERROR + ); + + g_test_assert_expected_messages(); + + database_statement_finalize( + statement + ); + + test_relation_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d'une relation inexistante. + */ +static void test_relation_evidence_dao_link_missing_relation(void) +{ + TestRelationEvidenceDaoFixture fixture = + test_relation_evidence_dao_fixture_create(); + + GError *error = + NULL; + + test_relation_evidence_dao_insert_evidence( + &fixture, + "50000000-0000-4000-8000-000000000004" + ); + + assert( + !relation_evidence_dao_link( + fixture.relation_evidence_dao, + "40000000-0000-4000-8000-000000000004", + "50000000-0000-4000-8000-000000000004", + &error + ) + ); + + test_relation_evidence_dao_assert_error( + error, + RELATION_EVIDENCE_DAO_ERROR_NOT_FOUND + ); + + g_clear_error( + &error + ); + + test_relation_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d'une preuve inexistante. + */ +static void test_relation_evidence_dao_link_missing_evidence(void) +{ + TestRelationEvidenceDaoFixture fixture = + test_relation_evidence_dao_fixture_create(); + + GError *error = + NULL; + + test_relation_evidence_dao_insert_standard_entities( + &fixture + ); + + test_relation_evidence_dao_insert_relation( + &fixture, + "40000000-0000-4000-8000-000000000005", + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + assert( + !relation_evidence_dao_link( + fixture.relation_evidence_dao, + "40000000-0000-4000-8000-000000000005", + "50000000-0000-4000-8000-000000000005", + &error + ) + ); + + test_relation_evidence_dao_assert_error( + error, + RELATION_EVIDENCE_DAO_ERROR_NOT_FOUND + ); + + g_clear_error( + &error + ); + + test_relation_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie unlink() et la conservation des deux objets. + */ +static void test_relation_evidence_dao_unlink_preserves_objects(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000006"; + + static const char *const evidence_identifier = + "50000000-0000-4000-8000-000000000006"; + + TestRelationEvidenceDaoFixture fixture = + test_relation_evidence_dao_fixture_create(); + + gboolean association_exists = + TRUE; + + GError *error = + NULL; + + test_relation_evidence_dao_insert_standard_entities( + &fixture + ); + + test_relation_evidence_dao_insert_relation( + &fixture, + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "owns" + ); + + test_relation_evidence_dao_insert_evidence( + &fixture, + evidence_identifier + ); + + assert( + relation_evidence_dao_link( + fixture.relation_evidence_dao, + relation_identifier, + evidence_identifier, + &error + ) + ); + + assert(error == NULL); + + assert( + relation_evidence_dao_unlink( + fixture.relation_evidence_dao, + relation_identifier, + evidence_identifier, + &error + ) + ); + + assert(error == NULL); + + assert( + relation_evidence_dao_exists( + fixture.relation_evidence_dao, + relation_identifier, + evidence_identifier, + &association_exists, + &error + ) + ); + + assert(error == NULL); + assert(!association_exists); + + test_relation_evidence_dao_assert_relation_exists( + &fixture, + relation_identifier + ); + + test_relation_evidence_dao_assert_evidence_exists( + &fixture, + evidence_identifier + ); + + test_relation_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus de supprimer une association absente. + */ +static void test_relation_evidence_dao_unlink_missing(void) +{ + TestRelationEvidenceDaoFixture fixture = + test_relation_evidence_dao_fixture_create(); + + GError *error = + NULL; + + assert( + !relation_evidence_dao_unlink( + fixture.relation_evidence_dao, + "40000000-0000-4000-8000-000000000007", + "50000000-0000-4000-8000-000000000007", + &error + ) + ); + + test_relation_evidence_dao_assert_error( + error, + RELATION_EVIDENCE_DAO_ERROR_NOT_FOUND + ); + + g_clear_error( + &error + ); + + test_relation_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie une relation associée à plusieurs preuves triées. + */ +static void test_relation_evidence_dao_list_evidence_identifiers(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000008"; + + static const char *const first_evidence_identifier = + "50000000-0000-4000-8000-000000000001"; + + static const char *const second_evidence_identifier = + "50000000-0000-4000-8000-000000000002"; + + static const char *const third_evidence_identifier = + "50000000-0000-4000-8000-000000000003"; + + TestRelationEvidenceDaoFixture fixture = + test_relation_evidence_dao_fixture_create(); + + GPtrArray *identifiers = + NULL; + + GError *error = + NULL; + + test_relation_evidence_dao_insert_standard_entities( + &fixture + ); + + test_relation_evidence_dao_insert_relation( + &fixture, + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + identifiers = + relation_evidence_dao_list_evidence_identifiers( + fixture.relation_evidence_dao, + relation_identifier, + &error + ); + + assert(identifiers != NULL); + assert(error == NULL); + assert(identifiers->len == 0); + + g_ptr_array_unref( + identifiers + ); + + test_relation_evidence_dao_insert_evidence( + &fixture, + third_evidence_identifier + ); + + test_relation_evidence_dao_insert_evidence( + &fixture, + first_evidence_identifier + ); + + test_relation_evidence_dao_insert_evidence( + &fixture, + second_evidence_identifier + ); + + assert( + relation_evidence_dao_link( + fixture.relation_evidence_dao, + relation_identifier, + third_evidence_identifier, + &error + ) + ); + + assert(error == NULL); + + assert( + relation_evidence_dao_link( + fixture.relation_evidence_dao, + relation_identifier, + first_evidence_identifier, + &error + ) + ); + + assert(error == NULL); + + assert( + relation_evidence_dao_link( + fixture.relation_evidence_dao, + relation_identifier, + second_evidence_identifier, + &error + ) + ); + + assert(error == NULL); + + identifiers = + relation_evidence_dao_list_evidence_identifiers( + fixture.relation_evidence_dao, + relation_identifier, + &error + ); + + assert(identifiers != NULL); + assert(error == NULL); + assert(identifiers->len == 3); + + assert( + strcmp( + g_ptr_array_index( + identifiers, + 0 + ), + first_evidence_identifier + ) == 0 + ); + + assert( + strcmp( + g_ptr_array_index( + identifiers, + 1 + ), + second_evidence_identifier + ) == 0 + ); + + assert( + strcmp( + g_ptr_array_index( + identifiers, + 2 + ), + third_evidence_identifier + ) == 0 + ); + + g_ptr_array_unref( + identifiers + ); + + test_relation_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie une preuve associée à plusieurs relations triées. + */ +static void test_relation_evidence_dao_list_relation_identifiers(void) +{ + static const char *const evidence_identifier = + "50000000-0000-4000-8000-000000000009"; + + static const char *const first_relation_identifier = + "40000000-0000-4000-8000-000000000001"; + + static const char *const second_relation_identifier = + "40000000-0000-4000-8000-000000000002"; + + static const char *const third_relation_identifier = + "40000000-0000-4000-8000-000000000003"; + + TestRelationEvidenceDaoFixture fixture = + test_relation_evidence_dao_fixture_create(); + + GPtrArray *identifiers = + NULL; + + GError *error = + NULL; + + test_relation_evidence_dao_insert_standard_entities( + &fixture + ); + + test_relation_evidence_dao_insert_evidence( + &fixture, + evidence_identifier + ); + + identifiers = + relation_evidence_dao_list_relation_identifiers( + fixture.relation_evidence_dao, + evidence_identifier, + &error + ); + + assert(identifiers != NULL); + assert(error == NULL); + assert(identifiers->len == 0); + + g_ptr_array_unref( + identifiers + ); + + test_relation_evidence_dao_insert_relation( + &fixture, + third_relation_identifier, + TEST_ENTITY_C_IDENTIFIER, + TEST_ENTITY_A_IDENTIFIER, + "knows" + ); + + test_relation_evidence_dao_insert_relation( + &fixture, + first_relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + test_relation_evidence_dao_insert_relation( + &fixture, + second_relation_identifier, + TEST_ENTITY_B_IDENTIFIER, + TEST_ENTITY_C_IDENTIFIER, + "controls" + ); + + assert( + relation_evidence_dao_link( + fixture.relation_evidence_dao, + third_relation_identifier, + evidence_identifier, + &error + ) + ); + + assert(error == NULL); + + assert( + relation_evidence_dao_link( + fixture.relation_evidence_dao, + first_relation_identifier, + evidence_identifier, + &error + ) + ); + + assert(error == NULL); + + assert( + relation_evidence_dao_link( + fixture.relation_evidence_dao, + second_relation_identifier, + evidence_identifier, + &error + ) + ); + + assert(error == NULL); + + identifiers = + relation_evidence_dao_list_relation_identifiers( + fixture.relation_evidence_dao, + evidence_identifier, + &error + ); + + assert(identifiers != NULL); + assert(error == NULL); + assert(identifiers->len == 3); + + assert( + strcmp( + g_ptr_array_index( + identifiers, + 0 + ), + first_relation_identifier + ) == 0 + ); + + assert( + strcmp( + g_ptr_array_index( + identifiers, + 1 + ), + second_relation_identifier + ) == 0 + ); + + assert( + strcmp( + g_ptr_array_index( + identifiers, + 2 + ), + third_relation_identifier + ) == 0 + ); + + g_ptr_array_unref( + identifiers + ); + + test_relation_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie les arguments invalides et GError facultatif. + */ +static void test_relation_evidence_dao_invalid_arguments(void) +{ + TestRelationEvidenceDaoFixture fixture = + test_relation_evidence_dao_fixture_create(); + + GPtrArray *identifiers = + NULL; + + gboolean association_exists = + FALSE; + + GError *error = + NULL; + + assert( + !relation_evidence_dao_link( + NULL, + "40000000-0000-4000-8000-000000000010", + "50000000-0000-4000-8000-000000000010", + &error + ) + ); + + test_relation_evidence_dao_assert_error( + error, + RELATION_EVIDENCE_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + assert( + !relation_evidence_dao_link( + fixture.relation_evidence_dao, + "relation-invalide", + "50000000-0000-4000-8000-000000000010", + &error + ) + ); + + test_relation_evidence_dao_assert_error( + error, + RELATION_EVIDENCE_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + assert( + !relation_evidence_dao_unlink( + fixture.relation_evidence_dao, + "40000000-0000-4000-8000-000000000010", + "preuve-invalide", + &error + ) + ); + + test_relation_evidence_dao_assert_error( + error, + RELATION_EVIDENCE_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + assert( + !relation_evidence_dao_exists( + fixture.relation_evidence_dao, + "40000000-0000-4000-8000-000000000010", + "50000000-0000-4000-8000-000000000010", + NULL, + &error + ) + ); + + test_relation_evidence_dao_assert_error( + error, + RELATION_EVIDENCE_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + identifiers = + relation_evidence_dao_list_evidence_identifiers( + fixture.relation_evidence_dao, + "relation-invalide", + &error + ); + + assert(identifiers == NULL); + + test_relation_evidence_dao_assert_error( + error, + RELATION_EVIDENCE_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + identifiers = + relation_evidence_dao_list_relation_identifiers( + NULL, + "50000000-0000-4000-8000-000000000010", + &error + ); + + assert(identifiers == NULL); + + test_relation_evidence_dao_assert_error( + error, + RELATION_EVIDENCE_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + assert( + !relation_evidence_dao_link( + fixture.relation_evidence_dao, + "relation-invalide", + "preuve-invalide", + NULL + ) + ); + + assert( + relation_evidence_dao_exists( + fixture.relation_evidence_dao, + "40000000-0000-4000-8000-000000000010", + "50000000-0000-4000-8000-000000000010", + &association_exists, + &error + ) + ); + + assert(error == NULL); + assert(!association_exists); + + test_relation_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le signalement d'une table absente. + */ +static void test_relation_evidence_dao_missing_table(void) +{ + Database *database = + NULL; + + RelationEvidenceDao *relation_evidence_dao = + NULL; + + GPtrArray *identifiers = + NULL; + + GError *error = + NULL; + + database = + database_open( + ":memory:" + ); + + assert(database != NULL); + + relation_evidence_dao = + relation_evidence_dao_new( + database, + &error + ); + + assert(relation_evidence_dao != NULL); + assert(error == NULL); + + g_test_expect_message( + NULL, + G_LOG_LEVEL_WARNING, + "*Impossible de préparer la requête SQL*" + ); + + identifiers = + relation_evidence_dao_list_evidence_identifiers( + relation_evidence_dao, + "40000000-0000-4000-8000-000000000011", + &error + ); + + g_test_assert_expected_messages(); + + assert(identifiers == NULL); + + test_relation_evidence_dao_assert_error( + error, + RELATION_EVIDENCE_DAO_ERROR_SCHEMA + ); + + g_clear_error( + &error + ); + + relation_evidence_dao_free( + relation_evidence_dao + ); + + database_close( + database + ); +} + +/** + * @brief Vérifie ON DELETE CASCADE lors de la suppression d'une relation. + */ +static void test_relation_evidence_dao_relation_delete_cascade(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000012"; + + static const char *const evidence_identifier = + "50000000-0000-4000-8000-000000000012"; + + TestRelationEvidenceDaoFixture fixture = + test_relation_evidence_dao_fixture_create(); + + DatabaseStatement *statement = + NULL; + + GError *error = + NULL; + + test_relation_evidence_dao_insert_standard_entities( + &fixture + ); + + test_relation_evidence_dao_insert_relation( + &fixture, + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + test_relation_evidence_dao_insert_evidence( + &fixture, + evidence_identifier + ); + + assert( + relation_evidence_dao_link( + fixture.relation_evidence_dao, + relation_identifier, + evidence_identifier, + &error + ) + ); + + assert(error == NULL); + + statement = + database_statement_prepare( + fixture.database, + "DELETE FROM relations " + "WHERE id = ?;" + ); + + assert(statement != NULL); + + assert( + database_statement_bind_text( + statement, + 1, + relation_identifier + ) + ); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize( + statement + ); + + assert( + test_relation_evidence_dao_count_rows( + fixture.database + ) == 0 + ); + + test_relation_evidence_dao_assert_evidence_exists( + &fixture, + evidence_identifier + ); + + test_relation_evidence_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie ON DELETE RESTRICT lors de la suppression d'une preuve. + */ +static void test_relation_evidence_dao_evidence_delete_restrict(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000013"; + + static const char *const evidence_identifier = + "50000000-0000-4000-8000-000000000013"; + + TestRelationEvidenceDaoFixture fixture = + test_relation_evidence_dao_fixture_create(); + + DatabaseStatement *statement = + NULL; + + GError *error = + NULL; + + test_relation_evidence_dao_insert_standard_entities( + &fixture + ); + + test_relation_evidence_dao_insert_relation( + &fixture, + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "controls" + ); + + test_relation_evidence_dao_insert_evidence( + &fixture, + evidence_identifier + ); + + assert( + relation_evidence_dao_link( + fixture.relation_evidence_dao, + relation_identifier, + evidence_identifier, + &error + ) + ); + + assert(error == NULL); + + statement = + database_statement_prepare( + fixture.database, + "DELETE FROM preuves " + "WHERE id = ?;" + ); + + assert(statement != NULL); + + assert( + database_statement_bind_text( + statement, + 1, + evidence_identifier + ) + ); + + g_test_expect_message( + NULL, + G_LOG_LEVEL_WARNING, + "*FOREIGN KEY constraint failed*" + ); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_ERROR + ); + + g_test_assert_expected_messages(); + + database_statement_finalize( + statement + ); + + assert( + test_relation_evidence_dao_count_rows( + fixture.database + ) == 1 + ); + + test_relation_evidence_dao_assert_relation_exists( + &fixture, + relation_identifier + ); + + test_relation_evidence_dao_assert_evidence_exists( + &fixture, + evidence_identifier + ); + + test_relation_evidence_dao_fixture_clear( + &fixture + ); +} + +int main( + int argc, + char **argv +) +{ + g_test_init( + &argc, + &argv, + NULL + ); + + test_relation_evidence_dao_new_null_database(); + test_relation_evidence_dao_lifecycle(); + test_relation_evidence_dao_link_and_exists(); + test_relation_evidence_dao_link_duplicate(); + test_relation_evidence_dao_composite_primary_key(); + test_relation_evidence_dao_link_missing_relation(); + test_relation_evidence_dao_link_missing_evidence(); + test_relation_evidence_dao_unlink_preserves_objects(); + test_relation_evidence_dao_unlink_missing(); + test_relation_evidence_dao_list_evidence_identifiers(); + test_relation_evidence_dao_list_relation_identifiers(); + test_relation_evidence_dao_invalid_arguments(); + test_relation_evidence_dao_missing_table(); + test_relation_evidence_dao_relation_delete_cascade(); + test_relation_evidence_dao_evidence_delete_restrict(); + + printf( + "RelationEvidenceDao : tous les tests sont valides.\n" + ); + + return 0; +}