feat(graph): integrate dropped extractions
This commit is contained in:
parent
8adfc3f106
commit
33a4a8be10
16 changed files with 964 additions and 51 deletions
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Glisser-déposer confirmé des extractions texte vers le graphe, avec
|
||||||
|
persistance de leur association à une entité.
|
||||||
|
|
||||||
- Initialisation du cycle de vie de l'application.
|
- Initialisation du cycle de vie de l'application.
|
||||||
- Création du module `Application`.
|
- Création du module `Application`.
|
||||||
- Intégration de GTK4.
|
- Intégration de GTK4.
|
||||||
|
|
|
||||||
24
Makefile
24
Makefile
|
|
@ -128,6 +128,7 @@ TEST_EML_ANALYZER := tests/test_eml_analyzer
|
||||||
TEST_IBAN_ANALYZER := tests/test_iban_analyzer
|
TEST_IBAN_ANALYZER := tests/test_iban_analyzer
|
||||||
TEST_EXIFTOOL_METADATA := tests/test_exiftool_metadata
|
TEST_EXIFTOOL_METADATA := tests/test_exiftool_metadata
|
||||||
TEST_PDF_PASSWORD_RECOVERY := tests/test_pdf_password_recovery
|
TEST_PDF_PASSWORD_RECOVERY := tests/test_pdf_password_recovery
|
||||||
|
TEST_EXTRACTION_DROP_SERVICE := tests/test_extraction_drop_service
|
||||||
|
|
||||||
all: $(TARGET)
|
all: $(TARGET)
|
||||||
|
|
||||||
|
|
@ -197,6 +198,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_EXTRACTION_DROP_SERVICE): \
|
||||||
|
tests/test_extraction_drop_service.c \
|
||||||
|
src/core/extraction_drop_service.c \
|
||||||
|
src/core/file_hash.c \
|
||||||
|
src/dao/evidence_dao.c \
|
||||||
|
src/dao/entity_dao.c \
|
||||||
|
src/dao/evidence_entity_dao.c \
|
||||||
|
src/models/evidence_record.c \
|
||||||
|
src/models/entity_record.c \
|
||||||
|
src/database/database.c \
|
||||||
|
src/database/schema.c \
|
||||||
|
src/database/statement.c \
|
||||||
|
src/database/transaction.c \
|
||||||
|
src/database/error.c
|
||||||
|
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
|
||||||
|
|
||||||
$(TEST_INVESTIGATION_RECORD): \
|
$(TEST_INVESTIGATION_RECORD): \
|
||||||
tests/test_investigation_record.c \
|
tests/test_investigation_record.c \
|
||||||
src/models/investigation_record.c
|
src/models/investigation_record.c
|
||||||
|
|
@ -746,7 +763,8 @@ test: \
|
||||||
$(TEST_EML_ANALYZER) \
|
$(TEST_EML_ANALYZER) \
|
||||||
$(TEST_IBAN_ANALYZER) \
|
$(TEST_IBAN_ANALYZER) \
|
||||||
$(TEST_EXIFTOOL_METADATA) \
|
$(TEST_EXIFTOOL_METADATA) \
|
||||||
$(TEST_PDF_PASSWORD_RECOVERY)
|
$(TEST_PDF_PASSWORD_RECOVERY) \
|
||||||
|
$(TEST_EXTRACTION_DROP_SERVICE)
|
||||||
@echo "Exécution des tests..."
|
@echo "Exécution des tests..."
|
||||||
@./$(TEST_NODE)
|
@./$(TEST_NODE)
|
||||||
@./$(TEST_TREE_MODEL)
|
@./$(TEST_TREE_MODEL)
|
||||||
|
|
@ -809,6 +827,7 @@ test: \
|
||||||
@$(TEST_IBAN_ANALYZER)
|
@$(TEST_IBAN_ANALYZER)
|
||||||
@$(TEST_EXIFTOOL_METADATA)
|
@$(TEST_EXIFTOOL_METADATA)
|
||||||
@$(TEST_PDF_PASSWORD_RECOVERY)
|
@$(TEST_PDF_PASSWORD_RECOVERY)
|
||||||
|
@$(TEST_EXTRACTION_DROP_SERVICE)
|
||||||
@echo "Tous les tests sont valides."
|
@echo "Tous les tests sont valides."
|
||||||
|
|
||||||
%.o: %.c
|
%.o: %.c
|
||||||
|
|
@ -874,7 +893,8 @@ clean:
|
||||||
$(TEST_SOCIAL_ACCOUNT_SERVICE) \
|
$(TEST_SOCIAL_ACCOUNT_SERVICE) \
|
||||||
$(TEST_SOCIAL_PLATFORM) \
|
$(TEST_SOCIAL_PLATFORM) \
|
||||||
$(TEST_PERSON_ENTITY_SERVICE) \
|
$(TEST_PERSON_ENTITY_SERVICE) \
|
||||||
$(TEST_EML_ANALYZER)
|
$(TEST_EML_ANALYZER) \
|
||||||
|
$(TEST_EXTRACTION_DROP_SERVICE)
|
||||||
|
|
||||||
-include $(DEP)
|
-include $(DEP)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,9 @@ Le socle actuel comprend notamment :
|
||||||
sorties standard et d'erreur, et objets créés ou réutilisés ;
|
sorties standard et d'erreur, et objets créés ou réutilisés ;
|
||||||
- vérification manuelle de l'intégrité des sorties OSINT enregistrées, sans
|
- vérification manuelle de l'intégrité des sorties OSINT enregistrées, sans
|
||||||
réécriture de l'empreinte ou des données contrôlées.
|
réécriture de l'empreinte ou des données contrôlées.
|
||||||
|
- glisser-déposer des extractions texte depuis l'arborescence vers le graphe,
|
||||||
|
avec confirmation explicite, création d'entité ou rattachement à une entité
|
||||||
|
existante, sans déplacement du fichier produit ;
|
||||||
|
|
||||||
Les outils actuellement présents dans le catalogue initial sont :
|
Les outils actuellement présents dans le catalogue initial sont :
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
# Ticket 105 — Glisser-déposer des extractions vers le graphe
|
|
||||||
|
|
||||||
## Objectif
|
|
||||||
|
|
||||||
Permettre de prendre une extraction depuis `Dossier → Extractions` et de la
|
|
||||||
déposer dans le graphe afin de créer ou compléter les liens de l’enquête.
|
|
||||||
|
|
||||||
## Fonctionnement attendu
|
|
||||||
|
|
||||||
1. Une extraction sélectionnée dans la Sidebar peut être déplacée.
|
|
||||||
2. Le graphe accepte le dépôt uniquement sur une zone valide.
|
|
||||||
3. Une fenêtre de confirmation affiche le fichier, sa source et son contenu
|
|
||||||
textuel disponible.
|
|
||||||
4. L’utilisateur choisit explicitement :
|
|
||||||
- créer une nouvelle entité à partir de l’extraction ;
|
|
||||||
- rattacher l’extraction à une entité existante ;
|
|
||||||
- annuler.
|
|
||||||
5. L’association est enregistrée en base et visible dans la fiche concernée.
|
|
||||||
6. Une relation graphique peut être créée sans déplacer ni dupliquer le
|
|
||||||
fichier original.
|
|
||||||
|
|
||||||
## Contraintes
|
|
||||||
|
|
||||||
- Ne jamais créer de relation silencieusement.
|
|
||||||
- Refuser les fichiers hors du dossier de l’enquête.
|
|
||||||
- Conserver les associations déjà présentes.
|
|
||||||
- Utiliser les API GTK de glisser-déposer et les commentaires Doxygen prévus
|
|
||||||
par le projet.
|
|
||||||
|
|
||||||
## Critères d’acceptation
|
|
||||||
|
|
||||||
- Un fichier `.txt` d’OSINT est déplaçable depuis la Sidebar.
|
|
||||||
- Le dépôt sur le graphe ouvre le dialogue de choix.
|
|
||||||
- L’entité ou l’association créée survit à la réouverture de l’enquête.
|
|
||||||
- Les dépôts annulés ne modifient ni la base ni le graphe.
|
|
||||||
- Tests unitaires des chemins invalides, du choix d’action et de la
|
|
||||||
persistance.
|
|
||||||
|
|
||||||
## Vérification
|
|
||||||
|
|
||||||
```sh
|
|
||||||
make -j2
|
|
||||||
make test
|
|
||||||
```
|
|
||||||
|
|
||||||
76
include/core/extraction_drop_service.h
Normal file
76
include/core/extraction_drop_service.h
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @file extraction_drop_service.h
|
||||||
|
* @brief Validation et intégration d'une extraction déposée sur le graphe.
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
#ifndef LABFY_INVESTIGATION_EXTRACTION_DROP_SERVICE_H
|
||||||
|
#define LABFY_INVESTIGATION_EXTRACTION_DROP_SERVICE_H
|
||||||
|
|
||||||
|
#include "database/database.h"
|
||||||
|
|
||||||
|
#include <glib.h>
|
||||||
|
|
||||||
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
|
typedef struct ExtractionDropInfo ExtractionDropInfo;
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_OUTSIDE_INVESTIGATION,
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_INVALID_FILE,
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_READ,
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_DATABASE
|
||||||
|
} ExtractionDropServiceError;
|
||||||
|
|
||||||
|
#define EXTRACTION_DROP_SERVICE_ERROR extraction_drop_service_error_quark()
|
||||||
|
|
||||||
|
GQuark extraction_drop_service_error_quark(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Valide et lit une extraction texte appartenant à l'enquête.
|
||||||
|
*
|
||||||
|
* Le fichier doit être un `.txt` régulier situé sous
|
||||||
|
* `02_Preuves_Traitees/Extractions`. Aucun fichier n'est déplacé ou modifié.
|
||||||
|
*/
|
||||||
|
ExtractionDropInfo *extraction_drop_service_prepare(
|
||||||
|
const char *investigation_root,
|
||||||
|
const char *file_path,
|
||||||
|
GError **error
|
||||||
|
);
|
||||||
|
|
||||||
|
void extraction_drop_info_free(ExtractionDropInfo *info);
|
||||||
|
const char *extraction_drop_info_get_file_path(const ExtractionDropInfo *info);
|
||||||
|
const char *extraction_drop_info_get_relative_path(
|
||||||
|
const ExtractionDropInfo *info);
|
||||||
|
const char *extraction_drop_info_get_file_name(const ExtractionDropInfo *info);
|
||||||
|
const char *extraction_drop_info_get_source(const ExtractionDropInfo *info);
|
||||||
|
const char *extraction_drop_info_get_content(const ExtractionDropInfo *info);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enregistre l'extraction comme preuve traitée et la lie à une entité.
|
||||||
|
*
|
||||||
|
* Une preuve ou une association déjà présente est réutilisée.
|
||||||
|
*/
|
||||||
|
gboolean extraction_drop_service_attach(
|
||||||
|
Database *database,
|
||||||
|
const ExtractionDropInfo *info,
|
||||||
|
const char *entity_identifier,
|
||||||
|
GError **error
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Crée une entité générique puis lui associe l'extraction.
|
||||||
|
*
|
||||||
|
* @return UUID nouvellement alloué dans out_entity_identifier, si demandé.
|
||||||
|
*/
|
||||||
|
gboolean extraction_drop_service_create_entity(
|
||||||
|
Database *database,
|
||||||
|
const ExtractionDropInfo *info,
|
||||||
|
char **out_entity_identifier,
|
||||||
|
GError **error
|
||||||
|
);
|
||||||
|
|
||||||
|
G_END_DECLS
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -126,6 +126,12 @@ typedef void (*MainWindowGraphNodeMovedCallback)(
|
||||||
gpointer user_data
|
gpointer user_data
|
||||||
);
|
);
|
||||||
|
|
||||||
|
typedef void (*MainWindowExtractionDropCallback)(
|
||||||
|
const char *file_path,
|
||||||
|
const char *target_entity_identifier,
|
||||||
|
gpointer user_data
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Callback appelé lors d'une demande de réinitialisation du graphe.
|
* @brief Callback appelé lors d'une demande de réinitialisation du graphe.
|
||||||
*
|
*
|
||||||
|
|
@ -246,6 +252,12 @@ void main_window_set_graph_node_moved_callback(
|
||||||
gpointer user_data
|
gpointer user_data
|
||||||
);
|
);
|
||||||
|
|
||||||
|
void main_window_set_extraction_drop_callback(
|
||||||
|
MainWindow *main_window,
|
||||||
|
MainWindowExtractionDropCallback callback,
|
||||||
|
gpointer user_data
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Définit le callback de demande de réinitialisation du graphe.
|
* @brief Définit le callback de demande de réinitialisation du graphe.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,13 @@ typedef void (*InvestigationGraphViewNodeMovedCallback)(
|
||||||
gpointer user_data
|
gpointer user_data
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** @brief Callback appelé lorsqu'une extraction est déposée sur le graphe. */
|
||||||
|
typedef void (*InvestigationGraphViewExtractionDropCallback)(
|
||||||
|
const char *file_path,
|
||||||
|
const char *target_entity_identifier,
|
||||||
|
gpointer user_data
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Crée une nouvelle vue graphique vide.
|
* @brief Crée une nouvelle vue graphique vide.
|
||||||
*
|
*
|
||||||
|
|
@ -170,6 +177,12 @@ void investigation_graph_view_set_node_moved_callback(
|
||||||
gpointer user_data
|
gpointer user_data
|
||||||
);
|
);
|
||||||
|
|
||||||
|
void investigation_graph_view_set_extraction_drop_callback(
|
||||||
|
InvestigationGraphView *graph_view,
|
||||||
|
InvestigationGraphViewExtractionDropCallback callback,
|
||||||
|
gpointer user_data
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Retourne l'entité actuellement sélectionnée.
|
* @brief Retourne l'entité actuellement sélectionnée.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,12 @@ typedef void (*WorkspaceGraphNodeMovedCallback)(
|
||||||
gpointer user_data
|
gpointer user_data
|
||||||
);
|
);
|
||||||
|
|
||||||
|
typedef void (*WorkspaceExtractionDropCallback)(
|
||||||
|
const char *file_path,
|
||||||
|
const char *target_entity_identifier,
|
||||||
|
gpointer user_data
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Callback appelé lors d'une demande de réinitialisation du graphe.
|
* @brief Callback appelé lors d'une demande de réinitialisation du graphe.
|
||||||
*
|
*
|
||||||
|
|
@ -317,6 +323,12 @@ void workspace_set_graph_node_moved_callback(
|
||||||
gpointer user_data
|
gpointer user_data
|
||||||
);
|
);
|
||||||
|
|
||||||
|
void workspace_set_extraction_drop_callback(
|
||||||
|
Workspace *workspace,
|
||||||
|
WorkspaceExtractionDropCallback callback,
|
||||||
|
gpointer user_data
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Définit le callback de demande de réinitialisation du graphe.
|
* @brief Définit le callback de demande de réinitialisation du graphe.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@
|
||||||
#include "core/exiftool_metadata.h"
|
#include "core/exiftool_metadata.h"
|
||||||
#include "views/metadata_analysis_dialog.h"
|
#include "views/metadata_analysis_dialog.h"
|
||||||
#include "core/pdf_password_recovery.h"
|
#include "core/pdf_password_recovery.h"
|
||||||
|
#include "core/extraction_drop_service.h"
|
||||||
#include "views/pdf_password_dialog.h"
|
#include "views/pdf_password_dialog.h"
|
||||||
|
|
||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
|
|
@ -225,6 +226,13 @@ typedef struct
|
||||||
char *started_at;
|
char *started_at;
|
||||||
} ApplicationOsintActionContext;
|
} ApplicationOsintActionContext;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
Application *application;
|
||||||
|
ExtractionDropInfo *info;
|
||||||
|
char *target_entity_identifier;
|
||||||
|
} ApplicationExtractionDropContext;
|
||||||
|
|
||||||
/** @brief Données possédées par l'étape de révision OSINT. */
|
/** @brief Données possédées par l'étape de révision OSINT. */
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
|
|
@ -250,6 +258,112 @@ static void application_present_error(
|
||||||
const char *message
|
const char *message
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static void application_extraction_drop_context_free(
|
||||||
|
ApplicationExtractionDropContext *context)
|
||||||
|
{
|
||||||
|
if (context == NULL) return;
|
||||||
|
extraction_drop_info_free(context->info);
|
||||||
|
g_free(context->target_entity_identifier);
|
||||||
|
g_free(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void application_on_extraction_choice_finished(
|
||||||
|
GObject *source_object, GAsyncResult *result, gpointer user_data)
|
||||||
|
{
|
||||||
|
ApplicationExtractionDropContext *context = user_data;
|
||||||
|
Application *application = context != NULL ? context->application : NULL;
|
||||||
|
GtkAlertDialog *dialog = GTK_ALERT_DIALOG(source_object);
|
||||||
|
const InvestigationProject *project = NULL;
|
||||||
|
Database *database = NULL;
|
||||||
|
GError *error = NULL;
|
||||||
|
int choice = gtk_alert_dialog_choose_finish(dialog, result, &error);
|
||||||
|
gboolean success = FALSE;
|
||||||
|
|
||||||
|
if (application == NULL || application->session == NULL) goto cleanup;
|
||||||
|
if (error != NULL || choice == 0)
|
||||||
|
{
|
||||||
|
main_window_set_status(application->main_window,
|
||||||
|
"Dépôt de l'extraction annulé.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
database = investigation_session_get_database(application->session);
|
||||||
|
if (choice == 1)
|
||||||
|
success = extraction_drop_service_create_entity(database,
|
||||||
|
context->info, NULL, &error);
|
||||||
|
else if (choice == 2 && context->target_entity_identifier != NULL)
|
||||||
|
success = extraction_drop_service_attach(database, context->info,
|
||||||
|
context->target_entity_identifier, &error);
|
||||||
|
else
|
||||||
|
g_set_error_literal(&error, EXTRACTION_DROP_SERVICE_ERROR,
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
|
||||||
|
"Déposez l'extraction directement sur une entité pour la rattacher.");
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
application_present_error(application,
|
||||||
|
"Intégration de l'extraction impossible",
|
||||||
|
error != NULL ? error->message : "L'association n'a pas été créée.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
main_window_set_status(application->main_window,
|
||||||
|
choice == 1 ? "Entité créée depuis l'extraction."
|
||||||
|
: "Extraction rattachée à l'entité.");
|
||||||
|
project = investigation_session_get_project(application->session);
|
||||||
|
if (project != NULL)
|
||||||
|
application_start_graph_loading(application,
|
||||||
|
investigation_project_get_database_path(project));
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
g_clear_error(&error);
|
||||||
|
application_extraction_drop_context_free(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void application_on_extraction_dropped(
|
||||||
|
const char *file_path,
|
||||||
|
const char *target_entity_identifier,
|
||||||
|
gpointer user_data)
|
||||||
|
{
|
||||||
|
Application *application = user_data;
|
||||||
|
ApplicationExtractionDropContext *context = NULL;
|
||||||
|
const InvestigationProject *project = NULL;
|
||||||
|
GtkAlertDialog *dialog = NULL;
|
||||||
|
const char *buttons[] = {
|
||||||
|
"Annuler", "Créer une entité", "Rattacher à l'entité", NULL
|
||||||
|
};
|
||||||
|
char *details = NULL;
|
||||||
|
GError *error = NULL;
|
||||||
|
|
||||||
|
if (application == NULL || application->session == NULL) return;
|
||||||
|
project = investigation_session_get_project(application->session);
|
||||||
|
context = g_new0(ApplicationExtractionDropContext, 1);
|
||||||
|
context->application = application;
|
||||||
|
context->target_entity_identifier = g_strdup(target_entity_identifier);
|
||||||
|
context->info = extraction_drop_service_prepare(
|
||||||
|
investigation_project_get_root_path(project), file_path, &error);
|
||||||
|
if (context->info == NULL)
|
||||||
|
{
|
||||||
|
application_present_error(application, "Dépôt refusé",
|
||||||
|
error != NULL ? error->message : "Extraction invalide.");
|
||||||
|
g_clear_error(&error);
|
||||||
|
application_extraction_drop_context_free(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
details = g_strdup_printf("Fichier : %s\nSource : %s\n\n%s",
|
||||||
|
extraction_drop_info_get_file_name(context->info),
|
||||||
|
extraction_drop_info_get_source(context->info),
|
||||||
|
extraction_drop_info_get_content(context->info));
|
||||||
|
dialog = gtk_alert_dialog_new("Intégrer cette extraction au graphe ?");
|
||||||
|
gtk_alert_dialog_set_detail(dialog, details);
|
||||||
|
gtk_alert_dialog_set_buttons(dialog, buttons);
|
||||||
|
gtk_alert_dialog_set_cancel_button(dialog, 0);
|
||||||
|
gtk_alert_dialog_set_default_button(dialog, 0);
|
||||||
|
gtk_alert_dialog_choose(dialog,
|
||||||
|
main_window_get_window(application->main_window), NULL,
|
||||||
|
application_on_extraction_choice_finished, context);
|
||||||
|
g_object_unref(dialog);
|
||||||
|
g_free(details);
|
||||||
|
}
|
||||||
|
|
||||||
static void application_present_next_batch_metadata(
|
static void application_present_next_batch_metadata(
|
||||||
ApplicationEvidenceBatchContext *context
|
ApplicationEvidenceBatchContext *context
|
||||||
);
|
);
|
||||||
|
|
@ -7644,6 +7758,9 @@ static void application_on_activate(
|
||||||
application_on_graph_node_moved,
|
application_on_graph_node_moved,
|
||||||
application
|
application
|
||||||
);
|
);
|
||||||
|
main_window_set_extraction_drop_callback(
|
||||||
|
application->main_window, application_on_extraction_dropped,
|
||||||
|
application);
|
||||||
|
|
||||||
main_window_set_reset_graph_layout_callback(
|
main_window_set_reset_graph_layout_callback(
|
||||||
application->main_window,
|
application->main_window,
|
||||||
|
|
|
||||||
361
src/core/extraction_drop_service.c
Normal file
361
src/core/extraction_drop_service.c
Normal file
|
|
@ -0,0 +1,361 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @file extraction_drop_service.c
|
||||||
|
* @brief Validation et intégration d'une extraction déposée sur le graphe.
|
||||||
|
******************************************************************************/
|
||||||
|
|
||||||
|
#include "core/extraction_drop_service.h"
|
||||||
|
|
||||||
|
#include "core/file_hash.h"
|
||||||
|
#include "dao/entity_dao.h"
|
||||||
|
#include "dao/evidence_dao.h"
|
||||||
|
#include "dao/evidence_entity_dao.h"
|
||||||
|
#include "database/transaction.h"
|
||||||
|
#include "models/entity_record.h"
|
||||||
|
#include "models/evidence_record.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
struct ExtractionDropInfo
|
||||||
|
{
|
||||||
|
char *file_path;
|
||||||
|
char *relative_path;
|
||||||
|
char *file_name;
|
||||||
|
char *source;
|
||||||
|
char *content;
|
||||||
|
};
|
||||||
|
|
||||||
|
GQuark extraction_drop_service_error_quark(void)
|
||||||
|
{
|
||||||
|
return g_quark_from_static_string("extraction-drop-service-error");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void extraction_drop_service_set_error(
|
||||||
|
GError **error,
|
||||||
|
ExtractionDropServiceError code,
|
||||||
|
const char *message)
|
||||||
|
{
|
||||||
|
if (error != NULL && *error == NULL)
|
||||||
|
{
|
||||||
|
g_set_error_literal(error, EXTRACTION_DROP_SERVICE_ERROR, code, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *extraction_drop_service_timestamp(void)
|
||||||
|
{
|
||||||
|
GDateTime *now = g_date_time_new_now_utc();
|
||||||
|
char *timestamp = now != NULL
|
||||||
|
? g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ") : NULL;
|
||||||
|
g_clear_pointer(&now, g_date_time_unref);
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *extraction_drop_service_source_from_content(const char *content)
|
||||||
|
{
|
||||||
|
const char *line_end = NULL;
|
||||||
|
if (content == NULL || content[0] == '\0')
|
||||||
|
{
|
||||||
|
return g_strdup("Extraction locale");
|
||||||
|
}
|
||||||
|
line_end = strchr(content, '\n');
|
||||||
|
if (line_end != NULL && line_end > content)
|
||||||
|
{
|
||||||
|
return g_strndup(content, (gsize) (line_end - content));
|
||||||
|
}
|
||||||
|
return g_strdup(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtractionDropInfo *extraction_drop_service_prepare(
|
||||||
|
const char *investigation_root,
|
||||||
|
const char *file_path,
|
||||||
|
GError **error)
|
||||||
|
{
|
||||||
|
ExtractionDropInfo *info = NULL;
|
||||||
|
char *canonical_root = NULL;
|
||||||
|
char *extractions_root = NULL;
|
||||||
|
char *canonical_extractions_root = NULL;
|
||||||
|
char *canonical_file = NULL;
|
||||||
|
char *content = NULL;
|
||||||
|
gsize content_length = 0U;
|
||||||
|
|
||||||
|
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
|
||||||
|
if (investigation_root == NULL || investigation_root[0] == '\0' ||
|
||||||
|
file_path == NULL || file_path[0] == '\0')
|
||||||
|
{
|
||||||
|
extraction_drop_service_set_error(error,
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
|
||||||
|
"Le chemin de l'extraction est invalide.");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
canonical_root = g_canonicalize_filename(investigation_root, NULL);
|
||||||
|
extractions_root = g_build_filename(canonical_root,
|
||||||
|
"02_Preuves_Traitees", "Extractions", NULL);
|
||||||
|
canonical_extractions_root = g_canonicalize_filename(extractions_root, NULL);
|
||||||
|
canonical_file = g_canonicalize_filename(file_path, NULL);
|
||||||
|
if (canonical_root == NULL || canonical_extractions_root == NULL ||
|
||||||
|
canonical_file == NULL)
|
||||||
|
{
|
||||||
|
extraction_drop_service_set_error(error,
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
|
||||||
|
"Impossible de normaliser le chemin de l'extraction.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
if (!g_str_has_prefix(canonical_file, canonical_extractions_root) ||
|
||||||
|
canonical_file[strlen(canonical_extractions_root)] != G_DIR_SEPARATOR)
|
||||||
|
{
|
||||||
|
extraction_drop_service_set_error(error,
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_OUTSIDE_INVESTIGATION,
|
||||||
|
"Le fichier ne se trouve pas dans les extractions de l'enquête.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
if (!g_file_test(canonical_file, G_FILE_TEST_IS_REGULAR) ||
|
||||||
|
g_file_test(canonical_file, G_FILE_TEST_IS_SYMLINK) ||
|
||||||
|
!g_str_has_suffix(canonical_file, ".txt"))
|
||||||
|
{
|
||||||
|
extraction_drop_service_set_error(error,
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_INVALID_FILE,
|
||||||
|
"Seuls les fichiers texte réguliers d'extraction sont acceptés.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
if (!g_file_get_contents(canonical_file, &content, &content_length, error))
|
||||||
|
{
|
||||||
|
if (error != NULL && *error != NULL)
|
||||||
|
{
|
||||||
|
(*error)->domain = EXTRACTION_DROP_SERVICE_ERROR;
|
||||||
|
(*error)->code = EXTRACTION_DROP_SERVICE_ERROR_READ;
|
||||||
|
}
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
if (!g_utf8_validate(content, (gssize) content_length, NULL))
|
||||||
|
{
|
||||||
|
extraction_drop_service_set_error(error,
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_INVALID_FILE,
|
||||||
|
"L'extraction texte n'est pas encodée en UTF-8.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
info = g_new0(ExtractionDropInfo, 1);
|
||||||
|
info->file_path = g_strdup(canonical_file);
|
||||||
|
info->relative_path = g_strdup(canonical_file + strlen(canonical_root) + 1U);
|
||||||
|
info->file_name = g_path_get_basename(canonical_file);
|
||||||
|
info->content = g_steal_pointer(&content);
|
||||||
|
info->source = extraction_drop_service_source_from_content(info->content);
|
||||||
|
if (info->file_path == NULL || info->relative_path == NULL ||
|
||||||
|
info->file_name == NULL || info->content == NULL || info->source == NULL)
|
||||||
|
{
|
||||||
|
extraction_drop_info_free(info);
|
||||||
|
info = NULL;
|
||||||
|
extraction_drop_service_set_error(error,
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
|
||||||
|
"Impossible d'allouer les informations de l'extraction.");
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
g_free(content);
|
||||||
|
g_free(canonical_file);
|
||||||
|
g_free(canonical_extractions_root);
|
||||||
|
g_free(extractions_root);
|
||||||
|
g_free(canonical_root);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
void extraction_drop_info_free(ExtractionDropInfo *info)
|
||||||
|
{
|
||||||
|
if (info == NULL) return;
|
||||||
|
g_free(info->file_path);
|
||||||
|
g_free(info->relative_path);
|
||||||
|
g_free(info->file_name);
|
||||||
|
g_free(info->source);
|
||||||
|
g_free(info->content);
|
||||||
|
g_free(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define EXTRACTION_DROP_GETTER(name, field) \
|
||||||
|
const char *name(const ExtractionDropInfo *info) \
|
||||||
|
{ return info != NULL ? info->field : NULL; }
|
||||||
|
|
||||||
|
EXTRACTION_DROP_GETTER(extraction_drop_info_get_file_path, file_path)
|
||||||
|
EXTRACTION_DROP_GETTER(extraction_drop_info_get_relative_path, relative_path)
|
||||||
|
EXTRACTION_DROP_GETTER(extraction_drop_info_get_file_name, file_name)
|
||||||
|
EXTRACTION_DROP_GETTER(extraction_drop_info_get_source, source)
|
||||||
|
EXTRACTION_DROP_GETTER(extraction_drop_info_get_content, content)
|
||||||
|
|
||||||
|
static char *extraction_drop_service_ensure_evidence(
|
||||||
|
Database *database,
|
||||||
|
const ExtractionDropInfo *info,
|
||||||
|
GError **error)
|
||||||
|
{
|
||||||
|
EvidenceDao *dao = NULL;
|
||||||
|
EvidenceRecord *record = NULL;
|
||||||
|
GPtrArray *records = NULL;
|
||||||
|
char *identifier = NULL;
|
||||||
|
char *sha256 = NULL;
|
||||||
|
char *timestamp = NULL;
|
||||||
|
guint64 size_bytes = 0U;
|
||||||
|
|
||||||
|
dao = evidence_dao_new(database, error);
|
||||||
|
if (dao == NULL) goto cleanup;
|
||||||
|
records = evidence_dao_list_all(dao, error);
|
||||||
|
if (records == NULL) goto cleanup;
|
||||||
|
for (guint index = 0U; index < records->len; index++)
|
||||||
|
{
|
||||||
|
const EvidenceRecord *candidate = g_ptr_array_index(records, index);
|
||||||
|
if (g_strcmp0(evidence_record_get_relative_path(candidate),
|
||||||
|
info->relative_path) == 0)
|
||||||
|
{
|
||||||
|
identifier = g_strdup(evidence_record_get_identifier(candidate));
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!file_hash_compute_sha256(info->file_path, NULL, &sha256,
|
||||||
|
&size_bytes, error)) goto cleanup;
|
||||||
|
identifier = g_uuid_string_random();
|
||||||
|
timestamp = extraction_drop_service_timestamp();
|
||||||
|
record = evidence_record_new(identifier, info->file_name, info->file_name,
|
||||||
|
info->relative_path, "text", size_bytes, sha256, timestamp, NULL,
|
||||||
|
info->source, "Extraction intégrée depuis le graphe.",
|
||||||
|
EVIDENCE_INTEGRITY_STATUS_VALID, error);
|
||||||
|
if (record == NULL || !evidence_dao_insert(dao, record, error))
|
||||||
|
{
|
||||||
|
g_clear_pointer(&identifier, g_free);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
evidence_record_free(record);
|
||||||
|
g_clear_pointer(&records, g_ptr_array_unref);
|
||||||
|
evidence_dao_free(dao);
|
||||||
|
g_free(timestamp);
|
||||||
|
g_free(sha256);
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean extraction_drop_service_attach_in_transaction(
|
||||||
|
Database *database,
|
||||||
|
const ExtractionDropInfo *info,
|
||||||
|
const char *entity_identifier,
|
||||||
|
GError **error)
|
||||||
|
{
|
||||||
|
EvidenceEntityDao *link_dao = NULL;
|
||||||
|
char *evidence_identifier = NULL;
|
||||||
|
gboolean exists = FALSE;
|
||||||
|
gboolean success = FALSE;
|
||||||
|
|
||||||
|
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||||
|
if (database == NULL || info == NULL || entity_identifier == NULL ||
|
||||||
|
!g_uuid_string_is_valid(entity_identifier))
|
||||||
|
{
|
||||||
|
extraction_drop_service_set_error(error,
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
|
||||||
|
"L'entité de destination est invalide.");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
evidence_identifier = extraction_drop_service_ensure_evidence(database,
|
||||||
|
info, error);
|
||||||
|
if (evidence_identifier == NULL) goto cleanup;
|
||||||
|
link_dao = evidence_entity_dao_new(database, error);
|
||||||
|
if (link_dao == NULL ||
|
||||||
|
!evidence_entity_dao_exists(link_dao, evidence_identifier,
|
||||||
|
entity_identifier, &exists, error) ||
|
||||||
|
(!exists && !evidence_entity_dao_link(link_dao, evidence_identifier,
|
||||||
|
entity_identifier, error))) goto cleanup;
|
||||||
|
success = TRUE;
|
||||||
|
cleanup:
|
||||||
|
evidence_entity_dao_free(link_dao);
|
||||||
|
g_free(evidence_identifier);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean extraction_drop_service_attach(
|
||||||
|
Database *database,
|
||||||
|
const ExtractionDropInfo *info,
|
||||||
|
const char *entity_identifier,
|
||||||
|
GError **error)
|
||||||
|
{
|
||||||
|
gboolean success = FALSE;
|
||||||
|
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||||
|
if (database == NULL || info == NULL || entity_identifier == NULL ||
|
||||||
|
!g_uuid_string_is_valid(entity_identifier))
|
||||||
|
{
|
||||||
|
extraction_drop_service_set_error(error,
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
|
||||||
|
"L'entité de destination est invalide.");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
if (!database_transaction_begin(database)) goto database_failure;
|
||||||
|
if (!extraction_drop_service_attach_in_transaction(database, info,
|
||||||
|
entity_identifier, error))
|
||||||
|
{
|
||||||
|
database_transaction_rollback(database);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
if (!database_transaction_commit(database)) goto database_failure;
|
||||||
|
success = TRUE;
|
||||||
|
return success;
|
||||||
|
|
||||||
|
database_failure:
|
||||||
|
extraction_drop_service_set_error(error,
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_DATABASE,
|
||||||
|
"Impossible d'enregistrer l'association de l'extraction.");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean extraction_drop_service_create_entity(
|
||||||
|
Database *database,
|
||||||
|
const ExtractionDropInfo *info,
|
||||||
|
char **out_entity_identifier,
|
||||||
|
GError **error)
|
||||||
|
{
|
||||||
|
EntityDao *entity_dao = NULL;
|
||||||
|
EntityRecord *entity = NULL;
|
||||||
|
char *identifier = NULL;
|
||||||
|
char *timestamp = NULL;
|
||||||
|
gboolean success = FALSE;
|
||||||
|
|
||||||
|
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||||
|
if (out_entity_identifier != NULL) *out_entity_identifier = NULL;
|
||||||
|
if (database == NULL || info == NULL)
|
||||||
|
{
|
||||||
|
extraction_drop_service_set_error(error,
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
|
||||||
|
"Les paramètres de création d'entité sont invalides.");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
identifier = g_uuid_string_random();
|
||||||
|
timestamp = extraction_drop_service_timestamp();
|
||||||
|
entity = entity_record_new(identifier, "other", info->file_name,
|
||||||
|
info->file_name, info->content, 50, timestamp, timestamp,
|
||||||
|
ENTITY_STATUS_ACTIVE, error);
|
||||||
|
if (!database_transaction_begin(database))
|
||||||
|
{
|
||||||
|
extraction_drop_service_set_error(error,
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_DATABASE,
|
||||||
|
"Impossible de démarrer la création de l'entité.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
entity_dao = entity_dao_new(database, error);
|
||||||
|
if (entity == NULL || entity_dao == NULL ||
|
||||||
|
!entity_dao_insert(entity_dao, entity, error) ||
|
||||||
|
!extraction_drop_service_attach_in_transaction(database, info,
|
||||||
|
identifier, error))
|
||||||
|
{
|
||||||
|
database_transaction_rollback(database);
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
if (!database_transaction_commit(database))
|
||||||
|
{
|
||||||
|
extraction_drop_service_set_error(error,
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_DATABASE,
|
||||||
|
"Impossible de valider la création de l'entité.");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
if (out_entity_identifier != NULL)
|
||||||
|
*out_entity_identifier = g_strdup(identifier);
|
||||||
|
success = TRUE;
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
entity_dao_free(entity_dao);
|
||||||
|
entity_record_free(entity);
|
||||||
|
g_free(timestamp);
|
||||||
|
g_free(identifier);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
@ -123,6 +123,9 @@ struct MainWindow
|
||||||
gpointer
|
gpointer
|
||||||
graph_node_moved_user_data;
|
graph_node_moved_user_data;
|
||||||
|
|
||||||
|
MainWindowExtractionDropCallback extraction_drop_callback;
|
||||||
|
gpointer extraction_drop_user_data;
|
||||||
|
|
||||||
MainWindowResetGraphLayoutCallback
|
MainWindowResetGraphLayoutCallback
|
||||||
reset_graph_layout_callback;
|
reset_graph_layout_callback;
|
||||||
|
|
||||||
|
|
@ -508,6 +511,15 @@ static void main_window_on_graph_node_moved(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void main_window_on_extraction_dropped(const char *file_path,
|
||||||
|
const char *target_entity_identifier, gpointer user_data)
|
||||||
|
{
|
||||||
|
MainWindow *main_window = user_data;
|
||||||
|
if (main_window != NULL && main_window->extraction_drop_callback != NULL)
|
||||||
|
main_window->extraction_drop_callback(file_path,
|
||||||
|
target_entity_identifier, main_window->extraction_drop_user_data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Relaie la demande de vérification provenant du Workspace.
|
* @brief Relaie la demande de vérification provenant du Workspace.
|
||||||
*/
|
*/
|
||||||
|
|
@ -954,6 +966,8 @@ MainWindow *main_window_new(
|
||||||
main_window_on_graph_node_moved,
|
main_window_on_graph_node_moved,
|
||||||
main_window
|
main_window
|
||||||
);
|
);
|
||||||
|
workspace_set_extraction_drop_callback(main_window->workspace,
|
||||||
|
main_window_on_extraction_dropped, main_window);
|
||||||
|
|
||||||
workspace_set_reset_graph_layout_callback(
|
workspace_set_reset_graph_layout_callback(
|
||||||
main_window->workspace,
|
main_window->workspace,
|
||||||
|
|
@ -1756,6 +1770,16 @@ void main_window_set_graph_node_moved_callback(
|
||||||
user_data;
|
user_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void main_window_set_extraction_drop_callback(
|
||||||
|
MainWindow *main_window,
|
||||||
|
MainWindowExtractionDropCallback callback,
|
||||||
|
gpointer user_data)
|
||||||
|
{
|
||||||
|
if (main_window == NULL) return;
|
||||||
|
main_window->extraction_drop_callback = callback;
|
||||||
|
main_window->extraction_drop_user_data = user_data;
|
||||||
|
}
|
||||||
|
|
||||||
void main_window_set_reset_graph_layout_callback(
|
void main_window_set_reset_graph_layout_callback(
|
||||||
MainWindow *main_window,
|
MainWindow *main_window,
|
||||||
MainWindowResetGraphLayoutCallback callback,
|
MainWindowResetGraphLayoutCallback callback,
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ struct EntityDetailsPanel
|
||||||
GtkWidget *person_name_button;
|
GtkWidget *person_name_button;
|
||||||
GtkWidget *person_evidence_button;
|
GtkWidget *person_evidence_button;
|
||||||
GtkWidget *person_evidence_summary_label;
|
GtkWidget *person_evidence_summary_label;
|
||||||
|
GtkWidget *entity_evidence_box;
|
||||||
|
|
||||||
char *selected_entity_identifier;
|
char *selected_entity_identifier;
|
||||||
char *selected_entity_type;
|
char *selected_entity_type;
|
||||||
|
|
@ -846,6 +847,8 @@ EntityDetailsPanel *entity_details_panel_new(void)
|
||||||
GTK_WIDGET(details_panel->person_name_entry));
|
GTK_WIDGET(details_panel->person_name_entry));
|
||||||
gtk_box_append(GTK_BOX(details_panel->person_role_box),
|
gtk_box_append(GTK_BOX(details_panel->person_role_box),
|
||||||
details_panel->person_name_button);
|
details_panel->person_name_button);
|
||||||
|
details_panel->entity_evidence_box = gtk_box_new(
|
||||||
|
GTK_ORIENTATION_VERTICAL, 6);
|
||||||
details_panel->person_evidence_button = gtk_button_new_with_label(
|
details_panel->person_evidence_button = gtk_button_new_with_label(
|
||||||
"Gérer les pièces jointes");
|
"Gérer les pièces jointes");
|
||||||
details_panel->person_evidence_summary_label = gtk_label_new(
|
details_panel->person_evidence_summary_label = gtk_label_new(
|
||||||
|
|
@ -856,13 +859,14 @@ EntityDetailsPanel *entity_details_panel_new(void)
|
||||||
details_panel->person_evidence_summary_label), TRUE);
|
details_panel->person_evidence_summary_label), TRUE);
|
||||||
gtk_label_set_selectable(GTK_LABEL(
|
gtk_label_set_selectable(GTK_LABEL(
|
||||||
details_panel->person_evidence_summary_label), TRUE);
|
details_panel->person_evidence_summary_label), TRUE);
|
||||||
gtk_box_append(GTK_BOX(details_panel->person_role_box),
|
gtk_box_append(GTK_BOX(details_panel->entity_evidence_box),
|
||||||
gtk_label_new("Preuves associées à cette personne"));
|
gtk_label_new("Preuves associées à cette entité"));
|
||||||
gtk_box_append(GTK_BOX(details_panel->person_role_box),
|
gtk_box_append(GTK_BOX(details_panel->entity_evidence_box),
|
||||||
details_panel->person_evidence_summary_label);
|
details_panel->person_evidence_summary_label);
|
||||||
gtk_box_append(GTK_BOX(details_panel->person_role_box),
|
gtk_box_append(GTK_BOX(details_panel->entity_evidence_box),
|
||||||
details_panel->person_evidence_button);
|
details_panel->person_evidence_button);
|
||||||
gtk_box_append(GTK_BOX(details_box), details_panel->person_role_box);
|
gtk_box_append(GTK_BOX(details_box), details_panel->person_role_box);
|
||||||
|
gtk_box_append(GTK_BOX(details_box), details_panel->entity_evidence_box);
|
||||||
g_signal_connect(details_panel->person_role_dropdown, "notify::selected",
|
g_signal_connect(details_panel->person_role_dropdown, "notify::selected",
|
||||||
G_CALLBACK(entity_details_panel_on_person_role_changed), details_panel);
|
G_CALLBACK(entity_details_panel_on_person_role_changed), details_panel);
|
||||||
g_signal_connect(details_panel->person_confidence_button, "clicked",
|
g_signal_connect(details_panel->person_confidence_button, "clicked",
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,8 @@ struct InvestigationGraphView
|
||||||
|
|
||||||
InvestigationGraphViewNodeMovedCallback node_moved_callback;
|
InvestigationGraphViewNodeMovedCallback node_moved_callback;
|
||||||
gpointer node_moved_user_data;
|
gpointer node_moved_user_data;
|
||||||
|
InvestigationGraphViewExtractionDropCallback extraction_drop_callback;
|
||||||
|
gpointer extraction_drop_user_data;
|
||||||
|
|
||||||
double node_drag_pointer_offset_x;
|
double node_drag_pointer_offset_x;
|
||||||
double node_drag_pointer_offset_y;
|
double node_drag_pointer_offset_y;
|
||||||
|
|
@ -128,8 +130,46 @@ struct InvestigationGraphView
|
||||||
gboolean dragging;
|
gboolean dragging;
|
||||||
gboolean node_dragging;
|
gboolean node_dragging;
|
||||||
gboolean click_suppressed;
|
gboolean click_suppressed;
|
||||||
|
GtkDropTarget *extraction_drop_target;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static gboolean investigation_graph_view_screen_to_logical(
|
||||||
|
const InvestigationGraphView *graph_view, double screen_x, double screen_y,
|
||||||
|
double *logical_x, double *logical_y);
|
||||||
|
static InvestigationGraphNodeLayout *investigation_graph_view_find_node_at(
|
||||||
|
InvestigationGraphView *graph_view, double logical_x, double logical_y);
|
||||||
|
|
||||||
|
/** @brief Relaie un chemin d'extraction et l'éventuelle entité visée. */
|
||||||
|
static gboolean investigation_graph_view_on_extraction_drop(
|
||||||
|
GtkDropTarget *target, const GValue *value, double x, double y,
|
||||||
|
gpointer user_data)
|
||||||
|
{
|
||||||
|
InvestigationGraphView *graph_view = user_data;
|
||||||
|
InvestigationGraphNodeLayout *node_layout = NULL;
|
||||||
|
const char *path = NULL;
|
||||||
|
const char *entity_identifier = NULL;
|
||||||
|
double logical_x = 0.0;
|
||||||
|
double logical_y = 0.0;
|
||||||
|
(void) target;
|
||||||
|
if (graph_view == NULL || value == NULL ||
|
||||||
|
!G_VALUE_HOLDS_STRING(value)) return FALSE;
|
||||||
|
path = g_value_get_string(value);
|
||||||
|
if (path == NULL || path[0] == '\0') return FALSE;
|
||||||
|
if (investigation_graph_view_screen_to_logical(graph_view, x, y,
|
||||||
|
&logical_x, &logical_y))
|
||||||
|
{
|
||||||
|
node_layout = investigation_graph_view_find_node_at(graph_view,
|
||||||
|
logical_x, logical_y);
|
||||||
|
if (node_layout != NULL && node_layout->entity_record != NULL)
|
||||||
|
entity_identifier = entity_record_get_identifier(
|
||||||
|
node_layout->entity_record);
|
||||||
|
}
|
||||||
|
if (graph_view->extraction_drop_callback == NULL) return FALSE;
|
||||||
|
graph_view->extraction_drop_callback(path, entity_identifier,
|
||||||
|
graph_view->extraction_drop_user_data);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
static const char *investigation_graph_view_get_node_identifier(
|
static const char *investigation_graph_view_get_node_identifier(
|
||||||
const InvestigationGraphNodeLayout *node_layout
|
const InvestigationGraphNodeLayout *node_layout
|
||||||
);
|
);
|
||||||
|
|
@ -3948,6 +3988,13 @@ InvestigationGraphView *investigation_graph_view_new(void)
|
||||||
TRUE
|
TRUE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
graph_view->extraction_drop_target = GTK_DROP_TARGET(
|
||||||
|
gtk_drop_target_new(G_TYPE_STRING, GDK_ACTION_COPY));
|
||||||
|
g_signal_connect(graph_view->extraction_drop_target, "drop",
|
||||||
|
G_CALLBACK(investigation_graph_view_on_extraction_drop), graph_view);
|
||||||
|
gtk_widget_add_controller(graph_view->drawing_area,
|
||||||
|
GTK_EVENT_CONTROLLER(graph_view->extraction_drop_target));
|
||||||
|
|
||||||
gtk_drawing_area_set_content_width(
|
gtk_drawing_area_set_content_width(
|
||||||
GTK_DRAWING_AREA(
|
GTK_DRAWING_AREA(
|
||||||
graph_view->drawing_area
|
graph_view->drawing_area
|
||||||
|
|
@ -4414,6 +4461,16 @@ void investigation_graph_view_set_node_moved_callback(
|
||||||
user_data;
|
user_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void investigation_graph_view_set_extraction_drop_callback(
|
||||||
|
InvestigationGraphView *graph_view,
|
||||||
|
InvestigationGraphViewExtractionDropCallback callback,
|
||||||
|
gpointer user_data)
|
||||||
|
{
|
||||||
|
if (graph_view == NULL) return;
|
||||||
|
graph_view->extraction_drop_callback = callback;
|
||||||
|
graph_view->extraction_drop_user_data = user_data;
|
||||||
|
}
|
||||||
|
|
||||||
const EntityRecord *investigation_graph_view_get_selected_entity(
|
const EntityRecord *investigation_graph_view_get_selected_entity(
|
||||||
const InvestigationGraphView *graph_view
|
const InvestigationGraphView *graph_view
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,38 @@
|
||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
/** @brief Prépare le chemin du fichier pour un glisser-déposer. */
|
||||||
|
static GdkContentProvider *investigation_tree_view_on_drag_prepare(
|
||||||
|
GtkDragSource *source, double x, double y, gpointer user_data)
|
||||||
|
{
|
||||||
|
const char *path = g_object_get_data(G_OBJECT(source),
|
||||||
|
"investigation-node-path");
|
||||||
|
(void) x; (void) y; (void) user_data;
|
||||||
|
if (path == NULL || path[0] == '\0') return NULL;
|
||||||
|
return gdk_content_provider_new_typed(G_TYPE_STRING, path);
|
||||||
|
}
|
||||||
|
/** @brief Vérifie qu'un nœud est un fichier `.txt` sous `Extractions`. */
|
||||||
|
static gboolean investigation_tree_view_node_is_extraction(
|
||||||
|
const InvestigationNode *node)
|
||||||
|
{
|
||||||
|
const InvestigationNode *ancestor = NULL;
|
||||||
|
const char *name = NULL;
|
||||||
|
|
||||||
|
if (node == NULL ||
|
||||||
|
investigation_node_get_type(node) != INVESTIGATION_NODE_FILE)
|
||||||
|
return FALSE;
|
||||||
|
name = investigation_node_get_name(node);
|
||||||
|
if (name == NULL || !g_str_has_suffix(name, ".txt")) return FALSE;
|
||||||
|
ancestor = investigation_node_get_parent(node);
|
||||||
|
while (ancestor != NULL)
|
||||||
|
{
|
||||||
|
if (g_strcmp0(investigation_node_get_name(ancestor),
|
||||||
|
"Extractions") == 0) return TRUE;
|
||||||
|
ancestor = investigation_node_get_parent(ancestor);
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* InvestigationTreeItem
|
* InvestigationTreeItem
|
||||||
* ---------------------
|
* ---------------------
|
||||||
|
|
@ -248,6 +280,7 @@ static void investigation_tree_view_factory_setup(
|
||||||
GtkWidget *row_box = NULL;
|
GtkWidget *row_box = NULL;
|
||||||
GtkWidget *image = NULL;
|
GtkWidget *image = NULL;
|
||||||
GtkWidget *label = NULL;
|
GtkWidget *label = NULL;
|
||||||
|
GtkDragSource *drag_source = NULL;
|
||||||
|
|
||||||
(void)factory;
|
(void)factory;
|
||||||
(void)user_data;
|
(void)user_data;
|
||||||
|
|
@ -292,6 +325,14 @@ static void investigation_tree_view_factory_setup(
|
||||||
row_box
|
row_box
|
||||||
);
|
);
|
||||||
|
|
||||||
|
drag_source = gtk_drag_source_new();
|
||||||
|
gtk_drag_source_set_actions(drag_source, GDK_ACTION_COPY);
|
||||||
|
g_signal_connect(drag_source, "prepare",
|
||||||
|
G_CALLBACK(investigation_tree_view_on_drag_prepare), NULL);
|
||||||
|
g_object_set_data(G_OBJECT(row_box), "investigation-drag-source",
|
||||||
|
drag_source);
|
||||||
|
gtk_widget_add_controller(row_box, GTK_EVENT_CONTROLLER(drag_source));
|
||||||
|
|
||||||
gtk_list_item_set_child(
|
gtk_list_item_set_child(
|
||||||
list_item,
|
list_item,
|
||||||
expander
|
expander
|
||||||
|
|
@ -406,6 +447,7 @@ static void investigation_tree_view_factory_bind(
|
||||||
GtkWidget *label = NULL;
|
GtkWidget *label = NULL;
|
||||||
|
|
||||||
const char *node_name = NULL;
|
const char *node_name = NULL;
|
||||||
|
GtkDragSource *drag_source = NULL;
|
||||||
|
|
||||||
(void)factory;
|
(void)factory;
|
||||||
(void)user_data;
|
(void)user_data;
|
||||||
|
|
@ -455,6 +497,13 @@ static void investigation_tree_view_factory_bind(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drag_source = GTK_DRAG_SOURCE(g_object_get_data(G_OBJECT(row_box),
|
||||||
|
"investigation-drag-source"));
|
||||||
|
if (drag_source != NULL)
|
||||||
|
g_object_set_data_full(G_OBJECT(drag_source), "investigation-node-path",
|
||||||
|
investigation_tree_view_node_is_extraction(node)
|
||||||
|
? g_strdup(investigation_node_get_path(node)) : NULL, g_free);
|
||||||
|
|
||||||
image = gtk_widget_get_first_child(
|
image = gtk_widget_get_first_child(
|
||||||
row_box
|
row_box
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,9 @@ struct Workspace
|
||||||
gpointer
|
gpointer
|
||||||
graph_node_moved_user_data;
|
graph_node_moved_user_data;
|
||||||
|
|
||||||
|
WorkspaceExtractionDropCallback extraction_drop_callback;
|
||||||
|
gpointer extraction_drop_user_data;
|
||||||
|
|
||||||
WorkspaceResetGraphLayoutCallback
|
WorkspaceResetGraphLayoutCallback
|
||||||
reset_graph_layout_callback;
|
reset_graph_layout_callback;
|
||||||
|
|
||||||
|
|
@ -960,6 +963,15 @@ static void workspace_on_person_evidence_requested(const char *identifier,
|
||||||
workspace->person_evidence_user_data);
|
workspace->person_evidence_user_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void workspace_on_extraction_dropped(const char *file_path,
|
||||||
|
const char *target_entity_identifier, gpointer user_data)
|
||||||
|
{
|
||||||
|
Workspace *workspace = user_data;
|
||||||
|
if (workspace != NULL && workspace->extraction_drop_callback != NULL)
|
||||||
|
workspace->extraction_drop_callback(file_path, target_entity_identifier,
|
||||||
|
workspace->extraction_drop_user_data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Désélectionne le nœud lorsque le volet est fermé manuellement.
|
* @brief Désélectionne le nœud lorsque le volet est fermé manuellement.
|
||||||
*/
|
*/
|
||||||
|
|
@ -1939,6 +1951,8 @@ Workspace *workspace_new(void)
|
||||||
workspace_on_graph_node_moved,
|
workspace_on_graph_node_moved,
|
||||||
workspace
|
workspace
|
||||||
);
|
);
|
||||||
|
investigation_graph_view_set_extraction_drop_callback(
|
||||||
|
workspace->graph_view, workspace_on_extraction_dropped, workspace);
|
||||||
|
|
||||||
entity_details_panel_set_close_callback(
|
entity_details_panel_set_close_callback(
|
||||||
workspace->entity_details_panel,
|
workspace->entity_details_panel,
|
||||||
|
|
@ -3406,6 +3420,16 @@ void workspace_set_graph_node_moved_callback(
|
||||||
user_data;
|
user_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void workspace_set_extraction_drop_callback(
|
||||||
|
Workspace *workspace,
|
||||||
|
WorkspaceExtractionDropCallback callback,
|
||||||
|
gpointer user_data)
|
||||||
|
{
|
||||||
|
if (workspace == NULL) return;
|
||||||
|
workspace->extraction_drop_callback = callback;
|
||||||
|
workspace->extraction_drop_user_data = user_data;
|
||||||
|
}
|
||||||
|
|
||||||
void workspace_set_reset_graph_layout_callback(
|
void workspace_set_reset_graph_layout_callback(
|
||||||
Workspace *workspace,
|
Workspace *workspace,
|
||||||
WorkspaceResetGraphLayoutCallback callback,
|
WorkspaceResetGraphLayoutCallback callback,
|
||||||
|
|
|
||||||
183
tests/test_extraction_drop_service.c
Normal file
183
tests/test_extraction_drop_service.c
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
#include "core/extraction_drop_service.h"
|
||||||
|
#include "dao/entity_dao.h"
|
||||||
|
#include "dao/evidence_dao.h"
|
||||||
|
#include "dao/evidence_entity_dao.h"
|
||||||
|
#include "database/database.h"
|
||||||
|
#include "models/entity_record.h"
|
||||||
|
|
||||||
|
#include <glib.h>
|
||||||
|
#include <glib/gstdio.h>
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
char *root;
|
||||||
|
char *extractions;
|
||||||
|
char *file_path;
|
||||||
|
char *database_path;
|
||||||
|
Database *database;
|
||||||
|
} ExtractionDropFixture;
|
||||||
|
|
||||||
|
static void fixture_setup(ExtractionDropFixture *fixture, gconstpointer data)
|
||||||
|
{
|
||||||
|
GError *error = NULL;
|
||||||
|
(void) data;
|
||||||
|
fixture->root = g_dir_make_tmp("labfy-extraction-drop-XXXXXX", &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
fixture->extractions = g_build_filename(fixture->root,
|
||||||
|
"02_Preuves_Traitees", "Extractions", "OSINT", NULL);
|
||||||
|
g_assert_cmpint(g_mkdir_with_parents(fixture->extractions, 0700), ==, 0);
|
||||||
|
fixture->file_path = g_build_filename(fixture->extractions,
|
||||||
|
"resultat.txt", NULL);
|
||||||
|
g_assert_true(g_file_set_contents(fixture->file_path,
|
||||||
|
"Cible : exemple.test\nAction : test\n\nRésultat public", -1, &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
fixture->database_path = g_build_filename(fixture->root,
|
||||||
|
"Enquete.sqlite", NULL);
|
||||||
|
g_assert_true(database_initialize(fixture->database_path,
|
||||||
|
"Extraction", fixture->root));
|
||||||
|
fixture->database = database_open(fixture->database_path);
|
||||||
|
g_assert_nonnull(fixture->database);
|
||||||
|
g_assert_true(database_migrate_to_latest(fixture->database));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fixture_teardown(ExtractionDropFixture *fixture,
|
||||||
|
gconstpointer data)
|
||||||
|
{
|
||||||
|
(void) data;
|
||||||
|
database_close(fixture->database);
|
||||||
|
g_remove(fixture->file_path);
|
||||||
|
g_remove(fixture->database_path);
|
||||||
|
g_rmdir(fixture->extractions);
|
||||||
|
{
|
||||||
|
char *directory = g_path_get_dirname(fixture->extractions);
|
||||||
|
g_rmdir(directory);
|
||||||
|
g_free(directory);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
char *directory = g_build_filename(fixture->root,
|
||||||
|
"02_Preuves_Traitees", NULL);
|
||||||
|
g_rmdir(directory);
|
||||||
|
g_free(directory);
|
||||||
|
}
|
||||||
|
g_rmdir(fixture->root);
|
||||||
|
g_free(fixture->database_path);
|
||||||
|
g_free(fixture->file_path);
|
||||||
|
g_free(fixture->extractions);
|
||||||
|
g_free(fixture->root);
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *insert_entity(Database *database)
|
||||||
|
{
|
||||||
|
EntityDao *dao = NULL;
|
||||||
|
EntityRecord *record = NULL;
|
||||||
|
char *identifier = g_uuid_string_random();
|
||||||
|
GError *error = NULL;
|
||||||
|
dao = entity_dao_new(database, &error);
|
||||||
|
record = entity_record_new(identifier, "other", "Destination",
|
||||||
|
"Destination", NULL, 50, "2026-07-23T12:00:00Z",
|
||||||
|
"2026-07-23T12:00:00Z", ENTITY_STATUS_ACTIVE, &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_true(entity_dao_insert(dao, record, &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
entity_record_free(record);
|
||||||
|
entity_dao_free(dao);
|
||||||
|
return identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_prepare_rejects_outside(ExtractionDropFixture *fixture,
|
||||||
|
gconstpointer data)
|
||||||
|
{
|
||||||
|
ExtractionDropInfo *info = NULL;
|
||||||
|
char *outside = g_build_filename(fixture->root, "hors.txt", NULL);
|
||||||
|
GError *error = NULL;
|
||||||
|
(void) data;
|
||||||
|
g_assert_true(g_file_set_contents(outside, "hors enquête", -1, &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
info = extraction_drop_service_prepare(fixture->root, outside, &error);
|
||||||
|
g_assert_null(info);
|
||||||
|
g_assert_error(error, EXTRACTION_DROP_SERVICE_ERROR,
|
||||||
|
EXTRACTION_DROP_SERVICE_ERROR_OUTSIDE_INVESTIGATION);
|
||||||
|
g_clear_error(&error);
|
||||||
|
g_remove(outside);
|
||||||
|
g_free(outside);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_attach_persists_and_is_idempotent(
|
||||||
|
ExtractionDropFixture *fixture, gconstpointer data)
|
||||||
|
{
|
||||||
|
ExtractionDropInfo *info = NULL;
|
||||||
|
EvidenceDao *evidence_dao = NULL;
|
||||||
|
EvidenceEntityDao *link_dao = NULL;
|
||||||
|
GPtrArray *records = NULL;
|
||||||
|
char *entity_identifier = NULL;
|
||||||
|
gboolean exists = FALSE;
|
||||||
|
GError *error = NULL;
|
||||||
|
(void) data;
|
||||||
|
|
||||||
|
info = extraction_drop_service_prepare(fixture->root,
|
||||||
|
fixture->file_path, &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_nonnull(info);
|
||||||
|
g_assert_cmpstr(extraction_drop_info_get_source(info), ==,
|
||||||
|
"Cible : exemple.test");
|
||||||
|
entity_identifier = insert_entity(fixture->database);
|
||||||
|
g_assert_true(extraction_drop_service_attach(fixture->database, info,
|
||||||
|
entity_identifier, &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_true(extraction_drop_service_attach(fixture->database, info,
|
||||||
|
entity_identifier, &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
evidence_dao = evidence_dao_new(fixture->database, &error);
|
||||||
|
records = evidence_dao_list_all(evidence_dao, &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_cmpuint(records->len, ==, 1U);
|
||||||
|
link_dao = evidence_entity_dao_new(fixture->database, &error);
|
||||||
|
g_assert_true(evidence_entity_dao_exists(link_dao,
|
||||||
|
evidence_record_get_identifier(g_ptr_array_index(records, 0U)),
|
||||||
|
entity_identifier, &exists, &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_true(exists);
|
||||||
|
evidence_entity_dao_free(link_dao);
|
||||||
|
g_ptr_array_unref(records);
|
||||||
|
evidence_dao_free(evidence_dao);
|
||||||
|
g_free(entity_identifier);
|
||||||
|
extraction_drop_info_free(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_create_entity_persists(ExtractionDropFixture *fixture,
|
||||||
|
gconstpointer data)
|
||||||
|
{
|
||||||
|
ExtractionDropInfo *info = NULL;
|
||||||
|
EntityDao *entity_dao = NULL;
|
||||||
|
EntityRecord *entity = NULL;
|
||||||
|
char *identifier = NULL;
|
||||||
|
GError *error = NULL;
|
||||||
|
(void) data;
|
||||||
|
info = extraction_drop_service_prepare(fixture->root,
|
||||||
|
fixture->file_path, &error);
|
||||||
|
g_assert_true(extraction_drop_service_create_entity(fixture->database,
|
||||||
|
info, &identifier, &error));
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_true(g_uuid_string_is_valid(identifier));
|
||||||
|
entity_dao = entity_dao_new(fixture->database, &error);
|
||||||
|
entity = entity_dao_find_by_identifier(entity_dao, identifier, &error);
|
||||||
|
g_assert_no_error(error);
|
||||||
|
g_assert_nonnull(entity);
|
||||||
|
g_assert_cmpstr(entity_record_get_type_identifier(entity), ==, "other");
|
||||||
|
entity_record_free(entity);
|
||||||
|
entity_dao_free(entity_dao);
|
||||||
|
g_free(identifier);
|
||||||
|
extraction_drop_info_free(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
g_test_init(&argc, &argv, NULL);
|
||||||
|
g_test_add("/extraction-drop/outside", ExtractionDropFixture, NULL,
|
||||||
|
fixture_setup, test_prepare_rejects_outside, fixture_teardown);
|
||||||
|
g_test_add("/extraction-drop/attach", ExtractionDropFixture, NULL,
|
||||||
|
fixture_setup, test_attach_persists_and_is_idempotent, fixture_teardown);
|
||||||
|
g_test_add("/extraction-drop/create", ExtractionDropFixture, NULL,
|
||||||
|
fixture_setup, test_create_entity_persists, fixture_teardown);
|
||||||
|
return g_test_run();
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue