erste module drin.

zwischenstand: Architektur aktualisiert, TOFU implementiert,
This commit is contained in:
br committed 2026-10-01 22:22:13 +02:00
1 parent 9d5789c5d7
commit c8df9d7651
41 files changed
+6455 -36

No files matched your search

+226
View File
@@ -0,0 +1,226 @@
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")
}