feat(dao): add investigation record loader

This commit is contained in:
grayTerminal-sh 2026-07-16 12:41:34 +02:00
parent 564d67a65b
commit 137e75722d
21 changed files with 1767 additions and 19 deletions

View file

@ -37,6 +37,8 @@ TEST_DATABASE = tests/test_database
TEST_STATEMENT = tests/test_statement TEST_STATEMENT = tests/test_statement
TEST_TRANSACTION = tests/test_transaction TEST_TRANSACTION = tests/test_transaction
TEST_ERROR = tests/test_error TEST_ERROR = tests/test_error
TEST_INVESTIGATION_RECORD = tests/test_investigation_record
TEST_INVESTIGATION_DAO := tests/test_investigation_dao
all: $(TARGET) all: $(TARGET)
@ -105,6 +107,22 @@ $(TEST_ERROR): \
src/database/error.c src/database/error.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
$(TEST_INVESTIGATION_RECORD): \
tests/test_investigation_record.c \
src/models/investigation_record.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
$(TEST_INVESTIGATION_DAO): \
tests/test_investigation_dao.c \
src/dao/investigation_dao.c \
src/models/investigation_record.c \
src/database/database.c \
src/database/schema.c \
src/database/statement.c \
src/database/transaction.c \
src/database/error.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
test: \ test: \
$(TEST_NODE) \ $(TEST_NODE) \
$(TEST_TREE_MODEL) \ $(TEST_TREE_MODEL) \
@ -113,7 +131,9 @@ test: \
$(TEST_DATABASE) \ $(TEST_DATABASE) \
$(TEST_STATEMENT) \ $(TEST_STATEMENT) \
$(TEST_TRANSACTION) \ $(TEST_TRANSACTION) \
$(TEST_ERROR) $(TEST_ERROR) \
$(TEST_INVESTIGATION_RECORD) \
$(TEST_INVESTIGATION_DAO)
@echo "Exécution des tests..." @echo "Exécution des tests..."
@./$(TEST_NODE) @./$(TEST_NODE)
@./$(TEST_TREE_MODEL) @./$(TEST_TREE_MODEL)
@ -123,6 +143,8 @@ test: \
@$(TEST_STATEMENT) @$(TEST_STATEMENT)
@$(TEST_TRANSACTION) @$(TEST_TRANSACTION)
@$(TEST_ERROR) @$(TEST_ERROR)
@$(TEST_INVESTIGATION_RECORD)
@$(TEST_INVESTIGATION_DAO)
@echo "Tous les tests sont valides." @echo "Tous les tests sont valides."
%.o: %.c %.o: %.c
@ -140,6 +162,8 @@ clean:
$(TEST_DATABASE) \ $(TEST_DATABASE) \
$(TEST_STATEMENT) \ $(TEST_STATEMENT) \
$(TEST_TRANSACTION) \ $(TEST_TRANSACTION) \
$(TEST_ERROR) $(TEST_ERROR) \
$(TEST_INVESTIGATION_RECORD) \
$(TEST_INVESTIGATION_DAO)
.PHONY: clean run test .PHONY: clean run test

View file

