package engine import ( "bytes" "context" "errors" "log/slog" "path/filepath" "testing" "git.cloudinit.dev/coreci/orca/internal/model" "git.cloudinit.dev/coreci/orca/internal/store" ) func newRegistryTestDB(t *testing.T) (*store.NodeRepo, *store.AuditRepo, *store.AuditRepo, func()) { t.Helper() path := filepath.Join(t.TempDir(), "test.db") db, err := store.Open(path) if err != nil { t.Fatalf("open db: %v", err) } return store.NewNodeRepo(db), store.NewAuditRepo(db), store.NewAuditRepo(db), func() { _ = db.Close() } } func TestNewNodeRegistry_NilLogger(t *testing.T) { nodeRepo, auditRepo, _, cleanup := newRegistryTestDB(t) defer cleanup() audit := NewAudit(auditRepo, nil) r := NewNodeRegistry(nodeRepo, audit, nil) if r == nil { t.Fatal("NewNodeRegistry returned nil") } } func TestNodeRegistry_Join_Success(t *testing.T) { nodeRepo, auditRepo, _, cleanup := newRegistryTestDB(t) defer cleanup() var buf bytes.Buffer audit := NewAudit(auditRepo, slog.New(slog.NewTextHandler(&buf, nil))) r := NewNodeRegistry(nodeRepo, audit, slog.New(slog.NewTextHandler(&buf, nil))) ctx := context.Background() n := &model.Node{ ID: "node-join-1", Name: "pve-1", Address: "10.0.0.1:8443", State: model.NodeStateReady, } if err := r.Join(ctx, n); err != nil { t.Fatalf("Join: %v", err) } got, err := r.Get(ctx, "node-join-1") if err != nil { t.Fatalf("Get after Join: %v", err) } if got.Name != "pve-1" { t.Errorf("Get: Name = %q, want pve-1", got.Name) } } func TestNodeRegistry_Join_Duplicate(t *testing.T) { nodeRepo, auditRepo, _, cleanup := newRegistryTestDB(t) defer cleanup() audit := NewAudit(auditRepo, nil) r := NewNodeRegistry(nodeRepo, audit, nil) ctx := context.Background() n := &model.Node{ID: "dup-1", Name: "n1", Address: "a:1", State: model.NodeStateReady} if err := r.Join(ctx, n); err != nil { t.Fatalf("first Join: %v", err) } err := r.Join(ctx, n) if err == nil { t.Fatal("expected error for duplicate Join") } } func TestNodeRegistry_Leave_Success(t *testing.T) { nodeRepo, auditRepo, _, cleanup := newRegistryTestDB(t) defer cleanup() audit := NewAudit(auditRepo, nil) r := NewNodeRegistry(nodeRepo, audit, nil) ctx := context.Background() n := &model.Node{ID: "leave-1", Name: "n1", Address: "a:1", State: model.NodeStateReady} if err := r.Join(ctx, n); err != nil { t.Fatalf("Join: %v", err) } if err := r.Leave(ctx, "leave-1"); err != nil { t.Fatalf("Leave: %v", err) } got, err := r.Get(ctx, "leave-1") if err != nil { t.Fatalf("Get after Leave: %v", err) } if got.State != model.NodeStateLeft { t.Errorf("State = %q, want %q", got.State, model.NodeStateLeft) } } func TestNodeRegistry_Leave_NotFound(t *testing.T) { nodeRepo, auditRepo, _, cleanup := newRegistryTestDB(t) defer cleanup() audit := NewAudit(auditRepo, nil) r := NewNodeRegistry(nodeRepo, audit, nil) err := r.Leave(context.Background(), "nonexistent") if err == nil { t.Fatal("expected error for Leave on missing node") } } func TestNodeRegistry_Forget_Success(t *testing.T) { nodeRepo, auditRepo, _, cleanup := newRegistryTestDB(t) defer cleanup() audit := NewAudit(auditRepo, nil) r := NewNodeRegistry(nodeRepo, audit, nil) ctx := context.Background() n := &model.Node{ID: "forget-1", Name: "n1", Address: "a:1", State: model.NodeStateReady} if err := r.Join(ctx, n); err != nil { t.Fatalf("Join: %v", err) } if err := r.Forget(ctx, "forget-1"); err != nil { t.Fatalf("Forget: %v", err) } if _, err := r.Get(ctx, "forget-1"); err == nil { t.Error("expected error after Forget") } } func TestNodeRegistry_Forget_NotFound(t *testing.T) { nodeRepo, auditRepo, _, cleanup := newRegistryTestDB(t) defer cleanup() audit := NewAudit(auditRepo, nil) r := NewNodeRegistry(nodeRepo, audit, nil) err := r.Forget(context.Background(), "nonexistent") if err == nil { t.Fatal("expected error for Forget on missing node") } } func TestNodeRegistry_List(t *testing.T) { nodeRepo, auditRepo, _, cleanup := newRegistryTestDB(t) defer cleanup() audit := NewAudit(auditRepo, nil) r := NewNodeRegistry(nodeRepo, audit, nil) ctx := context.Background() if got, err := r.List(ctx); err != nil { t.Fatalf("List empty: %v", err) } else if len(got) != 0 { t.Errorf("List empty: got %d, want 0", len(got)) } for _, id := range []string{"n3", "n1", "n2"} { if err := r.Join(ctx, &model.Node{ID: id, Name: id, Address: "a:1", State: model.NodeStateReady}); err != nil { t.Fatalf("Join %s: %v", id, err) } } got, err := r.List(ctx) if err != nil { t.Fatalf("List: %v", err) } if len(got) != 3 { t.Errorf("List: got %d, want 3", len(got)) } } func TestNodeRegistry_Get_NotFound(t *testing.T) { nodeRepo, auditRepo, _, cleanup := newRegistryTestDB(t) defer cleanup() audit := NewAudit(auditRepo, nil) r := NewNodeRegistry(nodeRepo, audit, nil) _, err := r.Get(context.Background(), "missing") if err == nil { t.Fatal("expected error for Get missing") } } func TestNewAudit_NilLogger(t *testing.T) { _, auditRepo, _, cleanup := newRegistryTestDB(t) defer cleanup() a := NewAudit(auditRepo, nil) if a == nil { t.Fatal("NewAudit returned nil") } } func TestAudit_Record_Success(t *testing.T) { _, auditRepo, _, cleanup := newRegistryTestDB(t) defer cleanup() var buf bytes.Buffer a := NewAudit(auditRepo, slog.New(slog.NewTextHandler(&buf, nil))) a.Record(context.Background(), "cli", "node.join", "node-1", "success", nil, map[string]any{"host": "10.0.0.1"}) entries, err := auditRepo.List(context.Background(), 10) if err != nil { t.Fatalf("List: %v", err) } if len(entries) != 1 { t.Fatalf("entries = %d, want 1", len(entries)) } if entries[0].Action != "node.join" || entries[0].Result != "success" { t.Errorf("entry = %+v", entries[0]) } } func TestAudit_Record_WithError(t *testing.T) { _, auditRepo, _, cleanup := newRegistryTestDB(t) defer cleanup() var buf bytes.Buffer a := NewAudit(auditRepo, slog.New(slog.NewTextHandler(&buf, nil))) a.Record(context.Background(), "cli", "node.join", "node-1", "failure", errors.New("boom"), nil) entries, err := auditRepo.List(context.Background(), 10) if err != nil { t.Fatalf("List: %v", err) } if len(entries) != 1 { t.Fatalf("entries = %d, want 1", len(entries)) } if entries[0].Error != "boom" { t.Errorf("Error = %q, want boom", entries[0].Error) } if !containsStr(buf.String(), "level=WARN") { t.Errorf("expected WARN level for error result, got: %s", buf.String()) } } func containsStr(s, sub string) bool { return len(sub) == 0 || (len(s) >= len(sub) && (s[0:len(sub)] == sub || containsStr(s[1:], sub))) }