From 9a9f326c12bcf81cb57ea7a6f317b53a6f6d955e Mon Sep 17 00:00:00 2001 From: grayTerminal-sh Date: Tue, 21 Jul 2026 08:01:55 +0200 Subject: [PATCH] feat: add transactional relation service --- Makefile | 37 +- include/core/relation_service.h | 133 ++ include/core/relation_service_test.h | 37 + src/core/relation_service.c | 777 ++++++++++ tests/test_relation_service.c | 2153 ++++++++++++++++++++++++++ 5 files changed, 3130 insertions(+), 7 deletions(-) create mode 100644 include/core/relation_service.h create mode 100644 include/core/relation_service_test.h create mode 100644 src/core/relation_service.c create mode 100644 tests/test_relation_service.c diff --git a/Makefile b/Makefile index 13a0363..c7ba565 100644 --- a/Makefile +++ b/Makefile @@ -35,12 +35,9 @@ EVIDENCE_IMPORT_DIALOG_TEST_CFLAGS := \ -Iinclude \ $(shell $(PKG_CONFIG) --cflags gtk4) -EVIDENCE_IMPORT_DIALOG_TEST_LDFLAGS := \ - $(shell $(PKG_CONFIG) --libs gtk4) +EVIDENCE_IMPORT_DIALOG_TEST_LDFLAGS := $(shell $(PKG_CONFIG) --libs gtk4) -EVIDENCE_INTEGRITY_VERIFIER_TEST_CFLAGS := \ - $(TEST_CFLAGS) \ - -Wpedantic +EVIDENCE_INTEGRITY_VERIFIER_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic ENTITY_RECORD_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic ENTITY_TYPE_DAO_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic ENTITY_DAO_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic @@ -48,6 +45,10 @@ 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 +RELATION_SERVICE_TEST_CFLAGS := \ + $(TEST_CFLAGS) \ + -Wpedantic \ + -DRELATION_SERVICE_ENABLE_TEST_HOOKS SRC := $(shell find src -name "*.c") @@ -96,6 +97,7 @@ 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 +TEST_RELATION_SERVICE := tests/test_relation_service all: $(TARGET) @@ -457,6 +459,24 @@ $(TEST_RELATION_EVIDENCE_DAO): \ src/database/error.c $(CC) $(RELATION_EVIDENCE_DAO_TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 +$(TEST_RELATION_SERVICE): \ + tests/test_relation_service.c \ + src/core/relation_service.c \ + src/dao/relation_dao.c \ + src/dao/relation_evidence_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_SERVICE_TEST_CFLAGS) $^ -o $@ \ + $(TEST_LDFLAGS) -lsqlite3 + test: \ $(TEST_NODE) \ $(TEST_TREE_MODEL) \ @@ -498,7 +518,8 @@ test: \ $(TEST_EVIDENCE_ENTITY_DAO) \ $(TEST_RELATION_RECORD) \ $(TEST_RELATION_DAO) \ - $(TEST_RELATION_EVIDENCE_DAO) + $(TEST_RELATION_EVIDENCE_DAO) \ + $(TEST_RELATION_SERVICE) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -541,6 +562,7 @@ test: \ @$(TEST_RELATION_RECORD) @$(TEST_RELATION_DAO) @$(TEST_RELATION_EVIDENCE_DAO) + @$(TEST_RELATION_SERVICE) @echo "Tous les tests sont valides." %.o: %.c @@ -589,6 +611,7 @@ clean: $(TEST_EVIDENCE_ENTITY_DAO) \ $(TEST_RELATION_RECORD) \ $(TEST_RELATION_DAO) \ - $(TEST_RELATION_EVIDENCE_DAO) + $(TEST_RELATION_EVIDENCE_DAO) \ + $(TEST_RELATION_SERVICE) .PHONY: clean run test diff --git a/include/core/relation_service.h b/include/core/relation_service.h new file mode 100644 index 0000000..8a29e17 --- /dev/null +++ b/include/core/relation_service.h @@ -0,0 +1,133 @@ +/****************************************************************************** + * @file relation_service.h + * @brief Orchestration transactionnelle de la création des relations. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_RELATION_SERVICE_H +#define LABFY_INVESTIGATION_RELATION_SERVICE_H + +#include "database/database.h" +#include "models/relation_record.h" + +#include + +G_BEGIN_DECLS + +/** + * @brief Service opaque de gestion transactionnelle des relations. + * + * Le service emprunte la connexion Database reçue à sa création. + */ +typedef struct RelationService RelationService; + +/** + * @brief Catégories d'erreurs produites par RelationService. + */ +typedef enum +{ + /** + * Un argument public est invalide. + */ + RELATION_SERVICE_ERROR_INVALID_ARGUMENT, + + /** + * Une allocation mémoire a échoué. + */ + RELATION_SERVICE_ERROR_MEMORY, + + /** + * La transaction SQLite n'a pas pu être démarrée. + */ + RELATION_SERVICE_ERROR_TRANSACTION_BEGIN, + + /** + * La relation n'a pas pu être insérée. + */ + RELATION_SERVICE_ERROR_RELATION_INSERT, + + /** + * Une preuve n'a pas pu être associée à la relation. + */ + RELATION_SERVICE_ERROR_EVIDENCE_LINK, + + /** + * La transaction SQLite n'a pas pu être validée. + */ + RELATION_SERVICE_ERROR_TRANSACTION_COMMIT, + + /** + * La transaction SQLite n'a pas pu être annulée. + */ + RELATION_SERVICE_ERROR_TRANSACTION_ROLLBACK +} RelationServiceError; + +/** + * @brief Domaine d'erreur du service. + */ +#define RELATION_SERVICE_ERROR \ + relation_service_error_quark() + +/** + * @brief Retourne le domaine d'erreur du service. + */ +GQuark relation_service_error_quark(void); + +/** + * @brief Crée un service utilisant une connexion Database existante. + * + * Le service emprunte la connexion et possède les DAO qu'il crée. + * + * @param database Connexion SQLite ouverte et initialisée. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau service, ou NULL en cas d'échec. + */ +RelationService *relation_service_new( + Database *database, + GError **error +); + +/** + * @brief Libère le service. + * + * Les DAO internes sont libérés, mais la connexion empruntée n'est + * jamais fermée. + * + * Cette fonction accepte NULL. + * + * @param relation_service Service à libérer. + */ +void relation_service_free( + RelationService *relation_service +); + +/** + * @brief Crée atomiquement une relation et ses associations aux preuves. + * + * relation_record, evidence_identifiers et les chaînes du tableau sont + * empruntés pendant l'appel. + * + * evidence_identifiers peut être NULL ou vide. + * + * Un retour TRUE garantit que la relation et toutes ses associations ont + * été validées par COMMIT. + * + * Un retour FALSE signifie que l'opération complète a échoué. + * + * @param relation_service Service valide. + * @param relation_record Relation à insérer. + * @param evidence_identifiers Tableau facultatif d'UUID de preuves. + * @param error Emplacement facultatif recevant une erreur. + * + * @return TRUE si l'opération complète est validée. + */ +gboolean relation_service_create( + RelationService *relation_service, + const RelationRecord *relation_record, + const GPtrArray *evidence_identifiers, + GError **error +); + +G_END_DECLS + +#endif diff --git a/include/core/relation_service_test.h b/include/core/relation_service_test.h new file mode 100644 index 0000000..4f70bd3 --- /dev/null +++ b/include/core/relation_service_test.h @@ -0,0 +1,37 @@ +/****************************************************************************** + * @file relation_service_test.h + * @brief Points d'injection réservés aux tests de RelationService. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_RELATION_SERVICE_TEST_H +#define LABFY_INVESTIGATION_RELATION_SERVICE_TEST_H + +#include + +G_BEGIN_DECLS + +/** + * @brief Réinitialise tous les points d'injection du service. + * + * Disponible uniquement lorsque relation_service.c est compilé avec + * RELATION_SERVICE_ENABLE_TEST_HOOKS. + */ +void relation_service_test_reset_hooks(void); + +/** + * @brief Simule l'échec du prochain COMMIT. + */ +void relation_service_test_set_commit_failure( + gboolean enabled +); + +/** + * @brief Simule l'échec du prochain ROLLBACK. + */ +void relation_service_test_set_rollback_failure( + gboolean enabled +); + +G_END_DECLS + +#endif diff --git a/src/core/relation_service.c b/src/core/relation_service.c new file mode 100644 index 0000000..9f52e18 --- /dev/null +++ b/src/core/relation_service.c @@ -0,0 +1,777 @@ +/****************************************************************************** + * @file relation_service.c + * @brief Orchestration transactionnelle de la création des relations. + ******************************************************************************/ + +#include "core/relation_service.h" + +#include "dao/relation_dao.h" +#include "dao/relation_evidence_dao.h" +#include "database/error.h" +#include "database/transaction.h" + +#include + +/** + * @brief Représentation privée du service. + * + * La connexion Database est empruntée. + * Les deux DAO sont possédés. + */ +struct RelationService +{ + Database *database; + + RelationDao *relation_dao; + RelationEvidenceDao *relation_evidence_dao; +}; + +#ifdef RELATION_SERVICE_ENABLE_TEST_HOOKS + +#include "core/relation_service_test.h" + +static gboolean relation_service_test_commit_failure = + FALSE; + +static gboolean relation_service_test_rollback_failure = + FALSE; + +/** + * @brief Consomme un point d'injection à usage unique. + */ +static gboolean relation_service_test_take_hook( + gboolean *hook +) +{ + gboolean enabled = + FALSE; + + if (hook == NULL) + { + return FALSE; + } + + enabled = + *hook; + + *hook = + FALSE; + + return enabled; +} + +void relation_service_test_reset_hooks(void) +{ + relation_service_test_commit_failure = + FALSE; + + relation_service_test_rollback_failure = + FALSE; +} + +void relation_service_test_set_commit_failure( + gboolean enabled +) +{ + relation_service_test_commit_failure = + enabled; +} + +void relation_service_test_set_rollback_failure( + gboolean enabled +) +{ + relation_service_test_rollback_failure = + enabled; +} + +#endif + +/** + * @brief Enregistre une erreur littérale du service. + */ +static void relation_service_set_error_literal( + GError **error, + RelationServiceError error_code, + const char *message +) +{ + if (error == NULL || + *error != NULL) + { + return; + } + + g_set_error_literal( + error, + RELATION_SERVICE_ERROR, + error_code, + message + ); +} + +/** + * @brief Conserve le contexte d'une erreur provenant d'un DAO. + */ +static void relation_service_set_nested_error( + GError **error, + RelationServiceError error_code, + const char *context, + const GError *nested_error +) +{ + if (error == NULL || + *error != NULL) + { + return; + } + + g_set_error( + error, + RELATION_SERVICE_ERROR, + error_code, + "%s : %s", + context, + nested_error != NULL + ? nested_error->message + : "erreur inconnue" + ); +} + +/** + * @brief Conserve le contexte de la dernière erreur Database. + */ +static void relation_service_set_database_error( + GError **error, + RelationServiceError error_code, + const char *context, + const Database *database +) +{ + const char *database_message = + NULL; + + if (error == NULL || + *error != NULL) + { + return; + } + + database_message = + database_error_get_message( + database + ); + + g_set_error( + error, + RELATION_SERVICE_ERROR, + error_code, + "%s : %s", + context, + database_message != NULL + ? database_message + : "erreur Database inconnue" + ); +} + +/** + * @brief Vérifie les champs obligatoires du modèle. + */ +static gboolean relation_service_record_is_valid( + const RelationRecord *relation_record +) +{ + if (relation_record == NULL) + { + return FALSE; + } + + if (relation_record_get_identifier( + relation_record + ) == NULL || + relation_record_get_source_entity_identifier( + relation_record + ) == NULL || + relation_record_get_target_entity_identifier( + relation_record + ) == NULL || + relation_record_get_relation_type( + relation_record + ) == NULL || + relation_record_get_created_at( + relation_record + ) == NULL || + relation_record_get_updated_at( + relation_record + ) == NULL) + { + return FALSE; + } + + return TRUE; +} + +/** + * @brief Vérifie les UUID de preuves avant toute transaction. + * + * Le contrôle des doublons est volontairement effectué sans allocation + * supplémentaire. Les listes de preuves liées à une relation restent + * normalement courtes et cette validation demeure simple et déterministe. + */ +static gboolean relation_service_evidence_identifiers_are_valid( + const GPtrArray *evidence_identifiers, + GError **error +) +{ + guint evidence_index = + 0; + + guint previous_index = + 0; + + if (evidence_identifiers == NULL) + { + return TRUE; + } + + for (evidence_index = 0; + evidence_index < evidence_identifiers->len; + evidence_index++) + { + const char *evidence_identifier = + g_ptr_array_index( + evidence_identifiers, + evidence_index + ); + + if (evidence_identifier == NULL || + !g_uuid_string_is_valid( + evidence_identifier + )) + { + relation_service_set_error_literal( + error, + RELATION_SERVICE_ERROR_INVALID_ARGUMENT, + "La demande contient un identifiant de preuve invalide." + ); + + return FALSE; + } + + for (previous_index = 0; + previous_index < evidence_index; + previous_index++) + { + const char *previous_identifier = + g_ptr_array_index( + evidence_identifiers, + previous_index + ); + + if (g_strcmp0( + evidence_identifier, + previous_identifier + ) == 0) + { + relation_service_set_error_literal( + error, + RELATION_SERVICE_ERROR_INVALID_ARGUMENT, + "La demande contient plusieurs fois la même preuve." + ); + + return FALSE; + } + } + } + + return TRUE; +} + +/** + * @brief Valide la transaction, sauf panne simulée par un test. + */ +static gboolean relation_service_commit_transaction( + Database *database +) +{ +#ifdef RELATION_SERVICE_ENABLE_TEST_HOOKS + + if (relation_service_test_take_hook( + &relation_service_test_commit_failure + )) + { + database_error_set( + database, + DATABASE_ERROR_SQLITE, + "Échec de COMMIT simulé par RelationService." + ); + + return FALSE; + } + +#endif + + return database_transaction_commit( + database + ); +} + +/** + * @brief Annule la transaction, sauf panne simulée par un test. + */ +static gboolean relation_service_execute_rollback( + Database *database +) +{ +#ifdef RELATION_SERVICE_ENABLE_TEST_HOOKS + + if (relation_service_test_take_hook( + &relation_service_test_rollback_failure + )) + { + /* + * La vraie transaction est tout de même annulée afin que le test + * ne laisse pas la connexion dans un état inutilisable. + */ + (void) database_transaction_rollback( + database + ); + + database_error_set( + database, + DATABASE_ERROR_SQLITE, + "Échec de ROLLBACK simulé par RelationService." + ); + + return FALSE; + } + +#endif + + return database_transaction_rollback( + database + ); +} + +/** + * @brief Annule la transaction et conserve l'erreur métier initiale. + */ +static gboolean relation_service_rollback_transaction( + Database *database, + GError **error +) +{ + const char *database_message = + NULL; + + char *primary_error_message = + NULL; + + if (database == NULL) + { + return FALSE; + } + + if (!database_transaction_is_active( + database + )) + { + return TRUE; + } + + if (relation_service_execute_rollback( + database + )) + { + return TRUE; + } + + database_message = + database_error_get_message( + database + ); + + if (error == NULL) + { + g_warning( + "Impossible d'annuler la transaction SQLite : %s", + database_message != NULL + ? database_message + : "erreur inconnue" + ); + + return FALSE; + } + + if (*error != NULL) + { + primary_error_message = + g_strdup( + (*error)->message + ); + + g_clear_error( + error + ); + } + + g_set_error( + error, + RELATION_SERVICE_ERROR, + RELATION_SERVICE_ERROR_TRANSACTION_ROLLBACK, + "Impossible d'annuler la transaction SQLite : %s. " + "Cause initiale : %s", + database_message != NULL + ? database_message + : "erreur inconnue", + primary_error_message != NULL + ? primary_error_message + : "erreur inconnue" + ); + + g_free( + primary_error_message + ); + + return FALSE; +} + +GQuark relation_service_error_quark(void) +{ + return g_quark_from_static_string( + "relation-service-error-quark" + ); +} + +RelationService *relation_service_new( + Database *database, + GError **error +) +{ + RelationService *relation_service = + NULL; + + RelationDao *relation_dao = + NULL; + + RelationEvidenceDao *relation_evidence_dao = + NULL; + + GError *dao_error = + NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (database == NULL) + { + relation_service_set_error_literal( + error, + RELATION_SERVICE_ERROR_INVALID_ARGUMENT, + "La connexion Database est obligatoire." + ); + + return NULL; + } + + relation_dao = + relation_dao_new( + database, + &dao_error + ); + + if (relation_dao == NULL) + { + relation_service_set_nested_error( + error, + RELATION_SERVICE_ERROR_MEMORY, + "Impossible de créer le DAO des relations", + dao_error + ); + + g_clear_error( + &dao_error + ); + + return NULL; + } + + relation_evidence_dao = + relation_evidence_dao_new( + database, + &dao_error + ); + + if (relation_evidence_dao == NULL) + { + relation_service_set_nested_error( + error, + RELATION_SERVICE_ERROR_MEMORY, + "Impossible de créer le DAO des associations relation-preuve", + dao_error + ); + + g_clear_error( + &dao_error + ); + + relation_dao_free( + relation_dao + ); + + return NULL; + } + + relation_service = + g_try_new0( + RelationService, + 1 + ); + + if (relation_service == NULL) + { + relation_evidence_dao_free( + relation_evidence_dao + ); + + relation_dao_free( + relation_dao + ); + + relation_service_set_error_literal( + error, + RELATION_SERVICE_ERROR_MEMORY, + "Impossible d'allouer le service de gestion des relations." + ); + + return NULL; + } + + relation_service->database = + database; + + relation_service->relation_dao = + relation_dao; + + relation_service->relation_evidence_dao = + relation_evidence_dao; + + return relation_service; +} + +void relation_service_free( + RelationService *relation_service +) +{ + if (relation_service == NULL) + { + return; + } + + relation_evidence_dao_free( + relation_service->relation_evidence_dao + ); + + relation_dao_free( + relation_service->relation_dao + ); + + relation_service->relation_evidence_dao = + NULL; + + relation_service->relation_dao = + NULL; + + relation_service->database = + NULL; + + g_free( + relation_service + ); +} + +gboolean relation_service_create( + RelationService *relation_service, + const RelationRecord *relation_record, + const GPtrArray *evidence_identifiers, + GError **error +) +{ + GError *dao_error = + NULL; + + guint evidence_index = + 0; + + gboolean transaction_started = + FALSE; + + gboolean success = + FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (relation_service == NULL || + relation_service->database == NULL || + relation_service->relation_dao == NULL || + relation_service->relation_evidence_dao == NULL) + { + relation_service_set_error_literal( + error, + RELATION_SERVICE_ERROR_INVALID_ARGUMENT, + "Le service de gestion des relations est invalide." + ); + + return FALSE; + } + + if (!relation_service_record_is_valid( + relation_record + )) + { + relation_service_set_error_literal( + error, + RELATION_SERVICE_ERROR_INVALID_ARGUMENT, + "Le modèle de relation est invalide." + ); + + return FALSE; + } + + if (!relation_service_evidence_identifiers_are_valid( + evidence_identifiers, + error + )) + { + return FALSE; + } + + if (!database_transaction_begin( + relation_service->database + )) + { + relation_service_set_database_error( + error, + RELATION_SERVICE_ERROR_TRANSACTION_BEGIN, + "Impossible de démarrer la transaction SQLite", + relation_service->database + ); + + return FALSE; + } + + transaction_started = + TRUE; + + if (!relation_dao_insert( + relation_service->relation_dao, + relation_record, + &dao_error + )) + { + relation_service_set_nested_error( + error, + RELATION_SERVICE_ERROR_RELATION_INSERT, + "Impossible d'enregistrer la relation", + dao_error + ); + + goto cleanup; + } + + g_clear_error( + &dao_error + ); + + if (evidence_identifiers != NULL) + { + for (evidence_index = 0; + evidence_index < evidence_identifiers->len; + evidence_index++) + { + const char *evidence_identifier = + g_ptr_array_index( + evidence_identifiers, + evidence_index + ); + + if (!relation_evidence_dao_link( + relation_service->relation_evidence_dao, + relation_record_get_identifier( + relation_record + ), + evidence_identifier, + &dao_error + )) + { + if (error != NULL && + *error == NULL) + { + g_set_error( + error, + RELATION_SERVICE_ERROR, + RELATION_SERVICE_ERROR_EVIDENCE_LINK, + "Impossible d'associer la preuve %s " + "à la relation : %s", + evidence_identifier, + dao_error != NULL + ? dao_error->message + : "erreur DAO inconnue" + ); + } + + goto cleanup; + } + + g_clear_error( + &dao_error + ); + } + } + + if (!relation_service_commit_transaction( + relation_service->database + )) + { + relation_service_set_database_error( + error, + RELATION_SERVICE_ERROR_TRANSACTION_COMMIT, + "Impossible de valider la transaction SQLite", + relation_service->database + ); + + goto cleanup; + } + + transaction_started = + FALSE; + + success = + TRUE; + +cleanup: + + g_clear_error( + &dao_error + ); + + if (!success && + transaction_started) + { + (void) relation_service_rollback_transaction( + relation_service->database, + error + ); + + transaction_started = + FALSE; + } + + return success; +} + diff --git a/tests/test_relation_service.c b/tests/test_relation_service.c new file mode 100644 index 0000000..5b2f9cf --- /dev/null +++ b/tests/test_relation_service.c @@ -0,0 +1,2153 @@ +/****************************************************************************** + * @file test_relation_service.c + * @brief Tests transactionnels du service de gestion des relations. + ******************************************************************************/ + +#include "core/relation_service.h" +#include "core/relation_service_test.h" + +#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 "database/transaction.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 RelationService. + */ +typedef struct +{ + char *temporary_directory; + char *database_path; + + Database *database; + + EntityDao *entity_dao; + EvidenceDao *evidence_dao; + RelationDao *relation_dao; + RelationEvidenceDao *relation_evidence_dao; + + RelationService *relation_service; +} TestRelationServiceFixture; + +/** + * @brief Vérifie le domaine et le code d'une erreur RelationService. + */ +static void test_relation_service_assert_error( + const GError *error, + RelationServiceError expected_code +) +{ + assert(error != NULL); + + assert( + error->domain == + RELATION_SERVICE_ERROR + ); + + assert( + error->code == + (gint) expected_code + ); + + assert(error->message != NULL); + assert(error->message[0] != '\0'); +} + +/** + * @brief Crée une base temporaire et les composants nécessaires. + */ +static TestRelationServiceFixture +test_relation_service_fixture_create(void) +{ + TestRelationServiceFixture fixture = + {0}; + + GError *error = + NULL; + + fixture.temporary_directory = + g_dir_make_tmp( + "labfy-relation-service-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_RelationService", + fixture.temporary_directory + ) + ); + + fixture.database = + database_open( + fixture.database_path + ); + + assert(fixture.database != NULL); + + fixture.entity_dao = + entity_dao_new( + fixture.database, + &error + ); + + assert(fixture.entity_dao != NULL); + assert(error == NULL); + + fixture.evidence_dao = + evidence_dao_new( + fixture.database, + &error + ); + + assert(fixture.evidence_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); + + fixture.relation_service = + relation_service_new( + fixture.database, + &error + ); + + assert(fixture.relation_service != NULL); + assert(error == NULL); + + relation_service_test_reset_hooks(); + + return fixture; +} + +/** + * @brief Libère la fixture et supprime la base temporaire. + */ +static void test_relation_service_fixture_clear( + TestRelationServiceFixture *fixture +) +{ + assert(fixture != NULL); + + relation_service_test_reset_hooks(); + + relation_service_free( + fixture->relation_service + ); + + relation_evidence_dao_free( + fixture->relation_evidence_dao + ); + + relation_dao_free( + fixture->relation_dao + ); + + evidence_dao_free( + fixture->evidence_dao + ); + + entity_dao_free( + fixture->entity_dao + ); + + database_close( + fixture->database + ); + + assert( + g_remove( + fixture->database_path + ) == 0 + ); + + assert( + g_rmdir( + fixture->temporary_directory + ) == 0 + ); + + g_free( + fixture->database_path + ); + + g_free( + fixture->temporary_directory + ); + + fixture->relation_service = + NULL; + + fixture->relation_evidence_dao = + NULL; + + fixture->relation_dao = + NULL; + + fixture->evidence_dao = + NULL; + + fixture->entity_dao = + NULL; + + fixture->database = + NULL; + + fixture->database_path = + NULL; + + fixture->temporary_directory = + NULL; +} + +/** + * @brief Crée une entité de test. + */ +static EntityRecord *test_relation_service_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-21T18:00:00Z", + "2026-07-21T18: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_service_insert_entity( + TestRelationServiceFixture *fixture, + const char *identifier, + const char *value +) +{ + EntityRecord *entity_record = + NULL; + + GError *error = + NULL; + + entity_record = + test_relation_service_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. + */ +static void test_relation_service_insert_standard_entities( + TestRelationServiceFixture *fixture +) +{ + test_relation_service_insert_entity( + fixture, + TEST_ENTITY_A_IDENTIFIER, + "Personne A" + ); + + test_relation_service_insert_entity( + fixture, + TEST_ENTITY_B_IDENTIFIER, + "Personne B" + ); + + test_relation_service_insert_entity( + fixture, + TEST_ENTITY_C_IDENTIFIER, + "Personne C" + ); +} + +/** + * @brief Crée une preuve possédant des valeurs uniques. + */ +static EvidenceRecord *test_relation_service_create_evidence( + const char *identifier +) +{ + EvidenceRecord *evidence_record = + NULL; + + char *internal_name = + NULL; + + char *relative_path = + NULL; + + char sha256[65] = + {0}; + + char hash_character = + 'a'; + + GError *error = + NULL; + + assert(identifier != NULL); + + hash_character = + identifier[strlen(identifier) - 1U]; + + if (!g_ascii_isxdigit( + hash_character + )) + { + hash_character = + 'a'; + } + + memset( + sha256, + hash_character, + 64U + ); + + sha256[64] = + '\0'; + + 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, + sha256, + "2026-07-21T18:10: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_service_insert_evidence( + TestRelationServiceFixture *fixture, + const char *identifier +) +{ + EvidenceRecord *evidence_record = + NULL; + + GError *error = + NULL; + + evidence_record = + test_relation_service_create_evidence( + identifier + ); + + assert( + evidence_dao_insert( + fixture->evidence_dao, + evidence_record, + &error + ) + ); + + assert(error == NULL); + + evidence_record_free( + evidence_record + ); +} + +/** + * @brief Crée une relation de test. + */ +static RelationRecord *test_relation_service_create_relation( + const char *identifier, + const char *source_identifier, + const char *target_identifier, + const char *relation_type +) +{ + RelationRecord *relation_record = + NULL; + + GError *error = + NULL; + + relation_record = + relation_record_new( + identifier, + source_identifier, + target_identifier, + relation_type, + NULL, + "Relation créée par un test transactionnel.", + 75, + "2026-07-21T18:20:00Z", + "2026-07-21T18:20:00Z", + RELATION_STATUS_ACTIVE, + &error + ); + + assert(relation_record != NULL); + assert(error == NULL); + + return relation_record; +} + +/** + * @brief Crée un tableau possédant des copies d'UUID. + */ +static GPtrArray *test_relation_service_create_identifier_array( + const char *first_identifier, + const char *second_identifier, + const char *third_identifier +) +{ + GPtrArray *identifiers = + NULL; + + identifiers = + g_ptr_array_new_with_free_func( + g_free + ); + + assert(identifiers != NULL); + + if (first_identifier != NULL) + { + g_ptr_array_add( + identifiers, + g_strdup( + first_identifier + ) + ); + } + + if (second_identifier != NULL) + { + g_ptr_array_add( + identifiers, + g_strdup( + second_identifier + ) + ); + } + + if (third_identifier != NULL) + { + g_ptr_array_add( + identifiers, + g_strdup( + third_identifier + ) + ); + } + + return identifiers; +} + +/** + * @brief Compte les lignes correspondant à un UUID. + */ +static int64_t test_relation_service_count_by_identifier( + Database *database, + const char *sql, + const char *identifier +) +{ + DatabaseStatement *statement = + NULL; + + int64_t row_count = + -1; + + statement = + database_statement_prepare( + database, + sql + ); + + assert(statement != NULL); + + assert( + database_statement_bind_text( + statement, + 1, + identifier + ) + ); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_ROW + ); + + assert( + database_statement_column_int64( + statement, + 0, + &row_count + ) + ); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize( + statement + ); + + return row_count; +} + +/** + * @brief Compte une relation précise. + */ +static int64_t test_relation_service_count_relation( + Database *database, + const char *relation_identifier +) +{ + return test_relation_service_count_by_identifier( + database, + "SELECT COUNT(*) " + "FROM relations " + "WHERE id = ?;", + relation_identifier + ); +} + +/** + * @brief Compte les associations d'une relation. + */ +static int64_t test_relation_service_count_associations( + Database *database, + const char *relation_identifier +) +{ + return test_relation_service_count_by_identifier( + database, + "SELECT COUNT(*) " + "FROM relation_preuves " + "WHERE relation_id = ?;", + relation_identifier + ); +} + +/** + * @brief Vérifie qu'une preuve existe encore. + */ +static void test_relation_service_assert_evidence_exists( + TestRelationServiceFixture *fixture, + const char *evidence_identifier +) +{ + EvidenceRecord *evidence_record = + NULL; + + GError *error = + NULL; + + evidence_record = + evidence_dao_find_by_identifier( + fixture->evidence_dao, + evidence_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_service_new_null_database(void) +{ + RelationService *relation_service = + NULL; + + GError *error = + NULL; + + relation_service = + relation_service_new( + NULL, + &error + ); + + assert(relation_service == NULL); + + test_relation_service_assert_error( + error, + RELATION_SERVICE_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); +} + +/** + * @brief Vérifie la création et la destruction du service. + */ +static void test_relation_service_lifecycle(void) +{ + TestRelationServiceFixture fixture = + test_relation_service_fixture_create(); + + DatabaseStatement *statement = + NULL; + + relation_service_free( + fixture.relation_service + ); + + fixture.relation_service = + NULL; + + /* + * La connexion doit rester exploitable après la destruction du service. + */ + statement = + database_statement_prepare( + fixture.database, + "SELECT 1;" + ); + + assert(statement != NULL); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_ROW + ); + + database_statement_finalize( + statement + ); + + relation_service_free( + NULL + ); + + test_relation_service_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie la création sans tableau de preuves. + */ +static void test_relation_service_create_without_array(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000001"; + + TestRelationServiceFixture fixture = + test_relation_service_fixture_create(); + + RelationRecord *relation_record = + NULL; + + GError *error = + NULL; + + test_relation_service_insert_standard_entities( + &fixture + ); + + relation_record = + test_relation_service_create_relation( + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + assert( + relation_service_create( + fixture.relation_service, + relation_record, + NULL, + &error + ) + ); + + assert(error == NULL); + + assert( + test_relation_service_count_relation( + fixture.database, + relation_identifier + ) == 1 + ); + + assert( + test_relation_service_count_associations( + fixture.database, + relation_identifier + ) == 0 + ); + + relation_record_free( + relation_record + ); + + test_relation_service_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie la création avec un tableau vide. + */ +static void test_relation_service_create_with_empty_array(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000002"; + + TestRelationServiceFixture fixture = + test_relation_service_fixture_create(); + + RelationRecord *relation_record = + NULL; + + GPtrArray *evidence_identifiers = + NULL; + + GError *error = + NULL; + + test_relation_service_insert_standard_entities( + &fixture + ); + + relation_record = + test_relation_service_create_relation( + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "controls" + ); + + evidence_identifiers = + g_ptr_array_new_with_free_func( + g_free + ); + + assert(evidence_identifiers != NULL); + + assert( + relation_service_create( + fixture.relation_service, + relation_record, + evidence_identifiers, + &error + ) + ); + + assert(error == NULL); + assert(evidence_identifiers->len == 0); + + assert( + test_relation_service_count_relation( + fixture.database, + relation_identifier + ) == 1 + ); + + g_ptr_array_unref( + evidence_identifiers + ); + + relation_record_free( + relation_record + ); + + test_relation_service_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie la création avec une preuve. + */ +static void test_relation_service_create_with_one_evidence(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000003"; + + static const char *const evidence_identifier = + "50000000-0000-4000-8000-000000000001"; + + TestRelationServiceFixture fixture = + test_relation_service_fixture_create(); + + RelationRecord *relation_record = + NULL; + + GPtrArray *evidence_identifiers = + NULL; + + gboolean association_exists = + FALSE; + + GError *error = + NULL; + + test_relation_service_insert_standard_entities( + &fixture + ); + + test_relation_service_insert_evidence( + &fixture, + evidence_identifier + ); + + relation_record = + test_relation_service_create_relation( + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "owns" + ); + + evidence_identifiers = + test_relation_service_create_identifier_array( + evidence_identifier, + NULL, + NULL + ); + + assert( + relation_service_create( + fixture.relation_service, + relation_record, + evidence_identifiers, + &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(evidence_identifiers->len == 1); + + assert( + strcmp( + g_ptr_array_index( + evidence_identifiers, + 0 + ), + evidence_identifier + ) == 0 + ); + + g_ptr_array_unref( + evidence_identifiers + ); + + relation_record_free( + relation_record + ); + + test_relation_service_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie la création avec plusieurs preuves. + */ +static void test_relation_service_create_with_multiple_evidence(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000004"; + + static const char *const first_evidence_identifier = + "50000000-0000-4000-8000-000000000002"; + + static const char *const second_evidence_identifier = + "50000000-0000-4000-8000-000000000003"; + + static const char *const third_evidence_identifier = + "50000000-0000-4000-8000-000000000004"; + + TestRelationServiceFixture fixture = + test_relation_service_fixture_create(); + + RelationRecord *relation_record = + NULL; + + GPtrArray *requested_identifiers = + NULL; + + GPtrArray *loaded_identifiers = + NULL; + + GError *error = + NULL; + + test_relation_service_insert_standard_entities( + &fixture + ); + + test_relation_service_insert_evidence( + &fixture, + first_evidence_identifier + ); + + test_relation_service_insert_evidence( + &fixture, + second_evidence_identifier + ); + + test_relation_service_insert_evidence( + &fixture, + third_evidence_identifier + ); + + relation_record = + test_relation_service_create_relation( + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_C_IDENTIFIER, + "knows" + ); + + requested_identifiers = + test_relation_service_create_identifier_array( + third_evidence_identifier, + first_evidence_identifier, + second_evidence_identifier + ); + + assert( + relation_service_create( + fixture.relation_service, + relation_record, + requested_identifiers, + &error + ) + ); + + assert(error == NULL); + + loaded_identifiers = + relation_evidence_dao_list_evidence_identifiers( + fixture.relation_evidence_dao, + relation_identifier, + &error + ); + + assert(loaded_identifiers != NULL); + assert(error == NULL); + assert(loaded_identifiers->len == 3); + + assert( + strcmp( + g_ptr_array_index( + loaded_identifiers, + 0 + ), + first_evidence_identifier + ) == 0 + ); + + assert( + strcmp( + g_ptr_array_index( + loaded_identifiers, + 1 + ), + second_evidence_identifier + ) == 0 + ); + + assert( + strcmp( + g_ptr_array_index( + loaded_identifiers, + 2 + ), + third_evidence_identifier + ) == 0 + ); + + g_ptr_array_unref( + loaded_identifiers + ); + + g_ptr_array_unref( + requested_identifiers + ); + + relation_record_free( + relation_record + ); + + test_relation_service_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie les arguments publics invalides. + */ +static void test_relation_service_invalid_arguments(void) +{ + TestRelationServiceFixture fixture = + test_relation_service_fixture_create(); + + RelationRecord *relation_record = + NULL; + + GPtrArray *identifiers = + NULL; + + GError *error = + NULL; + + test_relation_service_insert_standard_entities( + &fixture + ); + + relation_record = + test_relation_service_create_relation( + "40000000-0000-4000-8000-000000000005", + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + assert( + !relation_service_create( + NULL, + relation_record, + NULL, + &error + ) + ); + + test_relation_service_assert_error( + error, + RELATION_SERVICE_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + assert( + !relation_service_create( + fixture.relation_service, + NULL, + NULL, + &error + ) + ); + + test_relation_service_assert_error( + error, + RELATION_SERVICE_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + identifiers = + g_ptr_array_new(); + + assert(identifiers != NULL); + + g_ptr_array_add( + identifiers, + NULL + ); + + assert( + !relation_service_create( + fixture.relation_service, + relation_record, + identifiers, + &error + ) + ); + + test_relation_service_assert_error( + error, + RELATION_SERVICE_ERROR_INVALID_ARGUMENT + ); + + assert( + test_relation_service_count_relation( + fixture.database, + relation_record_get_identifier( + relation_record + ) + ) == 0 + ); + + g_clear_error( + &error + ); + + g_ptr_array_unref( + identifiers + ); + + relation_record_free( + relation_record + ); + + test_relation_service_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d'un UUID de preuve invalide avant transaction. + */ +static void test_relation_service_invalid_evidence_identifier(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000006"; + + TestRelationServiceFixture fixture = + test_relation_service_fixture_create(); + + RelationRecord *relation_record = + NULL; + + GPtrArray *identifiers = + NULL; + + GError *error = + NULL; + + test_relation_service_insert_standard_entities( + &fixture + ); + + relation_record = + test_relation_service_create_relation( + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + identifiers = + test_relation_service_create_identifier_array( + "preuve-invalide", + NULL, + NULL + ); + + assert( + !relation_service_create( + fixture.relation_service, + relation_record, + identifiers, + &error + ) + ); + + test_relation_service_assert_error( + error, + RELATION_SERVICE_ERROR_INVALID_ARGUMENT + ); + + assert( + test_relation_service_count_relation( + fixture.database, + relation_identifier + ) == 0 + ); + + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + g_clear_error( + &error + ); + + g_ptr_array_unref( + identifiers + ); + + relation_record_free( + relation_record + ); + + test_relation_service_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus des doublons avant transaction. + */ +static void test_relation_service_duplicate_evidence_request(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000007"; + + static const char *const evidence_identifier = + "50000000-0000-4000-8000-000000000005"; + + TestRelationServiceFixture fixture = + test_relation_service_fixture_create(); + + RelationRecord *relation_record = + NULL; + + GPtrArray *identifiers = + NULL; + + GError *error = + NULL; + + test_relation_service_insert_standard_entities( + &fixture + ); + + test_relation_service_insert_evidence( + &fixture, + evidence_identifier + ); + + relation_record = + test_relation_service_create_relation( + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "controls" + ); + + identifiers = + test_relation_service_create_identifier_array( + evidence_identifier, + evidence_identifier, + NULL + ); + + assert( + !relation_service_create( + fixture.relation_service, + relation_record, + identifiers, + &error + ) + ); + + test_relation_service_assert_error( + error, + RELATION_SERVICE_ERROR_INVALID_ARGUMENT + ); + + assert( + strstr( + error->message, + "plusieurs fois" + ) != NULL + ); + + assert( + test_relation_service_count_relation( + fixture.database, + relation_identifier + ) == 0 + ); + + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + g_clear_error( + &error + ); + + g_ptr_array_unref( + identifiers + ); + + relation_record_free( + relation_record + ); + + test_relation_service_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le rollback lorsque l'UUID de relation existe déjà. + */ +static void test_relation_service_duplicate_relation_rollback(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000008"; + + TestRelationServiceFixture fixture = + test_relation_service_fixture_create(); + + RelationRecord *existing_record = + NULL; + + RelationRecord *duplicate_record = + NULL; + + GError *error = + NULL; + + test_relation_service_insert_standard_entities( + &fixture + ); + + existing_record = + test_relation_service_create_relation( + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + assert( + relation_dao_insert( + fixture.relation_dao, + existing_record, + &error + ) + ); + + assert(error == NULL); + + duplicate_record = + test_relation_service_create_relation( + relation_identifier, + TEST_ENTITY_B_IDENTIFIER, + TEST_ENTITY_C_IDENTIFIER, + "controls" + ); + + assert( + !relation_service_create( + fixture.relation_service, + duplicate_record, + NULL, + &error + ) + ); + + test_relation_service_assert_error( + error, + RELATION_SERVICE_ERROR_RELATION_INSERT + ); + + assert( + test_relation_service_count_relation( + fixture.database, + relation_identifier + ) == 1 + ); + + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + g_clear_error( + &error + ); + + relation_record_free( + duplicate_record + ); + + relation_record_free( + existing_record + ); + + test_relation_service_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie l'atomicité lorsqu'une seconde preuve est absente. + */ +static void test_relation_service_missing_second_evidence_rollback(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000009"; + + static const char *const existing_evidence_identifier = + "50000000-0000-4000-8000-000000000006"; + + static const char *const missing_evidence_identifier = + "50000000-0000-4000-8000-000000000007"; + + TestRelationServiceFixture fixture = + test_relation_service_fixture_create(); + + RelationRecord *relation_record = + NULL; + + GPtrArray *identifiers = + NULL; + + GError *error = + NULL; + + test_relation_service_insert_standard_entities( + &fixture + ); + + test_relation_service_insert_evidence( + &fixture, + existing_evidence_identifier + ); + + relation_record = + test_relation_service_create_relation( + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_C_IDENTIFIER, + "owns" + ); + + identifiers = + test_relation_service_create_identifier_array( + existing_evidence_identifier, + missing_evidence_identifier, + NULL + ); + + assert( + !relation_service_create( + fixture.relation_service, + relation_record, + identifiers, + &error + ) + ); + + test_relation_service_assert_error( + error, + RELATION_SERVICE_ERROR_EVIDENCE_LINK + ); + + assert( + test_relation_service_count_relation( + fixture.database, + relation_identifier + ) == 0 + ); + + assert( + test_relation_service_count_associations( + fixture.database, + relation_identifier + ) == 0 + ); + + test_relation_service_assert_evidence_exists( + &fixture, + existing_evidence_identifier + ); + + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + g_clear_error( + &error + ); + + g_ptr_array_unref( + identifiers + ); + + relation_record_free( + relation_record + ); + + test_relation_service_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie que la connexion reste utilisable après un rollback. + */ +static void test_relation_service_recovery_after_failure(void) +{ + static const char *const failed_relation_identifier = + "40000000-0000-4000-8000-000000000010"; + + static const char *const valid_relation_identifier = + "40000000-0000-4000-8000-000000000011"; + + static const char *const existing_evidence_identifier = + "50000000-0000-4000-8000-000000000008"; + + static const char *const missing_evidence_identifier = + "50000000-0000-4000-8000-000000000009"; + + TestRelationServiceFixture fixture = + test_relation_service_fixture_create(); + + RelationRecord *failed_record = + NULL; + + RelationRecord *valid_record = + NULL; + + GPtrArray *failed_identifiers = + NULL; + + GPtrArray *valid_identifiers = + NULL; + + GError *error = + NULL; + + test_relation_service_insert_standard_entities( + &fixture + ); + + test_relation_service_insert_evidence( + &fixture, + existing_evidence_identifier + ); + + failed_record = + test_relation_service_create_relation( + failed_relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + failed_identifiers = + test_relation_service_create_identifier_array( + missing_evidence_identifier, + NULL, + NULL + ); + + assert( + !relation_service_create( + fixture.relation_service, + failed_record, + failed_identifiers, + &error + ) + ); + + test_relation_service_assert_error( + error, + RELATION_SERVICE_ERROR_EVIDENCE_LINK + ); + + g_clear_error( + &error + ); + + valid_record = + test_relation_service_create_relation( + valid_relation_identifier, + TEST_ENTITY_B_IDENTIFIER, + TEST_ENTITY_C_IDENTIFIER, + "controls" + ); + + valid_identifiers = + test_relation_service_create_identifier_array( + existing_evidence_identifier, + NULL, + NULL + ); + + assert( + relation_service_create( + fixture.relation_service, + valid_record, + valid_identifiers, + &error + ) + ); + + assert(error == NULL); + + assert( + test_relation_service_count_relation( + fixture.database, + failed_relation_identifier + ) == 0 + ); + + assert( + test_relation_service_count_relation( + fixture.database, + valid_relation_identifier + ) == 1 + ); + + assert( + test_relation_service_count_associations( + fixture.database, + valid_relation_identifier + ) == 1 + ); + + g_ptr_array_unref( + valid_identifiers + ); + + g_ptr_array_unref( + failed_identifiers + ); + + relation_record_free( + valid_record + ); + + relation_record_free( + failed_record + ); + + test_relation_service_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie l'échec du démarrage d'une transaction imbriquée. + */ +static void test_relation_service_transaction_begin_failure(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000012"; + + TestRelationServiceFixture fixture = + test_relation_service_fixture_create(); + + RelationRecord *relation_record = + NULL; + + GError *error = + NULL; + + test_relation_service_insert_standard_entities( + &fixture + ); + + relation_record = + test_relation_service_create_relation( + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + assert( + database_transaction_begin( + fixture.database + ) + ); + + assert( + !relation_service_create( + fixture.relation_service, + relation_record, + NULL, + &error + ) + ); + + test_relation_service_assert_error( + error, + RELATION_SERVICE_ERROR_TRANSACTION_BEGIN + ); + + assert( + test_relation_service_count_relation( + fixture.database, + relation_identifier + ) == 0 + ); + + g_clear_error( + &error + ); + + assert( + database_transaction_rollback( + fixture.database + ) + ); + + relation_record_free( + relation_record + ); + + test_relation_service_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le rollback après un échec simulé du COMMIT. + */ +static void test_relation_service_commit_failure_rollback(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000013"; + + static const char *const evidence_identifier = + "50000000-0000-4000-8000-00000000000a"; + + TestRelationServiceFixture fixture = + test_relation_service_fixture_create(); + + RelationRecord *relation_record = + NULL; + + GPtrArray *identifiers = + NULL; + + GError *error = + NULL; + + test_relation_service_insert_standard_entities( + &fixture + ); + + test_relation_service_insert_evidence( + &fixture, + evidence_identifier + ); + + relation_record = + test_relation_service_create_relation( + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "uses" + ); + + identifiers = + test_relation_service_create_identifier_array( + evidence_identifier, + NULL, + NULL + ); + + relation_service_test_set_commit_failure( + TRUE + ); + + assert( + !relation_service_create( + fixture.relation_service, + relation_record, + identifiers, + &error + ) + ); + + test_relation_service_assert_error( + error, + RELATION_SERVICE_ERROR_TRANSACTION_COMMIT + ); + + assert( + test_relation_service_count_relation( + fixture.database, + relation_identifier + ) == 0 + ); + + assert( + test_relation_service_count_associations( + fixture.database, + relation_identifier + ) == 0 + ); + + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + g_clear_error( + &error + ); + + g_ptr_array_unref( + identifiers + ); + + relation_record_free( + relation_record + ); + + test_relation_service_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le signalement explicite d'un échec de rollback. + */ +static void test_relation_service_rollback_failure_reported(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000014"; + + static const char *const missing_evidence_identifier = + "50000000-0000-4000-8000-00000000000b"; + + TestRelationServiceFixture fixture = + test_relation_service_fixture_create(); + + RelationRecord *relation_record = + NULL; + + GPtrArray *identifiers = + NULL; + + GError *error = + NULL; + + test_relation_service_insert_standard_entities( + &fixture + ); + + relation_record = + test_relation_service_create_relation( + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_C_IDENTIFIER, + "controls" + ); + + identifiers = + test_relation_service_create_identifier_array( + missing_evidence_identifier, + NULL, + NULL + ); + + relation_service_test_set_rollback_failure( + TRUE + ); + + assert( + !relation_service_create( + fixture.relation_service, + relation_record, + identifiers, + &error + ) + ); + + test_relation_service_assert_error( + error, + RELATION_SERVICE_ERROR_TRANSACTION_ROLLBACK + ); + + assert( + strstr( + error->message, + "Cause initiale" + ) != NULL + ); + + assert( + strstr( + error->message, + "ROLLBACK" + ) != NULL + ); + + /* + * Le point d'injection signale l'échec, mais effectue réellement + * l'annulation afin de préserver l'isolation du test. + */ + assert( + test_relation_service_count_relation( + fixture.database, + relation_identifier + ) == 0 + ); + + assert( + !database_transaction_is_active( + fixture.database + ) + ); + + g_clear_error( + &error + ); + + g_ptr_array_unref( + identifiers + ); + + relation_record_free( + relation_record + ); + + test_relation_service_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu'un GError facultatif n'est pas obligatoire. + */ +static void test_relation_service_optional_error(void) +{ + static const char *const relation_identifier = + "40000000-0000-4000-8000-000000000015"; + + TestRelationServiceFixture fixture = + test_relation_service_fixture_create(); + + RelationRecord *relation_record = + NULL; + + test_relation_service_insert_standard_entities( + &fixture + ); + + relation_record = + test_relation_service_create_relation( + relation_identifier, + TEST_ENTITY_A_IDENTIFIER, + TEST_ENTITY_B_IDENTIFIER, + "knows" + ); + + assert( + relation_service_create( + fixture.relation_service, + relation_record, + NULL, + NULL + ) + ); + + assert( + test_relation_service_count_relation( + fixture.database, + relation_identifier + ) == 1 + ); + + assert( + !relation_service_create( + NULL, + relation_record, + NULL, + NULL + ) + ); + + relation_record_free( + relation_record + ); + + test_relation_service_fixture_clear( + &fixture + ); +} + +int main( + int argc, + char **argv +) +{ + g_test_init( + &argc, + &argv, + NULL + ); + + test_relation_service_new_null_database(); + test_relation_service_lifecycle(); + test_relation_service_create_without_array(); + test_relation_service_create_with_empty_array(); + test_relation_service_create_with_one_evidence(); + test_relation_service_create_with_multiple_evidence(); + test_relation_service_invalid_arguments(); + test_relation_service_invalid_evidence_identifier(); + test_relation_service_duplicate_evidence_request(); + test_relation_service_duplicate_relation_rollback(); + test_relation_service_missing_second_evidence_rollback(); + test_relation_service_recovery_after_failure(); + test_relation_service_transaction_begin_failure(); + test_relation_service_commit_failure_rollback(); + test_relation_service_rollback_failure_reported(); + test_relation_service_optional_error(); + + printf( + "RelationService : tous les tests sont valides.\n" + ); + + return 0; +}