@ -0,0 +1,535 @@
# Ticket #027 — Ajouter le modèle de lecture et le DAO de lenquête
## Contexte
Les tickets précédents ont permis de mettre en place :
- le schéma SQLite V1 ;
- linfrastructure `Database` ;
- les requêtes préparées `DatabaseStatement` ;
- les transactions ;
- la gestion centralisée des erreurs ;
- linitialisation transactionnelle dune nouvelle enquête.
La table SQLite :
```text
investigation
```
contient les informations principales de lenquête courante :
```text
id
name
root_path
created_at
updated_at
```
Le projet possède déjà un type :
```C
Investigation
```
dans le module `core`.
Cet objet représente actuellement le contexte de travail sur le système de
fichiers :
- chemin racine de lenquête ;
- chemin du fichier SQLite.
Il ne doit pas être transformé directement en représentation dune ligne SQL.
Larchitecture du projet prévoit une séparation entre :
```text
Core
Models
DAO
Database
SQLite
```
Le DAO doit être responsable des requêtes SQL métier.
## Objectif
Créer un modèle de données en lecture seule représentant la ligne de la table
investigation, ainsi quun DAO permettant de charger cette ligne depuis une
connexion Database.
Le ticket doit permettre de lire les informations persistées dune enquête
sans utiliser directement lAPI SQLite hors de la couche Database.
## Architecture attendue
```
InvestigationRecord
│ construit par
InvestigationDao
DatabaseStatement
Database
SQLite
```
## Travail à réaliser
### Créer le modèle InvestigationRecord
Créer les fichiers :
```text
include/models/investigation_record.h
src/models/investigation_record.c
```
Le modèle doit être opaque :
```C
typedef struct InvestigationRecord InvestigationRecord;
```
Sa représentation privée doit contenir :
```C
struct InvestigationRecord
{
char *id;
char *name;
char *root_path;
char *created_at;
char *updated_at;
};
```
Le modèle ne doit dépendre ni de SQLite, ni de GTK.
### Ajouter le constructeur
Créer une fonction :
```C
InvestigationRecord *investigation_record_new(
const char *id,
const char *name,
const char *root_path,
const char *created_at,
const char *updated_at
);
```
Le constructeur doit :
- refuser les pointeurs `NULL` ;
- refuser les chaînes vides ;
- allouer une copie de chaque chaîne ;
- nettoyer correctement toutes les allocations en cas déchec ;
- retourner `NULL` si les données sont invalides.
### Ajouter la fonction de libération
Créer :
```C
void investigation_record_free(
InvestigationRecord *record
);
```
Cette fonction doit :
- accepter `NULL` ;
- libérer toutes les chaînes ;
- libérer la structure.
### Ajouter les accesseurs
Créer les accesseurs en lecture seule :
```C
const char *investigation_record_get_id(
const InvestigationRecord *record
);
const char *investigation_record_get_name(
const InvestigationRecord *record
);
const char *investigation_record_get_root_path(
const InvestigationRecord *record
);
const char *investigation_record_get_created_at(
const InvestigationRecord *record
);
const char *investigation_record_get_updated_at(
const InvestigationRecord *record
);
```
Les pointeurs retournés appartiennent au modèle et ne doivent pas être libérés.
Les accesseurs doivent retourner `NULL` si le modèle reçu est `NULL`.
### Créer le DAO de lenquête
Créer les fichiers :
```text
include/dao/investigation_dao.h
src/dao/investigation_dao.c
```
Déclarer la fonction :
```C
InvestigationRecord *investigation_dao_load(
Database *database
);
```
Cette fonction doit charger lunique ligne de la table :
```text
investigation
```
La requête doit lire :
```SQL
SELECT
id,
name,
root_path,
created_at,
updated_at
FROM investigation;
```
### Utiliser exclusivement DatabaseStatement
Le DAO doit utiliser :
```C
database_statement_prepare()
database_statement_step()
database_statement_column_text()
database_statement_finalize()
```
Le DAO ne doit pas utiliser directement :
```C
sqlite3_prepare_v2()
sqlite3_step()
sqlite3_column_text()
sqlite3_finalize()
```
Aucun type SQLite ne doit apparaître dans linterface publique du DAO.
### Vérifier le nombre de lignes
La base dune enquête doit contenir exactement une ligne dans la table
`investigation`.
Le DAO doit gérer les cas suivants :
Une ligne
```
ROW
DONE
```
Résultat :
```
succès
```
Le DAO construit et retourne un `InvestigationRecord`.
#### Aucune ligne
`DONE` dès le premier step
Résultat :
```
échec
```
Le DAO retourne `NULL` et enregistre :
```
DATABASE_ERROR_INVALID_STATE
```
#### Plusieurs lignes
```
ROW
ROW
```
Résultat :
`échec`
Le DAO libère le modèle temporaire, retourne `NULL` et enregistre :
```C
DATABASE_ERROR_INVALID_STATE
```
### Propager les erreurs
Les erreurs de préparation ou dexécution provenant de `DatabaseStatement`
doivent rester disponibles dans le contexte `Database`.
Le DAO doit enregistrer une erreur explicite pour :
- une table vide ;
- plusieurs lignes dans investigation ;
- une colonne obligatoire invalide ;
- une allocation impossible ;
- un état incohérent.
Les codes prévus sont :
```C
DATABASE_ERROR_INVALID_ARGUMENT
DATABASE_ERROR_INVALID_STATE
DATABASE_ERROR_MEMORY
DATABASE_ERROR_SQLITE
```
Une lecture réussie doit laisser :
```C
DATABASE_ERROR_NONE
```
### Gérer proprement la mémoire
Le DAO doit libérer toutes les chaînes temporaires retournées par :
```C
database_statement_column_text()
```
après la construction du modèle.
Toutes les sorties déchec doivent :
- finaliser la requête préparée ;
- libérer les chaînes déjà lues ;
- libérer le modèle partiellement construit ;
- ne provoquer aucune fuite mémoire.
- Tests à ajouter
#### Test du modèle
Créer :
```
tests/test_investigation_record.c
```
Vérifier :
- la création avec des données valides ;
- la copie des chaînes ;
- les cinq accesseurs ;
- le refus des paramètres `NULL` ;
- le refus des chaînes vides ;
- `investigation_record_free(NULL)`.
#### Test du DAO
Créer :
```
tests/test_investigation_dao.c
Chargement valide
```
Créer une base temporaire avec :
```C
database_initialize()
```
Puis :
- ouvrir la base avec `database_open()` ;
- charger lenquête avec `investigation_dao_load()` ;
- vérifier que le modèle nest pas `NULL` ;
- vérifier lUUID ;
- vérifier le nom ;
- vérifier le chemin racine ;
- vérifier `created_at` ;
- vérifier `updated_at` ;
- vérifier que lerreur Database vaut `DATABASE_ERROR_NONE`.
#### Paramètre invalide
Vérifier :
```C
investigation_dao_load(NULL) == NULL
```
#### Table vide
Créer une base contenant la table investigation sans ligne.
Vérifier :
```
résultat == NULL
DATABASE_ERROR_INVALID_STATE
message non vide
```
#### Plusieurs lignes
Créer une base contenant deux lignes dans la table investigation.
Vérifier :
```
résultat == NULL
DATABASE_ERROR_INVALID_STATE
message non vide
```
#### Données invalides
Créer une situation dans laquelle une colonne obligatoire ne peut pas être
correctement lue.
Vérifier :
```
résultat == NULL
erreur enregistrée
aucune fuite de mémoire
```
## Makefile
Ajouter les exécutables :
```
tests/test_investigation_record
tests/test_investigation_dao
```
Ajouter leurs règles de compilation.
Les tests du DAO devront notamment compiler avec :
```
src/models/investigation_record.c
src/dao/investigation_dao.c
src/database/database.c
src/database/schema.c
src/database/statement.c
src/database/transaction.c
src/database/error.c
```
Ajouter les nouveaux tests aux cibles :
```
test
clean
```
### Critères dacceptation
- [ ] Le type `InvestigationRecord` est opaque.
- [ ] Le modèle ne dépend ni de SQLite ni de GTK.
- [ ] Le constructeur valide tous les champs obligatoires.
- [ ] Toutes les chaînes sont copiées.
- [ ] Une fonction de libération complète est disponible.
- [ ] Les cinq accesseurs en lecture seule sont disponibles.
- [ ] `investigation_dao_load()` charge lenquête courante.
- [ ] Le DAO utilise uniquement lAPI `DatabaseStatement`.
- [ ] Aucun appel direct à SQLite napparaît dans le DAO.
- [ ] Une base contenant exactement une enquête est acceptée.
- [ ] Une table vide est refusée.
- [ ] Plusieurs lignes sont refusées.
- [ ] Les erreurs sont enregistrées dans `Database`.
- [ ] Toutes les ressources temporaires sont libérées.
- [ ] Les tests du modèle sont valides.
- [ ] Les tests du DAO sont valides.
- [ ] Tous les anciens tests restent valides.
- [ ] `make` réussit sans erreur.
- [ ] `make test` réussit.
- [ ] `git diff --check` ne retourne aucune erreur.
### Hors périmètre
Ce ticket ne doit pas ajouter :
- la création dune enquête par le DAO ;
- la modification du nom de lenquête ;
- la modification du chemin racine ;
- la suppression dune enquête ;
- un CRUD complet ;
- louverture automatique dans linterface GTK ;
- laffichage des informations dans la fenêtre ;
- les DAO des preuves, sources ou entités ;
- les migrations de schéma ;
- une nouvelle version du schéma SQLite.
### Fichiers principalement concernés
```
include/models/investigation_record.h
include/dao/investigation_dao.h
src/models/investigation_record.c
src/dao/investigation_dao.c
tests/test_investigation_record.c
tests/test_investigation_dao.c
Makefile
```
## Résultat attendu
À la fin du ticket, le projet doit être capable de charger les informations
persistées de lenquête courante dans un modèle C indépendant de SQLite.
Le DAO devient le premier point daccès métier structuré à la base de données.
### Commit attendu
Une fois tous les critères validés :
`feat(dao): add investigation record loader`
Avant le commit :
```bash
make clean
make
make test
git diff --check
git status --short
git add .
git diff --cached --stat
git diff --cached
git commit -m "feat(dao): add investigation record loader"
git push
```

