diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..b7bbc66 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,25 @@ +## Ce que fait ce MR + + + +## Couche concernée + +- [ ] `model` — entités et POJOs +- [ ] `hypervisor` — IHypervisor / VBoxOrchestrator +- [ ] `data` — DatabaseManager / schema SQL +- [ ] `pdf` — PdfExporter +- [ ] `controller` — LabController +- [ ] `ui` — vues JavaFX +- [ ] Config / build (pom.xml, module-info, CI) + +## Checklist avant review + +- [ ] `./mvnw test` passe en local (0 failure, 0 error) +- [ ] Javadoc présent sur toutes les classes et méthodes publiques ajoutées +- [ ] Pas de `System.out.println` de debug oublié +- [ ] Pas de TODO non intentionnel +- [ ] Les imports inutilisés ont été supprimés (IntelliJ : Ctrl+Alt+O) + +## Pour le reviewer + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d07d6d8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + branches: [ "main", "develop", "feature/**", "fix/**" ] + pull_request: + branches: [ "develop", "main" ] + +jobs: + build-and-test: + name: Build & Test (Java 17) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven # met le dépôt Maven en cache → builds 2x plus rapides + + - name: Build & run tests + run: ./mvnw test --no-transfer-progress + + - name: Upload Surefire reports on failure + if: failure() # s'exécute UNIQUEMENT si les tests échouent + uses: actions/upload-artifact@v4 + with: + name: surefire-reports + path: target/surefire-reports/ + retention-days: 7 diff --git a/.gitignore b/.gitignore index eef09ab..a6268c3 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ build/ ### Mac OS ### .DS_Store +target/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..baae740 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,74 @@ +# Guide de contribution — Cyber Lab Manager + +## Branches + +| Préfixe | Usage | Exemple | +|---|---|---| +| `feature/` | Nouvelle fonctionnalité | `feature/vbox-orchestrator` | +| `fix/` | Correction de bug | `fix/snapshot-uuid-null` | +| `chore/` | Config, CI, docs | `chore/github-config` | +| `test/` | Ajout ou correction de tests seuls | `test/hypervisor-coverage` | + +**Règle absolue :** on ne pousse jamais directement sur `develop` ou `main`. +Toujours passer par un MR depuis une branche `feature/` ou `fix/`. + +## Format des commits + +On suit [Conventional Commits](https://www.conventionalcommits.org/fr) : + +``` +(): +``` + +| Type | Quand l'utiliser | +|---|---| +| `feat` | Ajout d'une fonctionnalité | +| `fix` | Correction d'un bug | +| `test` | Ajout ou modification de tests | +| `docs` | Documentation (Javadoc, Markdown) | +| `refactor` | Refactoring sans changement de comportement | +| `chore` | Config build, CI, dépendances | + +**Exemples :** +``` +feat(model): add Snapshot uuid and online fields +fix(hypervisor): handle VBoxManage non-zero exit code +test(model): add LabTest unmodifiable list assertion +docs(hypervisor): complete Javadoc on IHypervisor +chore: add GitHub Actions CI workflow +``` + +## Ouvrir un MR + +1. Ta branche est à jour avec `develop` (`git rebase develop` ou `git merge develop`) +2. `./mvnw test` passe en local — **0 failure, 0 error** +3. Javadoc présent sur toutes les classes et méthodes publiques ajoutées +4. Ouvre le MR vers `develop` sur GitHub +5. Remplis le template de MR (pré-rempli automatiquement) +6. Assigne au moins un reviewer + +## Revue de code + +- Le reviewer a **48h** pour donner son retour +- Un commentaire `[bloquant]` doit être résolu avant le merge +- Un commentaire `[suggestion]` est optionnel +- Approuver = confirmer que les tests passent ET que le code est lisible + +## Standards de code + +- **Java 17**, style standard IntelliJ (Ctrl+Alt+L pour formatter) +- Javadoc obligatoire sur toutes les classes et méthodes `public` +- Pas de `System.out.println` → utiliser `java.util.logging.Logger` +- Imports inutilisés supprimés (Ctrl+Alt+O dans IntelliJ) +- Pas de TODO non documenté dans un MR + +## Responsabilités par package + +| Package | Responsable | +|---|---| +| `tg.cyberlabmanager.model` | Rôle 3 | +| `tg.cyberlabmanager.hypervisor` | Rôle 3 | +| `tg.cyberlabmanager.data` | Rôle 2 | +| `tg.cyberlabmanager.pdf` | Rôle 2 | +| `tg.cyberlabmanager.ui` | Rôle 1 | +| `tg.cyberlabmanager.controller` | Rôle 1 | diff --git a/docs/Markdowns/SETUP.md b/docs/Markdowns/SETUP.md new file mode 100644 index 0000000..738100d --- /dev/null +++ b/docs/Markdowns/SETUP.md @@ -0,0 +1,89 @@ +# SETUP — Environnement de développement + +## Prérequis + +| Outil | Version minimale | Vérification | +|---|---|---| +| JDK | 17 (LTS) | `java -version` | +| Maven | 3.8+ | `./mvnw -version` | +| VirtualBox | 7.0+ | Pour les tests d'intégration | +| Git | 2.x | `git --version` | + +> Le projet utilise le Maven Wrapper (`./mvnw`) — pas besoin d'installer Maven globalement. + +--- + +## Configuration IDE — point critique à lire + +Le projet compile en **Java 17** même si tu as un JDK plus récent (21, 22…) sur ta machine. +C'est contrôlé par cette ligne dans `pom.xml` : + +```xml +17 +``` + +Cette propriété fixe simultanément la syntaxe acceptée, le bytecode cible (class file version 61) +et les APIs accessibles — le compilateur de ton JDK s'adapte automatiquement. + +### IntelliJ IDEA + +**File → Project Structure → Project** : +- SDK : ton JDK installé (17, 21, peu importe) +- Language level : **17** + +**File → Settings → Build → Compiler → Java Compiler** : +- Target bytecode version : **17** + +### Pourquoi c'est important + +Si quelqu'un compile avec bytecode Java 21 et pousse du code, les autres obtiendront : +``` +UnsupportedClassVersionError: Unsupported major.minor version 65.0 +``` +La version 65 = Java 21. La version 61 = Java 17 (notre cible). +Garder tout le monde sur la même cible évite ce type de conflit au merge. + +--- + +## Lancer le projet + +```bash +# Compiler et lancer tous les tests +./mvnw test + +# Lancer l'application JavaFX +./mvnw javafx:run + +# Générer la Javadoc dans target/reports/apidocs/ +./mvnw javadoc:javadoc +``` + +> Si tu utilises un JDK non standard, préfixe avec JAVA_HOME : +> `JAVA_HOME=/usr/lib/jvm/jdk-21.0.11-oracle-x64 ./mvnw test` + +--- + +## Structure des branches + +``` +main ← releases stables uniquement +develop ← intégration continue (branche de référence) +feature/x ← développement d'une fonctionnalité +fix/x ← correction de bug +chore/x ← config, CI, documentation +``` + +Voir [CONTRIBUTING.md](../CONTRIBUTING.md) pour les conventions complètes. + +--- + +## Responsabilités par package + +| Package | Rôle | Contenu | +|---|---|---| +| `tg.cyberlabmanager.model` | Rôle 3 | Entités métier (Lab, VM, Snapshot…) | +| `tg.cyberlabmanager.hypervisor` | Rôle 3 | IHypervisor, VBoxOrchestrator | +| `tg.cyberlabmanager.data` | Rôle 2 | DatabaseManager, SQLite | +| `tg.cyberlabmanager.pdf` | Rôle 2 | PdfExporter | +| `tg.cyberlabmanager.ui` | Rôle 1 | Vues JavaFX | +| `tg.cyberlabmanager.controller` | Rôle 1 | LabController | diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 6bcfcc9..930b3ae 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,3 +1,6 @@ +/** + * Module principal de l'application Cyber Lab Manager. + */ module tg.cyberlabmanager { // ── JavaFX ────────────────────────────────────────────────────────── @@ -31,4 +34,4 @@ exports tg.cyberlabmanager.hypervisor; exports tg.cyberlabmanager.pdf; exports tg.cyberlabmanager.ui; -} \ No newline at end of file +} diff --git a/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java b/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java index 70b6866..d8770e1 100644 --- a/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java +++ b/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java @@ -7,7 +7,22 @@ import java.io.IOException; +/** + * Application JavaFX principale de Cyber Lab Manager. + */ public class CyberLabApp extends Application { + /** + * Cree l'application JavaFX. + */ + public CyberLabApp() { + } + + /** + * Initialise et affiche la fenetre principale JavaFX. + * + * @param stage scene principale fournie par JavaFX + * @throws IOException si le chargement du fichier FXML echoue + */ @Override public void start(Stage stage) throws IOException { FXMLLoader loader = new FXMLLoader(CyberLabApp.class.getResource("/tg/cyberlabmanager/ui/main-view.fxml")); diff --git a/src/main/java/tg/cyberlabmanager/app/Launcher.java b/src/main/java/tg/cyberlabmanager/app/Launcher.java index 7e25325..113aaca 100644 --- a/src/main/java/tg/cyberlabmanager/app/Launcher.java +++ b/src/main/java/tg/cyberlabmanager/app/Launcher.java @@ -2,7 +2,21 @@ import javafx.application.Application; +/** + * Point d'entree deleguant le lancement a l'application JavaFX. + */ public class Launcher { + /** + * Cree le lanceur de l'application. + */ + public Launcher() { + } + + /** + * Lance l'application JavaFX. + * + * @param args arguments de ligne de commande + */ public static void main(String[] args) { Application.launch(CyberLabApp.class, args); } diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index 732d284..7228bab 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -11,6 +11,9 @@ import java.util.List; +/** + * Controleur principal coordonnant l'interface, la persistance et l'hyperviseur. + */ public class LabController { private IHypervisor hypervisor; private DatabaseManager databaseManager; @@ -19,9 +22,19 @@ public class LabController { @FXML private Label statusLabel; + /** + * Cree un controleur vide utilise par JavaFX lors du chargement FXML. + */ public LabController() { } + /** + * Cree un controleur avec ses dependances injectees. + * + * @param hypervisor service de pilotage de l'hyperviseur + * @param databaseManager service de persistance + * @param pdfExporter service d'export PDF + */ public LabController(IHypervisor hypervisor, DatabaseManager databaseManager, PdfExporter pdfExporter) { this.hypervisor = hypervisor; this.databaseManager = databaseManager; @@ -35,54 +48,120 @@ private void initialize() { } } + /** + * Declenche le demarrage d'une machine virtuelle. + * + * @param vm machine virtuelle cible + */ public void onStartVMClicked(VirtualMachine vm) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Declenche l'arret d'une machine virtuelle. + * + * @param vm machine virtuelle cible + */ public void onStopVMClicked(VirtualMachine vm) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Declenche la sauvegarde d'etat d'une machine virtuelle. + * + * @param vm machine virtuelle cible + */ public void onSaveStateClicked(VirtualMachine vm) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Declenche la creation d'un snapshot pour une machine virtuelle. + * + * @param vm machine virtuelle cible + * @param name nom du snapshot + * @param description description du snapshot + */ public void onTakeSnapshotClicked(VirtualMachine vm, String name, String description) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Declenche la restauration d'un snapshot pour une machine virtuelle. + * + * @param vm machine virtuelle cible + * @param snapshotName nom ou identifiant du snapshot + */ public void onRestoreSnapshotClicked(VirtualMachine vm, String snapshotName) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Ajoute une note d'analyse a une machine virtuelle. + * + * @param vm machine virtuelle cible + * @param text contenu de la note + */ public void onAddJournalEntry(VirtualMachine vm, String text) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Declenche l'import des machines virtuelles disponibles. + */ public void onImportVMsClicked() { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Confirme l'import de machines virtuelles selectionnees. + * + * @param selectedVMs machines virtuelles selectionnees + * @param labId identifiant du laboratoire cible, ou {@code null} + */ public void onImportConfirm(List selectedVMs, Integer labId) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Declenche l'export PDF d'un laboratoire. + * + * @param lab laboratoire a exporter + */ public void onExportLabClicked(Lab lab) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Declenche l'export des machines virtuelles sans laboratoire. + */ public void onExportOrphansClicked() { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Retourne tous les laboratoires connus. + * + * @return liste des laboratoires + */ public List getAllLabs() { return databaseManager.getAllLabs(); } + /** + * Retourne les machines virtuelles non rattachees a un laboratoire. + * + * @return liste des machines virtuelles orphelines + */ public List getOrphanVMs() { return databaseManager.getOrphanVMs(); } + /** + * Persiste la configuration mise a jour. + * + * @param newConfig nouvelle configuration applicative + */ public void onConfigUpdated(AppConfig newConfig) { databaseManager.saveConfig(newConfig); } diff --git a/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java b/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java index 9cd04a9..ae19aed 100644 --- a/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java +++ b/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java @@ -5,72 +5,545 @@ import tg.cyberlabmanager.model.JournalEntry; import tg.cyberlabmanager.model.Lab; import tg.cyberlabmanager.model.Snapshot; +import tg.cyberlabmanager.model.VMStatus; import tg.cyberlabmanager.model.VirtualMachine; +import java.sql.*; +import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; +/** + * Service d'accès aux données de l'application. + * + *

