Files
felhom-controller/controller/internal/agentapi/netstorage_test.go
T
admin 364dc50794 controller v0.92.0: NAS network storage Part A2 (registry + UI + per-share health)
Controller-side of NAS network storage, proxying to agent A1 /netstorage/*. Distinct
'network' storage kind (no drive lifecycle); add/list/remove + per-share health UI;
unreachable NAS is a recoverable warning, never the drive missing/stop cascade; SMB
creds pass through to the agent, never persisted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxLA1mZurFq9kt8hneFeCs
2026-06-30 14:40:47 +02:00

107 lines
3.7 KiB
Go

package agentapi
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func netStub(t *testing.T) (*httptest.Server, string, *struct {
addBody AddNetStorageRequest
removed string
}) {
captured := &struct {
addBody AddNetStorageRequest
removed string
}{}
mux := http.NewServeMux()
mux.HandleFunc("POST /netstorage/add", func(w http.ResponseWriter, r *http.Request) {
_ = decodeJSON(r, &captured.addBody)
_, _ = w.Write([]byte(`{"ok":true,"data":{"name":"media","protocol":"nfs","where":"/mnt/felhom-drives/media","guest_path":"/mnt/felhom-drives/media","host_uid":101000,"host_gid":101000}}`))
})
mux.HandleFunc("GET /netstorage", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"ok":true,"data":{"vmid":8200,"network_mounts":[
{"name":"media","protocol":"nfs","server":"10.0.0.5","export":"/srv/media","where":"/mnt/felhom-drives/media","configured":true,"mounted":true,"reachable":true,"health":"ok"},
{"name":"photos","protocol":"smb","server":"10.0.0.5","export":"photos","where":"/mnt/felhom-drives/photos","configured":true,"mounted":false,"reachable":true,"health":"idle"},
{"name":"vids","protocol":"nfs","server":"10.0.0.6","export":"/srv/vids","where":"/mnt/felhom-drives/vids","configured":true,"mounted":true,"reachable":false,"health":"unreachable"}
]}}`))
})
mux.HandleFunc("POST /netstorage/remove", func(w http.ResponseWriter, r *http.Request) {
var body struct {
Name string `json:"name"`
}
_ = decodeJSON(r, &body)
captured.removed = body.Name
_, _ = w.Write([]byte(`{"ok":true,"data":{"name":"` + body.Name + `","removed":true}}`))
})
s := httptest.NewTLSServer(mux)
return s, strings.TrimPrefix(s.URL, "https://"), captured
}
func TestNetStorage_Add_ForwardsCredsAndMapping(t *testing.T) {
s, ep, cap := netStub(t)
defer s.Close()
c := clientFor(t, s, ep)
res, err := c.AddNetStorage(context.Background(), AddNetStorageRequest{
Name: "media", Protocol: "smb", Server: "10.0.0.5", Export: "media",
MappedUID: 1000, MappedGID: 1000, Username: "u", Password: "p",
})
if err != nil {
t.Fatal(err)
}
if res.GuestPath != "/mnt/felhom-drives/media" || res.HostUID != 101000 {
t.Fatalf("add result mismatch: %+v", res)
}
// The SMB creds + mapping must be forwarded to the agent verbatim (the agent writes the 0600 file).
if cap.addBody.Username != "u" || cap.addBody.Password != "p" {
t.Fatalf("creds not forwarded to the agent: %+v", cap.addBody)
}
if cap.addBody.MappedUID != 1000 || cap.addBody.Protocol != "smb" {
t.Fatalf("mapping/protocol not forwarded: %+v", cap.addBody)
}
}
func TestNetStorage_List_HealthStates(t *testing.T) {
s, ep, _ := netStub(t)
defer s.Close()
c := clientFor(t, s, ep)
mounts, err := c.ListNetStorage(context.Background())
if err != nil {
t.Fatal(err)
}
if len(mounts) != 3 {
t.Fatalf("want 3 mounts, got %d", len(mounts))
}
byName := map[string]NetworkMountStatus{}
for _, m := range mounts {
byName[m.Name] = m
}
// ok + idle are NOT degraded; only unreachable is.
if byName["media"].Unreachable() || byName["photos"].Unreachable() {
t.Fatalf("ok/idle must not be unreachable: %+v", byName)
}
if byName["photos"].Health != "idle" {
t.Fatalf("photos should be idle (benign), got %q", byName["photos"].Health)
}
if !byName["vids"].Unreachable() {
t.Fatalf("vids should be unreachable (degraded), got %q", byName["vids"].Health)
}
}
func TestNetStorage_Remove(t *testing.T) {
s, ep, cap := netStub(t)
defer s.Close()
c := clientFor(t, s, ep)
if err := c.RemoveNetStorage(context.Background(), "media"); err != nil {
t.Fatal(err)
}
if cap.removed != "media" {
t.Fatalf("remove did not forward the name, got %q", cap.removed)
}
}