View file

@ -0,0 +1,25 @@
/******************************************************************************
* @file investigation_dao.h
* @brief Accès aux informations persistées de l'enquête.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_INVESTIGATION_DAO_H
#define LABFY_INVESTIGATION_INVESTIGATION_DAO_H
#include "database/database.h"
#include "models/investigation_record.h"
/**
* @brief Charge l'unique enquête enregistrée dans la base.
*
* La fonction attend exactement une ligne dans la table investigation.
*
* @param database Connexion ouverte.
*
* @return Un nouveau modèle InvestigationRecord, ou NULL en cas d'échec.
*/
InvestigationRecord *investigation_dao_load(
Database *database
);
#endif

View file

@ -44,6 +44,21 @@ const char *database_error_get_message(
const Database *database const Database *database
); );
/**
* @brief Enregistre une erreur dans une connexion Database.
*
* Le message est copié par la connexion.
*
* @param database Connexion Database.
* @param error_code Code de l'erreur.
* @param error_message Message descriptif.
*/
void database_error_set(
Database *database,
DatabaseErrorCode error_code,
const char *error_message
);
/** /**
* @brief Efface la dernière erreur enregistrée. * @brief Efface la dernière erreur enregistrée.
* *

View file

@ -0,0 +1,92 @@
/******************************************************************************
* @file investigation_record.h
* @brief Modèle représentant les informations persistées d'une enquête.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_INVESTIGATION_RECORD_H
#define LABFY_INVESTIGATION_INVESTIGATION_RECORD_H
/**
* @brief Modèle opaque représentant une ligne de la table investigation.
*/
typedef struct InvestigationRecord InvestigationRecord;
/**
* @brief Crée un modèle représentant une enquête persistée.
*
* Toutes les chaînes sont copiées par le modèle.
*
* @param id Identifiant UUID de l'enquête.
* @param name Nom de l'enquête.
* @param root_path Chemin racine de l'enquête.
* @param created_at Date de création.
* @param updated_at Date de dernière modification.
*
* @return Un nouveau modèle, ou NULL si les données sont invalides
* ou si une allocation échoue.
*/
InvestigationRecord *investigation_record_new(
const char *id,
const char *name,
const char *root_path,
const char *created_at,
const char *updated_at
);
/**
* @brief Libère un modèle InvestigationRecord.
*
* Cette fonction accepte NULL.
*
* @param record Modèle à libérer.
*/
void investigation_record_free(
InvestigationRecord *record
);
/**
* @brief Retourne l'identifiant de l'enquête.
*
* @return Identifiant appartenant au modèle, ou NULL.
*/
const char *investigation_record_get_id(
const InvestigationRecord *record
);
/**
* @brief Retourne le nom de l'enquête.
*
* @return Nom appartenant au modèle, ou NULL.
*/
const char *investigation_record_get_name(
const InvestigationRecord *record
);
/**
* @brief Retourne le chemin racine de l'enquête.
*
* @return Chemin appartenant au modèle, ou NULL.
*/
const char *investigation_record_get_root_path(
const InvestigationRecord *record
);
/**
* @brief Retourne la date de création.
*
* @return Date appartenant au modèle, ou NULL.
*/
const char *investigation_record_get_created_at(
const InvestigationRecord *record
);
/**
* @brief Retourne la date de dernière modification.
*
* @return Date appartenant au modèle, ou NULL.
*/
const char *investigation_record_get_updated_at(
const InvestigationRecord *record
);
#endif