Maintient une connexion SQLite unique ouverte pendant toute la durée de + * vie de l'instance. Compatible avec SQLite en mémoire (:memory:) pour les + * tests.

+ */ public class DatabaseManager { + + private final Connection connection; + + // ========================================================================= + // Constructeurs + // ========================================================================= + + /** + * Crée un gestionnaire utilisant le fichier cyberlab.db dans le répertoire + * utilisateur. + */ + public DatabaseManager() { + this("jdbc:sqlite:" + System.getProperty("user.home") + "/cyberlab.db"); + } + + /** + * Crée un gestionnaire avec une URL JDBC explicite. + * Utiliser "jdbc:sqlite::memory:" pour les tests unitaires. + * + * @param jdbcUrl URL JDBC SQLite + */ + public DatabaseManager(String jdbcUrl) { + try { + Class.forName("org.sqlite.JDBC"); + this.connection = DriverManager.getConnection(jdbcUrl); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Driver SQLite introuvable", e); + } catch (SQLException e) { + throw new RuntimeException("Impossible d'ouvrir la base de données : " + e.getMessage(), e); + } + initDatabase(); + } + + private Connection getConnection() { + return this.connection; + } + + /** + * Crée toutes les tables si elles n'existent pas encore. + */ + private void initDatabase() { + // Activer les clés étrangères SQLite (désactivées par défaut) + try (Statement stmt = getConnection().createStatement()) { + stmt.execute("PRAGMA foreign_keys = ON"); + } catch (SQLException e) { + throw new RuntimeException("Impossible d'activer les clés étrangères : " + e.getMessage(), e); + } + String[] tables = { + "CREATE TABLE IF NOT EXISTS labo (" + + " id_labo INTEGER PRIMARY KEY AUTOINCREMENT," + + " titre TEXT NOT NULL," + + " description TEXT," + + " categorie TEXT" + + ");", + + "CREATE TABLE IF NOT EXISTS vm (" + + " id_vm INTEGER PRIMARY KEY AUTOINCREMENT," + + " nom_vm TEXT NOT NULL," + + " uuid TEXT UNIQUE," + + " id_labo INTEGER REFERENCES labo(id_labo) ON DELETE SET NULL," + + " lien_doc TEXT," + + " statut TEXT" + + ");", + + "CREATE TABLE IF NOT EXISTS snapshot (" + + " id_snap INTEGER PRIMARY KEY AUTOINCREMENT," + + " id_vm INTEGER NOT NULL REFERENCES vm(id_vm) ON DELETE CASCADE," + + " uuid TEXT," + + " nom_snap TEXT," + + " date_creation TEXT," + + " description TEXT," + + " online INTEGER DEFAULT 0" + + ");", + + "CREATE TABLE IF NOT EXISTS journal_entry (" + + " id_entry INTEGER PRIMARY KEY AUTOINCREMENT," + + " id_vm INTEGER NOT NULL REFERENCES vm(id_vm) ON DELETE CASCADE," + + " horodatage TEXT NOT NULL," + + " contenu TEXT NOT NULL" + + ");", + + "CREATE TABLE IF NOT EXISTS audit_log (" + + " id_log INTEGER PRIMARY KEY AUTOINCREMENT," + + " horodatage TEXT NOT NULL," + + " id_vm INTEGER REFERENCES vm(id_vm) ON DELETE SET NULL," + + " action TEXT NOT NULL," + + " details TEXT," + + " lab_name TEXT" + + ");", + + "CREATE TABLE IF NOT EXISTS app_config (" + + " cle TEXT PRIMARY KEY," + + " valeur TEXT" + + ");" + }; + + try (Statement stmt = getConnection().createStatement()) { + for (String sql : tables) { + stmt.execute(sql); + } + } catch (SQLException e) { + throw new RuntimeException("Impossible d'initialiser la base de données : " + e.getMessage(), e); + } + } + + // ========================================================================= + // LABORATOIRES + // ========================================================================= + + /** + * Enregistre ou met à jour un laboratoire. + * + * @param lab laboratoire à persister + */ public void saveLab(Lab lab) { - throw new UnsupportedOperationException("Not implemented yet"); + if (lab.getId() == 0) { + String sql = "INSERT INTO labo (titre, description, categorie) VALUES (?, ?, ?)"; + try (PreparedStatement ps = getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setString(1, lab.getTitle()); + ps.setString(2, lab.getDescription()); + ps.setString(3, lab.getCategory()); + ps.executeUpdate(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys.next()) lab.setId(keys.getInt(1)); + } catch (SQLException e) { + throw new RuntimeException("Erreur saveLab (INSERT) : " + e.getMessage(), e); + } + } else { + String sql = "UPDATE labo SET titre=?, description=?, categorie=? WHERE id_labo=?"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setString(1, lab.getTitle()); + ps.setString(2, lab.getDescription()); + ps.setString(3, lab.getCategory()); + ps.setInt(4, lab.getId()); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Erreur saveLab (UPDATE) : " + e.getMessage(), e); + } + } } + /** + * Récupère un laboratoire par son identifiant local. + * + * @param id identifiant local du laboratoire + * @return laboratoire correspondant, ou null si introuvable + */ public Lab getLab(int id) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "SELECT id_labo, titre, description, categorie FROM labo WHERE id_labo=?"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setInt(1, id); + ResultSet rs = ps.executeQuery(); + if (rs.next()) return mapLab(rs); + } catch (SQLException e) { + throw new RuntimeException("Erreur getLab : " + e.getMessage(), e); + } + return null; } + /** + * Récupère tous les laboratoires. + * + * @return liste des laboratoires + */ public List getAllLabs() { - throw new UnsupportedOperationException("Not implemented yet"); + List labs = new ArrayList<>(); + String sql = "SELECT id_labo, titre, description, categorie FROM labo ORDER BY titre"; + try (Statement stmt = getConnection().createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + while (rs.next()) labs.add(mapLab(rs)); + } catch (SQLException e) { + throw new RuntimeException("Erreur getAllLabs : " + e.getMessage(), e); + } + return labs; } + /** + * Supprime un laboratoire sans supprimer les VM physiques. + * + * @param lab laboratoire à supprimer + */ public void deleteLab(Lab lab) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "DELETE FROM labo WHERE id_labo=?"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setInt(1, lab.getId()); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Erreur deleteLab : " + e.getMessage(), e); + } + } + + private Lab mapLab(ResultSet rs) throws SQLException { + Lab lab = new Lab(); + lab.setId(rs.getInt("id_labo")); + lab.setTitle(rs.getString("titre")); + lab.setDescription(rs.getString("description")); + lab.setCategory(rs.getString("categorie")); + return lab; } + // ========================================================================= + // MACHINES VIRTUELLES + // ========================================================================= + + /** + * Enregistre ou met à jour une machine virtuelle. + * + * @param vm machine virtuelle à persister + * @param labId identifiant du laboratoire rattaché, ou {@code null} + */ public void saveVirtualMachine(VirtualMachine vm, Integer labId) { - throw new UnsupportedOperationException("Not implemented yet"); + String statut = vm.getStatus() != null ? vm.getStatus().name() : VMStatus.UNKNOWN.name(); + + if (vm.getId() == 0) { + String sql = "INSERT INTO vm (nom_vm, uuid, id_labo, lien_doc, statut) VALUES (?,?,?,?,?)"; + try (PreparedStatement ps = getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setString(1, vm.getName()); + ps.setString(2, vm.getUuid()); + if (labId != null) ps.setInt(3, labId); else ps.setNull(3, Types.INTEGER); + ps.setString(4, vm.getDocumentationUrl()); + ps.setString(5, statut); + ps.executeUpdate(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys.next()) vm.setId(keys.getInt(1)); + } catch (SQLException e) { + throw new RuntimeException("Erreur saveVirtualMachine (INSERT) : " + e.getMessage(), e); + } + } else { + String sql = "UPDATE vm SET nom_vm=?, uuid=?, id_labo=?, lien_doc=?, statut=? WHERE id_vm=?"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setString(1, vm.getName()); + ps.setString(2, vm.getUuid()); + if (labId != null) ps.setInt(3, labId); else ps.setNull(3, Types.INTEGER); + ps.setString(4, vm.getDocumentationUrl()); + ps.setString(5, statut); + ps.setInt(6, vm.getId()); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Erreur saveVirtualMachine (UPDATE) : " + e.getMessage(), e); + } + } } + /** + * Récupère les machines virtuelles rattachées à un laboratoire. + * + * @param labId identifiant local du laboratoire + * @return liste des machines virtuelles rattachées + */ public List getVMsForLab(int labId) { - throw new UnsupportedOperationException("Not implemented yet"); + List vms = new ArrayList<>(); + String sql = "SELECT * FROM vm WHERE id_labo=?"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setInt(1, labId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) vms.add(mapVM(rs)); + } catch (SQLException e) { + throw new RuntimeException("Erreur getVMsForLab : " + e.getMessage(), e); + } + return vms; } + /** + * Récupère les machines virtuelles sans laboratoire. + * + * @return liste des machines virtuelles orphelines + */ public List getOrphanVMs() { - throw new UnsupportedOperationException("Not implemented yet"); + List vms = new ArrayList<>(); + String sql = "SELECT * FROM vm WHERE id_labo IS NULL"; + try (Statement stmt = getConnection().createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + while (rs.next()) vms.add(mapVM(rs)); + } catch (SQLException e) { + throw new RuntimeException("Erreur getOrphanVMs : " + e.getMessage(), e); + } + return vms; } + /** + * Détache une machine virtuelle de son laboratoire. + * + * @param vm machine virtuelle à détacher + */ public void removeVMFromLab(VirtualMachine vm) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "UPDATE vm SET id_labo=NULL WHERE id_vm=?"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setInt(1, vm.getId()); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Erreur removeVMFromLab : " + e.getMessage(), e); + } + } + + private VirtualMachine mapVM(ResultSet rs) throws SQLException { + VirtualMachine vm = new VirtualMachine(); + vm.setId(rs.getInt("id_vm")); + vm.setName(rs.getString("nom_vm")); + vm.setUuid(rs.getString("uuid")); + vm.setDocumentationUrl(rs.getString("lien_doc")); + String statut = rs.getString("statut"); + try { + vm.setStatus(statut != null ? VMStatus.valueOf(statut) : VMStatus.UNKNOWN); + } catch (IllegalArgumentException e) { + vm.setStatus(VMStatus.UNKNOWN); + } + return vm; } + // ========================================================================= + // SNAPSHOTS + // ========================================================================= + + /** + * Enregistre un snapshot pour une machine virtuelle. + * + * @param snap snapshot à persister + * @param vmId identifiant local de la machine virtuelle + */ public void saveSnapshot(Snapshot snap, int vmId) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "INSERT INTO snapshot (id_vm, uuid, nom_snap, date_creation, description, online) VALUES (?,?,?,?,?,?)"; + try (PreparedStatement ps = getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setInt(1, vmId); + ps.setString(2, snap.getUuid()); + ps.setString(3, snap.getName()); + ps.setString(4, snap.getCreatedAt() != null ? snap.getCreatedAt().toString() : null); + ps.setString(5, snap.getDescription()); + ps.setInt(6, snap.isOnline() ? 1 : 0); + ps.executeUpdate(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys.next()) snap.setId(keys.getInt(1)); + } catch (SQLException e) { + throw new RuntimeException("Erreur saveSnapshot : " + e.getMessage(), e); + } } + /** + * Récupère les snapshots d'une machine virtuelle. + * + * @param vmId identifiant local de la machine virtuelle + * @return liste des snapshots rattachés + */ public List getSnapshotsForVM(int vmId) { - throw new UnsupportedOperationException("Not implemented yet"); + List snaps = new ArrayList<>(); + String sql = "SELECT * FROM snapshot WHERE id_vm=? ORDER BY date_creation DESC"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setInt(1, vmId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) snaps.add(mapSnapshot(rs)); + } catch (SQLException e) { + throw new RuntimeException("Erreur getSnapshotsForVM : " + e.getMessage(), e); + } + return snaps; + } + + private Snapshot mapSnapshot(ResultSet rs) throws SQLException { + Snapshot s = new Snapshot(); + s.setId(rs.getInt("id_snap")); + s.setUuid(rs.getString("uuid")); + s.setName(rs.getString("nom_snap")); + String dateStr = rs.getString("date_creation"); + s.setCreatedAt(dateStr != null ? LocalDateTime.parse(dateStr) : null); + s.setDescription(rs.getString("description")); + s.setOnline(rs.getInt("online") == 1); + return s; } + // ========================================================================= + // JOURNAL DE BORD + // ========================================================================= + + /** + * Ajoute une note d'analyse pour une machine virtuelle. + * + * @param entry note d'analyse à persister + * @param vmId identifiant local de la machine virtuelle + */ public void addJournalEntry(JournalEntry entry, int vmId) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "INSERT INTO journal_entry (id_vm, horodatage, contenu) VALUES (?,?,?)"; + try (PreparedStatement ps = getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setInt(1, vmId); + ps.setString(2, entry.getTimestamp() != null ? entry.getTimestamp().toString() : LocalDateTime.now().toString()); + ps.setString(3, entry.getContent()); + ps.executeUpdate(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys.next()) entry.setId(keys.getInt(1)); + } catch (SQLException e) { + throw new RuntimeException("Erreur addJournalEntry : " + e.getMessage(), e); + } } + /** + * Récupère les notes d'analyse d'une machine virtuelle. + * + * @param vmId identifiant local de la machine virtuelle + * @return liste des notes d'analyse + */ public List getJournalEntriesForVM(int vmId) { - throw new UnsupportedOperationException("Not implemented yet"); + List entries = new ArrayList<>(); + String sql = "SELECT * FROM journal_entry WHERE id_vm=? ORDER BY horodatage DESC"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setInt(1, vmId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) entries.add(mapJournalEntry(rs)); + } catch (SQLException e) { + throw new RuntimeException("Erreur getJournalEntriesForVM : " + e.getMessage(), e); + } + return entries; } + private JournalEntry mapJournalEntry(ResultSet rs) throws SQLException { + JournalEntry e = new JournalEntry(); + e.setId(rs.getInt("id_entry")); + String dateStr = rs.getString("horodatage"); + e.setTimestamp(dateStr != null ? LocalDateTime.parse(dateStr) : null); + e.setContent(rs.getString("contenu")); + return e; + } + + // ========================================================================= + // AUDIT + // ========================================================================= + + /** + * Ajoute une entrée d'audit. + * + * @param entry entrée d'audit à persister + */ public void addAuditEntry(AuditEntry entry) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "INSERT INTO audit_log (horodatage, id_vm, action, details, lab_name) VALUES (?,?,?,?,?)"; + try (PreparedStatement ps = getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setString(1, entry.getTimestamp() != null ? entry.getTimestamp().toString() : LocalDateTime.now().toString()); + if (entry.getVmId() != null) ps.setInt(2, entry.getVmId()); else ps.setNull(2, Types.INTEGER); + ps.setString(3, entry.getAction()); + ps.setString(4, entry.getDetails()); + ps.setString(5, entry.getLabName()); + ps.executeUpdate(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys.next()) entry.setId(keys.getInt(1)); + } catch (SQLException e) { + throw new RuntimeException("Erreur addAuditEntry : " + e.getMessage(), e); + } } + /** + * Récupère les entrées d'audit d'une machine virtuelle. + * + * @param vmId identifiant local de la machine virtuelle + * @return liste des entrées d'audit + */ public List getAuditLogsForVM(int vmId) { - throw new UnsupportedOperationException("Not implemented yet"); + List logs = new ArrayList<>(); + String sql = "SELECT * FROM audit_log WHERE id_vm=? ORDER BY horodatage DESC"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setInt(1, vmId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) logs.add(mapAuditEntry(rs)); + } catch (SQLException e) { + throw new RuntimeException("Erreur getAuditLogsForVM : " + e.getMessage(), e); + } + return logs; } + private AuditEntry mapAuditEntry(ResultSet rs) throws SQLException { + AuditEntry a = new AuditEntry(); + a.setId(rs.getInt("id_log")); + String dateStr = rs.getString("horodatage"); + a.setTimestamp(dateStr != null ? LocalDateTime.parse(dateStr) : null); + int vmIdValue = rs.getInt("id_vm"); + a.setVmId(rs.wasNull() ? null : vmIdValue); + a.setAction(rs.getString("action")); + a.setDetails(rs.getString("details")); + a.setLabName(rs.getString("lab_name")); + return a; + } + + // ========================================================================= + // CONFIGURATION + // ========================================================================= + + /** + * Récupère la configuration applicative. + * + * @return configuration applicative + */ public AppConfig getConfig() { - throw new UnsupportedOperationException("Not implemented yet"); + AppConfig config = new AppConfig(); + config.setVboxManagePath(getConfigValue("vboxManagePath", "VBoxManage")); + config.setPdfExportDirectory(getConfigValue("pdfExportDirectory", + System.getProperty("user.home"))); + return config; } + /** + * Enregistre la configuration applicative. + * + * @param config configuration à persister + */ public void saveConfig(AppConfig config) { - throw new UnsupportedOperationException("Not implemented yet"); + upsertConfig("vboxManagePath", config.getVboxManagePath()); + upsertConfig("pdfExportDirectory", config.getPdfExportDirectory()); + } + + private String getConfigValue(String key, String defaultValue) { + String sql = "SELECT valeur FROM app_config WHERE cle=?"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setString(1, key); + ResultSet rs = ps.executeQuery(); + if (rs.next()) return rs.getString("valeur"); + } catch (SQLException e) { + throw new RuntimeException("Erreur getConfigValue : " + e.getMessage(), e); + } + return defaultValue; + } + + private void upsertConfig(String key, String value) { + String sql = "INSERT OR REPLACE INTO app_config (cle, valeur) VALUES (?,?)"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setString(1, key); + ps.setString(2, value); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Erreur upsertConfig : " + e.getMessage(), e); + } } -} +} \ No newline at end of file diff --git a/src/main/java/tg/cyberlabmanager/hypervisor/HypervisorException.java b/src/main/java/tg/cyberlabmanager/hypervisor/HypervisorException.java index e4c0b3c..d59bf62 100644 --- a/src/main/java/tg/cyberlabmanager/hypervisor/HypervisorException.java +++ b/src/main/java/tg/cyberlabmanager/hypervisor/HypervisorException.java @@ -1,10 +1,24 @@ package tg.cyberlabmanager.hypervisor; +/** + * Exception commune aux erreurs d'orchestration d'hyperviseur. + */ public class HypervisorException extends Exception { + /** + * Cree une exception d'hyperviseur avec un message. + * + * @param message message decrivant l'erreur + */ public HypervisorException(String message) { super(message); } + /** + * Cree une exception d'hyperviseur avec un message et une cause. + * + * @param message message decrivant l'erreur + * @param cause cause initiale de l'erreur + */ public HypervisorException(String message, Throwable cause) { super(message, cause); } diff --git a/src/main/java/tg/cyberlabmanager/hypervisor/IHypervisor.java b/src/main/java/tg/cyberlabmanager/hypervisor/IHypervisor.java index c83757b..99ad3cf 100644 --- a/src/main/java/tg/cyberlabmanager/hypervisor/IHypervisor.java +++ b/src/main/java/tg/cyberlabmanager/hypervisor/IHypervisor.java @@ -4,18 +4,67 @@ import java.util.List; +/** + * Contrat minimal pour piloter un hyperviseur depuis l'application. + */ public interface IHypervisor { + /** + * Liste les machines virtuelles disponibles dans l'hyperviseur. + * + * @return liste de couples ou tableaux de donnees representant les VM + * @throws HypervisorException si la recuperation echoue + */ List listVMs() throws HypervisorException; + /** + * Retourne le statut courant d'une machine virtuelle. + * + * @param uuid UUID de la machine virtuelle + * @return statut courant de la VM + * @throws HypervisorException si le statut ne peut pas etre recupere + */ VMStatus getStatus(String uuid) throws HypervisorException; + /** + * Demarre une machine virtuelle. + * + * @param uuid UUID de la machine virtuelle + * @throws HypervisorException si le demarrage echoue + */ void startVM(String uuid) throws HypervisorException; + /** + * Arrete une machine virtuelle. + * + * @param uuid UUID de la machine virtuelle + * @throws HypervisorException si l'arret echoue + */ void stopVM(String uuid) throws HypervisorException; + /** + * Sauvegarde l'etat d'une machine virtuelle. + * + * @param uuid UUID de la machine virtuelle + * @throws HypervisorException si la sauvegarde d'etat echoue + */ void saveState(String uuid) throws HypervisorException; + /** + * Cree un snapshot pour une machine virtuelle. + * + * @param uuid UUID de la machine virtuelle + * @param name nom du snapshot + * @param description description du snapshot + * @throws HypervisorException si la creation du snapshot echoue + */ void takeSnapshot(String uuid, String name, String description) throws HypervisorException; + /** + * Restaure un snapshot pour une machine virtuelle. + * + * @param uuid UUID de la machine virtuelle + * @param snapshotName nom ou identifiant du snapshot + * @throws HypervisorException si la restauration echoue + */ void restoreSnapshot(String uuid, String snapshotName) throws HypervisorException; } diff --git a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java index 7a513e0..9851744 100644 --- a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java +++ b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java @@ -1,19 +1,41 @@ package tg.cyberlabmanager.hypervisor; +/** + * Exception specifique aux commandes VirtualBox executees via VBoxManage. + */ public class VBoxException extends HypervisorException { + /** Code de sortie retourne par VBoxManage. */ private final int exitCode; + /** Sortie standard ou erreur retournee par VBoxManage. */ private final String output; + /** + * Cree une exception VirtualBox avec le code de sortie et la sortie texte. + * + * @param message message decrivant l'erreur + * @param exitCode code de sortie de VBoxManage + * @param output sortie standard ou erreur produite par VBoxManage + */ public VBoxException(String message, int exitCode, String output) { super(message); this.exitCode = exitCode; this.output = output; } + /** + * Retourne le code de sortie de VBoxManage. + * + * @return code de sortie + */ public int getExitCode() { return exitCode; } + /** + * Retourne la sortie produite par VBoxManage. + * + * @return sortie standard ou erreur + */ public String getOutput() { return output; } diff --git a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java index 4228933..b8f432d 100644 --- a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java +++ b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java @@ -4,37 +4,53 @@ import java.util.List; +/** + * Implementation de {@link IHypervisor} pour VirtualBox via VBoxManage. + */ public class VBoxOrchestrator implements IHypervisor { + /** + * Cree un orchestrateur VirtualBox. + */ + public VBoxOrchestrator() { + } + + /** {@inheritDoc} */ @Override public List listVMs() throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); } + /** {@inheritDoc} */ @Override public VMStatus getStatus(String uuid) throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); } + /** {@inheritDoc} */ @Override public void startVM(String uuid) throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); } + /** {@inheritDoc} */ @Override public void stopVM(String uuid) throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); } + /** {@inheritDoc} */ @Override public void saveState(String uuid) throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); } + /** {@inheritDoc} */ @Override public void takeSnapshot(String uuid, String name, String description) throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); } + /** {@inheritDoc} */ @Override public void restoreSnapshot(String uuid, String snapshotName) throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); diff --git a/src/main/java/tg/cyberlabmanager/model/AppConfig.java b/src/main/java/tg/cyberlabmanager/model/AppConfig.java index 6d6c891..2a3fa9a 100644 --- a/src/main/java/tg/cyberlabmanager/model/AppConfig.java +++ b/src/main/java/tg/cyberlabmanager/model/AppConfig.java @@ -1,21 +1,53 @@ package tg.cyberlabmanager.model; +/** + * Configuration persistante de l'application. + * + *

Elle contient les chemins utilisés par les services techniques, notamment + * VBoxManage et le répertoire d'export PDF.

+ */ public class AppConfig { private String vboxManagePath; private String pdfExportDirectory; + /** + * Crée une configuration vide. + */ + public AppConfig() { + } + + /** + * Retourne le chemin de l'exécutable VBoxManage. + * + * @return chemin configuré vers VBoxManage + */ public String getVboxManagePath() { return vboxManagePath; } + /** + * Définit le chemin de l'exécutable VBoxManage. + * + * @param vboxManagePath chemin vers VBoxManage + */ public void setVboxManagePath(String vboxManagePath) { this.vboxManagePath = vboxManagePath; } + /** + * Retourne le répertoire d'export des rapports PDF. + * + * @return répertoire d'export PDF + */ public String getPdfExportDirectory() { return pdfExportDirectory; } + /** + * Définit le répertoire d'export des rapports PDF. + * + * @param pdfExportDirectory répertoire d'export PDF + */ public void setPdfExportDirectory(String pdfExportDirectory) { this.pdfExportDirectory = pdfExportDirectory; } diff --git a/src/main/java/tg/cyberlabmanager/model/AuditEntry.java b/src/main/java/tg/cyberlabmanager/model/AuditEntry.java index 4c6731f..7c87fea 100644 --- a/src/main/java/tg/cyberlabmanager/model/AuditEntry.java +++ b/src/main/java/tg/cyberlabmanager/model/AuditEntry.java @@ -1,51 +1,158 @@ package tg.cyberlabmanager.model; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +/** + * Trace automatique d'une action de contrôle effectuée sur une machine virtuelle. + * + *

Cette entité est séparée de {@link VirtualMachine} afin de conserver un + * historique potentiellement volumineux sans alourdir le modèle de la VM.

+ */ public class AuditEntry { + private static final DateTimeFormatter LOG_TIMESTAMP_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + private int id; private LocalDateTime timestamp; private Integer vmId; private String action; private String details; + private String labName; + + /** + * Crée une entrée d'audit vide. + */ + public AuditEntry() { + } + /** + * Retourne l'identifiant local de l'entrée d'audit. + * + * @return identifiant local + */ public int getId() { return id; } + /** + * Définit l'identifiant local de l'entrée d'audit. + * + * @param id identifiant local + */ public void setId(int id) { this.id = id; } + /** + * Retourne l'horodatage de l'action. + * + * @return date et heure de l'action + */ public LocalDateTime getTimestamp() { return timestamp; } + /** + * Définit l'horodatage de l'action. + * + * @param timestamp date et heure de l'action + */ public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; } + /** + * Retourne l'identifiant local de la VM concernée. + * + * @return identifiant de la VM, ou {@code null} si aucune VM n'est liée + */ public Integer getVmId() { return vmId; } + /** + * Définit l'identifiant local de la VM concernée. + * + * @param vmId identifiant de la VM, ou {@code null} + */ public void setVmId(Integer vmId) { this.vmId = vmId; } + /** + * Retourne l'action auditée. + * + * @return action auditée + */ public String getAction() { return action; } + /** + * Définit l'action auditée. + * + * @param action action auditée + */ public void setAction(String action) { this.action = action; } + /** + * Retourne les détails de l'action. + * + * @return détails de l'action + */ public String getDetails() { return details; } + /** + * Définit les détails de l'action. + * + * @param details détails de l'action + */ public void setDetails(String details) { this.details = details; } + + /** + * Retourne le nom du laboratoire concerné. + * + * @return nom du laboratoire + */ + public String getLabName() { + return labName; + } + + /** + * Définit le nom du laboratoire concerné. + * + * @param labName nom du laboratoire + */ + public void setLabName(String labName) { + this.labName = labName; + } + + /** + * Construit une représentation textuelle de l'entrée d'audit. + * + * @return ligne de log lisible + */ + public String getLog() { + StringBuilder log = new StringBuilder(); + log.append(timestamp == null ? "unknown-time" : timestamp.format(LOG_TIMESTAMP_FORMATTER)); + log.append(" | action=").append(action == null ? "UNKNOWN" : action); + + if (vmId != null) { + log.append(" | vmId=").append(vmId); + } + if (labName != null && !labName.isBlank()) { + log.append(" | lab=").append(labName); + } + if (details != null && !details.isBlank()) { + log.append(" | details=").append(details); + } + + return log.toString(); + } } diff --git a/src/main/java/tg/cyberlabmanager/model/JournalEntry.java b/src/main/java/tg/cyberlabmanager/model/JournalEntry.java index ec7031f..d2287ad 100644 --- a/src/main/java/tg/cyberlabmanager/model/JournalEntry.java +++ b/src/main/java/tg/cyberlabmanager/model/JournalEntry.java @@ -2,31 +2,73 @@ import java.time.LocalDateTime; +/** + * Note d'analyse horodatée saisie par l'analyste pour une machine virtuelle. + * + *

Une entrée de journal est rattachée au cycle de vie logique d'une + * {@link VirtualMachine}.

+ */ public class JournalEntry { private int id; private LocalDateTime timestamp; private String content; + /** + * Crée une entrée de journal vide. + */ + public JournalEntry() { + } + + /** + * Retourne l'identifiant local de l'entrée de journal. + * + * @return identifiant local + */ public int getId() { return id; } + /** + * Définit l'identifiant local de l'entrée de journal. + * + * @param id identifiant local + */ public void setId(int id) { this.id = id; } + /** + * Retourne l'horodatage de la note. + * + * @return date et heure de la note + */ public LocalDateTime getTimestamp() { return timestamp; } + /** + * Définit l'horodatage de la note. + * + * @param timestamp date et heure de la note + */ public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; } + /** + * Retourne le contenu de la note. + * + * @return contenu saisi par l'analyste + */ public String getContent() { return content; } + /** + * Définit le contenu de la note. + * + * @param content contenu saisi par l'analyste + */ public void setContent(String content) { this.content = content; } diff --git a/src/main/java/tg/cyberlabmanager/model/Lab.java b/src/main/java/tg/cyberlabmanager/model/Lab.java index 9e02ad8..8f82900 100644 --- a/src/main/java/tg/cyberlabmanager/model/Lab.java +++ b/src/main/java/tg/cyberlabmanager/model/Lab.java @@ -4,6 +4,13 @@ import java.util.Collections; import java.util.List; +/** + * Entité racine représentant un laboratoire thématique. + * + *

Ses attributs correspondent aux colonnes de la table {@code lab}. Les VM + * rattachées sont exposées en lecture seule et modifiées via les méthodes + * d'agrégation.

+ */ public class Lab { private int id; private String title; @@ -11,9 +18,20 @@ public class Lab { private String category; private final List virtualMachines = new ArrayList<>(); + /** + * Crée un laboratoire vide. + */ public Lab() { } + /** + * Crée un laboratoire avec ses champs principaux. + * + * @param id identifiant local du laboratoire + * @param title titre du laboratoire + * @param description description du laboratoire + * @param category catégorie du laboratoire + */ public Lab(int id, String title, String description, String category) { this.id = id; this.title = title; @@ -21,46 +39,101 @@ public Lab(int id, String title, String description, String category) { this.category = category; } + /** + * Retourne l'identifiant local du laboratoire. + * + * @return identifiant local + */ public int getId() { return id; } + /** + * Définit l'identifiant local du laboratoire. + * + * @param id identifiant local + */ public void setId(int id) { this.id = id; } + /** + * Retourne le titre du laboratoire. + * + * @return titre du laboratoire + */ public String getTitle() { return title; } + /** + * Définit le titre du laboratoire. + * + * @param title titre du laboratoire + */ public void setTitle(String title) { this.title = title; } + /** + * Retourne la description du laboratoire. + * + * @return description du laboratoire + */ public String getDescription() { return description; } + /** + * Définit la description du laboratoire. + * + * @param description description du laboratoire + */ public void setDescription(String description) { this.description = description; } + /** + * Retourne la catégorie du laboratoire. + * + * @return catégorie du laboratoire + */ public String getCategory() { return category; } + /** + * Définit la catégorie du laboratoire. + * + * @param category catégorie du laboratoire + */ public void setCategory(String category) { this.category = category; } + /** + * Retourne les machines virtuelles rattachées au laboratoire. + * + * @return liste non modifiable des machines virtuelles + */ public List getVirtualMachines() { return Collections.unmodifiableList(virtualMachines); } + /** + * Rattache une machine virtuelle au laboratoire. + * + * @param virtualMachine machine virtuelle à ajouter + */ public void addVirtualMachine(VirtualMachine virtualMachine) { virtualMachines.add(virtualMachine); } + /** + * Retire une machine virtuelle du laboratoire. + * + * @param virtualMachine machine virtuelle à retirer + */ public void removeVirtualMachine(VirtualMachine virtualMachine) { virtualMachines.remove(virtualMachine); } diff --git a/src/main/java/tg/cyberlabmanager/model/Snapshot.java b/src/main/java/tg/cyberlabmanager/model/Snapshot.java index bd5e879..cf69b33 100644 --- a/src/main/java/tg/cyberlabmanager/model/Snapshot.java +++ b/src/main/java/tg/cyberlabmanager/model/Snapshot.java @@ -2,41 +2,132 @@ import java.time.LocalDateTime; +/** + * Instantané VirtualBox rattaché à une machine virtuelle. + * + *

Le modèle conserve l'identité locale en base, l'UUID VirtualBox, les + * métadonnées d'affichage et l'information indiquant si l'instantané a été pris + * pendant que la VM était en exécution.

+ */ public class Snapshot { private int id; + private String uuid; private String name; private LocalDateTime createdAt; private String description; + private boolean online; + /** + * Crée un snapshot vide. + */ + public Snapshot() { + } + + /** + * Retourne l'identifiant local du snapshot. + * + * @return identifiant local + */ public int getId() { return id; } + /** + * Définit l'identifiant local du snapshot. + * + * @param id identifiant local + */ public void setId(int id) { this.id = id; } + /** + * Retourne l'UUID VirtualBox du snapshot. + * + * @return UUID VirtualBox + */ + public String getUuid() { + return uuid; + } + + /** + * Définit l'UUID VirtualBox du snapshot. + * + * @param uuid UUID VirtualBox + */ + public void setUuid(String uuid) { + this.uuid = uuid; + } + + /** + * Retourne le nom du snapshot. + * + * @return nom du snapshot + */ public String getName() { return name; } + /** + * Définit le nom du snapshot. + * + * @param name nom du snapshot + */ public void setName(String name) { this.name = name; } + /** + * Retourne la date de création du snapshot. + * + * @return date de création + */ public LocalDateTime getCreatedAt() { return createdAt; } + /** + * Définit la date de création du snapshot. + * + * @param createdAt date de création + */ public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + /** + * Retourne la description du snapshot. + * + * @return description du snapshot + */ public String getDescription() { return description; } + /** + * Définit la description du snapshot. + * + * @param description description du snapshot + */ public void setDescription(String description) { this.description = description; } + + /** + * Indique si le snapshot a été pris VM allumée. + * + * @return {@code true} si le snapshot inclut l'état en exécution + */ + public boolean isOnline() { + return online; + } + + /** + * Définit si le snapshot a été pris VM allumée. + * + * @param online {@code true} si le snapshot inclut l'état en exécution + */ + public void setOnline(boolean online) { + this.online = online; + } } diff --git a/src/main/java/tg/cyberlabmanager/model/VMStatus.java b/src/main/java/tg/cyberlabmanager/model/VMStatus.java index b6a8744..3b0130a 100644 --- a/src/main/java/tg/cyberlabmanager/model/VMStatus.java +++ b/src/main/java/tg/cyberlabmanager/model/VMStatus.java @@ -1,9 +1,17 @@ package tg.cyberlabmanager.model; +/** + * États stables d'une machine virtuelle tels qu'exposés par l'hyperviseur. + */ public enum VMStatus { + /** La VM est en cours d'exécution. */ RUNNING, + /** La VM est éteinte. */ POWERED_OFF, + /** La VM est dans un état sauvegardé. */ SAVED, + /** La VM est en pause. */ PAUSED, + /** Le statut de la VM n'a pas pu être déterminé. */ UNKNOWN } diff --git a/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java b/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java index f031a89..e8442cd 100644 --- a/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java +++ b/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java @@ -4,6 +4,13 @@ import java.util.Collections; import java.util.List; +/** + * Machine virtuelle importée depuis VirtualBox. + * + *

L'UUID VirtualBox fournit une référence stable. Les snapshots et notes + * d'analyse sont encapsulés pour éviter l'exposition directe des listes + * internes.

+ */ public class VirtualMachine { private int id; private String name; @@ -13,59 +20,135 @@ public class VirtualMachine { private final List snapshots = new ArrayList<>(); private final List journalEntries = new ArrayList<>(); + /** + * Crée une machine virtuelle vide. + */ + public VirtualMachine() { + } + + /** + * Retourne l'identifiant local de la VM. + * + * @return identifiant local + */ public int getId() { return id; } + /** + * Définit l'identifiant local de la VM. + * + * @param id identifiant local + */ public void setId(int id) { this.id = id; } + /** + * Retourne le nom de la VM. + * + * @return nom de la VM + */ public String getName() { return name; } + /** + * Définit le nom de la VM. + * + * @param name nom de la VM + */ public void setName(String name) { this.name = name; } + /** + * Retourne l'UUID VirtualBox de la VM. + * + * @return UUID VirtualBox + */ public String getUuid() { return uuid; } + /** + * Définit l'UUID VirtualBox de la VM. + * + * @param uuid UUID VirtualBox + */ public void setUuid(String uuid) { this.uuid = uuid; } + /** + * Retourne l'URL de documentation associée à la VM. + * + * @return URL de documentation + */ public String getDocumentationUrl() { return documentationUrl; } + /** + * Définit l'URL de documentation associée à la VM. + * + * @param documentationUrl URL de documentation + */ public void setDocumentationUrl(String documentationUrl) { this.documentationUrl = documentationUrl; } + /** + * Retourne le statut courant de la VM. + * + * @return statut de la VM + */ public VMStatus getStatus() { return status; } + /** + * Définit le statut courant de la VM. + * + * @param status statut de la VM + */ public void setStatus(VMStatus status) { this.status = status; } + /** + * Retourne les snapshots rattachés à la VM. + * + * @return liste non modifiable des snapshots + */ public List getSnapshots() { return Collections.unmodifiableList(snapshots); } + /** + * Ajoute un snapshot à la VM. + * + * @param snapshot snapshot à ajouter + */ public void addSnapshot(Snapshot snapshot) { snapshots.add(snapshot); } + /** + * Retourne les notes d'analyse rattachées à la VM. + * + * @return liste non modifiable des notes d'analyse + */ public List getJournalEntries() { return Collections.unmodifiableList(journalEntries); } - public void addJournalEntry(JournalEntry journalEntry) { + /** + * Ajoute une note d'analyse à la VM. + * + * @param journalEntry note d'analyse à ajouter + */ + public void addJournalEntry(JournalEntry journalEntry) { journalEntries.add(journalEntry); } } diff --git a/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java b/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java index 5c670d7..7a6679f 100644 --- a/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java +++ b/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java @@ -5,17 +5,254 @@ import tg.cyberlabmanager.model.Snapshot; import tg.cyberlabmanager.model.VirtualMachine; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; + import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; +/** + * Génère un rapport PDF récapitulatif d'un laboratoire Cyber Lab Manager. + * Le rapport contient : en-tête du labo, liste des VMs, snapshots et journal de bord. + * + * Utilise Apache PDFBox 3.x. + */ public class PdfExporter { - public void exportLabToPdf( - Lab lab, - List vms, - List snapshots, - List journals, - String filePath - ) throws IOException { - throw new UnsupportedOperationException("Not implemented yet"); - } -} + + private static final float MARGIN = 50f; + private static final float PAGE_HEIGHT = PDRectangle.A4.getHeight(); + private static final float PAGE_WIDTH = PDRectangle.A4.getWidth(); + private static final float CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN; + + private static final float FONT_TITLE = 18f; + private static final float FONT_HEADING = 13f; + private static final float FONT_SUBHEAD = 11f; + private static final float FONT_BODY = 10f; + + private static final PDType1Font FONT_BOLD = new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD); + private static final PDType1Font FONT_REGULAR = new PDType1Font(Standard14Fonts.FontName.HELVETICA); + private static final PDType1Font FONT_OBLIQUE = new PDType1Font(Standard14Fonts.FontName.HELVETICA_OBLIQUE); + + private static final DateTimeFormatter DISPLAY_FORMAT = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"); + + private PDDocument document; + private PDPageContentStream content; + private float cursorY; + + /** + * Génère et sauvegarde le rapport PDF d'un laboratoire. + * + * @param lab le laboratoire exporté + * @param vms liste des VMs du labo (déjà filtrée par le contrôleur) + * @param snapshots liste des snapshots (déjà filtrée par le contrôleur) + * @param journals liste des entrées de journal (déjà filtrée par le contrôleur) + * @param filePath chemin complet du fichier PDF à créer + * @throws IOException si l'écriture du fichier échoue + */ + public void exportLabToPdf(Lab lab, + List vms, + List snapshots, + List journals, + String filePath) throws IOException { + + document = new PDDocument(); + newPage(); + + // En-tête + writeLine("Rapport – " + nvl(lab.getTitle(), "Sans titre"), FONT_BOLD, FONT_TITLE, true); + writeLine("Catégorie : " + nvl(lab.getCategory(), "Non définie"), FONT_REGULAR, FONT_BODY, false); + writeLine("Généré le : " + now(), FONT_REGULAR, FONT_BODY, false); + if (lab.getDescription() != null && !lab.getDescription().isBlank()) { + writeLine("Description : " + lab.getDescription(), FONT_OBLIQUE, FONT_BODY, false); + } + spacer(12f); + separator(); + spacer(8f); + + // Section 1 : Machines virtuelles + writeLine("1. Machines virtuelles (" + vms.size() + ")", FONT_BOLD, FONT_HEADING, false); + spacer(6f); + + if (vms.isEmpty()) { + writeLine(" Aucune machine virtuelle dans ce laboratoire.", FONT_OBLIQUE, FONT_BODY, false); + } else { + for (VirtualMachine vm : vms) { + checkPageBreak(60f); + writeLine(" ▸ " + nvl(vm.getName(), "Sans nom"), FONT_BOLD, FONT_SUBHEAD, false); + writeLine(" UUID : " + nvl(vm.getUuid(), "—"), FONT_REGULAR, FONT_BODY, false); + writeLine(" Statut : " + (vm.getStatus() != null ? vm.getStatus().name() : "UNKNOWN"), + FONT_REGULAR, FONT_BODY, false); + if (vm.getDocumentationUrl() != null && !vm.getDocumentationUrl().isBlank()) { + writeLine(" Doc : " + vm.getDocumentationUrl(), FONT_REGULAR, FONT_BODY, false); + } + spacer(5f); + } + } + + spacer(8f); + separator(); + spacer(8f); + + // Section 2 : Snapshots + writeLine("2. Snapshots (" + snapshots.size() + ")", FONT_BOLD, FONT_HEADING, false); + spacer(6f); + + if (snapshots.isEmpty()) { + writeLine(" Aucun snapshot enregistré.", FONT_OBLIQUE, FONT_BODY, false); + } else { + for (Snapshot snap : snapshots) { + checkPageBreak(40f); + writeLine(" ▸ " + nvl(snap.getName(), "Sans nom"), FONT_BOLD, FONT_SUBHEAD, false); + writeLine(" Date : " + formatDate(snap.getCreatedAt()), FONT_REGULAR, FONT_BODY, false); + writeLine(" Type : " + (snap.isOnline() ? "À chaud (VM allumée)" : "À froid (VM éteinte)"), + FONT_REGULAR, FONT_BODY, false); + if (snap.getDescription() != null && !snap.getDescription().isBlank()) { + writeLine(" Note : " + snap.getDescription(), FONT_OBLIQUE, FONT_BODY, false); + } + spacer(4f); + } + } + + spacer(8f); + separator(); + spacer(8f); + + // Section 3 : Journal de bord + writeLine("3. Journal de bord (" + journals.size() + " entrée(s))", FONT_BOLD, FONT_HEADING, false); + spacer(6f); + + if (journals.isEmpty()) { + writeLine(" Aucune note dans le journal.", FONT_OBLIQUE, FONT_BODY, false); + } else { + for (JournalEntry entry : journals) { + checkPageBreak(40f); + writeLine(" [" + formatDate(entry.getTimestamp()) + "]", FONT_BOLD, FONT_BODY, false); + writeWrappedText(" " + nvl(entry.getContent(), ""), FONT_REGULAR, FONT_BODY); + spacer(5f); + } + } + + writeFooter(); + + content.close(); + document.save(filePath); + document.close(); + } + + + // Gestion des pages + + + private void newPage() throws IOException { + if (content != null) { + content.close(); + } + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + content = new PDPageContentStream(document, page); + cursorY = PAGE_HEIGHT - MARGIN; + } + + private void checkPageBreak(float needed) throws IOException { + if (cursorY - needed < MARGIN + 20f) { + newPage(); + } + } + + // Écriture de contenu + + private void writeLine(String text, PDType1Font font, float size, boolean underline) + throws IOException { + checkPageBreak(size + 6f); + cursorY -= size + 2f; + + content.beginText(); + content.setFont(font, size); + content.newLineAtOffset(MARGIN, cursorY); + content.showText(sanitize(text)); + content.endText(); + + if (underline) { + float textWidth = font.getStringWidth(sanitize(text)) / 1000 * size; + content.moveTo(MARGIN, cursorY - 2f); + content.lineTo(MARGIN + textWidth, cursorY - 2f); + content.stroke(); + cursorY -= 4f; + } + } + + private void writeWrappedText(String text, PDType1Font font, float size) throws IOException { + float maxWidth = CONTENT_WIDTH; + String[] words = sanitize(text).split(" "); + StringBuilder line = new StringBuilder(); + + for (String word : words) { + String candidate = line.isEmpty() ? word : line + " " + word; + float w = font.getStringWidth(candidate) / 1000 * size; + if (w > maxWidth && !line.isEmpty()) { + writeLine(line.toString(), font, size, false); + line = new StringBuilder(word); + } else { + line = new StringBuilder(candidate); + } + } + if (!line.isEmpty()) { + writeLine(line.toString(), font, size, false); + } + } + + private void spacer(float points) { + cursorY -= points; + } + + private void separator() throws IOException { + content.setLineWidth(0.5f); + content.moveTo(MARGIN, cursorY); + content.lineTo(PAGE_WIDTH - MARGIN, cursorY); + content.stroke(); + cursorY -= 2f; + } + + private void writeFooter() throws IOException { + float footerY = MARGIN - 10f; + content.beginText(); + content.setFont(FONT_OBLIQUE, 8f); + content.newLineAtOffset(MARGIN, footerY); + content.showText("Cyber Lab Manager – Rapport généré le " + now()); + content.endText(); + } + + + // Utilitaires + + private String nvl(String value, String fallback) { + return (value != null && !value.isBlank()) ? value : fallback; + } + + private String now() { + return LocalDateTime.now().format(DISPLAY_FORMAT); + } + + private String formatDate(LocalDateTime dt) { + return dt != null ? dt.format(DISPLAY_FORMAT) : "—"; + } + + /** + * Nettoie les caractères non supportés par les polices standard PDF (Latin-1 uniquement). + * Remplace les caractères hors plage par '?'. + */ + private String sanitize(String text) { + if (text == null) return ""; + StringBuilder sb = new StringBuilder(); + for (char c : text.toCharArray()) { + sb.append(c <= 255 ? c : '?'); + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/tg/cyberlabmanager/ui/LabListView.java b/src/main/java/tg/cyberlabmanager/ui/LabListView.java index 7a10e42..8e06fb6 100644 --- a/src/main/java/tg/cyberlabmanager/ui/LabListView.java +++ b/src/main/java/tg/cyberlabmanager/ui/LabListView.java @@ -1,4 +1,12 @@ package tg.cyberlabmanager.ui; +/** + * Vue chargee d'afficher la liste des laboratoires. + */ public class LabListView { + /** + * Cree la vue de liste des laboratoires. + */ + public LabListView() { + } } diff --git a/src/main/java/tg/cyberlabmanager/ui/MainView.java b/src/main/java/tg/cyberlabmanager/ui/MainView.java index 0e763a1..b38c414 100644 --- a/src/main/java/tg/cyberlabmanager/ui/MainView.java +++ b/src/main/java/tg/cyberlabmanager/ui/MainView.java @@ -1,4 +1,12 @@ package tg.cyberlabmanager.ui; +/** + * Vue principale de l'application. + */ public class MainView { + /** + * Cree la vue principale. + */ + public MainView() { + } } diff --git a/src/main/java/tg/cyberlabmanager/ui/VmControlPanel.java b/src/main/java/tg/cyberlabmanager/ui/VmControlPanel.java index 49b4eea..e0692bc 100644 --- a/src/main/java/tg/cyberlabmanager/ui/VmControlPanel.java +++ b/src/main/java/tg/cyberlabmanager/ui/VmControlPanel.java @@ -1,4 +1,12 @@ package tg.cyberlabmanager.ui; +/** + * Vue chargee des controles utilisateur pour les machines virtuelles. + */ public class VmControlPanel { + /** + * Cree le panneau de controle des machines virtuelles. + */ + public VmControlPanel() { + } } diff --git a/src/main/resources/tg/cyberlabmanager/data/schema.sql b/src/main/resources/tg/cyberlabmanager/data/schema.sql index fde532e..96160eb 100644 --- a/src/main/resources/tg/cyberlabmanager/data/schema.sql +++ b/src/main/resources/tg/cyberlabmanager/data/schema.sql @@ -35,6 +35,7 @@ CREATE TABLE IF NOT EXISTS audit_entry ( id_audit INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, id_vm INTEGER, + lab_name TEXT, action TEXT NOT NULL, details TEXT, FOREIGN KEY (id_vm) REFERENCES virtual_machine(id_vm) ON DELETE SET NULL diff --git a/src/test/java/tg/cyberlabmanager/data/DatabaseManagerTest.java b/src/test/java/tg/cyberlabmanager/data/DatabaseManagerTest.java new file mode 100644 index 0000000..9c34f1d --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/data/DatabaseManagerTest.java @@ -0,0 +1,510 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ +package tg.cyberlabmanager.data; + +/** + * + * @author agokoli + */ +import tg.cyberlabmanager.model.*; + +import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * Tests unitaires de DatabaseManager. + * Utilise SQLite en mémoire (:memory:) — aucun fichier créé sur le disque. + * Chaque test repart d'une base vide grâce au @BeforeEach. + */ +class DatabaseManagerTest { + + private DatabaseManager db; + + @BeforeEach + void setUp() { + // Base en mémoire : rapide, isolée, détruite après chaque test + db = new DatabaseManager("jdbc:sqlite::memory:"); + } + + // ========================================================================= + // LABORATOIRES + // ========================================================================= + + @Test + @DisplayName("saveLab : un nouveau labo reçoit un id généré") + void testSaveLabInsert() { + Lab lab = new Lab(); + lab.setTitle("Labo Malware"); + lab.setDescription("Analyse de malware"); + lab.setCategory("malware"); + + db.saveLab(lab); + + assertNotEquals(0, lab.getId(), "L'id doit être assigné après l'INSERT"); + } + + @Test + @DisplayName("getLab : retrouve un labo par son id") + void testGetLab() { + Lab lab = new Lab(); + lab.setTitle("Labo Pentest"); + lab.setCategory("pentest"); + db.saveLab(lab); + + Lab retrieved = db.getLab(lab.getId()); + + assertNotNull(retrieved); + assertEquals("Labo Pentest", retrieved.getTitle()); + assertEquals("pentest", retrieved.getCategory()); + } + + @Test + @DisplayName("getLab : retourne null si l'id n'existe pas") + void testGetLabNotFound() { + Lab result = db.getLab(9999); + assertNull(result); + } + + @Test + @DisplayName("saveLab : met à jour un labo existant") + void testSaveLabUpdate() { + Lab lab = new Lab(); + lab.setTitle("Ancien titre"); + lab.setCategory("reseau"); + db.saveLab(lab); + + lab.setTitle("Nouveau titre"); + db.saveLab(lab); // doit faire un UPDATE + + Lab updated = db.getLab(lab.getId()); + assertEquals("Nouveau titre", updated.getTitle()); + } + + @Test + @DisplayName("getAllLabs : retourne tous les labos") + void testGetAllLabs() { + Lab lab1 = new Lab(); lab1.setTitle("Labo A"); lab1.setCategory("malware"); + Lab lab2 = new Lab(); lab2.setTitle("Labo B"); lab2.setCategory("pentest"); + Lab lab3 = new Lab(); lab3.setTitle("Labo C"); lab3.setCategory("reseau"); + + db.saveLab(lab1); + db.saveLab(lab2); + db.saveLab(lab3); + + List labs = db.getAllLabs(); + assertEquals(3, labs.size()); + } + + @Test + @DisplayName("getAllLabs : retourne une liste vide si aucun labo") + void testGetAllLabsEmpty() { + List labs = db.getAllLabs(); + assertNotNull(labs); + assertTrue(labs.isEmpty()); + } + + @Test + @DisplayName("deleteLab : supprime un labo existant") + void testDeleteLab() { + Lab lab = new Lab(); + lab.setTitle("Labo à supprimer"); + db.saveLab(lab); + + db.deleteLab(lab); + + assertNull(db.getLab(lab.getId())); + assertEquals(0, db.getAllLabs().size()); + } + + @Test + @DisplayName("deleteLab : les VMs rattachées deviennent orphelines") + void testDeleteLabOrphansVMs() { + Lab lab = new Lab(); + lab.setTitle("Labo avec VMs"); + db.saveLab(lab); + + VirtualMachine vm = new VirtualMachine(); + vm.setName("Kali"); + vm.setUuid("uuid-kali"); + db.saveVirtualMachine(vm, lab.getId()); + + db.deleteLab(lab); // les VMs doivent passer en orphelines + + List orphans = db.getOrphanVMs(); + assertEquals(1, orphans.size()); + assertEquals("Kali", orphans.get(0).getName()); + } + + // ========================================================================= + // MACHINES VIRTUELLES + // ========================================================================= + + @Test + @DisplayName("saveVirtualMachine : une nouvelle VM reçoit un id généré") + void testSaveVMInsert() { + Lab lab = new Lab(); lab.setTitle("Labo"); db.saveLab(lab); + + VirtualMachine vm = new VirtualMachine(); + vm.setName("Ubuntu-22"); + vm.setUuid("uuid-ubuntu"); + vm.setStatus(VMStatus.POWERED_OFF); + + db.saveVirtualMachine(vm, lab.getId()); + + assertNotEquals(0, vm.getId(), "L'id doit être assigné après l'INSERT"); + } + + @Test + @DisplayName("saveVirtualMachine : labId null crée une VM orpheline") + void testSaveVMOrpheline() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM-Orpheline"); + vm.setUuid("uuid-orphan"); + + db.saveVirtualMachine(vm, null); + + List orphans = db.getOrphanVMs(); + assertEquals(1, orphans.size()); + assertEquals("VM-Orpheline", orphans.get(0).getName()); + } + + @Test + @DisplayName("getVMsForLab : retourne les VMs d'un labo") + void testGetVMsForLab() { + Lab lab = new Lab(); lab.setTitle("Labo"); db.saveLab(lab); + + VirtualMachine vm1 = new VirtualMachine(); vm1.setName("VM1"); vm1.setUuid("uuid-1"); + VirtualMachine vm2 = new VirtualMachine(); vm2.setName("VM2"); vm2.setUuid("uuid-2"); + + db.saveVirtualMachine(vm1, lab.getId()); + db.saveVirtualMachine(vm2, lab.getId()); + + List vms = db.getVMsForLab(lab.getId()); + assertEquals(2, vms.size()); + } + + @Test + @DisplayName("getVMsForLab : ne retourne pas les VMs d'un autre labo") + void testGetVMsForLabIsolation() { + Lab lab1 = new Lab(); lab1.setTitle("Labo 1"); db.saveLab(lab1); + Lab lab2 = new Lab(); lab2.setTitle("Labo 2"); db.saveLab(lab2); + + VirtualMachine vm = new VirtualMachine(); vm.setName("VM du labo 1"); vm.setUuid("uuid-x"); + db.saveVirtualMachine(vm, lab1.getId()); + + List vmsLab2 = db.getVMsForLab(lab2.getId()); + assertTrue(vmsLab2.isEmpty()); + } + + @Test + @DisplayName("getOrphanVMs : retourne uniquement les VMs sans labo") + void testGetOrphanVMs() { + Lab lab = new Lab(); lab.setTitle("Labo"); db.saveLab(lab); + + VirtualMachine vmRattachee = new VirtualMachine(); + vmRattachee.setName("VM rattachée"); vmRattachee.setUuid("uuid-r"); + db.saveVirtualMachine(vmRattachee, lab.getId()); + + VirtualMachine vmOrpheline = new VirtualMachine(); + vmOrpheline.setName("VM orpheline"); vmOrpheline.setUuid("uuid-o"); + db.saveVirtualMachine(vmOrpheline, null); + + List orphans = db.getOrphanVMs(); + assertEquals(1, orphans.size()); + assertEquals("VM orpheline", orphans.get(0).getName()); + } + + @Test + @DisplayName("removeVMFromLab : détache une VM sans la supprimer") + void testRemoveVMFromLab() { + Lab lab = new Lab(); lab.setTitle("Labo"); db.saveLab(lab); + + VirtualMachine vm = new VirtualMachine(); + vm.setName("Kali"); vm.setUuid("uuid-kali"); + db.saveVirtualMachine(vm, lab.getId()); + + db.removeVMFromLab(vm); + + // La VM ne doit plus être dans le labo + assertTrue(db.getVMsForLab(lab.getId()).isEmpty()); + // Mais elle doit être orpheline + assertEquals(1, db.getOrphanVMs().size()); + } + + @Test + @DisplayName("saveVirtualMachine : le statut est bien persisté") + void testSaveVMStatus() { + Lab lab = new Lab(); lab.setTitle("Labo"); db.saveLab(lab); + + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM Running"); vm.setUuid("uuid-run"); + vm.setStatus(VMStatus.RUNNING); + db.saveVirtualMachine(vm, lab.getId()); + + List vms = db.getVMsForLab(lab.getId()); + assertEquals(VMStatus.RUNNING, vms.get(0).getStatus()); + } + + @Test + @DisplayName("saveVirtualMachine : documentationUrl est bien persistée") + void testSaveVMDocUrl() { + Lab lab = new Lab(); lab.setTitle("Labo"); db.saveLab(lab); + + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM Doc"); vm.setUuid("uuid-doc"); + vm.setDocumentationUrl("https://doc.exemple.com"); + db.saveVirtualMachine(vm, lab.getId()); + + List vms = db.getVMsForLab(lab.getId()); + assertEquals("https://doc.exemple.com", vms.get(0).getDocumentationUrl()); + } + + // ========================================================================= + // SNAPSHOTS + // ========================================================================= + + @Test + @DisplayName("saveSnapshot : un nouveau snapshot reçoit un id généré") + void testSaveSnapshot() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-snap"); + db.saveVirtualMachine(vm, null); + + Snapshot snap = new Snapshot(); + snap.setName("Snapshot initial"); + snap.setDescription("Etat propre"); + snap.setCreatedAt(LocalDateTime.of(2026, 6, 1, 10, 0)); + snap.setOnline(false); + + db.saveSnapshot(snap, vm.getId()); + + assertNotEquals(0, snap.getId()); + } + + @Test + @DisplayName("getSnapshotsForVM : retourne les snapshots dans l'ordre décroissant") + void testGetSnapshotsForVM() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-snaps"); + db.saveVirtualMachine(vm, null); + + Snapshot s1 = new Snapshot(); s1.setName("Snap 1"); + s1.setCreatedAt(LocalDateTime.of(2026, 1, 1, 0, 0)); + Snapshot s2 = new Snapshot(); s2.setName("Snap 2"); + s2.setCreatedAt(LocalDateTime.of(2026, 6, 1, 0, 0)); + + db.saveSnapshot(s1, vm.getId()); + db.saveSnapshot(s2, vm.getId()); + + List snaps = db.getSnapshotsForVM(vm.getId()); + assertEquals(2, snaps.size()); + // Le plus récent en premier + assertEquals("Snap 2", snaps.get(0).getName()); + } + + @Test + @DisplayName("getSnapshotsForVM : retourne liste vide si aucun snapshot") + void testGetSnapshotsEmpty() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-no-snap"); + db.saveVirtualMachine(vm, null); + + assertTrue(db.getSnapshotsForVM(vm.getId()).isEmpty()); + } + + @Test + @DisplayName("saveSnapshot : le champ online est bien persisté") + void testSnapshotOnline() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-online"); + db.saveVirtualMachine(vm, null); + + Snapshot snap = new Snapshot(); + snap.setName("Snap chaud"); + snap.setCreatedAt(LocalDateTime.now()); + snap.setOnline(true); + db.saveSnapshot(snap, vm.getId()); + + List snaps = db.getSnapshotsForVM(vm.getId()); + assertTrue(snaps.get(0).isOnline()); + } + + // ========================================================================= + // JOURNAL DE BORD + // ========================================================================= + + @Test + @DisplayName("addJournalEntry : une nouvelle entrée reçoit un id généré") + void testAddJournalEntry() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-journal"); + db.saveVirtualMachine(vm, null); + + JournalEntry entry = new JournalEntry(); + entry.setTimestamp(LocalDateTime.now()); + entry.setContent("Analyse en cours — comportement suspect détecté"); + + db.addJournalEntry(entry, vm.getId()); + + assertNotEquals(0, entry.getId()); + } + + @Test + @DisplayName("getJournalEntriesForVM : retourne les entrées de la plus récente à la plus ancienne") + void testGetJournalEntries() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-j2"); + db.saveVirtualMachine(vm, null); + + JournalEntry e1 = new JournalEntry(); + e1.setTimestamp(LocalDateTime.of(2026, 1, 1, 8, 0)); + e1.setContent("Première note"); + + JournalEntry e2 = new JournalEntry(); + e2.setTimestamp(LocalDateTime.of(2026, 6, 1, 9, 0)); + e2.setContent("Note récente"); + + db.addJournalEntry(e1, vm.getId()); + db.addJournalEntry(e2, vm.getId()); + + List entries = db.getJournalEntriesForVM(vm.getId()); + assertEquals(2, entries.size()); + assertEquals("Note récente", entries.get(0).getContent()); + } + + @Test + @DisplayName("getJournalEntriesForVM : retourne liste vide si aucune note") + void testGetJournalEntriesEmpty() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-j-empty"); + db.saveVirtualMachine(vm, null); + + assertTrue(db.getJournalEntriesForVM(vm.getId()).isEmpty()); + } + + // ========================================================================= + // AUDIT + // ========================================================================= + + @Test + @DisplayName("addAuditEntry : une entrée d'audit reçoit un id généré") + void testAddAuditEntry() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-audit"); + db.saveVirtualMachine(vm, null); + + AuditEntry entry = new AuditEntry(); + entry.setTimestamp(LocalDateTime.now()); + entry.setVmId(vm.getId()); + entry.setAction("START"); + entry.setDetails("Démarrage manuel"); + + db.addAuditEntry(entry); + + assertNotEquals(0, entry.getId()); + } + + @Test + @DisplayName("getAuditLogsForVM : retourne les logs d'une VM") + void testGetAuditLogs() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-audit2"); + db.saveVirtualMachine(vm, null); + + AuditEntry e1 = new AuditEntry(); + e1.setTimestamp(LocalDateTime.now()); + e1.setVmId(vm.getId()); + e1.setAction("START"); + + AuditEntry e2 = new AuditEntry(); + e2.setTimestamp(LocalDateTime.now()); + e2.setVmId(vm.getId()); + e2.setAction("STOP"); + + db.addAuditEntry(e1); + db.addAuditEntry(e2); + + List logs = db.getAuditLogsForVM(vm.getId()); + assertEquals(2, logs.size()); + } + + @Test + @DisplayName("addAuditEntry : vmId null est accepté") + void testAddAuditEntryNullVmId() { + AuditEntry entry = new AuditEntry(); + entry.setTimestamp(LocalDateTime.now()); + entry.setVmId(null); // pas de VM liée + entry.setAction("APP_START"); + entry.setDetails("Démarrage de l'application"); + + assertDoesNotThrow(() -> db.addAuditEntry(entry)); + assertNotEquals(0, entry.getId()); + } + + @Test + @DisplayName("addAuditEntry : labName est bien persisté") + void testAuditEntryLabName() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-labname"); + db.saveVirtualMachine(vm, null); + + AuditEntry entry = new AuditEntry(); + entry.setTimestamp(LocalDateTime.now()); + entry.setVmId(vm.getId()); + entry.setAction("SNAPSHOT_TAKEN"); + entry.setLabName("Labo Malware"); + + db.addAuditEntry(entry); + + List logs = db.getAuditLogsForVM(vm.getId()); + assertEquals("Labo Malware", logs.get(0).getLabName()); + } + + // ========================================================================= + // CONFIGURATION + // ========================================================================= + + @Test + @DisplayName("getConfig : retourne des valeurs par défaut si rien n'est sauvegardé") + void testGetConfigDefaults() { + AppConfig config = db.getConfig(); + + assertNotNull(config); + assertEquals("VBoxManage", config.getVboxManagePath()); + assertNotNull(config.getPdfExportDirectory()); + } + + @Test + @DisplayName("saveConfig et getConfig : les valeurs sont bien persistées") + void testSaveAndGetConfig() { + AppConfig config = new AppConfig(); + config.setVboxManagePath("/usr/bin/VBoxManage"); + config.setPdfExportDirectory("/home/agokoli/rapports"); + + db.saveConfig(config); + + AppConfig loaded = db.getConfig(); + assertEquals("/usr/bin/VBoxManage", loaded.getVboxManagePath()); + assertEquals("/home/agokoli/rapports", loaded.getPdfExportDirectory()); + } + + @Test + @DisplayName("saveConfig : une deuxième sauvegarde écrase la première") + void testSaveConfigOverwrite() { + AppConfig config1 = new AppConfig(); + config1.setVboxManagePath("/chemin/ancien"); + db.saveConfig(config1); + + AppConfig config2 = new AppConfig(); + config2.setVboxManagePath("/chemin/nouveau"); + db.saveConfig(config2); + + AppConfig loaded = db.getConfig(); + assertEquals("/chemin/nouveau", loaded.getVboxManagePath()); + } +} \ No newline at end of file diff --git a/src/test/java/tg/cyberlabmanager/model/.gitkeep b/src/test/java/tg/cyberlabmanager/model/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/src/test/java/tg/cyberlabmanager/model/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/test/java/tg/cyberlabmanager/model/AppConfigTest.java b/src/test/java/tg/cyberlabmanager/model/AppConfigTest.java new file mode 100644 index 0000000..5b24c55 --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/AppConfigTest.java @@ -0,0 +1,18 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AppConfigTest { + @Test + void shouldStoreConfigurationPaths() { + AppConfig config = new AppConfig(); + + config.setVboxManagePath("/usr/bin/VBoxManage"); + config.setPdfExportDirectory("/tmp/reports"); + + assertEquals("/usr/bin/VBoxManage", config.getVboxManagePath()); + assertEquals("/tmp/reports", config.getPdfExportDirectory()); + } +} diff --git a/src/test/java/tg/cyberlabmanager/model/AuditEntryTest.java b/src/test/java/tg/cyberlabmanager/model/AuditEntryTest.java new file mode 100644 index 0000000..493d86a --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/AuditEntryTest.java @@ -0,0 +1,51 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AuditEntryTest { + @Test + void shouldStoreAuditFields() { + LocalDateTime timestamp = LocalDateTime.of(2026, 5, 29, 20, 30); + AuditEntry entry = new AuditEntry(); + + entry.setId(7); + entry.setTimestamp(timestamp); + entry.setVmId(12); + entry.setAction("START"); + entry.setDetails("VM started from UI"); + entry.setLabName("Malware Lab"); + + assertEquals(7, entry.getId()); + assertEquals(timestamp, entry.getTimestamp()); + assertEquals(12, entry.getVmId()); + assertEquals("START", entry.getAction()); + assertEquals("VM started from UI", entry.getDetails()); + assertEquals("Malware Lab", entry.getLabName()); + } + + @Test + void shouldBuildReadableLogLine() { + AuditEntry entry = new AuditEntry(); + entry.setTimestamp(LocalDateTime.of(2026, 5, 29, 20, 30)); + entry.setVmId(12); + entry.setAction("SNAPSHOT_TAKEN"); + entry.setLabName("Pentest Lab"); + entry.setDetails("Snapshot baseline created"); + + assertEquals( + "2026-05-29T20:30:00 | action=SNAPSHOT_TAKEN | vmId=12 | lab=Pentest Lab | details=Snapshot baseline created", + entry.getLog() + ); + } + + @Test + void shouldBuildFallbackLogWhenOptionalFieldsAreMissing() { + AuditEntry entry = new AuditEntry(); + + assertEquals("unknown-time | action=UNKNOWN", entry.getLog()); + } +} diff --git a/src/test/java/tg/cyberlabmanager/model/JournalEntryTest.java b/src/test/java/tg/cyberlabmanager/model/JournalEntryTest.java new file mode 100644 index 0000000..1abd563 --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/JournalEntryTest.java @@ -0,0 +1,23 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class JournalEntryTest { + @Test + void shouldStoreJournalEntryFields() { + LocalDateTime timestamp = LocalDateTime.of(2026, 5, 29, 21, 15); + JournalEntry entry = new JournalEntry(); + + entry.setId(3); + entry.setTimestamp(timestamp); + entry.setContent("Observed suspicious network traffic."); + + assertEquals(3, entry.getId()); + assertEquals(timestamp, entry.getTimestamp()); + assertEquals("Observed suspicious network traffic.", entry.getContent()); + } +} diff --git a/src/test/java/tg/cyberlabmanager/model/LabTest.java b/src/test/java/tg/cyberlabmanager/model/LabTest.java new file mode 100644 index 0000000..e8d5bc8 --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/LabTest.java @@ -0,0 +1,64 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LabTest { + @Test + void shouldCreateLabWithConstructorValues() { + Lab lab = new Lab(1, "Malware Analysis", "Static and dynamic malware lab", "malware"); + + assertEquals(1, lab.getId()); + assertEquals("Malware Analysis", lab.getTitle()); + assertEquals("Static and dynamic malware lab", lab.getDescription()); + assertEquals("malware", lab.getCategory()); + } + + @Test + void shouldUpdateLabFieldsWithSetters() { + Lab lab = new Lab(); + + lab.setId(2); + lab.setTitle("Pentest"); + lab.setDescription("Web attack lab"); + lab.setCategory("pentest"); + + assertEquals(2, lab.getId()); + assertEquals("Pentest", lab.getTitle()); + assertEquals("Web attack lab", lab.getDescription()); + assertEquals("pentest", lab.getCategory()); + } + + @Test + void shouldAddAndRemoveVirtualMachines() { + Lab lab = new Lab(); + VirtualMachine vm = new VirtualMachine(); + vm.setName("Kali"); + + lab.addVirtualMachine(vm); + + List virtualMachines = lab.getVirtualMachines(); + assertEquals(1, virtualMachines.size()); + assertSame(vm, virtualMachines.get(0)); + + lab.removeVirtualMachine(vm); + + assertTrue(lab.getVirtualMachines().isEmpty()); + } + + @Test + void shouldExposeUnmodifiableVirtualMachineList() { + Lab lab = new Lab(); + + assertThrows( + UnsupportedOperationException.class, + () -> lab.getVirtualMachines().add(new VirtualMachine()) + ); + } +} diff --git a/src/test/java/tg/cyberlabmanager/model/SnapshotTest.java b/src/test/java/tg/cyberlabmanager/model/SnapshotTest.java new file mode 100644 index 0000000..9501f63 --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/SnapshotTest.java @@ -0,0 +1,30 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SnapshotTest { + @Test + void shouldStoreSnapshotFields() { + LocalDateTime createdAt = LocalDateTime.of(2026, 5, 29, 22, 0); + Snapshot snapshot = new Snapshot(); + + snapshot.setId(4); + snapshot.setUuid("snap-uuid-123"); + snapshot.setName("baseline"); + snapshot.setCreatedAt(createdAt); + snapshot.setDescription("Clean VM state"); + snapshot.setOnline(true); + + assertEquals(4, snapshot.getId()); + assertEquals("snap-uuid-123", snapshot.getUuid()); + assertEquals("baseline", snapshot.getName()); + assertEquals(createdAt, snapshot.getCreatedAt()); + assertEquals("Clean VM state", snapshot.getDescription()); + assertTrue(snapshot.isOnline()); + } +} diff --git a/src/test/java/tg/cyberlabmanager/model/VMStatusTest.java b/src/test/java/tg/cyberlabmanager/model/VMStatusTest.java new file mode 100644 index 0000000..5f86ffe --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/VMStatusTest.java @@ -0,0 +1,18 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +class VMStatusTest { + @Test + void shouldExposeExpectedStableStatuses() { + assertEquals(5, VMStatus.values().length); + assertSame(VMStatus.RUNNING, VMStatus.valueOf("RUNNING")); + assertSame(VMStatus.POWERED_OFF, VMStatus.valueOf("POWERED_OFF")); + assertSame(VMStatus.SAVED, VMStatus.valueOf("SAVED")); + assertSame(VMStatus.PAUSED, VMStatus.valueOf("PAUSED")); + assertSame(VMStatus.UNKNOWN, VMStatus.valueOf("UNKNOWN")); + } +} diff --git a/src/test/java/tg/cyberlabmanager/model/VirtualMachineTest.java b/src/test/java/tg/cyberlabmanager/model/VirtualMachineTest.java new file mode 100644 index 0000000..c986258 --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/VirtualMachineTest.java @@ -0,0 +1,61 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class VirtualMachineTest { + @Test + void shouldUseUnknownStatusByDefault() { + VirtualMachine vm = new VirtualMachine(); + + assertSame(VMStatus.UNKNOWN, vm.getStatus()); + } + + @Test + void shouldStoreVirtualMachineFields() { + VirtualMachine vm = new VirtualMachine(); + + vm.setId(5); + vm.setName("Ubuntu Lab"); + vm.setUuid("vm-uuid-456"); + vm.setDocumentationUrl("https://example.test/doc"); + vm.setStatus(VMStatus.RUNNING); + + assertEquals(5, vm.getId()); + assertEquals("Ubuntu Lab", vm.getName()); + assertEquals("vm-uuid-456", vm.getUuid()); + assertEquals("https://example.test/doc", vm.getDocumentationUrl()); + assertSame(VMStatus.RUNNING, vm.getStatus()); + } + + @Test + void shouldAddSnapshotsAndJournalEntries() { + VirtualMachine vm = new VirtualMachine(); + Snapshot snapshot = new Snapshot(); + JournalEntry journalEntry = new JournalEntry(); + + vm.addSnapshot(snapshot); + vm.addJournalEntry(journalEntry); + + List snapshots = vm.getSnapshots(); + List journalEntries = vm.getJournalEntries(); + + assertEquals(1, snapshots.size()); + assertSame(snapshot, snapshots.get(0)); + assertEquals(1, journalEntries.size()); + assertSame(journalEntry, journalEntries.get(0)); + } + + @Test + void shouldExposeUnmodifiableCollections() { + VirtualMachine vm = new VirtualMachine(); + + assertThrows(UnsupportedOperationException.class, () -> vm.getSnapshots().add(new Snapshot())); + assertThrows(UnsupportedOperationException.class, () -> vm.getJournalEntries().add(new JournalEntry())); + } +}