diff --git a/Makefile b/Makefile index ac4c089..32b3f2b 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,8 @@ TEST_DATABASE = tests/test_database TEST_STATEMENT = tests/test_statement TEST_TRANSACTION = tests/test_transaction TEST_ERROR = tests/test_error +TEST_INVESTIGATION_RECORD = tests/test_investigation_record +TEST_INVESTIGATION_DAO := tests/test_investigation_dao all: $(TARGET) @@ -105,6 +107,22 @@ $(TEST_ERROR): \ src/database/error.c $(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_NODE) \ $(TEST_TREE_MODEL) \ @@ -113,7 +131,9 @@ test: \ $(TEST_DATABASE) \ $(TEST_STATEMENT) \ $(TEST_TRANSACTION) \ - $(TEST_ERROR) + $(TEST_ERROR) \ + $(TEST_INVESTIGATION_RECORD) \ + $(TEST_INVESTIGATION_DAO) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -123,6 +143,8 @@ test: \ @$(TEST_STATEMENT) @$(TEST_TRANSACTION) @$(TEST_ERROR) + @$(TEST_INVESTIGATION_RECORD) + @$(TEST_INVESTIGATION_DAO) @echo "Tous les tests sont valides." %.o: %.c @@ -140,6 +162,8 @@ clean: $(TEST_DATABASE) \ $(TEST_STATEMENT) \ $(TEST_TRANSACTION) \ - $(TEST_ERROR) + $(TEST_ERROR) \ + $(TEST_INVESTIGATION_RECORD) \ + $(TEST_INVESTIGATION_DAO) .PHONY: clean run test diff --git a/docs/tickets/open/TICKET-026.md b/docs/tickets/closed/TICKET-026.md similarity index 100% rename from docs/tickets/open/TICKET-026.md rename to docs/tickets/closed/TICKET-026.md diff --git a/docs/tickets/open/TICKET-027.md b/docs/tickets/open/TICKET-027.md new file mode 100644 index 0000000..9b22d28 --- /dev/null +++ b/docs/tickets/open/TICKET-027.md @@ -0,0 +1,535 @@ +# Ticket #027 — Ajouter le modèle de lecture et le DAO de l’enquête + +## Contexte + +Les tickets précédents ont permis de mettre en place : + +- le schéma SQLite V1 ; +- l’infrastructure `Database` ; +- les requêtes préparées `DatabaseStatement` ; +- les transactions ; +- la gestion centralisée des erreurs ; +- l’initialisation transactionnelle d’une nouvelle enquête. + +La table SQLite : + +```text +investigation +``` +contient les informations principales de l’enquê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 l’enquête ; +- chemin du fichier SQLite. + +Il ne doit pas être transformé directement en représentation d’une ligne SQL. + +L’architecture 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 qu’un DAO permettant de charger cette ligne depuis une +connexion Database. + +Le ticket doit permettre de lire les informations persistées d’une enquête +sans utiliser directement l’API 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 l’enquê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 l’unique 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 l’interface publique du DAO. + +### Vérifier le nombre de lignes + +La base d’une 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 d’exé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 l’enquête avec `investigation_dao_load()` ; +- vérifier que le modèle n’est pas `NULL` ; +- vérifier l’UUID ; +- vérifier le nom ; +- vérifier le chemin racine ; +- vérifier `created_at` ; +- vérifier `updated_at` ; +- vérifier que l’erreur 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 d’acceptation + +- [ ] 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 l’enquête courante. +- [ ] Le DAO utilise uniquement l’API `DatabaseStatement`. +- [ ] Aucun appel direct à SQLite n’apparaî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 d’une enquête par le DAO ; +- la modification du nom de l’enquête ; +- la modification du chemin racine ; +- la suppression d’une enquête ; +- un CRUD complet ; +- l’ouverture automatique dans l’interface GTK ; +- l’affichage 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 l’enquête courante dans un modèle C indépendant de SQLite. + +Le DAO devient le premier point d’accè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 +``` diff --git a/include/dao/investigation_dao.h b/include/dao/investigation_dao.h new file mode 100644 index 0000000..486838f --- /dev/null +++ b/include/dao/investigation_dao.h @@ -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 diff --git a/include/database/error.h b/include/database/error.h index 3eff2ff..6fa9860 100644 --- a/include/database/error.h +++ b/include/database/error.h @@ -44,6 +44,21 @@ const char *database_error_get_message( 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. * diff --git a/include/models/investigation_record.h b/include/models/investigation_record.h new file mode 100644 index 0000000..33dea10 --- /dev/null +++ b/include/models/investigation_record.h @@ -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 diff --git a/labfy-investigation b/labfy-investigation index 21dd012..c28c4e8 100755 Binary files a/labfy-investigation and b/labfy-investigation differ diff --git a/src/dao/investigation_dao.c b/src/dao/investigation_dao.c new file mode 100644 index 0000000..177700b --- /dev/null +++ b/src/dao/investigation_dao.c @@ -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 + +/** + * @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; +} diff --git a/src/database/error.c b/src/database/error.c index 1a77f9a..7df89ff 100644 --- a/src/database/error.c +++ b/src/database/error.c @@ -22,6 +22,19 @@ const char *database_error_get_message( 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( Database *database ) diff --git a/src/database/statement.c b/src/database/statement.c index 67b4c03..5825b97 100644 --- a/src/database/statement.c +++ b/src/database/statement.c @@ -590,33 +590,24 @@ void database_statement_finalize( DatabaseStatement *statement ) { - sqlite3 *database_handle = NULL; - int result = SQLITE_OK; - if (statement == NULL) { return; } - database_handle = database_get_handle( - statement->database - ); - 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 ); - if (result != SQLITE_OK) - { - g_warning( - "Impossible de finaliser la requête SQL : %s", - database_handle != NULL - ? sqlite3_errmsg(database_handle) - : sqlite3_errstr(result) - ); - } + statement->handle = NULL; } g_free(statement); diff --git a/src/models/investigation_record.c b/src/models/investigation_record.c new file mode 100644 index 0000000..20f5361 --- /dev/null +++ b/src/models/investigation_record.c @@ -0,0 +1,199 @@ +/****************************************************************************** + * @file investigation_record.c + * @brief Implémentation du modèle InvestigationRecord. + ******************************************************************************/ + +#include "models/investigation_record.h" + +#include +#include + +/** + * @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; +} diff --git a/tests/test_database b/tests/test_database index c402d3f..2a1ae25 100755 Binary files a/tests/test_database and b/tests/test_database differ diff --git a/tests/test_database.c b/tests/test_database.c index 9404a18..1741900 100644 --- a/tests/test_database.c +++ b/tests/test_database.c @@ -133,6 +133,7 @@ static void test_database_initialize_valid_database(void) char *investigation_created_at = NULL; char *investigation_id = NULL; char *investigation_count = NULL; + char *investigation_updated_at = NULL; sqlite3 *database = NULL; GError *error = NULL; @@ -231,6 +232,12 @@ static void test_database_initialize_valid_database(void) "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( database, "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_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); assert(result == SQLITE_OK); diff --git a/tests/test_error b/tests/test_error index 60f0127..1185635 100755 Binary files a/tests/test_error and b/tests/test_error differ diff --git a/tests/test_investigation_dao b/tests/test_investigation_dao new file mode 100755 index 0000000..0234ddd Binary files /dev/null and b/tests/test_investigation_dao differ diff --git a/tests/test_investigation_dao.c b/tests/test_investigation_dao.c new file mode 100644 index 0000000..5b9cc18 --- /dev/null +++ b/tests/test_investigation_dao.c @@ -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 +#include +#include + +#include +#include + +/** + * @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; +} diff --git a/tests/test_investigation_project b/tests/test_investigation_project index ebee53a..ab7a276 100755 Binary files a/tests/test_investigation_project and b/tests/test_investigation_project differ diff --git a/tests/test_investigation_record b/tests/test_investigation_record new file mode 100755 index 0000000..ba2880b Binary files /dev/null and b/tests/test_investigation_record differ diff --git a/tests/test_investigation_record.c b/tests/test_investigation_record.c new file mode 100644 index 0000000..ef5c1e1 --- /dev/null +++ b/tests/test_investigation_record.c @@ -0,0 +1,269 @@ +/****************************************************************************** + * @file test_investigation_record.c + * @brief Tests du modèle InvestigationRecord. + ******************************************************************************/ + +#include "models/investigation_record.h" + +#include +#include +#include + +/** + * @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; +} diff --git a/tests/test_statement b/tests/test_statement index d1be0bc..79b937b 100755 Binary files a/tests/test_statement and b/tests/test_statement differ diff --git a/tests/test_transaction b/tests/test_transaction index 7291ecd..3533f19 100755 Binary files a/tests/test_transaction and b/tests/test_transaction differ