Binary file not shown.

197
src/dao/investigation_dao.c Normal file
View file

@ -0,0 +1,197 @@
/******************************************************************************
* @file investigation_dao.c
* @brief Chargement de l'enquête persistée.
******************************************************************************/
#include "dao/investigation_dao.h"
#include "database/error.h"
#include "database/statement.h"
#include <glib.h>
/**
* @brief Requête de chargement de l'enquête.
*/
static const char *const investigation_dao_load_sql =
"SELECT "
" id, "
" name, "
" root_path, "
" created_at, "
" updated_at "
"FROM investigation;";
/**
* @brief Vérifie qu'une valeur obligatoire est présente.
*/
static bool investigation_dao_text_is_valid(
const char *text
)
{
return text != NULL && text[0] != '\0';
}
InvestigationRecord *investigation_dao_load(
Database *database
)
{
DatabaseStatement *statement = NULL;
InvestigationRecord *record = NULL;
char *id = NULL;
char *name = NULL;
char *root_path = NULL;
char *created_at = NULL;
char *updated_at = NULL;
DatabaseStatementStepResult step_result =
DATABASE_STATEMENT_STEP_ERROR;
if (database == NULL)
{
return NULL;
}
statement = database_statement_prepare(
database,
investigation_dao_load_sql
);
if (statement == NULL)
{
goto cleanup;
}
step_result = database_statement_step(
statement
);
if (step_result == DATABASE_STATEMENT_STEP_ERROR)
{
goto cleanup;
}
if (step_result == DATABASE_STATEMENT_STEP_DONE)
{
database_error_set(
database,
DATABASE_ERROR_INVALID_STATE,
"La table investigation ne contient aucune ligne."
);
goto cleanup;
}
if (!database_statement_column_text(
statement,
0,
&id
) ||
!database_statement_column_text(
statement,
1,
&name
) ||
!database_statement_column_text(
statement,
2,
&root_path
) ||
!database_statement_column_text(
statement,
3,
&created_at
) ||
!database_statement_column_text(
statement,
4,
&updated_at
))
{
database_error_set(
database,
DATABASE_ERROR_INVALID_STATE,
"Impossible de lire les colonnes de l'enquête."
);
goto cleanup;
}
if (!investigation_dao_text_is_valid(id) ||
!investigation_dao_text_is_valid(name) ||
!investigation_dao_text_is_valid(root_path) ||
!investigation_dao_text_is_valid(created_at) ||
!investigation_dao_text_is_valid(updated_at))
{
database_error_set(
database,
DATABASE_ERROR_INVALID_STATE,
"L'enquête contient une colonne obligatoire invalide."
);
goto cleanup;
}
record = investigation_record_new(
id,
name,
root_path,
created_at,
updated_at
);
if (record == NULL)
{
database_error_set(
database,
DATABASE_ERROR_MEMORY,
"Impossible d'allouer le modèle de l'enquête."
);
goto cleanup;
}
/*
* Une seconde ligne indiquerait une base incohérente.
*/
step_result = database_statement_step(
statement
);
if (step_result == DATABASE_STATEMENT_STEP_ERROR)
{
investigation_record_free(record);
record = NULL;
goto cleanup;
}
if (step_result == DATABASE_STATEMENT_STEP_ROW)
{
database_error_set(
database,
DATABASE_ERROR_INVALID_STATE,
"La table investigation contient plusieurs lignes."
);
investigation_record_free(record);
record = NULL;
goto cleanup;
}
database_error_clear(database);
cleanup:
g_free(updated_at);
g_free(created_at);
g_free(root_path);
g_free(name);
g_free(id);
database_statement_finalize(statement);
return record;
}

