From c7d20b412429b7976b5f9257a3b8e4bdc6d2bbe1 Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Tue, 31 Mar 2026 20:03:52 +0200 Subject: [PATCH] f-19: test: ajouter des test Signed-off-by: GnomeZworc --- internal/config/agent/config_test.go | 83 +++++++++++ internal/dhcp/dhcp_test.go | 157 +++++++++++++++++++++ internal/metadata/metadata_test.go | 200 +++++++++++++++++++++++++++ pkg/db/kv/kv_test.go | 153 ++++++++++++++++++++ 4 files changed, 593 insertions(+) create mode 100644 internal/config/agent/config_test.go create mode 100644 internal/dhcp/dhcp_test.go create mode 100644 internal/metadata/metadata_test.go create mode 100644 pkg/db/kv/kv_test.go diff --git a/internal/config/agent/config_test.go b/internal/config/agent/config_test.go new file mode 100644 index 0000000..d0f3d31 --- /dev/null +++ b/internal/config/agent/config_test.go @@ -0,0 +1,83 @@ +package configuration + +import ( + "os" + "path/filepath" + "testing" +) + +func writeYAML(t *testing.T, content string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "config.yml") + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatalf("impossible d'écrire le fichier de config : %v", err) + } + return path +} + +// --- LoadConfig --- + +func TestLoadConfig_ValidFile(t *testing.T) { + path := writeYAML(t, ` +database: + path: /tmp/mydb +`) + cfg, err := LoadConfig(path) + if err != nil { + t.Fatalf("LoadConfig a échoué : %v", err) + } + if cfg.Database.Path != "/tmp/mydb" { + t.Errorf("database.path attendu %q, obtenu %q", "/tmp/mydb", cfg.Database.Path) + } +} + +func TestLoadConfig_DefaultPath(t *testing.T) { + // Fichier vide → viper applique la valeur par défaut + path := writeYAML(t, "") + cfg, err := LoadConfig(path) + if err != nil { + t.Fatalf("LoadConfig a échoué : %v", err) + } + if cfg.Database.Path != "/var/lib/two/data/" { + t.Errorf("valeur par défaut attendue %q, obtenu %q", "/var/lib/two/data/", cfg.Database.Path) + } +} + +func TestLoadConfig_MissingFile_UsesDefaults(t *testing.T) { + // Fichier inexistant : viper ignore l'erreur ReadInConfig et retourne les défauts + cfg, err := LoadConfig("/chemin/inexistant/config.yml") + if err != nil { + t.Fatalf("LoadConfig devrait retourner les défauts si le fichier est absent : %v", err) + } + if cfg.Database.Path != "/var/lib/two/data/" { + t.Errorf("valeur par défaut attendue, obtenu %q", cfg.Database.Path) + } +} + +func TestLoadConfig_PartialConfig_MissingDatabaseKey(t *testing.T) { + // Fichier sans la clé database → valeur par défaut + path := writeYAML(t, ` +autrekey: valeur +`) + cfg, err := LoadConfig(path) + if err != nil { + t.Fatalf("LoadConfig a échoué : %v", err) + } + if cfg.Database.Path != "/var/lib/two/data/" { + t.Errorf("valeur par défaut attendue, obtenu %q", cfg.Database.Path) + } +} + +func TestLoadConfig_CustomPath(t *testing.T) { + path := writeYAML(t, ` +database: + path: /opt/two/data +`) + cfg, err := LoadConfig(path) + if err != nil { + t.Fatalf("LoadConfig a échoué : %v", err) + } + if cfg.Database.Path != "/opt/two/data" { + t.Errorf("attendu %q, obtenu %q", "/opt/two/data", cfg.Database.Path) + } +} diff --git a/internal/dhcp/dhcp_test.go b/internal/dhcp/dhcp_test.go new file mode 100644 index 0000000..c4a659f --- /dev/null +++ b/internal/dhcp/dhcp_test.go @@ -0,0 +1,157 @@ +package dhcp + +import ( + "net" + "os" + "path/filepath" + "strings" + "testing" +) + +func parseNet(t *testing.T, cidr string) *net.IPNet { + t.Helper() + _, network, err := net.ParseCIDR(cidr) + if err != nil { + t.Fatalf("ParseCIDR(%q) : %v", cidr, err) + } + return network +} + +// --- cloneIP --- + +func TestCloneIP_IsIndependent(t *testing.T) { + ip := net.ParseIP("10.0.0.1").To4() + clone := cloneIP(ip) + clone[3] = 99 + if ip[3] == 99 { + t.Error("cloneIP devrait retourner une copie indépendante") + } +} + +// --- incrementIP --- + +func TestIncrementIP_Simple(t *testing.T) { + ip := net.ParseIP("10.0.0.1").To4() + incrementIP(ip) + if ip.String() != "10.0.0.2" { + t.Errorf("attendu 10.0.0.2, obtenu %s", ip) + } +} + +func TestIncrementIP_Carry(t *testing.T) { + ip := net.ParseIP("10.0.0.255").To4() + incrementIP(ip) + if ip.String() != "10.0.1.0" { + t.Errorf("attendu 10.0.1.0, obtenu %s", ip) + } +} + +// --- GenerateConfig --- + +func newConf(t *testing.T, cidr string) Config { + t.Helper() + _, network, _ := net.ParseCIDR(cidr) + return Config{ + Network: network, + Gateway: net.ParseIP("192.168.1.1").To4(), + Name: "test", + ConfDir: t.TempDir(), + } +} + +func TestGenerateConfig_CreatesFile(t *testing.T) { + conf := newConf(t, "192.168.1.0/29") // 6 hôtes + path, err := GenerateConfig(conf) + if err != nil { + t.Fatalf("GenerateConfig a échoué : %v", err) + } + + if _, err := os.Stat(path); os.IsNotExist(err) { + t.Errorf("le fichier %q n'a pas été créé", path) + } +} + +func TestGenerateConfig_FilenameMatchesName(t *testing.T) { + conf := newConf(t, "192.168.1.0/29") + path, err := GenerateConfig(conf) + if err != nil { + t.Fatalf("GenerateConfig a échoué : %v", err) + } + + expected := filepath.Join(conf.ConfDir, "test.conf") + if path != expected { + t.Errorf("chemin attendu %q, obtenu %q", expected, path) + } +} + +func TestGenerateConfig_ContainsGateway(t *testing.T) { + conf := newConf(t, "192.168.1.0/29") + path, _ := GenerateConfig(conf) + content, _ := os.ReadFile(path) + + if !strings.Contains(string(content), "dhcp-option=3,192.168.1.1") { + t.Errorf("gateway absente du fichier généré :\n%s", content) + } +} + +func TestGenerateConfig_ContainsDhcpRange(t *testing.T) { + _, network, _ := net.ParseCIDR("10.10.0.0/24") + conf := Config{ + Network: network, + Gateway: net.ParseIP("10.10.0.1").To4(), + Name: "vpc1", + ConfDir: t.TempDir(), + } + path, _ := GenerateConfig(conf) + content, _ := os.ReadFile(path) + + if !strings.Contains(string(content), "dhcp-range=10.10.0.0,static,255.255.255.0,12h") { + t.Errorf("dhcp-range absent ou incorrect :\n%s", content) + } +} + +func TestGenerateConfig_OneHostEntryPerIP(t *testing.T) { + // /29 = réseau + broadcast + 6 hôtes → 8 adresses + conf := newConf(t, "10.0.0.0/29") + path, _ := GenerateConfig(conf) + content, _ := os.ReadFile(path) + + lines := strings.Split(string(content), "\n") + count := 0 + for _, l := range lines { + if strings.HasPrefix(l, "dhcp-host=") { + count++ + } + } + // /29 contient 8 adresses (0 à 7) + if count != 8 { + t.Errorf("attendu 8 entrées dhcp-host, obtenu %d", count) + } +} + +func TestGenerateConfig_MACPrefix(t *testing.T) { + conf := newConf(t, "10.0.0.0/30") // 4 adresses + path, _ := GenerateConfig(conf) + content, _ := os.ReadFile(path) + + if !strings.Contains(string(content), "00:22:33:") { + t.Errorf("préfixe MAC 00:22:33: absent :\n%s", content) + } +} + +func TestGenerateConfig_CreatesConfDir(t *testing.T) { + dir := filepath.Join(t.TempDir(), "sous", "dossier") + _, network, _ := net.ParseCIDR("10.0.0.0/30") + conf := Config{ + Network: network, + Gateway: net.ParseIP("10.0.0.1").To4(), + Name: "net", + ConfDir: dir, + } + if _, err := GenerateConfig(conf); err != nil { + t.Fatalf("GenerateConfig devrait créer les répertoires manquants : %v", err) + } + if _, err := os.Stat(dir); os.IsNotExist(err) { + t.Errorf("répertoire %q non créé", dir) + } +} diff --git a/internal/metadata/metadata_test.go b/internal/metadata/metadata_test.go new file mode 100644 index 0000000..62a2830 --- /dev/null +++ b/internal/metadata/metadata_test.go @@ -0,0 +1,200 @@ +package metadata + +import ( + "strings" + "testing" + + "git.g3e.fr/syonad/two/pkg/db/kv" +) + +func newCfg() NoCloudConfig { + return NoCloudConfig{ + VpcName: "vpc-test", + BindIP: "169.254.169.254", + BindPort: "80", + Name: "vm1", + Password: "s3cr3t", + SSHKEY: "ssh-ed25519 AAAA... user@host", + } +} + +func newTestDB(t *testing.T) interface{ Close() error } { + t.Helper() + db := kv.InitDB(kv.Config{Path: t.TempDir()}, false) + t.Cleanup(func() { db.Close() }) + return db +} + +// --- RenderConfig --- + +func TestRenderConfig_MetaData(t *testing.T) { + cfg := newCfg() + out, err := RenderConfig("templates/meta-data.tmpl", cfg) + if err != nil { + t.Fatalf("RenderConfig meta-data : %v", err) + } + if !strings.Contains(out, "instance-id: vm1") { + t.Errorf("instance-id absent :\n%s", out) + } + if !strings.Contains(out, "local-hostname: vm1") { + t.Errorf("local-hostname absent :\n%s", out) + } +} + +func TestRenderConfig_VendorData_ContainsPassword(t *testing.T) { + cfg := newCfg() + out, err := RenderConfig("templates/vendor-data.tmpl", cfg) + if err != nil { + t.Fatalf("RenderConfig vendor-data : %v", err) + } + if !strings.Contains(out, "s3cr3t") { + t.Errorf("password absent du vendor-data :\n%s", out) + } +} + +func TestRenderConfig_VendorData_ContainsSSHKey(t *testing.T) { + cfg := newCfg() + out, err := RenderConfig("templates/vendor-data.tmpl", cfg) + if err != nil { + t.Fatalf("RenderConfig vendor-data : %v", err) + } + if !strings.Contains(out, "ssh-ed25519 AAAA... user@host") { + t.Errorf("clé SSH absente du vendor-data :\n%s", out) + } +} + +func TestRenderConfig_NetworkConfig(t *testing.T) { + cfg := newCfg() + out, err := RenderConfig("templates/network-config.tmpl", cfg) + if err != nil { + t.Fatalf("RenderConfig network-config : %v", err) + } + if !strings.Contains(out, "dhcp4: true") { + t.Errorf("dhcp4 absent du network-config :\n%s", out) + } +} + +func TestRenderConfig_UserData(t *testing.T) { + cfg := newCfg() + out, err := RenderConfig("templates/user-data.tmpl", cfg) + if err != nil { + t.Fatalf("RenderConfig user-data : %v", err) + } + if !strings.Contains(out, "passwd -d root") { + t.Errorf("user-data inattendu :\n%s", out) + } +} + +func TestRenderConfig_InvalidTemplate(t *testing.T) { + _, err := RenderConfig("templates/inexistant.tmpl", newCfg()) + if err == nil { + t.Error("RenderConfig devrait retourner une erreur pour un template inexistant") + } +} + +func TestRenderConfig_SpecialCharsInName(t *testing.T) { + cfg := newCfg() + cfg.Name = "vm-prod-01" + out, err := RenderConfig("templates/meta-data.tmpl", cfg) + if err != nil { + t.Fatalf("RenderConfig : %v", err) + } + if !strings.Contains(out, "vm-prod-01") { + t.Errorf("nom vm-prod-01 absent :\n%s", out) + } +} + +// --- LoadNcCloudInDB / UnLoadNoCloudInDB --- + +func TestLoadNcCloudInDB_StoresAllKeys(t *testing.T) { + db := kv.InitDB(kv.Config{Path: t.TempDir()}, false) + t.Cleanup(func() { db.Close() }) + + cfg := newCfg() + LoadNcCloudInDB(cfg, db) + + keys := []string{ + "metadata/vm1/meta-data", + "metadata/vm1/user-data", + "metadata/vm1/network-config", + "metadata/vm1/vendor-data", + "metadata/vm1/vpc", + "metadata/vm1/bind_ip", + "metadata/vm1/bind_port", + } + for _, key := range keys { + val, err := kv.GetFromDB(db, key) + if err != nil { + t.Errorf("clé %q absente après LoadNcCloudInDB : %v", key, err) + } + if val == "" && key != "metadata/vm1/user-data" { + t.Errorf("clé %q vide après LoadNcCloudInDB", key) + } + } +} + +func TestLoadNcCloudInDB_VpcAndBindValues(t *testing.T) { + db := kv.InitDB(kv.Config{Path: t.TempDir()}, false) + t.Cleanup(func() { db.Close() }) + + cfg := newCfg() + LoadNcCloudInDB(cfg, db) + + vpc, _ := kv.GetFromDB(db, "metadata/vm1/vpc") + if vpc != "vpc-test" { + t.Errorf("vpc attendu %q, obtenu %q", "vpc-test", vpc) + } + + ip, _ := kv.GetFromDB(db, "metadata/vm1/bind_ip") + if ip != "169.254.169.254" { + t.Errorf("bind_ip attendu %q, obtenu %q", "169.254.169.254", ip) + } + + port, _ := kv.GetFromDB(db, "metadata/vm1/bind_port") + if port != "80" { + t.Errorf("bind_port attendu %q, obtenu %q", "80", port) + } +} + +func TestUnLoadNoCloudInDB_RemovesAllKeys(t *testing.T) { + db := kv.InitDB(kv.Config{Path: t.TempDir()}, false) + t.Cleanup(func() { db.Close() }) + + cfg := newCfg() + LoadNcCloudInDB(cfg, db) + UnLoadNoCloudInDB("vm1", db) + + keys := []string{ + "metadata/vm1/meta-data", + "metadata/vm1/user-data", + "metadata/vm1/network-config", + "metadata/vm1/vendor-data", + "metadata/vm1/vpc", + "metadata/vm1/bind_ip", + "metadata/vm1/bind_port", + } + for _, key := range keys { + _, err := kv.GetFromDB(db, key) + if err == nil { + t.Errorf("clé %q devrait être supprimée après UnLoadNoCloudInDB", key) + } + } +} + +func TestUnLoadNoCloudInDB_DoesNotAffectOtherVMs(t *testing.T) { + db := kv.InitDB(kv.Config{Path: t.TempDir()}, false) + t.Cleanup(func() { db.Close() }) + + cfg1 := newCfg() + cfg2 := newCfg() + cfg2.Name = "vm2" + LoadNcCloudInDB(cfg1, db) + LoadNcCloudInDB(cfg2, db) + + UnLoadNoCloudInDB("vm1", db) + + _, err := kv.GetFromDB(db, "metadata/vm2/vpc") + if err != nil { + t.Errorf("vm2 ne devrait pas être supprimée : %v", err) + } +} diff --git a/pkg/db/kv/kv_test.go b/pkg/db/kv/kv_test.go new file mode 100644 index 0000000..de4fd68 --- /dev/null +++ b/pkg/db/kv/kv_test.go @@ -0,0 +1,153 @@ +package kv + +import ( + "errors" + "testing" + + "github.com/dgraph-io/badger/v4" +) + +// newTestDB ouvre une base BadgerDB dans un répertoire temporaire. +// La base est fermée automatiquement en fin de test. +func newTestDB(t *testing.T) *badger.DB { + t.Helper() + db := InitDB(Config{Path: t.TempDir()}, false) + t.Cleanup(func() { db.Close() }) + return db +} + +// --- InitDB --- + +func TestInitDB_ValidPath(t *testing.T) { + db := newTestDB(t) + if db == nil { + t.Fatal("InitDB devrait retourner une DB non-nil") + } +} + +func TestInitDB_InvalidPath_Panics(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatal("InitDB avec un chemin invalide devrait paniquer") + } + }() + InitDB(Config{Path: "/chemin/inexistant/absolu"}, false) +} + +// --- AddInDB --- + +func TestAddInDB_NewKey(t *testing.T) { + db := newTestDB(t) + if err := AddInDB(db, "vpc/test", "valeur"); err != nil { + t.Fatalf("AddInDB a échoué : %v", err) + } +} + +func TestAddInDB_OverwriteExistingKey(t *testing.T) { + db := newTestDB(t) + AddInDB(db, "vpc/test", "premiere") + if err := AddInDB(db, "vpc/test", "deuxieme"); err != nil { + t.Fatalf("AddInDB (écrasement) a échoué : %v", err) + } + + val, _ := GetFromDB(db, "vpc/test") + if val != "deuxieme" { + t.Errorf("valeur attendue %q, obtenu %q", "deuxieme", val) + } +} + +// --- GetFromDB --- + +func TestGetFromDB_ExistingKey(t *testing.T) { + db := newTestDB(t) + AddInDB(db, "vpc/foo", "bar") + + val, err := GetFromDB(db, "vpc/foo") + if err != nil { + t.Fatalf("GetFromDB a échoué : %v", err) + } + if val != "bar" { + t.Errorf("valeur attendue %q, obtenu %q", "bar", val) + } +} + +func TestGetFromDB_MissingKey(t *testing.T) { + db := newTestDB(t) + + _, err := GetFromDB(db, "inexistant") + if !errors.Is(err, badger.ErrKeyNotFound) { + t.Errorf("erreur attendue ErrKeyNotFound, obtenu : %v", err) + } +} + +func TestGetFromDB_EmptyValue(t *testing.T) { + db := newTestDB(t) + AddInDB(db, "vpc/vide", "") + + val, err := GetFromDB(db, "vpc/vide") + if err != nil { + t.Fatalf("GetFromDB a échoué : %v", err) + } + if val != "" { + t.Errorf("valeur attendue vide, obtenu %q", val) + } +} + +// --- DeleteInDB --- + +func TestDeleteInDB_SimpleKey(t *testing.T) { + db := newTestDB(t) + AddInDB(db, "vpc/a", "v") + + if err := DeleteInDB(db, "vpc/a"); err != nil { + t.Fatalf("DeleteInDB a échoué : %v", err) + } + + _, err := GetFromDB(db, "vpc/a") + if !errors.Is(err, badger.ErrKeyNotFound) { + t.Errorf("la clé devrait être supprimée, obtenu : %v", err) + } +} + +func TestDeleteInDB_WithSubkeys(t *testing.T) { + db := newTestDB(t) + // Clé parente + sous-clés (préfixe "vpc/net1/") + AddInDB(db, "vpc/net1", "parent") + AddInDB(db, "vpc/net1/ip", "10.0.0.1") + AddInDB(db, "vpc/net1/gw", "10.0.0.254") + + if err := DeleteInDB(db, "vpc/net1"); err != nil { + t.Fatalf("DeleteInDB a échoué : %v", err) + } + + for _, key := range []string{"vpc/net1", "vpc/net1/ip", "vpc/net1/gw"} { + _, err := GetFromDB(db, key) + if !errors.Is(err, badger.ErrKeyNotFound) { + t.Errorf("clé %q devrait être supprimée, obtenu : %v", key, err) + } + } +} + +func TestDeleteInDB_DoesNotDeleteSiblings(t *testing.T) { + db := newTestDB(t) + AddInDB(db, "vpc/net1", "a") + AddInDB(db, "vpc/net2", "b") // ne doit pas être supprimée + + DeleteInDB(db, "vpc/net1") + + val, err := GetFromDB(db, "vpc/net2") + if err != nil { + t.Fatalf("vpc/net2 ne devrait pas être supprimée : %v", err) + } + if val != "b" { + t.Errorf("valeur attendue %q, obtenu %q", "b", val) + } +} + +func TestDeleteInDB_MissingKey(t *testing.T) { + db := newTestDB(t) + // Supprimer une clé inexistante ne doit pas crasher + if err := DeleteInDB(db, "inexistant"); err != nil { + t.Logf("DeleteInDB clé inexistante retourne : %v (non bloquant)", err) + } +}