package store import ( "errors" "reflect" "testing" "time" ) func sampleNode(id, publicKey string) Node { return Node{ ID: id, Hostname: "host-" + id, PublicKey: publicKey, IPAddress: "100.64.0.42", } } func mustNode(t *testing.T, s *Store, id, tenantID, publicKey string) Node { t.Helper() n := sampleNode(id, publicKey) n.TenantID = tenantID created, err := s.CreateNode(ctx, n) if err != nil { t.Fatalf("CreateNode(%s): %v", id, err) } return created } func TestCreateNode_RoundTrip(t *testing.T) { s, clock := openTest(t) mustTenant(t, s, "t1") n := sampleNode("node-proxmox-vm-42", "MCowBQYDK2VwAyEAabc=") n.TenantID = "t1" created, err := s.CreateNode(ctx, n) if err != nil { t.Fatalf("CreateNode: %v", err) } if created.Status != NodeOnline { t.Errorf("Default-Status = %q, erwartet online", created.Status) } if !created.LastSeen.Equal(clock.now()) { t.Errorf("LastSeen = %v, erwartet jetzt", created.LastSeen) } byID, err := s.GetNode(ctx, "node-proxmox-vm-42") if err != nil { t.Fatal(err) } byKey, err := s.GetNodeByPublicKey(ctx, "MCowBQYDK2VwAyEAabc=") if err != nil { t.Fatal(err) } if !reflect.DeepEqual(byID, created) || !reflect.DeepEqual(byKey, created) { t.Errorf("gelesen weicht ab\n byID: %+v\nbyKey: %+v\n want: %+v", byID, byKey, created) } } func TestCreateNode_KeepsExplicitValues(t *testing.T) { s, _ := openTest(t) mustTenant(t, s, "t1") seen := time.Date(2026, 9, 1, 8, 0, 0, 0, time.UTC) n := sampleNode("n1", "k1") n.TenantID, n.Status, n.LastSeen = "t1", NodeDrained, seen created, err := s.CreateNode(ctx, n) if err != nil { t.Fatal(err) } if created.Status != NodeDrained || !created.LastSeen.Equal(seen) { t.Errorf("explizite Werte überschrieben: %+v", created) } } func TestCreateNode_Errors(t *testing.T) { s, _ := openTest(t) mustTenant(t, s, "t1") mustNode(t, s, "n1", "t1", "key1") unknownTenant := sampleNode("n2", "key2") unknownTenant.TenantID = "fehlt" if _, err := s.CreateNode(ctx, unknownTenant); !errors.Is(err, ErrNotFound) { t.Errorf("unbekannter Mandant: erwartet ErrNotFound, bekommen %v", err) } dupID := sampleNode("n1", "key-neu") dupID.TenantID = "t1" if _, err := s.CreateNode(ctx, dupID); !errors.Is(err, ErrExists) { t.Errorf("doppelte ID: erwartet ErrExists, bekommen %v", err) } dupKey := sampleNode("n-neu", "key1") dupKey.TenantID = "t1" if _, err := s.CreateNode(ctx, dupKey); !errors.Is(err, ErrExists) { t.Errorf("doppelter Public Key: erwartet ErrExists, bekommen %v", err) } } func TestCreateNode_Validation(t *testing.T) { tests := map[string]func(*Node){ "ID leer": func(n *Node) { n.ID = "" }, "Mandant leer": func(n *Node) { n.TenantID = "" }, "Hostname leer": func(n *Node) { n.Hostname = "" }, "Public Key leer": func(n *Node) { n.PublicKey = " " }, "IP leer": func(n *Node) { n.IPAddress = "" }, "unbekannter Status": func(n *Node) { n.Status = "schläft" }, } for name, mutate := range tests { t.Run(name, func(t *testing.T) { s, _ := openTest(t) mustTenant(t, s, "t1") n := sampleNode("n1", "k1") n.TenantID = "t1" mutate(&n) if _, err := s.CreateNode(ctx, n); !errors.Is(err, ErrInvalid) { t.Errorf("erwartet ErrInvalid, bekommen %v", err) } }) } } func TestGetNode_NotFound(t *testing.T) { s, _ := openTest(t) if _, err := s.GetNode(ctx, "fehlt"); !errors.Is(err, ErrNotFound) { t.Errorf("GetNode: erwartet ErrNotFound, bekommen %v", err) } if _, err := s.GetNodeByPublicKey(ctx, "fehlt"); !errors.Is(err, ErrNotFound) { t.Errorf("GetNodeByPublicKey: erwartet ErrNotFound, bekommen %v", err) } } func TestListNodes(t *testing.T) { s, _ := openTest(t) mustTenant(t, s, "t1") mustTenant(t, s, "t2") mustNode(t, s, "c", "t1", "k-c") mustNode(t, s, "a", "t1", "k-a") mustNode(t, s, "b", "t2", "k-b") ids := func(tenantID string) []string { t.Helper() nodes, err := s.ListNodes(ctx, tenantID) if err != nil { t.Fatal(err) } out := []string{} for _, n := range nodes { out = append(out, n.ID) } return out } if got := ids("t1"); !reflect.DeepEqual(got, []string{"a", "c"}) { t.Errorf("t1: %v", got) } if got := ids(""); !reflect.DeepEqual(got, []string{"a", "b", "c"}) { t.Errorf("alle: %v", got) } if got := ids("leer"); len(got) != 0 { t.Errorf("unbekannter Mandant: %v", got) } } func TestTouchNode(t *testing.T) { s, clock := openTest(t) mustTenant(t, s, "t1") mustNode(t, s, "n1", "t1", "k1") if err := s.SetNodeStatus(ctx, "n1", NodeDrained); err != nil { t.Fatal(err) } clock.advance(45 * time.Second) if err := s.TouchNode(ctx, "n1", "192.168.1.5"); err != nil { t.Fatalf("TouchNode: %v", err) } got, _ := s.GetNode(ctx, "n1") if !got.LastSeen.Equal(clock.now()) || got.IPAddress != "192.168.1.5" { t.Errorf("last_seen=%v ip=%q", got.LastSeen, got.IPAddress) } if got.Status != NodeDrained { t.Errorf("TouchNode darf den Status nicht ändern, ist %q", got.Status) } if err := s.TouchNode(ctx, "fehlt", "1.2.3.4"); !errors.Is(err, ErrNotFound) { t.Errorf("unbekannt: erwartet ErrNotFound, bekommen %v", err) } if err := s.TouchNode(ctx, "n1", ""); !errors.Is(err, ErrInvalid) { t.Errorf("leere IP: erwartet ErrInvalid, bekommen %v", err) } } func TestSetNodeStatus(t *testing.T) { s, _ := openTest(t) mustTenant(t, s, "t1") mustNode(t, s, "n1", "t1", "k1") for _, st := range []NodeStatus{NodeOffline, NodeDrained, NodeOnline} { if err := s.SetNodeStatus(ctx, "n1", st); err != nil { t.Fatalf("%s: %v", st, err) } if got, _ := s.GetNode(ctx, "n1"); got.Status != st { t.Errorf("Status = %q, erwartet %q", got.Status, st) } } if err := s.SetNodeStatus(ctx, "n1", "kaputt"); !errors.Is(err, ErrInvalid) { t.Errorf("ungültig: erwartet ErrInvalid, bekommen %v", err) } if err := s.SetNodeStatus(ctx, "fehlt", NodeOnline); !errors.Is(err, ErrNotFound) { t.Errorf("unbekannt: erwartet ErrNotFound, bekommen %v", err) } } func TestDeleteNode(t *testing.T) { s, _ := openTest(t) mustTenant(t, s, "t1") mustNode(t, s, "n1", "t1", "k1") if err := s.DeleteNode(ctx, "n1"); err != nil { t.Fatal(err) } if _, err := s.GetNode(ctx, "n1"); !errors.Is(err, ErrNotFound) { t.Errorf("Knoten noch da: %v", err) } if err := s.DeleteNode(ctx, "n1"); !errors.Is(err, ErrNotFound) { t.Errorf("zweites Löschen: erwartet ErrNotFound, bekommen %v", err) } // Public Key ist nach dem Löschen wieder frei (z. B. für re_enroll). mustNode(t, s, "n1-neu", "t1", "k1") }