View file

@ -22,6 +22,19 @@ const char *database_error_get_message(
return database_get_error_message_internal(database); return database_get_error_message_internal(database);
} }
void database_error_set(
Database *database,
DatabaseErrorCode error_code,
const char *error_message
)
{
database_set_error(
database,
error_code,
error_message
);
}
void database_error_clear( void database_error_clear(
Database *database Database *database
) )

View file

@ -590,33 +590,24 @@ void database_statement_finalize(
DatabaseStatement *statement DatabaseStatement *statement
) )
{ {
sqlite3 *database_handle = NULL;
int result = SQLITE_OK;
if (statement == NULL) if (statement == NULL)
{ {
return; return;
} }
database_handle = database_get_handle(
statement->database
);
if (statement->handle != NULL) if (statement->handle != NULL)
{ {
result = sqlite3_finalize( /*
* sqlite3_finalize() peut retourner l'erreur produite par le dernier
* sqlite3_step(), même si la requête est correctement détruite.
*
* Cette erreur est déjà enregistrée par database_statement_step().
*/
sqlite3_finalize(
statement->handle statement->handle
); );
if (result != SQLITE_OK) statement->handle = NULL;
{
g_warning(
"Impossible de finaliser la requête SQL : %s",
database_handle != NULL
? sqlite3_errmsg(database_handle)
: sqlite3_errstr(result)
);
}
} }
g_free(statement); g_free(statement);

View file

@ -0,0 +1,199 @@
/******************************************************************************
* @file investigation_record.c
* @brief Implémentation du modèle InvestigationRecord.
******************************************************************************/
#include "models/investigation_record.h"
#include <glib.h>
#include <string.h>
/**
* @brief Représentation privée d'une enquête persistée.
*/
struct InvestigationRecord
{
char *id;
char *name;
char *root_path;
char *created_at;
char *updated_at;
};
/**
* @brief Vérifie qu'une chaîne obligatoire est présente et non vide.
*/
static gboolean investigation_record_text_is_valid(
const char *text
)
{
return text != NULL && text[0] != '\0';
}
/**
* @brief Copie une chaîne en utilisant une allocation non fatale.
*
* Contrairement à g_strdup(), cette fonction retourne NULL si
* l'allocation échoue au lieu d'interrompre le programme.
*/
static char *investigation_record_copy_text(
const char *text
)
{
char *text_copy = NULL;
gsize text_length = 0;
if (text == NULL)
{
return NULL;
}
text_length = strlen(text);
if (text_length == G_MAXSIZE)
{
return NULL;
}
text_copy = g_try_malloc(
text_length + 1
);
if (text_copy == NULL)
{
return NULL;
}
memcpy(
text_copy,
text,
text_length + 1
);
return text_copy;
}
InvestigationRecord *investigation_record_new(
const char *id,
const char *name,
const char *root_path,
const char *created_at,
const char *updated_at
)
{
InvestigationRecord *record = NULL;
if (!investigation_record_text_is_valid(id) ||
!investigation_record_text_is_valid(name) ||
!investigation_record_text_is_valid(root_path) ||
!investigation_record_text_is_valid(created_at) ||
!investigation_record_text_is_valid(updated_at))
{
return NULL;
}
record = g_try_new0(
InvestigationRecord,
1
);
if (record == NULL)
{
return NULL;
}
record->id = investigation_record_copy_text(id);
record->name = investigation_record_copy_text(name);
record->root_path = investigation_record_copy_text(root_path);
record->created_at = investigation_record_copy_text(created_at);
record->updated_at = investigation_record_copy_text(updated_at);
if (record->id == NULL ||
record->name == NULL ||
record->root_path == NULL ||
record->created_at == NULL ||
record->updated_at == NULL)
{
investigation_record_free(record);
return NULL;
}
return record;
}
void investigation_record_free(
InvestigationRecord *record
)
{
if (record == NULL)
{
return;
}
g_free(record->updated_at);
g_free(record->created_at);
g_free(record->root_path);
g_free(record->name);
g_free(record->id);
g_free(record);
}
const char *investigation_record_get_id(
const InvestigationRecord *record
)
{
if (record == NULL)
{
return NULL;
}
return record->id;
}
const char *investigation_record_get_name(
const InvestigationRecord *record
)
{
if (record == NULL)
{
return NULL;
}
return record->name;
}
const char *investigation_record_get_root_path(
const InvestigationRecord *record
)
{
if (record == NULL)
{
return NULL;
}
return record->root_path;
}
const char *investigation_record_get_created_at(
const InvestigationRecord *record
)
{
if (record == NULL)
{
return NULL;
}
return record->created_at;
}
const char *investigation_record_get_updated_at(
const InvestigationRecord *record
)
{
if (record == NULL)
{
return NULL;
}
return record->updated_at;
}

Binary file not shown.

View file

@ -133,6 +133,7 @@ static void test_database_initialize_valid_database(void)
char *investigation_created_at = NULL; char *investigation_created_at = NULL;
char *investigation_id = NULL; char *investigation_id = NULL;
char *investigation_count = NULL; char *investigation_count = NULL;
char *investigation_updated_at = NULL;
sqlite3 *database = NULL; sqlite3 *database = NULL;
GError *error = NULL; GError *error = NULL;
@ -231,6 +232,12 @@ static void test_database_initialize_valid_database(void)
"LIMIT 1;" "LIMIT 1;"
); );
investigation_updated_at = test_database_read_single_text(
database,
"SELECT updated_at FROM investigation "
"LIMIT 1;"
);
investigation_id = test_database_read_single_text( investigation_id = test_database_read_single_text(
database, database,
"SELECT id FROM investigation " "SELECT id FROM investigation "
@ -253,6 +260,13 @@ static void test_database_initialize_valid_database(void)
assert(strcmp(investigation_root_path, temporary_directory) == 0); assert(strcmp(investigation_root_path, temporary_directory) == 0);
assert(strcmp(investigation_created_at, created_at) == 0); assert(strcmp(investigation_created_at, created_at) == 0);
assert(investigation_updated_at[0] != '\0');
assert(strcmp(investigation_updated_at,created_at) == 0);
assert(strcmp(investigation_id,investigation_uuid) == 0
);
result = sqlite3_close(database); result = sqlite3_close(database);
assert(result == SQLITE_OK); assert(result == SQLITE_OK);

Binary file not shown.

BIN
tests/test_investigation_dao Executable file

Binary file not shown.

View file

@ -0,0 +1,374 @@
/******************************************************************************
* @file test_investigation_dao.c
* @brief Tests du DAO de l'enquête.
******************************************************************************/
#include "dao/investigation_dao.h"
#include "database/database.h"
#include "database/error.h"
#include "database/statement.h"
#include "models/investigation_record.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <glib.h>
#include <glib/gstdio.h>
/**
* @brief Crée une table investigation minimale pour les tests du DAO.
*/
static void test_investigation_dao_create_table(
Database *database
)
{
DatabaseStatement *statement = NULL;
assert(database != NULL);
statement = database_statement_prepare(
database,
"CREATE TABLE investigation"
"("
" id TEXT NOT NULL,"
" name TEXT NOT NULL,"
" root_path TEXT NOT NULL,"
" created_at TEXT NOT NULL,"
" updated_at TEXT NOT NULL"
");"
);
assert(statement != NULL);
assert(
database_statement_step(statement) ==
DATABASE_STATEMENT_STEP_DONE
);
database_statement_finalize(statement);
}
/**
* @brief Insère une ligne dans la table investigation de test.
*/
static void test_investigation_dao_insert_row(
Database *database,
const char *id,
const char *name
)
{
DatabaseStatement *statement = NULL;
assert(database != NULL);
assert(id != NULL);
assert(name != NULL);
statement = database_statement_prepare(
database,
"INSERT INTO investigation"
"("
" id,"
" name,"
" root_path,"
" created_at,"
" updated_at"
")"
"VALUES (?, ?, ?, ?, ?);"
);
assert(statement != NULL);
assert(
database_statement_bind_text(
statement,
1,
id
)
);
assert(
database_statement_bind_text(
statement,
2,
name
)
);
assert(
database_statement_bind_text(
statement,
3,
"/tmp/enquete-dao"
)
);
assert(
database_statement_bind_text(
statement,
4,
"2026-07-16T12:00:00Z"
)
);
assert(
database_statement_bind_text(
statement,
5,
"2026-07-16T12:00:00Z"
)
);
assert(
database_statement_step(statement) ==
DATABASE_STATEMENT_STEP_DONE
);
database_statement_finalize(statement);
}
/**
* @brief Vérifie le refus d'une table investigation vide.
*/
static void test_investigation_dao_load_empty_table(void)
{
Database *database = NULL;
InvestigationRecord *record = NULL;
const char *error_message = NULL;
database = database_open(":memory:");
assert(database != NULL);
test_investigation_dao_create_table(database);
record = investigation_dao_load(database);
assert(record == NULL);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_INVALID_STATE
);
error_message = database_error_get_message(database);
assert(error_message != NULL);
assert(error_message[0] != '\0');
database_close(database);
}
/**
* @brief Vérifie le refus de plusieurs enquêtes dans la même base.
*/
static void test_investigation_dao_load_multiple_rows(void)
{
Database *database = NULL;
InvestigationRecord *record = NULL;
const char *error_message = NULL;
database = database_open(":memory:");
assert(database != NULL);
test_investigation_dao_create_table(database);
test_investigation_dao_insert_row(
database,
"11111111-1111-4111-8111-111111111111",
"Premiere_Enquete"
);
test_investigation_dao_insert_row(
database,
"22222222-2222-4222-8222-222222222222",
"Seconde_Enquete"
);
record = investigation_dao_load(database);
assert(record == NULL);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_INVALID_STATE
);
error_message = database_error_get_message(database);
assert(error_message != NULL);
assert(error_message[0] != '\0');
database_close(database);
}
/**
* @brief Vérifie le chargement d'une enquête valide.
*/
static void test_investigation_dao_load_valid(void)
{
char *temporary_directory = NULL;
char *database_path = NULL;
Database *database = NULL;
InvestigationRecord *record = NULL;
const char *id = NULL;
const char *created_at = NULL;
const char *updated_at = NULL;
GError *error = NULL;
temporary_directory = g_dir_make_tmp(
"labfy-investigation-dao-test-XXXXXX",
&error
);
assert(temporary_directory != NULL);
assert(error == NULL);
database_path = g_build_filename(
temporary_directory,
"Enquete.sqlite",
NULL
);
assert(database_path != NULL);
assert(
database_initialize(
database_path,
"Enquete_DAO",
temporary_directory
)
);
database = database_open(
database_path
);
assert(database != NULL);
record = investigation_dao_load(
database
);
assert(record != NULL);
id = investigation_record_get_id(record);
created_at = investigation_record_get_created_at(record);
updated_at = investigation_record_get_updated_at(record);
assert(id != NULL);
assert(g_uuid_string_is_valid(id));
assert(
strcmp(
investigation_record_get_name(record),
"Enquete_DAO"
) == 0
);
assert(
strcmp(
investigation_record_get_root_path(record),
temporary_directory
) == 0
);
assert(created_at != NULL);
assert(created_at[0] != '\0');
assert(updated_at != NULL);
assert(updated_at[0] != '\0');
assert(
strcmp(
created_at,
updated_at
) == 0
);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_NONE
);
investigation_record_free(record);
database_close(database);
assert(g_remove(database_path) == 0);
assert(g_rmdir(temporary_directory) == 0);
g_free(database_path);
g_free(temporary_directory);
}
/**
* @brief Vérifie le refus d'une connexion absente.
*/
static void test_investigation_dao_load_null_database(void)
{
assert(
investigation_dao_load(NULL) == NULL
);
}
/**
* @brief Vérifie le refus d'une enquête contenant une valeur obligatoire vide.
*/
static void test_investigation_dao_load_invalid_row(void)
{
Database *database = NULL;
InvestigationRecord *record = NULL;
const char *error_message = NULL;
database = database_open(":memory:");
assert(database != NULL);
test_investigation_dao_create_table(database);
/*
* La colonne name est NOT NULL, mais une chaîne vide reste autorisée
* par SQLite. Le DAO doit la considérer comme invalide.
*/
test_investigation_dao_insert_row(
database,
"33333333-3333-4333-8333-333333333333",
""
);
record = investigation_dao_load(database);
assert(record == NULL);
assert(
database_error_get_code(database) ==
DATABASE_ERROR_INVALID_STATE
);
error_message = database_error_get_message(database);
assert(error_message != NULL);
assert(error_message[0] != '\0');
database_close(database);
}
int main(void)
{
test_investigation_dao_load_valid();
test_investigation_dao_load_null_database();
test_investigation_dao_load_empty_table();
test_investigation_dao_load_multiple_rows();
test_investigation_dao_load_invalid_row();
printf(
"InvestigationDao : tous les tests sont valides.\n"
);
return 0;
}

