feat: add entity type DAO

This commit is contained in:
grayTerminal-sh 2026-07-20 20:40:40 +02:00
parent 59d05b6683
commit 95839efba6
4 changed files with 1233 additions and 1 deletions

View file

@ -42,6 +42,7 @@ EVIDENCE_INTEGRITY_VERIFIER_TEST_CFLAGS := \
$(TEST_CFLAGS) \
-Wpedantic
ENTITY_RECORD_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic
ENTITY_TYPE_DAO_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic
SRC := $(shell find src -name "*.c")
@ -84,6 +85,7 @@ TEST_EVIDENCE_CATEGORY_ITEM := tests/test_evidence_category_item
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
all: $(TARGET)
@ -375,6 +377,17 @@ $(TEST_ENTITY_RECORD): \
src/models/entity_record.c
$(CC) $(ENTITY_RECORD_TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
$(TEST_ENTITY_TYPE_DAO): \
tests/test_entity_type_dao.c \
src/dao/entity_type_dao.c \
src/models/entity_type.c \
src/database/database.c \
src/database/schema.c \
src/database/statement.c \
src/database/transaction.c \
src/database/error.c
$(CC) $(ENTITY_TYPE_DAO_TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
test: \
$(TEST_NODE) \
$(TEST_TREE_MODEL) \
@ -410,7 +423,8 @@ test: \
$(TEST_EVIDENCE_IMPORT_TASK) \
$(TEST_EVIDENCE_IMPORT_DIALOG) \
$(TEST_EVIDENCE_INTEGRITY_TASK) \
$(TEST_ENTITY_RECORD)
$(TEST_ENTITY_RECORD) \
$(TEST_ENTITY_TYPE_DAO)
@echo "Exécution des tests..."
@./$(TEST_NODE)
@./$(TEST_TREE_MODEL)
@ -447,6 +461,7 @@ test: \
@$(TEST_EVIDENCE_IMPORT_DIALOG)
@$(TEST_EVIDENCE_INTEGRITY_TASK)
@$(TEST_ENTITY_RECORD)
@$(TEST_ENTITY_TYPE_DAO)
@echo "Tous les tests sont valides."
%.o: %.c

View file

@ -0,0 +1,121 @@
/******************************************************************************
* @file entity_type_dao.h
* @brief Lecture des types d'entité depuis SQLite.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_ENTITY_TYPE_DAO_H
#define LABFY_INVESTIGATION_ENTITY_TYPE_DAO_H
#include "database/database.h"
#include <glib.h>
G_BEGIN_DECLS
/**
* @brief DAO opaque donnant accès aux types d'entité.
*
* Le DAO emprunte la connexion Database reçue lors de sa création.
*/
typedef struct EntityTypeDao EntityTypeDao;
/**
* @brief Catégories d'erreurs produites par EntityTypeDao.
*/
typedef enum
{
/**
* Un argument public est invalide.
*/
ENTITY_TYPE_DAO_ERROR_INVALID_ARGUMENT,
/**
* L'allocation d'une ressource a échoué.
*/
ENTITY_TYPE_DAO_ERROR_MEMORY,
/**
* La requête SQLite n'a pas pu être préparée.
*/
ENTITY_TYPE_DAO_ERROR_PREPARE,
/**
* Une ligne SQLite n'a pas pu être lue.
*/
ENTITY_TYPE_DAO_ERROR_READ,
/**
* Une ligne SQLite ne peut pas produire un EntityType valide.
*/
ENTITY_TYPE_DAO_ERROR_MODEL,
/**
* La table ou le schéma attendu est absent ou invalide.
*/
ENTITY_TYPE_DAO_ERROR_SCHEMA
} EntityTypeDaoError;
/**
* @brief Domaine d'erreur du DAO des types d'entité.
*/
#define ENTITY_TYPE_DAO_ERROR \
entity_type_dao_error_quark()
/**
* @brief Retourne le domaine d'erreur du DAO.
*
* @return Quark GLib associé à EntityTypeDao.
*/
GQuark entity_type_dao_error_quark(void);
/**
* @brief Crée un DAO utilisant une connexion Database existante.
*
* La connexion est empruntée et doit rester valide pendant toute
* la durée de vie du DAO.
*
* @param database Connexion SQLite ouverte.
* @param error Emplacement facultatif recevant une erreur.
*
* @return Nouveau DAO, ou NULL en cas d'échec.
*/
EntityTypeDao *entity_type_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_type_dao DAO à libérer.
*/
void entity_type_dao_free(
EntityTypeDao *entity_type_dao
);
/**
* @brief Charge tous les types d'entité.
*
* Les types sont triés par identifiant numérique croissant.
*
* Le tableau retourné appartient à l'appelant et utilise
* entity_type_free() comme fonction de destruction.
*
* Une table vide produit un tableau vide, sans erreur.
*
* @param entity_type_dao DAO valide.
* @param error Emplacement facultatif recevant une erreur.
*
* @return Nouveau GPtrArray, ou NULL en cas d'échec.
*/
GPtrArray *entity_type_dao_list_all(
EntityTypeDao *entity_type_dao,
GError **error
);
G_END_DECLS
#endif

518
src/dao/entity_type_dao.c Normal file
View file

@ -0,0 +1,518 @@
/******************************************************************************
* @file entity_type_dao.c
* @brief Lecture des types d'entité depuis SQLite.
******************************************************************************/
#include "dao/entity_type_dao.h"
#include "database/error.h"
#include "database/statement.h"
#include "models/entity_type.h"
#include <glib.h>
#include <stdint.h>
/**
* @brief Requête listant les types d'entité.
*/
static const char *const entity_type_dao_list_all_sql =
"SELECT "
" id, "
" code, "
" label, "
" description "
"FROM types_entite "
"ORDER BY id ASC;";
/**
* @struct EntityTypeDao
* @brief Représentation interne du DAO.
*/
struct EntityTypeDao
{
Database *database;
};
/**
* @brief Libère un EntityType contenu dans un GPtrArray.
*/
static void entity_type_dao_destroy_type(
gpointer user_data
)
{
EntityType *entity_type =
user_data;
entity_type_free(
entity_type
);
}
/**
* @brief Détermine si un message SQLite indique un schéma incompatible.
*/
static gboolean entity_type_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 Enregistre une erreur de préparation SQL.
*/
static void entity_type_dao_set_prepare_error(
EntityTypeDao *entity_type_dao,
GError **error
)
{
const char *database_message =
NULL;
EntityTypeDaoError error_code =
ENTITY_TYPE_DAO_ERROR_PREPARE;
if (error == NULL ||
*error != NULL)
{
return;
}
if (entity_type_dao != NULL)
{
database_message =
database_error_get_message(
entity_type_dao->database
);
}
if (entity_type_dao_is_schema_error(
database_message
))
{
error_code =
ENTITY_TYPE_DAO_ERROR_SCHEMA;
}
g_set_error(
error,
ENTITY_TYPE_DAO_ERROR,
error_code,
"Impossible de préparer la lecture "
"des types d'entité : %s",
database_message != NULL
? database_message
: "erreur Database inconnue"
);
}
/**
* @brief Enregistre une erreur de lecture SQLite.
*/
static void entity_type_dao_set_read_error(
EntityTypeDao *entity_type_dao,
GError **error,
const char *context
)
{
const char *database_message =
NULL;
if (error == NULL ||
*error != NULL)
{
return;
}
if (entity_type_dao != NULL)
{
database_message =
database_error_get_message(
entity_type_dao->database
);
}
g_set_error(
error,
ENTITY_TYPE_DAO_ERROR,
ENTITY_TYPE_DAO_ERROR_READ,
"%s : %s",
context != NULL
? context
: "Impossible de lire un type d'entité",
database_message != NULL
? database_message
: "donnée SQLite invalide"
);
}
/**
* @brief Construit un EntityType depuis la ligne SQLite courante.
*/
static EntityType *entity_type_dao_read_current_row(
EntityTypeDao *entity_type_dao,
DatabaseStatement *statement,
GError **error
)
{
EntityType *entity_type =
NULL;
int64_t identifier =
0;
char *code =
NULL;
char *label =
NULL;
char *description =
NULL;
GError *model_error =
NULL;
if (entity_type_dao == NULL ||
statement == NULL)
{
g_set_error_literal(
error,
ENTITY_TYPE_DAO_ERROR,
ENTITY_TYPE_DAO_ERROR_INVALID_ARGUMENT,
"La ligne du type d'entité ne peut pas être lue."
);
return NULL;
}
if (!database_statement_column_int64(
statement,
0,
&identifier
))
{
entity_type_dao_set_read_error(
entity_type_dao,
error,
"Impossible de lire l'identifiant du type d'entité"
);
goto cleanup;
}
if (!database_statement_column_text(
statement,
1,
&code
))
{
entity_type_dao_set_read_error(
entity_type_dao,
error,
"Impossible de lire le code du type d'entité"
);
goto cleanup;
}
if (!database_statement_column_text(
statement,
2,
&label
))
{
entity_type_dao_set_read_error(
entity_type_dao,
error,
"Impossible de lire le libellé du type d'entité"
);
goto cleanup;
}
if (!database_statement_column_text(
statement,
3,
&description
))
{
entity_type_dao_set_read_error(
entity_type_dao,
error,
"Impossible de lire la description du type d'entité"
);
goto cleanup;
}
entity_type =
entity_type_new(
(gint64) identifier,
code,
label,
description,
&model_error
);
if (entity_type == NULL)
{
g_set_error(
error,
ENTITY_TYPE_DAO_ERROR,
ENTITY_TYPE_DAO_ERROR_MODEL,
"Le type d'entité lu depuis SQLite est invalide : %s",
model_error != NULL
? model_error->message
: "erreur de modèle inconnue"
);
}
cleanup:
g_clear_error(
&model_error
);
g_free(
description
);
g_free(
label
);
g_free(
code
);
return entity_type;
}
GQuark entity_type_dao_error_quark(void)
{
return g_quark_from_static_string(
"entity-type-dao-error-quark"
);
}
EntityTypeDao *entity_type_dao_new(
Database *database,
GError **error
)
{
EntityTypeDao *entity_type_dao =
NULL;
g_return_val_if_fail(
error == NULL || *error == NULL,
NULL
);
if (database == NULL)
{
g_set_error_literal(
error,
ENTITY_TYPE_DAO_ERROR,
ENTITY_TYPE_DAO_ERROR_INVALID_ARGUMENT,
"La connexion Database est obligatoire."
);
return NULL;
}
entity_type_dao =
g_try_new0(
EntityTypeDao,
1
);
if (entity_type_dao == NULL)
{
g_set_error_literal(
error,
ENTITY_TYPE_DAO_ERROR,
ENTITY_TYPE_DAO_ERROR_MEMORY,
"Impossible d'allouer le DAO des types d'entité."
);
return NULL;
}
/*
* La connexion reste possédée par la session ou le code appelant.
*/
entity_type_dao->database =
database;
return entity_type_dao;
}
void entity_type_dao_free(
EntityTypeDao *entity_type_dao
)
{
if (entity_type_dao == NULL)
{
return;
}
/*
* Database est empruntée et ne doit pas être fermée ici.
*/
entity_type_dao->database =
NULL;
g_free(
entity_type_dao
);
}
GPtrArray *entity_type_dao_list_all(
EntityTypeDao *entity_type_dao,
GError **error
)
{
DatabaseStatement *statement =
NULL;
GPtrArray *entity_types =
NULL;
DatabaseStatementStepResult step_result =
DATABASE_STATEMENT_STEP_ERROR;
gboolean success =
FALSE;
g_return_val_if_fail(
error == NULL || *error == NULL,
NULL
);
if (entity_type_dao == NULL ||
entity_type_dao->database == NULL)
{
g_set_error_literal(
error,
ENTITY_TYPE_DAO_ERROR,
ENTITY_TYPE_DAO_ERROR_INVALID_ARGUMENT,
"Le DAO des types d'entité est invalide."
);
return NULL;
}
entity_types =
g_ptr_array_new_with_free_func(
entity_type_dao_destroy_type
);
if (entity_types == NULL)
{
g_set_error_literal(
error,
ENTITY_TYPE_DAO_ERROR,
ENTITY_TYPE_DAO_ERROR_MEMORY,
"Impossible d'allouer la liste des types d'entité."
);
return NULL;
}
statement =
database_statement_prepare(
entity_type_dao->database,
entity_type_dao_list_all_sql
);
if (statement == NULL)
{
entity_type_dao_set_prepare_error(
entity_type_dao,
error
);
goto cleanup;
}
while (TRUE)
{
EntityType *entity_type =
NULL;
step_result =
database_statement_step(
statement
);
if (step_result ==
DATABASE_STATEMENT_STEP_DONE)
{
success =
TRUE;
break;
}
if (step_result ==
DATABASE_STATEMENT_STEP_ERROR)
{
entity_type_dao_set_read_error(
entity_type_dao,
error,
"Impossible de parcourir les types d'entité"
);
break;
}
entity_type =
entity_type_dao_read_current_row(
entity_type_dao,
statement,
error
);
if (entity_type == NULL)
{
break;
}
g_ptr_array_add(
entity_types,
entity_type
);
}
cleanup:
database_statement_finalize(
statement
);
if (!success)
{
g_ptr_array_unref(
entity_types
);
return NULL;
}
return entity_types;
}

View file

@ -0,0 +1,578 @@
/******************************************************************************
* @file test_entity_type_dao.c
* @brief Tests du DAO des types d'entité.
******************************************************************************/
#include "dao/entity_type_dao.h"
#include "database/database.h"
#include "database/statement.h"
#include "models/entity_type.h"
#include <glib.h>
#include <glib/gstdio.h>
/**
* @brief Ressources utilisées par un test EntityTypeDao.
*/
typedef struct
{
char *temporary_directory;
char *database_path;
Database *database;
EntityTypeDao *entity_type_dao;
} TestEntityTypeDaoFixture;
/**
* @brief Exécute une requête SQL ne retournant aucune ligne.
*/
static void test_entity_type_dao_execute_sql(
Database *database,
const char *sql
)
{
DatabaseStatement *statement =
NULL;
g_assert_nonnull(
database
);
g_assert_nonnull(
sql
);
statement =
database_statement_prepare(
database,
sql
);
g_assert_nonnull(
statement
);
g_assert_cmpint(
database_statement_step(
statement
),
==,
DATABASE_STATEMENT_STEP_DONE
);
database_statement_finalize(
statement
);
}
/**
* @brief Crée une base temporaire contenant le schéma de l'application.
*/
static TestEntityTypeDaoFixture
test_entity_type_dao_fixture_create(void)
{
TestEntityTypeDaoFixture fixture =
{0};
GError *error =
NULL;
fixture.temporary_directory =
g_dir_make_tmp(
"labfy-entity-type-dao-test-XXXXXX",
&error
);
g_assert_no_error(
error
);
g_assert_nonnull(
fixture.temporary_directory
);
fixture.database_path =
g_build_filename(
fixture.temporary_directory,
"Enquete.sqlite",
NULL
);
g_assert_nonnull(
fixture.database_path
);
g_assert_true(
database_initialize(
fixture.database_path,
"Enquete_EntityTypeDao",
fixture.temporary_directory
)
);
fixture.database =
database_open(
fixture.database_path
);
g_assert_nonnull(
fixture.database
);
fixture.entity_type_dao =
entity_type_dao_new(
fixture.database,
&error
);
g_assert_no_error(
error
);
g_assert_nonnull(
fixture.entity_type_dao
);
return fixture;
}
/**
* @brief Libère la fixture et supprime la base temporaire.
*/
static void test_entity_type_dao_fixture_clear(
TestEntityTypeDaoFixture *fixture
)
{
g_assert_nonnull(
fixture
);
entity_type_dao_free(
fixture->entity_type_dao
);
database_close(
fixture->database
);
g_assert_cmpint(
g_remove(
fixture->database_path
),
==,
0
);
g_assert_cmpint(
g_rmdir(
fixture->temporary_directory
),
==,
0
);
g_free(
fixture->database_path
);
g_free(
fixture->temporary_directory
);
fixture->entity_type_dao =
NULL;
fixture->database =
NULL;
fixture->database_path =
NULL;
fixture->temporary_directory =
NULL;
}
/**
* @brief Vérifie le refus d'une connexion absente.
*/
static void test_entity_type_dao_new_invalid_database(void)
{
EntityTypeDao *entity_type_dao =
NULL;
GError *error =
NULL;
entity_type_dao =
entity_type_dao_new(
NULL,
&error
);
g_assert_null(
entity_type_dao
);
g_assert_error(
error,
ENTITY_TYPE_DAO_ERROR,
ENTITY_TYPE_DAO_ERROR_INVALID_ARGUMENT
);
g_clear_error(
&error
);
}
/**
* @brief Vérifie le chargement du catalogue initial.
*/
static void test_entity_type_dao_list_all(void)
{
static const char *const expected_codes[] =
{
"email_address",
"bank_account",
"facebook_account",
"instagram_account",
"identity_document",
"iban",
"person",
"pseudonym",
"phone_number",
"website",
"domain_name",
"ip_address",
"organization",
"other"
};
static const char *const expected_labels[] =
{
"Adresse email",
"Compte bancaire",
"Compte Facebook",
"Compte Instagram",
"Document d'identité",
"IBAN",
"Personne",
"Pseudonyme",
"Numéro de téléphone",
"Site web",
"Nom de domaine",
"Adresse IP",
"Organisation",
"Autre"
};
TestEntityTypeDaoFixture fixture =
test_entity_type_dao_fixture_create();
GPtrArray *entity_types =
NULL;
GError *error =
NULL;
guint index =
0;
entity_types =
entity_type_dao_list_all(
fixture.entity_type_dao,
&error
);
g_assert_no_error(
error
);
g_assert_nonnull(
entity_types
);
g_assert_cmpuint(
entity_types->len,
==,
G_N_ELEMENTS(
expected_codes
)
);
for (index = 0;
index < entity_types->len;
index++)
{
const EntityType *entity_type =
NULL;
entity_type =
g_ptr_array_index(
entity_types,
index
);
g_assert_nonnull(
entity_type
);
/*
* La requête utilise ORDER BY id ASC.
*/
g_assert_cmpint(
entity_type_get_identifier(
entity_type
),
==,
(gint64) index + 1
);
g_assert_cmpstr(
entity_type_get_code(
entity_type
),
==,
expected_codes[index]
);
g_assert_cmpstr(
entity_type_get_label(
entity_type
),
==,
expected_labels[index]
);
/*
* Le catalogue V1 ne renseigne aucune description.
*/
g_assert_null(
entity_type_get_description(
entity_type
)
);
}
g_ptr_array_unref(
entity_types
);
test_entity_type_dao_fixture_clear(
&fixture
);
}
/**
* @brief Vérifie qu'une table vide retourne un tableau vide.
*/
static void test_entity_type_dao_list_empty(void)
{
TestEntityTypeDaoFixture fixture =
test_entity_type_dao_fixture_create();
GPtrArray *entity_types =
NULL;
GError *error =
NULL;
test_entity_type_dao_execute_sql(
fixture.database,
"DELETE FROM types_entite;"
);
entity_types =
entity_type_dao_list_all(
fixture.entity_type_dao,
&error
);
g_assert_no_error(
error
);
g_assert_nonnull(
entity_types
);
g_assert_cmpuint(
entity_types->len,
==,
0
);
g_ptr_array_unref(
entity_types
);
test_entity_type_dao_fixture_clear(
&fixture
);
}
/**
* @brief Vérifie le signalement d'une table absente.
*/
static void test_entity_type_dao_missing_table(void)
{
Database *database =
NULL;
EntityTypeDao *entity_type_dao =
NULL;
GPtrArray *entity_types =
NULL;
GError *error =
NULL;
database =
database_open(
":memory:"
);
g_assert_nonnull(
database
);
entity_type_dao =
entity_type_dao_new(
database,
&error
);
g_assert_no_error(
error
);
g_assert_nonnull(
entity_type_dao
);
g_test_expect_message(
NULL,
G_LOG_LEVEL_WARNING,
"*Impossible de préparer la requête SQL*"
);
entity_types =
entity_type_dao_list_all(
entity_type_dao,
&error
);
g_test_assert_expected_messages();
g_assert_null(
entity_types
);
g_assert_error(
error,
ENTITY_TYPE_DAO_ERROR,
ENTITY_TYPE_DAO_ERROR_SCHEMA
);
g_clear_error(
&error
);
entity_type_dao_free(
entity_type_dao
);
database_close(
database
);
}
/**
* @brief Vérifie le refus d'une ligne produisant un modèle invalide.
*/
static void test_entity_type_dao_invalid_model(void)
{
TestEntityTypeDaoFixture fixture =
test_entity_type_dao_fixture_create();
GPtrArray *entity_types =
NULL;
GError *error =
NULL;
/*
* Le schéma interdit NULL mais autorise actuellement
* une chaîne uniquement composée d'espaces.
*/
test_entity_type_dao_execute_sql(
fixture.database,
"UPDATE types_entite "
"SET code = ' ' "
"WHERE id = 1;"
);
entity_types =
entity_type_dao_list_all(
fixture.entity_type_dao,
&error
);
g_assert_null(
entity_types
);
g_assert_error(
error,
ENTITY_TYPE_DAO_ERROR,
ENTITY_TYPE_DAO_ERROR_MODEL
);
g_clear_error(
&error
);
test_entity_type_dao_fixture_clear(
&fixture
);
}
int main(
int argc,
char **argv
)
{
g_test_init(
&argc,
&argv,
NULL
);
g_test_add_func(
"/entity-type-dao/new-invalid-database",
test_entity_type_dao_new_invalid_database
);
g_test_add_func(
"/entity-type-dao/list-all",
test_entity_type_dao_list_all
);
g_test_add_func(
"/entity-type-dao/list-empty",
test_entity_type_dao_list_empty
);
g_test_add_func(
"/entity-type-dao/missing-table",
test_entity_type_dao_missing_table
);
g_test_add_func(
"/entity-type-dao/invalid-model",
test_entity_type_dao_invalid_model
);
return g_test_run();
}