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) } } }