Binary file not shown.

BIN
tests/test_investigation_record Executable file

Binary file not shown.

View file

@ -0,0 +1,269 @@
/******************************************************************************
* @file test_investigation_record.c
* @brief Tests du modèle InvestigationRecord.
******************************************************************************/
#include "models/investigation_record.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
/**
* @brief Vérifie la création et les accesseurs du modèle.
*/
static void test_investigation_record_valid(void)
{
InvestigationRecord *record = NULL;
char id[] = "6e62b9af-2046-4efd-b3b9-29869f816951";
char name[] = "Enquete_Test";
char root_path[] = "/tmp/enquete-test";
char created_at[] = "2026-07-16T10:00:00Z";
char updated_at[] = "2026-07-16T10:00:00Z";
record = investigation_record_new(
id,
name,
root_path,
created_at,
updated_at
);
assert(record != NULL);
assert(
strcmp(
investigation_record_get_id(record),
id
) == 0
);
assert(
strcmp(
investigation_record_get_name(record),
name
) == 0
);
assert(
strcmp(
investigation_record_get_root_path(record),
root_path
) == 0
);
assert(
strcmp(
investigation_record_get_created_at(record),
created_at
) == 0
);
assert(
strcmp(
investigation_record_get_updated_at(record),
updated_at
) == 0
);
/*
* Le modèle doit posséder ses propres copies.
*/
id[0] = 'X';
name[0] = 'X';
root_path[0] = 'X';
created_at[0] = 'X';
updated_at[0] = 'X';
assert(
strcmp(
investigation_record_get_id(record),
"6e62b9af-2046-4efd-b3b9-29869f816951"
) == 0
);
assert(
strcmp(
investigation_record_get_name(record),
"Enquete_Test"
) == 0
);
assert(
strcmp(
investigation_record_get_root_path(record),
"/tmp/enquete-test"
) == 0
);
assert(
strcmp(
investigation_record_get_created_at(record),
"2026-07-16T10:00:00Z"
) == 0
);
assert(
strcmp(
investigation_record_get_updated_at(record),
"2026-07-16T10:00:00Z"
) == 0
);
investigation_record_free(record);
}
/**
* @brief Vérifie le refus des pointeurs NULL.
*/
static void test_investigation_record_null_parameters(void)
{
assert(
investigation_record_new(
NULL,
"Enquete",
"/tmp/enquete",
"2026-07-16T10:00:00Z",
"2026-07-16T10:00:00Z"
) == NULL
);
assert(
investigation_record_new(
"id",
NULL,
"/tmp/enquete",
"2026-07-16T10:00:00Z",
"2026-07-16T10:00:00Z"
) == NULL
);
assert(
investigation_record_new(
"id",
"Enquete",
NULL,
"2026-07-16T10:00:00Z",
"2026-07-16T10:00:00Z"
) == NULL
);
assert(
investigation_record_new(
"id",
"Enquete",
"/tmp/enquete",
NULL,
"2026-07-16T10:00:00Z"
) == NULL
);
assert(
investigation_record_new(
"id",
"Enquete",
"/tmp/enquete",
"2026-07-16T10:00:00Z",
NULL
) == NULL
);
}
/**
* @brief Vérifie le refus des chaînes vides.
*/
static void test_investigation_record_empty_parameters(void)
{
assert(
investigation_record_new(
"",
"Enquete",
"/tmp/enquete",
"2026-07-16T10:00:00Z",
"2026-07-16T10:00:00Z"
) == NULL
);
assert(
investigation_record_new(
"id",
"",
"/tmp/enquete",
"2026-07-16T10:00:00Z",
"2026-07-16T10:00:00Z"
) == NULL
);
assert(
investigation_record_new(
"id",
"Enquete",
"",
"2026-07-16T10:00:00Z",
"2026-07-16T10:00:00Z"
) == NULL
);
assert(
investigation_record_new(
"id",
"Enquete",
"/tmp/enquete",
"",
"2026-07-16T10:00:00Z"
) == NULL
);
assert(
investigation_record_new(
"id",
"Enquete",
"/tmp/enquete",
"2026-07-16T10:00:00Z",
""
) == NULL
);
}
/**
* @brief Vérifie les opérations acceptant un modèle NULL.
*/
static void test_investigation_record_null_instance(void)
{
assert(
investigation_record_get_id(NULL) == NULL
);
assert(
investigation_record_get_name(NULL) == NULL
);
assert(
investigation_record_get_root_path(NULL) == NULL
);
assert(
investigation_record_get_created_at(NULL) == NULL
);
assert(
investigation_record_get_updated_at(NULL) == NULL
);
investigation_record_free(NULL);
}
int main(void)
{
test_investigation_record_valid();
test_investigation_record_null_parameters();
test_investigation_record_empty_parameters();
test_investigation_record_null_instance();
printf(
"InvestigationRecord : tous les tests sont valides.\n"
);
return 0;
}

Binary file not shown.

Binary file not shown.