diff --git a/Makefile b/Makefile index 89bd421..d24f13e 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,7 @@ EVIDENCE_INTEGRITY_VERIFIER_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 SRC := $(shell find src -name "*.c") @@ -86,6 +87,7 @@ TEST_EVIDENCE_CATEGORY_MODEL := tests/test_evidence_category_model TEST_EVIDENCE_INTEGRITY_TASK := tests/test_evidence_integrity_task TEST_ENTITY_RECORD := tests/test_entity_record TEST_ENTITY_TYPE_DAO := tests/test_entity_type_dao +TEST_ENTITY_DAO := tests/test_entity_dao all: $(TARGET) @@ -388,6 +390,18 @@ $(TEST_ENTITY_TYPE_DAO): \ src/database/error.c $(CC) $(ENTITY_TYPE_DAO_TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 +$(TEST_ENTITY_DAO): \ + tests/test_entity_dao.c \ + src/dao/entity_dao.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) $(ENTITY_DAO_TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 + + test: \ $(TEST_NODE) \ $(TEST_TREE_MODEL) \ @@ -424,7 +438,8 @@ test: \ $(TEST_EVIDENCE_IMPORT_DIALOG) \ $(TEST_EVIDENCE_INTEGRITY_TASK) \ $(TEST_ENTITY_RECORD) \ - $(TEST_ENTITY_TYPE_DAO) + $(TEST_ENTITY_TYPE_DAO) \ + $(TEST_ENTITY_DAO) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -462,6 +477,7 @@ test: \ @$(TEST_EVIDENCE_INTEGRITY_TASK) @$(TEST_ENTITY_RECORD) @$(TEST_ENTITY_TYPE_DAO) + @$(TEST_ENTITY_DAO) @echo "Tous les tests sont valides." %.o: %.c @@ -505,7 +521,8 @@ clean: $(TEST_EVIDENCE_IMPORTER) \ $(TEST_EVIDENCE_IMPORT_TASK) \ $(TEST_EVIDENCE_IMPORT_DIALOG) \ - $(TEST_EVIDENCE_INTEGRITY_TASK) + $(TEST_EVIDENCE_INTEGRITY_TASK) \ + $(TEST_ENTITY_DAO) .PHONY: clean run test diff --git a/include/dao/entity_dao.h b/include/dao/entity_dao.h new file mode 100644 index 0000000..5547be8 --- /dev/null +++ b/include/dao/entity_dao.h @@ -0,0 +1,151 @@ +/****************************************************************************** + * @file entity_dao.h + * @brief Persistance des entités OSINT dans SQLite. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_ENTITY_DAO_H +#define LABFY_INVESTIGATION_ENTITY_DAO_H + +#include "database/database.h" +#include "models/entity_record.h" + +#include + +G_BEGIN_DECLS + +/** + * @brief Catégories d'erreurs produites par EntityDao. + */ +typedef enum +{ + ENTITY_DAO_ERROR_INVALID_ARGUMENT, + ENTITY_DAO_ERROR_MEMORY, + ENTITY_DAO_ERROR_PREPARE, + ENTITY_DAO_ERROR_BIND, + ENTITY_DAO_ERROR_EXECUTE, + ENTITY_DAO_ERROR_CONSTRAINT, + ENTITY_DAO_ERROR_READ, + ENTITY_DAO_ERROR_MODEL, + ENTITY_DAO_ERROR_SCHEMA, + ENTITY_DAO_ERROR_RANGE +} EntityDaoError; + +/** + * @brief Domaine d'erreur du DAO des entités. + */ +#define ENTITY_DAO_ERROR \ + entity_dao_error_quark() + +/** + * @brief Retourne le domaine d'erreur du DAO. + */ +GQuark entity_dao_error_quark(void); + +/** + * @brief DAO opaque donnant accès aux entités persistées. + * + * Le DAO emprunte la connexion Database reçue lors de sa création. + */ +typedef struct EntityDao EntityDao; + +/** + * @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. + */ +EntityDao *entity_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 entity_dao DAO à libérer. + */ +void entity_dao_free( + EntityDao *entity_dao +); + +/** + * @brief Insère une nouvelle entité. + * + * Aucun enregistrement existant n'est remplacé. + * + * @param entity_dao DAO valide. + * @param entity_record Entité empruntée. + * @param error Emplacement facultatif recevant une erreur. + * + * @return TRUE si l'insertion réussit. + */ +gboolean entity_dao_insert( + EntityDao *entity_dao, + const EntityRecord *entity_record, + GError **error +); + +/** + * @brief Recherche une entité par son UUID. + * + * Une entité absente retourne NULL sans produire d'erreur. + * + * @param entity_dao DAO valide. + * @param identifier UUID recherché. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau modèle possédé par l'appelant, ou NULL. + */ +EntityRecord *entity_dao_find_by_identifier( + EntityDao *entity_dao, + const char *identifier, + GError **error +); + +/** + * @brief Charge toutes les entités dans un ordre déterministe. + * + * L'ordre utilisé est : + * + * - date de création croissante ; + * - UUID croissant en cas d'égalité. + * + * Le tableau retourné utilise entity_record_free() comme fonction + * de destruction. + * + * @param entity_dao DAO valide. + * @param error Emplacement facultatif recevant une erreur. + * + * @return Nouveau GPtrArray, ou NULL en cas d'échec. + */ +GPtrArray *entity_dao_list_all( + EntityDao *entity_dao, + GError **error +); + +/** + * @brief Compte les entités persistées. + * + * @param entity_dao DAO valide. + * @param out_count Destination du nombre d'entités. + * @param error Emplacement facultatif recevant une erreur. + * + * @return TRUE si le comptage réussit. + */ +gboolean entity_dao_count( + EntityDao *entity_dao, + guint64 *out_count, + GError **error +); + +G_END_DECLS + +#endif diff --git a/src/dao/entity_dao.c b/src/dao/entity_dao.c new file mode 100644 index 0000000..8ab5a55 --- /dev/null +++ b/src/dao/entity_dao.c @@ -0,0 +1,1683 @@ +/****************************************************************************** + * @file entity_dao.c + * @brief Persistance des entités OSINT dans SQLite. + ******************************************************************************/ + +#include "dao/entity_dao.h" + +#include "database/error.h" +#include "database/statement.h" + +#include +#include +#include + +/** + * @brief Représentation privée du DAO. + * + * La connexion Database est empruntée. + */ +struct EntityDao +{ + Database *database; +}; + +/** + * @brief Requête d'insertion d'une entité. + */ +static const char *const entity_dao_insert_sql = + "INSERT INTO entites" + "(" + " id," + " type_id," + " valeur," + " label," + " description," + " confiance," + " created_at," + " updated_at," + " status" + ")" + "VALUES" + "(" + " ?," + " (" + " SELECT id " + " FROM types_entite " + " WHERE code = ?" + " )," + " ?," + " ?," + " ?," + " ?," + " ?," + " ?," + " ?" + ");"; + +/** + * @brief Requête de recherche par UUID. + */ +static const char *const entity_dao_find_by_identifier_sql = + "SELECT " + " entites.id," + " types_entite.code," + " entites.valeur," + " entites.label," + " entites.description," + " entites.confiance," + " entites.created_at," + " entites.updated_at," + " entites.status " + "FROM entites " + "LEFT JOIN types_entite " + "ON types_entite.id = entites.type_id " + "WHERE entites.id = ?;"; + +/** + * @brief Requête de chargement de toutes les entités. + */ +static const char *const entity_dao_list_all_sql = + "SELECT " + " entites.id," + " types_entite.code," + " entites.valeur," + " entites.label," + " entites.description," + " entites.confiance," + " entites.created_at," + " entites.updated_at," + " entites.status " + "FROM entites " + "LEFT JOIN types_entite " + "ON types_entite.id = entites.type_id " + "ORDER BY " + " entites.created_at ASC," + " entites.id ASC;"; + +/** + * @brief Requête de comptage. + */ +static const char *const entity_dao_count_sql = + "SELECT COUNT(*) " + "FROM entites;"; + +/** + * @brief Requête de détection d'un UUID existant. + */ +static const char *const entity_dao_identifier_exists_sql = + "SELECT 1 " + "FROM entites " + "WHERE id = ? " + "LIMIT 1;"; + +/** + * @brief Requête de détection d'un type. + */ +static const char *const entity_dao_type_exists_sql = + "SELECT 1 " + "FROM types_entite " + "WHERE code = ? " + "LIMIT 1;"; + +/** + * @brief Requête de détection du couple type et valeur. + */ +static const char *const entity_dao_type_value_exists_sql = + "SELECT 1 " + "FROM entites " + "INNER JOIN types_entite " + "ON types_entite.id = entites.type_id " + "WHERE types_entite.code = ? " + "AND entites.valeur = ? " + "LIMIT 1;"; + +/** + * @brief Enregistre une erreur littérale. + */ +static void entity_dao_set_error_literal( + GError **error, + EntityDaoError error_code, + const char *message +) +{ + if (error == NULL || + *error != NULL) + { + return; + } + + g_set_error_literal( + error, + ENTITY_DAO_ERROR, + error_code, + message + ); +} + +/** + * @brief Détermine si SQLite signale un schéma incompatible. + */ +static gboolean entity_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 Transforme la dernière erreur Database en GError du DAO. + */ +static void entity_dao_set_database_error( + EntityDao *entity_dao, + GError **error, + EntityDaoError error_code, + const char *context +) +{ + const char *database_message = + NULL; + + if (error == NULL || + *error != NULL) + { + return; + } + + if (entity_dao != NULL && + entity_dao->database != NULL) + { + database_message = + database_error_get_message( + entity_dao->database + ); + } + + if (error_code == ENTITY_DAO_ERROR_PREPARE && + entity_dao_is_schema_error( + database_message + )) + { + error_code = + ENTITY_DAO_ERROR_SCHEMA; + } + + if (database_message != NULL && + database_message[0] != '\0') + { + g_set_error( + error, + ENTITY_DAO_ERROR, + error_code, + "%s : %s", + context, + database_message + ); + + return; + } + + g_set_error_literal( + error, + ENTITY_DAO_ERROR, + error_code, + context + ); +} + +/** + * @brief Convertit un statut métier en texte SQLite. + */ +static const char *entity_dao_status_to_text( + EntityStatus status +) +{ + switch (status) + { + case ENTITY_STATUS_ACTIVE: + return "active"; + + case ENTITY_STATUS_ARCHIVED: + return "archived"; + + case ENTITY_STATUS_DELETED: + return "deleted"; + + case ENTITY_STATUS_UNKNOWN: + default: + return NULL; + } +} + +/** + * @brief Convertit un statut SQLite en statut métier. + */ +static gboolean entity_dao_status_from_text( + const char *status_text, + EntityStatus *out_status +) +{ + if (status_text == NULL || + out_status == NULL) + { + return FALSE; + } + + if (g_strcmp0( + status_text, + "active" + ) == 0) + { + *out_status = + ENTITY_STATUS_ACTIVE; + + return TRUE; + } + + if (g_strcmp0( + status_text, + "archived" + ) == 0) + { + *out_status = + ENTITY_STATUS_ARCHIVED; + + return TRUE; + } + + if (g_strcmp0( + status_text, + "deleted" + ) == 0) + { + *out_status = + ENTITY_STATUS_DELETED; + + return TRUE; + } + + return FALSE; +} + +/** + * @brief Lie un texte facultatif ou SQL NULL. + */ +static gboolean entity_dao_bind_optional_text( + DatabaseStatement *statement, + int parameter_index, + const char *value +) +{ + if (value == NULL) + { + return database_statement_bind_null( + statement, + parameter_index + ); + } + + return database_statement_bind_text( + statement, + parameter_index, + value + ); +} + +/** + * @brief Exécute une recherche d'existence avec une valeur. + */ +static gboolean entity_dao_value_exists( + EntityDao *entity_dao, + const char *sql, + const char *value, + gboolean *out_exists, + GError **error +) +{ + DatabaseStatement *statement = + NULL; + + DatabaseStatementStepResult step_result = + DATABASE_STATEMENT_STEP_ERROR; + + gboolean success = + FALSE; + + if (entity_dao == NULL || + entity_dao->database == NULL || + sql == NULL || + value == NULL || + out_exists == NULL) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_INVALID_ARGUMENT, + "Les paramètres de la recherche d'existence sont invalides." + ); + + return FALSE; + } + + *out_exists = + FALSE; + + statement = + database_statement_prepare( + entity_dao->database, + sql + ); + + if (statement == NULL) + { + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_DAO_ERROR_PREPARE, + "Impossible de préparer la recherche d'existence" + ); + + goto cleanup; + } + + if (!database_statement_bind_text( + statement, + 1, + value + )) + { + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_DAO_ERROR_BIND, + "Impossible de lier la valeur recherchée" + ); + + goto cleanup; + } + + step_result = + database_statement_step( + statement + ); + + if (step_result == + DATABASE_STATEMENT_STEP_ERROR) + { + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_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 Recherche un couple type et valeur existant. + */ +static gboolean entity_dao_type_value_exists( + EntityDao *entity_dao, + const char *type_identifier, + const char *value, + gboolean *out_exists, + GError **error +) +{ + DatabaseStatement *statement = + NULL; + + DatabaseStatementStepResult step_result = + DATABASE_STATEMENT_STEP_ERROR; + + gboolean success = + FALSE; + + if (entity_dao == NULL || + entity_dao->database == NULL || + type_identifier == NULL || + value == NULL || + out_exists == NULL) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_INVALID_ARGUMENT, + "Les paramètres de détection du doublon sont invalides." + ); + + return FALSE; + } + + *out_exists = + FALSE; + + statement = + database_statement_prepare( + entity_dao->database, + entity_dao_type_value_exists_sql + ); + + if (statement == NULL) + { + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_DAO_ERROR_PREPARE, + "Impossible de préparer la recherche du doublon" + ); + + goto cleanup; + } + + if (!database_statement_bind_text( + statement, + 1, + type_identifier + ) || + !database_statement_bind_text( + statement, + 2, + value + )) + { + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_DAO_ERROR_BIND, + "Impossible de lier le type et la valeur" + ); + + goto cleanup; + } + + step_result = + database_statement_step( + statement + ); + + if (step_result == + DATABASE_STATEMENT_STEP_ERROR) + { + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_DAO_ERROR_EXECUTE, + "Impossible d'exécuter la recherche du doublon" + ); + + goto cleanup; + } + + *out_exists = + step_result == + DATABASE_STATEMENT_STEP_ROW; + + success = + TRUE; + +cleanup: + + database_statement_finalize( + statement + ); + + return success; +} + +/** + * @brief Convertit une erreur SQLite d'insertion. + */ +static void entity_dao_set_insert_error( + EntityDao *entity_dao, + GError **error +) +{ + const char *database_message = + NULL; + + if (entity_dao != NULL && + entity_dao->database != NULL) + { + database_message = + database_error_get_message( + entity_dao->database + ); + } + + if (database_message != NULL && + strstr( + database_message, + "entites.id" + ) != NULL) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_CONSTRAINT, + "Une entité possède déjà cet identifiant." + ); + + return; + } + + if (database_message != NULL && + strstr( + database_message, + "entites.type_id" + ) != NULL && + strstr( + database_message, + "entites.valeur" + ) != NULL) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_CONSTRAINT, + "Une entité de ce type utilise déjà cette valeur." + ); + + return; + } + + if (database_message != NULL && + ( + strstr( + database_message, + "constraint" + ) != NULL || + strstr( + database_message, + "CONSTRAINT" + ) != NULL + )) + { + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_DAO_ERROR_CONSTRAINT, + "L'entité viole une contrainte SQLite" + ); + + return; + } + + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_DAO_ERROR_EXECUTE, + "Impossible d'insérer l'entité" + ); +} + +/** + * @brief Construit un EntityRecord depuis la ligne courante. + */ +static EntityRecord *entity_dao_read_current_record( + EntityDao *entity_dao, + DatabaseStatement *statement, + GError **error +) +{ + EntityRecord *entity_record = + NULL; + + char *identifier = + NULL; + + char *type_identifier = + NULL; + + char *value = + NULL; + + char *label = + NULL; + + char *description = + NULL; + + char *created_at = + NULL; + + char *updated_at = + NULL; + + char *status_text = + NULL; + + int64_t confidence_value = + -1; + + EntityStatus status = + ENTITY_STATUS_UNKNOWN; + + GError *model_error = + NULL; + + if (entity_dao == NULL || + entity_dao->database == NULL || + statement == NULL) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_INVALID_ARGUMENT, + "Impossible de lire une entité avec des paramètres invalides." + ); + + return NULL; + } + + if (!database_statement_column_text( + statement, + 0, + &identifier + ) || + !database_statement_column_text( + statement, + 1, + &type_identifier + ) || + !database_statement_column_text( + statement, + 2, + &value + ) || + !database_statement_column_text( + statement, + 3, + &label + ) || + !database_statement_column_text( + statement, + 4, + &description + ) || + !database_statement_column_int64( + statement, + 5, + &confidence_value + ) || + !database_statement_column_text( + statement, + 6, + &created_at + ) || + !database_statement_column_text( + statement, + 7, + &updated_at + ) || + !database_statement_column_text( + statement, + 8, + &status_text + )) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_READ, + "Impossible de lire les colonnes de l'entité." + ); + + goto cleanup; + } + + if (confidence_value < 0 || + confidence_value > 100) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_RANGE, + "L'entité persistée possède un niveau de confiance invalide." + ); + + goto cleanup; + } + + if (!entity_dao_status_from_text( + status_text, + &status + )) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_MODEL, + "L'entité persistée possède un statut invalide." + ); + + goto cleanup; + } + + entity_record = + entity_record_new( + identifier, + type_identifier, + value, + label, + description, + (gint) confidence_value, + created_at, + updated_at, + status, + &model_error + ); + + if (entity_record == NULL) + { + if (model_error != NULL) + { + g_set_error( + error, + ENTITY_DAO_ERROR, + ENTITY_DAO_ERROR_MODEL, + "L'entité persistée est invalide : %s", + model_error->message + ); + } + else + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_MODEL, + "L'entité persistée ne peut pas être reconstruite." + ); + } + } + +cleanup: + + g_clear_error( + &model_error + ); + + g_free( + status_text + ); + + g_free( + updated_at + ); + + g_free( + created_at + ); + + g_free( + description + ); + + g_free( + label + ); + + g_free( + value + ); + + g_free( + type_identifier + ); + + g_free( + identifier + ); + + return entity_record; +} + +/** + * @brief Libère un modèle contenu dans un GPtrArray. + */ +static void entity_dao_record_destroy( + gpointer user_data +) +{ + entity_record_free( + user_data + ); +} + +GQuark entity_dao_error_quark(void) +{ + return g_quark_from_static_string( + "entity-dao-error-quark" + ); +} + +EntityDao *entity_dao_new( + Database *database, + GError **error +) +{ + EntityDao *entity_dao = + NULL; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (database == NULL) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_INVALID_ARGUMENT, + "La connexion Database est absente." + ); + + return NULL; + } + + entity_dao = + g_try_new0( + EntityDao, + 1 + ); + + if (entity_dao == NULL) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_MEMORY, + "Impossible d'allouer le DAO des entités." + ); + + return NULL; + } + + entity_dao->database = + database; + + return entity_dao; +} + +void entity_dao_free( + EntityDao *entity_dao +) +{ + if (entity_dao == NULL) + { + return; + } + + entity_dao->database = + NULL; + + g_free( + entity_dao + ); +} + +gboolean entity_dao_insert( + EntityDao *entity_dao, + const EntityRecord *entity_record, + GError **error +) +{ + DatabaseStatement *statement = + NULL; + + const char *identifier = + NULL; + + const char *type_identifier = + NULL; + + const char *value = + NULL; + + const char *label = + NULL; + + const char *description = + NULL; + + const char *created_at = + NULL; + + const char *updated_at = + NULL; + + const char *status_text = + NULL; + + gint confidence = + 0; + + gboolean value_exists = + FALSE; + + gboolean success = + FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (entity_dao == NULL || + entity_dao->database == NULL || + entity_record == NULL) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_INVALID_ARGUMENT, + "Le DAO ou l'entité est absent." + ); + + return FALSE; + } + + identifier = + entity_record_get_identifier( + entity_record + ); + + type_identifier = + entity_record_get_type_identifier( + entity_record + ); + + value = + entity_record_get_value( + entity_record + ); + + label = + entity_record_get_label( + entity_record + ); + + description = + entity_record_get_description( + entity_record + ); + + confidence = + entity_record_get_confidence( + entity_record + ); + + created_at = + entity_record_get_created_at( + entity_record + ); + + updated_at = + entity_record_get_updated_at( + entity_record + ); + + status_text = + entity_dao_status_to_text( + entity_record_get_status( + entity_record + ) + ); + + if (identifier == NULL || + type_identifier == NULL || + value == NULL || + created_at == NULL || + updated_at == NULL || + status_text == NULL) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_INVALID_ARGUMENT, + "L'entité contient une valeur obligatoire absente." + ); + + return FALSE; + } + + if (confidence < 0 || + confidence > 100) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_RANGE, + "Le niveau de confiance dépasse les limites autorisées." + ); + + return FALSE; + } + + if (!entity_dao_value_exists( + entity_dao, + entity_dao_identifier_exists_sql, + identifier, + &value_exists, + error + )) + { + return FALSE; + } + + if (value_exists) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_CONSTRAINT, + "Une entité possède déjà cet identifiant." + ); + + return FALSE; + } + + if (!entity_dao_value_exists( + entity_dao, + entity_dao_type_exists_sql, + type_identifier, + &value_exists, + error + )) + { + return FALSE; + } + + if (!value_exists) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_CONSTRAINT, + "Le type d'entité n'existe pas dans le catalogue SQLite." + ); + + return FALSE; + } + + if (!entity_dao_type_value_exists( + entity_dao, + type_identifier, + value, + &value_exists, + error + )) + { + return FALSE; + } + + if (value_exists) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_CONSTRAINT, + "Une entité de ce type utilise déjà cette valeur." + ); + + return FALSE; + } + + statement = + database_statement_prepare( + entity_dao->database, + entity_dao_insert_sql + ); + + if (statement == NULL) + { + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_DAO_ERROR_PREPARE, + "Impossible de préparer l'insertion de l'entité" + ); + + goto cleanup; + } + + if (!database_statement_bind_text( + statement, + 1, + identifier + ) || + !database_statement_bind_text( + statement, + 2, + type_identifier + ) || + !database_statement_bind_text( + statement, + 3, + value + ) || + !entity_dao_bind_optional_text( + statement, + 4, + label + ) || + !entity_dao_bind_optional_text( + statement, + 5, + description + ) || + !database_statement_bind_int64( + statement, + 6, + (int64_t) confidence + ) || + !database_statement_bind_text( + statement, + 7, + created_at + ) || + !database_statement_bind_text( + statement, + 8, + updated_at + ) || + !database_statement_bind_text( + statement, + 9, + status_text + )) + { + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_DAO_ERROR_BIND, + "Impossible de lier les données de l'entité" + ); + + goto cleanup; + } + + if (database_statement_step( + statement + ) != DATABASE_STATEMENT_STEP_DONE) + { + entity_dao_set_insert_error( + entity_dao, + error + ); + + goto cleanup; + } + + success = + TRUE; + +cleanup: + + database_statement_finalize( + statement + ); + + return success; +} + +EntityRecord *entity_dao_find_by_identifier( + EntityDao *entity_dao, + const char *identifier, + GError **error +) +{ + DatabaseStatement *statement = + NULL; + + EntityRecord *entity_record = + NULL; + + DatabaseStatementStepResult step_result = + DATABASE_STATEMENT_STEP_ERROR; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (entity_dao == NULL || + entity_dao->database == NULL || + identifier == NULL || + !g_uuid_string_is_valid( + identifier + )) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_INVALID_ARGUMENT, + "L'identifiant d'entité est invalide." + ); + + return NULL; + } + + statement = + database_statement_prepare( + entity_dao->database, + entity_dao_find_by_identifier_sql + ); + + if (statement == NULL) + { + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_DAO_ERROR_PREPARE, + "Impossible de préparer la recherche de l'entité" + ); + + goto cleanup; + } + + if (!database_statement_bind_text( + statement, + 1, + identifier + )) + { + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_DAO_ERROR_BIND, + "Impossible de lier l'identifiant de l'entité" + ); + + goto cleanup; + } + + step_result = + database_statement_step( + statement + ); + + if (step_result == + DATABASE_STATEMENT_STEP_ERROR) + { + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_DAO_ERROR_EXECUTE, + "Impossible d'exécuter la recherche de l'entité" + ); + + goto cleanup; + } + + if (step_result == + DATABASE_STATEMENT_STEP_DONE) + { + goto cleanup; + } + + entity_record = + entity_dao_read_current_record( + entity_dao, + statement, + error + ); + + if (entity_record == NULL) + { + goto cleanup; + } + + step_result = + database_statement_step( + statement + ); + + if (step_result != + DATABASE_STATEMENT_STEP_DONE) + { + if (step_result == + DATABASE_STATEMENT_STEP_ROW) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_SCHEMA, + "Plusieurs entités possèdent le même identifiant." + ); + } + else + { + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_DAO_ERROR_EXECUTE, + "Impossible de terminer la lecture de l'entité" + ); + } + + entity_record_free( + entity_record + ); + + entity_record = + NULL; + } + +cleanup: + + database_statement_finalize( + statement + ); + + return entity_record; +} + +GPtrArray *entity_dao_list_all( + EntityDao *entity_dao, + GError **error +) +{ + DatabaseStatement *statement = + NULL; + + GPtrArray *entity_records = + NULL; + + EntityRecord *entity_record = + NULL; + + DatabaseStatementStepResult step_result = + DATABASE_STATEMENT_STEP_ERROR; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (entity_dao == NULL || + entity_dao->database == NULL) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_INVALID_ARGUMENT, + "Le DAO des entités est invalide." + ); + + return NULL; + } + + entity_records = + g_ptr_array_new_with_free_func( + entity_dao_record_destroy + ); + + if (entity_records == NULL) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_MEMORY, + "Impossible d'allouer la liste des entités." + ); + + return NULL; + } + + statement = + database_statement_prepare( + entity_dao->database, + entity_dao_list_all_sql + ); + + if (statement == NULL) + { + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_DAO_ERROR_PREPARE, + "Impossible de préparer le chargement des entités" + ); + + goto error; + } + + while (TRUE) + { + step_result = + database_statement_step( + statement + ); + + if (step_result == + DATABASE_STATEMENT_STEP_DONE) + { + break; + } + + if (step_result == + DATABASE_STATEMENT_STEP_ERROR) + { + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_DAO_ERROR_EXECUTE, + "Impossible d'exécuter le chargement des entités" + ); + + goto error; + } + + entity_record = + entity_dao_read_current_record( + entity_dao, + statement, + error + ); + + if (entity_record == NULL) + { + goto error; + } + + g_ptr_array_add( + entity_records, + entity_record + ); + + entity_record = + NULL; + } + + database_statement_finalize( + statement + ); + + return entity_records; + +error: + + entity_record_free( + entity_record + ); + + database_statement_finalize( + statement + ); + + g_ptr_array_unref( + entity_records + ); + + return NULL; +} + +gboolean entity_dao_count( + EntityDao *entity_dao, + guint64 *out_count, + GError **error +) +{ + DatabaseStatement *statement = + NULL; + + DatabaseStatementStepResult step_result = + DATABASE_STATEMENT_STEP_ERROR; + + int64_t count_value = + -1; + + gboolean success = + FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (entity_dao == NULL || + entity_dao->database == NULL || + out_count == NULL) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_INVALID_ARGUMENT, + "Les paramètres du comptage des entités sont invalides." + ); + + return FALSE; + } + + *out_count = + 0; + + statement = + database_statement_prepare( + entity_dao->database, + entity_dao_count_sql + ); + + if (statement == NULL) + { + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_DAO_ERROR_PREPARE, + "Impossible de préparer le comptage des entités" + ); + + goto cleanup; + } + + step_result = + database_statement_step( + statement + ); + + if (step_result == + DATABASE_STATEMENT_STEP_ERROR) + { + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_DAO_ERROR_EXECUTE, + "Impossible d'exécuter le comptage des entités" + ); + + goto cleanup; + } + + if (step_result != + DATABASE_STATEMENT_STEP_ROW) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_READ, + "Le comptage des entités n'a retourné aucune ligne." + ); + + goto cleanup; + } + + if (!database_statement_column_int64( + statement, + 0, + &count_value + )) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_READ, + "Impossible de lire le nombre d'entités." + ); + + goto cleanup; + } + + if (count_value < 0) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_RANGE, + "Le nombre d'entités retourné par SQLite est invalide." + ); + + goto cleanup; + } + + step_result = + database_statement_step( + statement + ); + + if (step_result != + DATABASE_STATEMENT_STEP_DONE) + { + if (step_result == + DATABASE_STATEMENT_STEP_ROW) + { + entity_dao_set_error_literal( + error, + ENTITY_DAO_ERROR_SCHEMA, + "Le comptage des entités a retourné plusieurs lignes." + ); + } + else + { + entity_dao_set_database_error( + entity_dao, + error, + ENTITY_DAO_ERROR_EXECUTE, + "Impossible de terminer le comptage des entités" + ); + } + + goto cleanup; + } + + *out_count = + (guint64) count_value; + + success = + TRUE; + +cleanup: + + database_statement_finalize( + statement + ); + + return success; +} diff --git a/tests/test_entity_dao.c b/tests/test_entity_dao.c new file mode 100644 index 0000000..b136522 --- /dev/null +++ b/tests/test_entity_dao.c @@ -0,0 +1,1528 @@ +/****************************************************************************** + * @file test_entity_dao.c + * @brief Tests du DAO des entités OSINT. + ******************************************************************************/ + +#include "dao/entity_dao.h" + +#include "database/database.h" +#include "database/statement.h" +#include "models/entity_record.h" + +#include +#include +#include +#include +#include + +#include +#include + +/** + * @brief Environnement temporaire d'un test EntityDao. + */ +typedef struct +{ + char *temporary_directory; + char *database_path; + + Database *database; + EntityDao *entity_dao; +} TestEntityDaoFixture; + +/** + * @brief Crée une base temporaire contenant le schéma de l'application. + */ +static TestEntityDaoFixture test_entity_dao_fixture_create(void) +{ + TestEntityDaoFixture fixture = + {0}; + + GError *error = + NULL; + + fixture.temporary_directory = + g_dir_make_tmp( + "labfy-entity-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_EntityDao", + 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); + + return fixture; +} + +/** + * @brief Détruit une fixture et ses fichiers temporaires. + */ +static void test_entity_dao_fixture_clear( + TestEntityDaoFixture *fixture +) +{ + assert(fixture != NULL); + + 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->entity_dao = + NULL; + + fixture->database = + NULL; + + fixture->database_path = + NULL; + + fixture->temporary_directory = + NULL; +} + +/** + * @brief Crée une entité minimale valide pour les tests. + */ +static EntityRecord *test_entity_dao_create_record( + const char *identifier, + const char *type_identifier, + const char *value, + const char *created_at, + EntityStatus status +) +{ + EntityRecord *entity_record = + NULL; + + GError *error = + NULL; + + entity_record = + entity_record_new( + identifier, + type_identifier, + value, + NULL, + NULL, + 50, + created_at, + created_at, + status, + &error + ); + + assert(entity_record != NULL); + assert(error == NULL); + + return entity_record; +} + +/** + * @brief Retourne le nombre d'entités présentes dans la base de test. + */ +static int64_t test_entity_dao_count_rows( + Database *database +) +{ + DatabaseStatement *statement = + NULL; + + int64_t entity_count = + -1; + + assert(database != NULL); + + statement = + database_statement_prepare( + database, + "SELECT COUNT(*) FROM entites;" + ); + + assert(statement != NULL); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_ROW + ); + + assert( + database_statement_column_int64( + statement, + 0, + &entity_count + ) + ); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize( + statement + ); + + return entity_count; +} + +/** + * @brief Vérifie le refus d'une connexion absente. + */ +static void test_entity_dao_new_null_database(void) +{ + EntityDao *entity_dao = + NULL; + + GError *error = + NULL; + + entity_dao = + entity_dao_new( + NULL, + &error + ); + + assert(entity_dao == NULL); + assert(error != NULL); + assert(error->domain == ENTITY_DAO_ERROR); + + assert( + error->code == + (gint) ENTITY_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); +} + +/** + * @brief Vérifie la création et la destruction d'un DAO valide. + */ +static void test_entity_dao_new_valid(void) +{ + Database *database = + NULL; + + EntityDao *entity_dao = + NULL; + + GError *error = + NULL; + + database = + database_open( + ":memory:" + ); + + assert(database != NULL); + + entity_dao = + entity_dao_new( + database, + &error + ); + + assert(entity_dao != NULL); + assert(error == NULL); + + /* + * La destruction du DAO ne doit pas fermer la connexion empruntée. + */ + entity_dao_free( + entity_dao + ); + + database_close( + database + ); +} + +/** + * @brief Vérifie que la destruction accepte NULL. + */ +static void test_entity_dao_free_null(void) +{ + entity_dao_free( + NULL + ); +} + +/** + * @brief Vérifie l'insertion complète d'une entité valide. + */ +static void test_entity_dao_insert_valid_full(void) +{ + TestEntityDaoFixture fixture = + test_entity_dao_fixture_create(); + + EntityRecord *entity_record = + NULL; + + DatabaseStatement *statement = + NULL; + + char *identifier = + NULL; + + char *type_identifier = + NULL; + + char *value = + NULL; + + char *label = + NULL; + + char *description = + NULL; + + char *created_at = + NULL; + + char *updated_at = + NULL; + + char *status = + NULL; + + int64_t confidence = + -1; + + GError *error = + NULL; + + entity_record = + entity_record_new( + "11111111-1111-4111-8111-111111111111", + "email_address", + "suspect@example.org", + "Adresse utilisée pour la transaction", + "Adresse communiquée dans la conversation.", + 85, + "2026-07-20T18:00:00Z", + "2026-07-20T18:30:00Z", + ENTITY_STATUS_ACTIVE, + &error + ); + + assert(entity_record != NULL); + assert(error == NULL); + + assert( + entity_dao_insert( + fixture.entity_dao, + entity_record, + &error + ) + ); + + assert(error == NULL); + + statement = + database_statement_prepare( + fixture.database, + "SELECT " + " entites.id," + " types_entite.code," + " entites.valeur," + " entites.label," + " entites.description," + " entites.confiance," + " entites.created_at," + " entites.updated_at," + " entites.status " + "FROM entites " + "INNER JOIN types_entite " + "ON types_entite.id = entites.type_id " + "WHERE entites.id = ?;" + ); + + assert(statement != NULL); + + assert( + database_statement_bind_text( + statement, + 1, + "11111111-1111-4111-8111-111111111111" + ) + ); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_ROW + ); + + assert(database_statement_column_text(statement, 0, &identifier)); + assert(database_statement_column_text(statement, 1, &type_identifier)); + assert(database_statement_column_text(statement, 2, &value)); + assert(database_statement_column_text(statement, 3, &label)); + assert(database_statement_column_text(statement, 4, &description)); + assert(database_statement_column_int64(statement, 5, &confidence)); + assert(database_statement_column_text(statement, 6, &created_at)); + assert(database_statement_column_text(statement, 7, &updated_at)); + assert(database_statement_column_text(statement, 8, &status)); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_DONE + ); + + assert( + strcmp( + identifier, + "11111111-1111-4111-8111-111111111111" + ) == 0 + ); + + assert( + strcmp( + type_identifier, + "email_address" + ) == 0 + ); + + assert( + strcmp( + value, + "suspect@example.org" + ) == 0 + ); + + assert( + strcmp( + label, + "Adresse utilisée pour la transaction" + ) == 0 + ); + + assert( + strcmp( + description, + "Adresse communiquée dans la conversation." + ) == 0 + ); + + assert(confidence == 85); + + assert( + strcmp( + created_at, + "2026-07-20T18:00:00Z" + ) == 0 + ); + + assert( + strcmp( + updated_at, + "2026-07-20T18:30:00Z" + ) == 0 + ); + + assert( + strcmp( + status, + "active" + ) == 0 + ); + + database_statement_finalize( + statement + ); + + g_free(status); + g_free(updated_at); + g_free(created_at); + g_free(description); + g_free(label); + g_free(value); + g_free(type_identifier); + g_free(identifier); + + entity_record_free( + entity_record + ); + + test_entity_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie l'insertion avec des champs facultatifs absents. + */ +static void test_entity_dao_insert_optional_null(void) +{ + TestEntityDaoFixture fixture = + test_entity_dao_fixture_create(); + + EntityRecord *entity_record = + NULL; + + DatabaseStatement *statement = + NULL; + + bool label_is_null = + false; + + bool description_is_null = + false; + + GError *error = + NULL; + + entity_record = + test_entity_dao_create_record( + "22222222-2222-4222-8222-222222222222", + "iban", + "FR7612345678901234567890123", + "2026-07-20T19:00:00Z", + ENTITY_STATUS_ACTIVE + ); + + assert( + entity_dao_insert( + fixture.entity_dao, + entity_record, + &error + ) + ); + + assert(error == NULL); + + statement = + database_statement_prepare( + fixture.database, + "SELECT label, description " + "FROM entites " + "WHERE id = ?;" + ); + + assert(statement != NULL); + + assert( + database_statement_bind_text( + statement, + 1, + "22222222-2222-4222-8222-222222222222" + ) + ); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_ROW + ); + + assert( + database_statement_column_is_null( + statement, + 0, + &label_is_null + ) + ); + + assert( + database_statement_column_is_null( + statement, + 1, + &description_is_null + ) + ); + + assert(label_is_null); + assert(description_is_null); + + assert( + database_statement_step( + statement + ) == DATABASE_STATEMENT_STEP_DONE + ); + + database_statement_finalize( + statement + ); + + entity_record_free( + entity_record + ); + + test_entity_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus des arguments d'insertion invalides. + */ +static void test_entity_dao_insert_invalid_arguments(void) +{ + TestEntityDaoFixture fixture = + test_entity_dao_fixture_create(); + + EntityRecord *entity_record = + NULL; + + GError *error = + NULL; + + entity_record = + test_entity_dao_create_record( + "33333333-3333-4333-8333-333333333333", + "person", + "Jean Dupont", + "2026-07-20T19:10:00Z", + ENTITY_STATUS_ACTIVE + ); + + assert( + !entity_dao_insert( + NULL, + entity_record, + &error + ) + ); + + assert(error != NULL); + assert(error->domain == ENTITY_DAO_ERROR); + + assert( + error->code == + (gint) ENTITY_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + assert( + !entity_dao_insert( + fixture.entity_dao, + NULL, + &error + ) + ); + + assert(error != NULL); + + assert( + error->code == + (gint) ENTITY_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + entity_record_free( + entity_record + ); + + test_entity_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d'un UUID déjà utilisé. + */ +static void test_entity_dao_insert_duplicate_identifier(void) +{ + TestEntityDaoFixture fixture = + test_entity_dao_fixture_create(); + + EntityRecord *first_record = + NULL; + + EntityRecord *second_record = + NULL; + + GError *error = + NULL; + + first_record = + test_entity_dao_create_record( + "44444444-4444-4444-8444-444444444444", + "email_address", + "first@example.org", + "2026-07-20T19:20:00Z", + ENTITY_STATUS_ACTIVE + ); + + second_record = + test_entity_dao_create_record( + "44444444-4444-4444-8444-444444444444", + "email_address", + "second@example.org", + "2026-07-20T19:25:00Z", + ENTITY_STATUS_ACTIVE + ); + + assert( + entity_dao_insert( + fixture.entity_dao, + first_record, + &error + ) + ); + + assert(error == NULL); + + assert( + !entity_dao_insert( + fixture.entity_dao, + second_record, + &error + ) + ); + + assert(error != NULL); + assert(error->domain == ENTITY_DAO_ERROR); + + assert( + error->code == + (gint) ENTITY_DAO_ERROR_CONSTRAINT + ); + + assert( + strstr( + error->message, + "identifiant" + ) != NULL + ); + + assert( + test_entity_dao_count_rows( + fixture.database + ) == 1 + ); + + g_clear_error( + &error + ); + + entity_record_free(second_record); + entity_record_free(first_record); + + test_entity_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d'un couple type et valeur déjà utilisé. + */ +static void test_entity_dao_insert_duplicate_type_value(void) +{ + TestEntityDaoFixture fixture = + test_entity_dao_fixture_create(); + + EntityRecord *first_record = + NULL; + + EntityRecord *second_record = + NULL; + + GError *error = + NULL; + + first_record = + test_entity_dao_create_record( + "55555555-5555-4555-8555-555555555555", + "phone_number", + "+33601020304", + "2026-07-20T19:30:00Z", + ENTITY_STATUS_ACTIVE + ); + + second_record = + test_entity_dao_create_record( + "66666666-6666-4666-8666-666666666666", + "phone_number", + "+33601020304", + "2026-07-20T19:35:00Z", + ENTITY_STATUS_ACTIVE + ); + + assert( + entity_dao_insert( + fixture.entity_dao, + first_record, + &error + ) + ); + + assert(error == NULL); + + assert( + !entity_dao_insert( + fixture.entity_dao, + second_record, + &error + ) + ); + + assert(error != NULL); + assert(error->domain == ENTITY_DAO_ERROR); + + assert( + error->code == + (gint) ENTITY_DAO_ERROR_CONSTRAINT + ); + + assert( + strstr( + error->message, + "type" + ) != NULL + ); + + assert( + test_entity_dao_count_rows( + fixture.database + ) == 1 + ); + + g_clear_error( + &error + ); + + entity_record_free(second_record); + entity_record_free(first_record); + + test_entity_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d'un type absent du catalogue SQLite. + */ +static void test_entity_dao_insert_unknown_type(void) +{ + TestEntityDaoFixture fixture = + test_entity_dao_fixture_create(); + + EntityRecord *entity_record = + NULL; + + GError *error = + NULL; + + entity_record = + test_entity_dao_create_record( + "77777777-7777-4777-8777-777777777777", + "unknown_type", + "Valeur inconnue", + "2026-07-20T19:40:00Z", + ENTITY_STATUS_ACTIVE + ); + + assert( + !entity_dao_insert( + fixture.entity_dao, + entity_record, + &error + ) + ); + + assert(error != NULL); + assert(error->domain == ENTITY_DAO_ERROR); + + assert( + error->code == + (gint) ENTITY_DAO_ERROR_CONSTRAINT + ); + + assert( + strstr( + error->message, + "type d'entité" + ) != NULL + ); + + assert( + test_entity_dao_count_rows( + fixture.database + ) == 0 + ); + + g_clear_error( + &error + ); + + entity_record_free( + entity_record + ); + + test_entity_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie la recherche d'une entité existante. + */ +static void test_entity_dao_find_existing(void) +{ + TestEntityDaoFixture fixture = + test_entity_dao_fixture_create(); + + EntityRecord *inserted_record = + NULL; + + EntityRecord *loaded_record = + NULL; + + GError *error = + NULL; + + inserted_record = + test_entity_dao_create_record( + "88888888-8888-4888-8888-888888888888", + "domain_name", + "example.org", + "2026-07-20T20:00:00Z", + ENTITY_STATUS_ARCHIVED + ); + + assert( + entity_dao_insert( + fixture.entity_dao, + inserted_record, + &error + ) + ); + + assert(error == NULL); + + loaded_record = + entity_dao_find_by_identifier( + fixture.entity_dao, + "88888888-8888-4888-8888-888888888888", + &error + ); + + assert(loaded_record != NULL); + assert(error == NULL); + + assert( + strcmp( + entity_record_get_identifier( + loaded_record + ), + "88888888-8888-4888-8888-888888888888" + ) == 0 + ); + + assert( + strcmp( + entity_record_get_type_identifier( + loaded_record + ), + "domain_name" + ) == 0 + ); + + assert( + strcmp( + entity_record_get_value( + loaded_record + ), + "example.org" + ) == 0 + ); + + assert( + entity_record_get_confidence( + loaded_record + ) == 50 + ); + + assert( + entity_record_get_status( + loaded_record + ) == ENTITY_STATUS_ARCHIVED + ); + + entity_record_free(loaded_record); + entity_record_free(inserted_record); + + test_entity_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie la conversion du statut deleted. + */ +static void test_entity_dao_find_deleted_status(void) +{ + TestEntityDaoFixture fixture = + test_entity_dao_fixture_create(); + + EntityRecord *inserted_record = + NULL; + + EntityRecord *loaded_record = + NULL; + + GError *error = + NULL; + + inserted_record = + test_entity_dao_create_record( + "99999999-9999-4999-8999-999999999999", + "pseudonym", + "vendeur_76", + "2026-07-20T20:10:00Z", + ENTITY_STATUS_DELETED + ); + + assert( + entity_dao_insert( + fixture.entity_dao, + inserted_record, + &error + ) + ); + + assert(error == NULL); + + loaded_record = + entity_dao_find_by_identifier( + fixture.entity_dao, + "99999999-9999-4999-8999-999999999999", + &error + ); + + assert(loaded_record != NULL); + assert(error == NULL); + + assert( + entity_record_get_status( + loaded_record + ) == ENTITY_STATUS_DELETED + ); + + entity_record_free(loaded_record); + entity_record_free(inserted_record); + + test_entity_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu'une entité absente retourne NULL sans erreur. + */ +static void test_entity_dao_find_missing(void) +{ + TestEntityDaoFixture fixture = + test_entity_dao_fixture_create(); + + EntityRecord *entity_record = + NULL; + + GError *error = + NULL; + + entity_record = + entity_dao_find_by_identifier( + fixture.entity_dao, + "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + &error + ); + + assert(entity_record == NULL); + assert(error == NULL); + + test_entity_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d'un UUID de recherche invalide. + */ +static void test_entity_dao_find_invalid_identifier(void) +{ + TestEntityDaoFixture fixture = + test_entity_dao_fixture_create(); + + EntityRecord *entity_record = + NULL; + + GError *error = + NULL; + + entity_record = + entity_dao_find_by_identifier( + fixture.entity_dao, + "identifiant-invalide", + &error + ); + + assert(entity_record == NULL); + assert(error != NULL); + assert(error->domain == ENTITY_DAO_ERROR); + + assert( + error->code == + (gint) ENTITY_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + test_entity_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le comptage d'une table vide. + */ +static void test_entity_dao_count_empty(void) +{ + TestEntityDaoFixture fixture = + test_entity_dao_fixture_create(); + + guint64 entity_count = + 42U; + + GError *error = + NULL; + + assert( + entity_dao_count( + fixture.entity_dao, + &entity_count, + &error + ) + ); + + assert(error == NULL); + assert(entity_count == 0U); + + test_entity_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le comptage de plusieurs entités. + */ +static void test_entity_dao_count_records(void) +{ + TestEntityDaoFixture fixture = + test_entity_dao_fixture_create(); + + EntityRecord *first_record = + NULL; + + EntityRecord *second_record = + NULL; + + guint64 entity_count = + 0U; + + GError *error = + NULL; + + first_record = + test_entity_dao_create_record( + "aaaaaaaa-1111-4111-8111-111111111111", + "ip_address", + "192.0.2.10", + "2026-07-20T20:20:00Z", + ENTITY_STATUS_ACTIVE + ); + + second_record = + test_entity_dao_create_record( + "bbbbbbbb-2222-4222-8222-222222222222", + "website", + "https://example.org", + "2026-07-20T20:25:00Z", + ENTITY_STATUS_ACTIVE + ); + + assert(entity_dao_insert(fixture.entity_dao, first_record, &error)); + assert(error == NULL); + + assert(entity_dao_insert(fixture.entity_dao, second_record, &error)); + assert(error == NULL); + + assert( + entity_dao_count( + fixture.entity_dao, + &entity_count, + &error + ) + ); + + assert(error == NULL); + assert(entity_count == 2U); + + entity_record_free(second_record); + entity_record_free(first_record); + + test_entity_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus des paramètres de comptage invalides. + */ +static void test_entity_dao_count_invalid_arguments(void) +{ + TestEntityDaoFixture fixture = + test_entity_dao_fixture_create(); + + guint64 entity_count = + 123U; + + GError *error = + NULL; + + assert( + !entity_dao_count( + NULL, + &entity_count, + &error + ) + ); + + assert(error != NULL); + assert(error->domain == ENTITY_DAO_ERROR); + + assert( + error->code == + (gint) ENTITY_DAO_ERROR_INVALID_ARGUMENT + ); + + assert(entity_count == 123U); + + g_clear_error( + &error + ); + + assert( + !entity_dao_count( + fixture.entity_dao, + NULL, + &error + ) + ); + + assert(error != NULL); + + assert( + error->code == + (gint) ENTITY_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + test_entity_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le chargement d'une liste vide. + */ +static void test_entity_dao_list_empty(void) +{ + TestEntityDaoFixture fixture = + test_entity_dao_fixture_create(); + + GPtrArray *entity_records = + NULL; + + GError *error = + NULL; + + entity_records = + entity_dao_list_all( + fixture.entity_dao, + &error + ); + + assert(entity_records != NULL); + assert(error == NULL); + assert(entity_records->len == 0U); + + g_ptr_array_unref( + entity_records + ); + + test_entity_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le chargement de plusieurs entités dans un ordre stable. + */ +static void test_entity_dao_list_order(void) +{ + TestEntityDaoFixture fixture = + test_entity_dao_fixture_create(); + + EntityRecord *first_inserted_record = + NULL; + + EntityRecord *second_inserted_record = + NULL; + + EntityRecord *third_inserted_record = + NULL; + + const EntityRecord *first_loaded_record = + NULL; + + const EntityRecord *second_loaded_record = + NULL; + + const EntityRecord *third_loaded_record = + NULL; + + GPtrArray *entity_records = + NULL; + + GError *error = + NULL; + + third_inserted_record = + test_entity_dao_create_record( + "30000000-0000-4000-8000-000000000003", + "organization", + "Organisation C", + "2026-07-20T22:00:00Z", + ENTITY_STATUS_ACTIVE + ); + + second_inserted_record = + test_entity_dao_create_record( + "20000000-0000-4000-8000-000000000002", + "person", + "Personne B", + "2026-07-20T21:00:00Z", + ENTITY_STATUS_ACTIVE + ); + + first_inserted_record = + test_entity_dao_create_record( + "10000000-0000-4000-8000-000000000001", + "person", + "Personne A", + "2026-07-20T21:00:00Z", + ENTITY_STATUS_ACTIVE + ); + + assert(entity_dao_insert(fixture.entity_dao, third_inserted_record, &error)); + assert(error == NULL); + + assert(entity_dao_insert(fixture.entity_dao, second_inserted_record, &error)); + assert(error == NULL); + + assert(entity_dao_insert(fixture.entity_dao, first_inserted_record, &error)); + assert(error == NULL); + + entity_records = + entity_dao_list_all( + fixture.entity_dao, + &error + ); + + assert(entity_records != NULL); + assert(error == NULL); + assert(entity_records->len == 3U); + + first_loaded_record = + g_ptr_array_index( + entity_records, + 0U + ); + + second_loaded_record = + g_ptr_array_index( + entity_records, + 1U + ); + + third_loaded_record = + g_ptr_array_index( + entity_records, + 2U + ); + + assert(first_loaded_record != NULL); + assert(second_loaded_record != NULL); + assert(third_loaded_record != NULL); + + assert( + strcmp( + entity_record_get_identifier( + first_loaded_record + ), + "10000000-0000-4000-8000-000000000001" + ) == 0 + ); + + assert( + strcmp( + entity_record_get_identifier( + second_loaded_record + ), + "20000000-0000-4000-8000-000000000002" + ) == 0 + ); + + assert( + strcmp( + entity_record_get_identifier( + third_loaded_record + ), + "30000000-0000-4000-8000-000000000003" + ) == 0 + ); + + g_ptr_array_unref( + entity_records + ); + + entity_record_free(first_inserted_record); + entity_record_free(second_inserted_record); + entity_record_free(third_inserted_record); + + test_entity_dao_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d'un DAO absent lors du chargement. + */ +static void test_entity_dao_list_invalid_arguments(void) +{ + GPtrArray *entity_records = + NULL; + + GError *error = + NULL; + + entity_records = + entity_dao_list_all( + NULL, + &error + ); + + assert(entity_records == NULL); + assert(error != NULL); + assert(error->domain == ENTITY_DAO_ERROR); + + assert( + error->code == + (gint) ENTITY_DAO_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); +} + +int main(void) +{ + test_entity_dao_new_null_database(); + test_entity_dao_new_valid(); + test_entity_dao_free_null(); + test_entity_dao_insert_valid_full(); + test_entity_dao_insert_optional_null(); + test_entity_dao_insert_invalid_arguments(); + test_entity_dao_insert_duplicate_identifier(); + test_entity_dao_insert_duplicate_type_value(); + test_entity_dao_insert_unknown_type(); + test_entity_dao_find_existing(); + test_entity_dao_find_deleted_status(); + test_entity_dao_find_missing(); + test_entity_dao_find_invalid_identifier(); + test_entity_dao_count_empty(); + test_entity_dao_count_records(); + test_entity_dao_count_invalid_arguments(); + test_entity_dao_list_empty(); + test_entity_dao_list_order(); + test_entity_dao_list_invalid_arguments(); + + printf( + "EntityDao : tous les tests sont valides.\n" + ); + + return 0; +}