package vm import ( "context" "errors" "net/http" "net/http/httptest" "testing" "forgejo.digital-droplets.de/philschlo/proxui/platform/cluster" "forgejo.digital-droplets.de/philschlo/proxui/platform/jobs" ) func TestStartVMCallsProxmoxEnqueuesPollAndWritesAudit(t *testing.T) { repository := &stubRepository{ getFound: true, vm: VM{ ID: "vm-1", TenantID: "tenant-1", ClusterID: "cluster-1", ProxmoxVMID: 100, Node: "pve", MembershipRole: "member", }, } clusters := &stubClusterRepository{ found: true, cluster: cluster.Cluster{ ID: "cluster-1", }, } powerClient := &stubPowerClient{upid: "UPID:pve:1"} tasks := &stubTaskEnqueuer{} audit := &stubPowerAudit{} handler := NewHandler(repository, WithPower(PowerDependencies{ Clusters: clusters, ClientFactory: func(cluster.Cluster) (PowerClient, error) { return powerClient, nil }, Tasks: tasks, Audit: audit, })) req := requestWithPrincipal(http.MethodPost, "/vms/vm-1/start") req.SetPathValue("vmID", "vm-1") rec := httptest.NewRecorder() handler.StartVM(rec, req) if rec.Code != http.StatusAccepted { t.Fatalf("status = %d, want %d", rec.Code, http.StatusAccepted) } if powerClient.action != "start" { t.Fatalf("action = %q, want start", powerClient.action) } if powerClient.vmid != 100 { t.Fatalf("vmid = %d, want 100", powerClient.vmid) } if tasks.payload.UPID != "UPID:pve:1" { t.Fatalf("UPID = %q, want UPID:pve:1", tasks.payload.UPID) } if tasks.payload.SuccessStatus != "running" { t.Fatalf("SuccessStatus = %q, want running", tasks.payload.SuccessStatus) } if audit.event.Action != "vm.power.start" { t.Fatalf("audit action = %q, want vm.power.start", audit.event.Action) } } func TestPowerVMRejectsViewer(t *testing.T) { handler := NewHandler(&stubRepository{ getFound: true, vm: VM{ ID: "vm-1", MembershipRole: "viewer", }, }, WithPower(validPowerDependencies())) req := requestWithPrincipal(http.MethodPost, "/vms/vm-1/start") req.SetPathValue("vmID", "vm-1") rec := httptest.NewRecorder() handler.StartVM(rec, req) if rec.Code != http.StatusForbidden { t.Fatalf("status = %d, want %d", rec.Code, http.StatusForbidden) } } func TestPowerVMReturnsErrorWhenEnqueueFails(t *testing.T) { deps := validPowerDependencies() deps.Tasks = &stubTaskEnqueuer{err: errors.New("redis failed")} handler := NewHandler(&stubRepository{ getFound: true, vm: VM{ ID: "vm-1", TenantID: "tenant-1", ClusterID: "cluster-1", ProxmoxVMID: 100, Node: "pve", MembershipRole: "member", }, }, WithPower(deps)) req := requestWithPrincipal(http.MethodPost, "/vms/vm-1/start") req.SetPathValue("vmID", "vm-1") rec := httptest.NewRecorder() handler.StartVM(rec, req) if rec.Code != http.StatusInternalServerError { t.Fatalf("status = %d, want %d", rec.Code, http.StatusInternalServerError) } } func validPowerDependencies() PowerDependencies { return PowerDependencies{ Clusters: &stubClusterRepository{ found: true, cluster: cluster.Cluster{ID: "cluster-1"}, }, ClientFactory: func(cluster.Cluster) (PowerClient, error) { return &stubPowerClient{upid: "UPID:pve:1"}, nil }, Tasks: &stubTaskEnqueuer{}, Audit: &stubPowerAudit{}, } } type stubClusterRepository struct { id string cluster cluster.Cluster found bool err error } func (s *stubClusterRepository) GetCluster(_ context.Context, id string) (cluster.Cluster, bool, error) { s.id = id return s.cluster, s.found, s.err } type stubPowerClient struct { node string vmid int action string upid string err error } func (s *stubPowerClient) PowerVM(_ context.Context, node string, vmid int, action string) (string, error) { s.node = node s.vmid = vmid s.action = action return s.upid, s.err } type stubTaskEnqueuer struct { payload jobs.ProxmoxTaskPollPayload err error } func (s *stubTaskEnqueuer) EnqueueProxmoxTaskPoll(_ context.Context, payload jobs.ProxmoxTaskPollPayload) error { s.payload = payload return s.err } type stubPowerAudit struct { event VMPowerAuditEvent err error } func (s *stubPowerAudit) WriteVMPowerAudit(_ context.Context, event VMPowerAuditEvent) error { s.event = event return s.err }