29a9dcdd8c
Controller-only UI/orchestration over the agent's disk endpoints + StoragePath registry. New: storage overview (data_bearing badges), guided init (format -> resolve fs UUID -> assign -> register; data-bearing REFUSAL surfaces the felhom-opsign command, no force-format), guided attach, eject (+deregister, dependent-guest warning). agentapi: DiskInfo.DurableID/FSUUID + FormatResult. PendingOp (parsed from the 403). Honest buttons (migrate disabled, no 404s). Phase 3: removed dead CrossDrive blocks in deploy.html/backups.html. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
161 lines
5.7 KiB
Go
161 lines
5.7 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"log"
|
|
"path/filepath"
|
|
"testing"
|
|
"text/template"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
)
|
|
|
|
// TestTemplatesParse forces every HTML template (incl. the new storage wizards and the de-priv
|
|
// cleanups) to parse — they are otherwise only parsed at server startup (template.Must).
|
|
func TestTemplatesParse(t *testing.T) {
|
|
s := &Server{}
|
|
if _, err := template.New("").Funcs(s.templateFuncMap()).ParseFS(templateFS, "templates/*.html"); err != nil {
|
|
t.Fatalf("templates parse: %v", err)
|
|
}
|
|
}
|
|
|
|
// mockAgent records calls so tests can assert the refusal path performs NO mount/destructive action.
|
|
type mockAgent struct {
|
|
disks agentapi.DisksResponse
|
|
formatRes agentapi.FormatResult
|
|
formatErr error
|
|
assignErr error
|
|
assignCalls []assignCall
|
|
disksCalls int
|
|
}
|
|
|
|
type assignCall struct{ uuid, where, fstype string }
|
|
|
|
func (m *mockAgent) Disks(context.Context) (agentapi.DisksResponse, error) {
|
|
m.disksCalls++
|
|
return m.disks, nil
|
|
}
|
|
func (m *mockAgent) FormatDisk(_ context.Context, device, fstype string) (agentapi.FormatResult, error) {
|
|
return m.formatRes, m.formatErr
|
|
}
|
|
func (m *mockAgent) AssignDisk(_ context.Context, uuid, where, fstype, _ string) error {
|
|
m.assignCalls = append(m.assignCalls, assignCall{uuid, where, fstype})
|
|
return m.assignErr
|
|
}
|
|
func (m *mockAgent) EjectDisk(_ context.Context, where string) (agentapi.EjectResult, error) {
|
|
return agentapi.EjectResult{Ejected: where}, nil
|
|
}
|
|
|
|
func testServer(t *testing.T) *Server {
|
|
t.Helper()
|
|
lg := log.New(io.Discard, "", 0)
|
|
sett, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), lg)
|
|
if err != nil {
|
|
t.Fatalf("settings: %v", err)
|
|
}
|
|
return &Server{settings: sett, logger: lg, cfg: &config.Config{}}
|
|
}
|
|
|
|
// SECURITY: a data-bearing refusal must surface the opsign command and perform NO assign/register.
|
|
func TestRunStorageInit_DataBearingRefusal(t *testing.T) {
|
|
s := testServer(t)
|
|
agent := &mockAgent{
|
|
formatErr: agentapi.ErrFormatRefused,
|
|
formatRes: agentapi.FormatResult{
|
|
Device: "/dev/sdb1", DataBearing: true, Formatted: false, Reason: "ext4 signature",
|
|
PendingOp: &agentapi.PendingOp{Op: "storage_wipe", HostScope: "host-1", DurableID: "byuuid:1234", FSType: "ext4"},
|
|
},
|
|
}
|
|
res, err := s.runStorageInit(context.Background(), agent, "/dev/sdb1", "ext4", "/mnt/hdd1", "HDD", true)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !res.Refused {
|
|
t.Fatal("expected Refused=true on a data-bearing device")
|
|
}
|
|
if res.Opsign != "felhom-opsign -op storage_wipe -host host-1 -durable-id byuuid:1234" {
|
|
t.Errorf("opsign command not surfaced: %q", res.Opsign)
|
|
}
|
|
if len(agent.assignCalls) != 0 {
|
|
t.Fatalf("REFUSAL MUST NOT mount: got %d assign call(s)", len(agent.assignCalls))
|
|
}
|
|
if len(s.settings.GetStoragePaths()) != 0 {
|
|
t.Fatal("REFUSAL MUST NOT register a StoragePath")
|
|
}
|
|
}
|
|
|
|
// Happy path: format → resolve new fs UUID from the disk list → assign with that UUID → register.
|
|
func TestRunStorageInit_Success(t *testing.T) {
|
|
s := testServer(t)
|
|
agent := &mockAgent{
|
|
formatRes: agentapi.FormatResult{Device: "/dev/sdb1", Formatted: true, DataBearing: false},
|
|
disks: agentapi.DisksResponse{Disks: []agentapi.DiskInfo{
|
|
{Name: "felhom-usb", BackingDevice: "/dev/sdb1", DurableID: "uuid:NEW-9999"},
|
|
}},
|
|
}
|
|
res, err := s.runStorageInit(context.Background(), agent, "/dev/sdb1", "ext4", "/mnt/hdd1", "Külső HDD", true)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !res.Registered || res.Where != "/mnt/hdd1" {
|
|
t.Fatalf("expected registered at /mnt/hdd1, got %+v", res)
|
|
}
|
|
if len(agent.assignCalls) != 1 || agent.assignCalls[0].uuid != "NEW-9999" || agent.assignCalls[0].where != "/mnt/hdd1" {
|
|
t.Fatalf("assign must use the resolved fs UUID + mount path: %+v", agent.assignCalls)
|
|
}
|
|
paths := s.settings.GetStoragePaths()
|
|
if len(paths) != 1 || paths[0].Path != "/mnt/hdd1" || paths[0].Label != "Külső HDD" || !paths[0].IsDefault || !paths[0].Schedulable {
|
|
t.Fatalf("StoragePath not registered as expected: %+v", paths)
|
|
}
|
|
}
|
|
|
|
// Attach is non-destructive: resolve UUID → assign → register (no format).
|
|
func TestRunStorageAttach_Success(t *testing.T) {
|
|
s := testServer(t)
|
|
agent := &mockAgent{
|
|
disks: agentapi.DisksResponse{Disks: []agentapi.DiskInfo{
|
|
{Name: "felhom-usb", BackingDevice: "/dev/sdb1", DurableID: "uuid:EXISTING-42"},
|
|
}},
|
|
}
|
|
res, err := s.runStorageAttach(context.Background(), agent, "/dev/sdb1", "", "/mnt/media", "Média", false)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !res.Registered {
|
|
t.Fatal("expected registered")
|
|
}
|
|
if len(agent.assignCalls) != 1 || agent.assignCalls[0].uuid != "EXISTING-42" {
|
|
t.Fatalf("attach must assign by the existing fs UUID: %+v", agent.assignCalls)
|
|
}
|
|
}
|
|
|
|
func TestFSUUIDForDevice(t *testing.T) {
|
|
disks := agentapi.DisksResponse{Disks: []agentapi.DiskInfo{
|
|
{BackingDevice: "/dev/sda1", DurableID: "uuid:AAAA"},
|
|
{BackingDevice: "/dev/sdb1", DurableID: "store:lvm"}, // non-fs identity → no UUID
|
|
}}
|
|
if got := fsUUIDForDevice(disks, "/dev/sda1"); got != "AAAA" {
|
|
t.Errorf("fsUUIDForDevice(sda1) = %q, want AAAA", got)
|
|
}
|
|
if got := fsUUIDForDevice(disks, "/dev/sdb1"); got != "" {
|
|
t.Errorf("fsUUIDForDevice(non-fs) = %q, want empty", got)
|
|
}
|
|
if got := fsUUIDForDevice(disks, "/dev/sdc1"); got != "" {
|
|
t.Errorf("fsUUIDForDevice(absent) = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestMountWhere(t *testing.T) {
|
|
if w, err := mountWhere("hdd_1"); err != nil || w != "/mnt/hdd_1" {
|
|
t.Errorf("mountWhere(hdd_1) = %q, %v", w, err)
|
|
}
|
|
for _, bad := range []string{"", "../etc", "a/b", "x y", "/abs"} {
|
|
if _, err := mountWhere(bad); err == nil {
|
|
t.Errorf("mountWhere(%q) should be rejected", bad)
|
|
}
|
|
}
|
|
}
|