banger/internal/webui/server_test.go
Thales Maciel 497e6dca3d
Rename experimental Void image to void
Replace the old `void-exp` repository defaults with `void` so the Make targets,
registration helper, example config, verification messaging, and sample test
fixtures all line up with the new managed image name.

Keep the scope to repo-facing naming only: config overrides, helper output, and
test fixtures now expect `void`, while runtime compatibility for existing local
`void-exp` VMs remains an operational concern outside this commit.

Validation: go test ./..., make build, and a local `banger vm create --image void`
smoke boot with ssh and opencode ports up.
2026-04-01 20:15:28 -03:00

231 lines
8.1 KiB
Go

package webui
import (
"context"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"banger/internal/api"
"banger/internal/model"
"banger/internal/paths"
)
type fakeBackend struct {
layout paths.Layout
config model.DaemonConfig
summary api.DashboardSummary
vms []model.VMRecord
images []model.Image
vm model.VMRecord
image model.Image
ports api.VMPortsResult
createOp api.VMCreateOperation
buildOp api.ImageBuildOperation
}
func (f fakeBackend) Config() model.DaemonConfig { return f.config }
func (f fakeBackend) Layout() paths.Layout { return f.layout }
func (f fakeBackend) DashboardSummary(context.Context) (api.DashboardSummary, error) {
return f.summary, nil
}
func (f fakeBackend) ListVMs(context.Context) ([]model.VMRecord, error) { return f.vms, nil }
func (f fakeBackend) FindVM(context.Context, string) (model.VMRecord, error) { return f.vm, nil }
func (f fakeBackend) GetVMStats(context.Context, string) (model.VMRecord, model.VMStats, error) {
return f.vm, f.vm.Stats, nil
}
func (f fakeBackend) BeginVMCreate(context.Context, api.VMCreateParams) (api.VMCreateOperation, error) {
return f.createOp, nil
}
func (f fakeBackend) VMCreateStatus(context.Context, string) (api.VMCreateOperation, error) {
return f.createOp, nil
}
func (f fakeBackend) StartVM(context.Context, string) (model.VMRecord, error) { return f.vm, nil }
func (f fakeBackend) StopVM(context.Context, string) (model.VMRecord, error) { return f.vm, nil }
func (f fakeBackend) RestartVM(context.Context, string) (model.VMRecord, error) { return f.vm, nil }
func (f fakeBackend) DeleteVM(context.Context, string) (model.VMRecord, error) { return f.vm, nil }
func (f fakeBackend) SetVM(context.Context, api.VMSetParams) (model.VMRecord, error) {
return f.vm, nil
}
func (f fakeBackend) PortsVM(context.Context, string) (api.VMPortsResult, error) { return f.ports, nil }
func (f fakeBackend) ListImages(context.Context) ([]model.Image, error) { return f.images, nil }
func (f fakeBackend) FindImage(context.Context, string) (model.Image, error) { return f.image, nil }
func (f fakeBackend) BeginImageBuild(context.Context, api.ImageBuildParams) (api.ImageBuildOperation, error) {
return f.buildOp, nil
}
func (f fakeBackend) ImageBuildStatus(context.Context, string) (api.ImageBuildOperation, error) {
return f.buildOp, nil
}
func (f fakeBackend) RegisterImage(context.Context, api.ImageRegisterParams) (model.Image, error) {
return f.image, nil
}
func (f fakeBackend) PromoteImage(context.Context, string) (model.Image, error) { return f.image, nil }
func (f fakeBackend) DeleteImage(context.Context, string) (model.Image, error) { return f.image, nil }
func TestDashboardPageRendersSummaryAndTables(t *testing.T) {
backend := fakeBackend{
layout: paths.Layout{StateDir: t.TempDir()},
config: model.DaemonConfig{SSHKeyPath: "/tmp/id"},
summary: api.DashboardSummary{
Host: api.HostSummary{CPUCount: 8, TotalMemoryBytes: 16 << 30, StateFilesystemFreeBytes: 9 << 30, StateFilesystemTotalBytes: 20 << 30},
Sudo: api.SudoStatus{Available: true, Command: "sudo -v"},
Banger: api.BangerSummary{
VMCount: 1, RunningVMCount: 1, ImageCount: 1, ManagedImageCount: 1, ConfiguredVCPUCount: 2,
ConfiguredMemoryBytes: 1 << 30,
ConfiguredDiskBytes: 8 << 30,
UsedWorkDiskBytes: 3 << 30,
},
},
vms: []model.VMRecord{{ID: "vm-1", Name: "smth", State: model.VMStateRunning, CreatedAt: model.Now(), Runtime: model.VMRuntime{GuestIP: "172.16.0.2"}, Spec: model.VMSpec{VCPUCount: 2, MemoryMiB: 1024, WorkDiskSizeBytes: 8 << 30}}},
images: []model.Image{{ID: "img-1", Name: "void", Managed: true, RootfsPath: "/tmp/rootfs.ext4", CreatedAt: model.Now()}},
}
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
NewHandler(backend).ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", rec.Code)
}
body := rec.Body.String()
for _, want := range []string{"vCPU", "2 / 8", "1G / 16G", "8G / 20G", "9G free", "smth", "void", "Create VM"} {
if !strings.Contains(body, want) {
t.Fatalf("body missing %q\n%s", want, body)
}
}
if len(rec.Result().Cookies()) == 0 {
t.Fatal("expected csrf cookie to be set")
}
}
func TestVMActionRejectsMissingCSRF(t *testing.T) {
backend := fakeBackend{
layout: paths.Layout{StateDir: t.TempDir()},
summary: api.DashboardSummary{Sudo: api.SudoStatus{Available: true}},
vm: model.VMRecord{ID: "vm-1", Name: "smth"},
}
req := httptest.NewRequest(http.MethodPost, "/vms/vm-1/start", strings.NewReader(""))
req.Header.Set("Origin", "http://example.com")
rec := httptest.NewRecorder()
NewHandler(backend).ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("status = %d, want 403", rec.Code)
}
}
func TestFSAPIListsEntries(t *testing.T) {
dir := t.TempDir()
if err := os.Mkdir(filepath.Join(dir, "nested"), 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
if err := os.WriteFile(filepath.Join(dir, "rootfs.ext4"), []byte("data"), 0o644); err != nil {
t.Fatalf("write: %v", err)
}
backend := fakeBackend{
layout: paths.Layout{StateDir: dir},
summary: api.DashboardSummary{Sudo: api.SudoStatus{Available: true}},
}
req := httptest.NewRequest(http.MethodGet, "/api/fs?path="+url.QueryEscape(dir)+"&kind=file", nil)
rec := httptest.NewRecorder()
NewHandler(backend).ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", rec.Code)
}
data, err := io.ReadAll(rec.Body)
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
body := string(data)
for _, want := range []string{"rootfs.ext4", "nested"} {
if !strings.Contains(body, want) {
t.Fatalf("body missing %q\n%s", want, body)
}
}
}
func TestVMShowPageRendersRunningActions(t *testing.T) {
backend := fakeBackend{
layout: paths.Layout{StateDir: t.TempDir()},
config: model.DaemonConfig{SSHKeyPath: "/tmp/id"},
summary: api.DashboardSummary{Sudo: api.SudoStatus{Available: true, Command: "sudo -v"}},
vm: model.VMRecord{
ID: "vm-1",
Name: "smth",
State: model.VMStateRunning,
Runtime: model.VMRuntime{
GuestIP: "172.16.0.2",
},
Spec: model.VMSpec{
VCPUCount: 2,
MemoryMiB: 1024,
WorkDiskSizeBytes: 8 << 30,
},
Stats: model.VMStats{
CPUPercent: 12.5,
RSSBytes: 64 << 20,
SystemOverlayBytes: 2 << 20,
WorkDiskBytes: 32 << 20,
},
},
image: model.Image{ID: "img-1", Name: "void"},
ports: api.VMPortsResult{
Name: "smth",
Ports: []api.VMPort{
{Proto: "tcp", Port: 4096, Endpoint: "http://172.16.0.2:4096", Process: "opencode"},
},
},
}
req := httptest.NewRequest(http.MethodGet, "/vms/vm-1", nil)
rec := httptest.NewRecorder()
NewHandler(backend).ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", rec.Code)
}
body := rec.Body.String()
for _, want := range []string{"Stop", "Restart", "href=\"http://172.16.0.2:4096\"", "data-confirm=\"Stop VM smth?\"", "data-confirm=\"Delete VM smth?\""} {
if !strings.Contains(body, want) {
t.Fatalf("body missing %q\n%s", want, body)
}
}
for _, unwanted := range []string{"opencode attach", "root@172.16.0.2"} {
if strings.Contains(body, unwanted) {
t.Fatalf("body unexpectedly contains %q\n%s", unwanted, body)
}
}
}
func TestVMListShowsImageNameAndLink(t *testing.T) {
backend := fakeBackend{
layout: paths.Layout{StateDir: t.TempDir()},
summary: api.DashboardSummary{Sudo: api.SudoStatus{Available: true}},
vms: []model.VMRecord{
{ID: "vm-1", Name: "smth", ImageID: "img-1", State: model.VMStateRunning, CreatedAt: model.Now(), Spec: model.VMSpec{VCPUCount: 2, MemoryMiB: 1024, WorkDiskSizeBytes: 8 << 30}},
},
images: []model.Image{
{ID: "img-1", Name: "void"},
},
}
req := httptest.NewRequest(http.MethodGet, "/vms", nil)
rec := httptest.NewRecorder()
NewHandler(backend).ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", rec.Code)
}
body := rec.Body.String()
for _, want := range []string{">void</a>", "href=\"/images/img-1\""} {
if !strings.Contains(body, want) {
t.Fatalf("body missing %q\n%s", want, body)
}
}
}