controller: F8 share-row stub fusion + F4 mapped_uid range validation (WIP, pre-build)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017CDMFpFx84pfviCTVuGGhf
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
||||
)
|
||||
|
||||
// F8 (CAMPAIGN-3): the share row's health FUSES the agent view with the consuming-namespace
|
||||
// classification, so it can never contradict the stacks-page stub badge (both read classifyFSPath).
|
||||
func TestFuseNetHealth_Table(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
agentHealth string
|
||||
class string
|
||||
want string
|
||||
}{
|
||||
// THE F8 contradiction, resolved: agent says benign idle+reachable, namespace says stub → stub.
|
||||
{"idle+stub→stub", "idle", system.FSClassStub, netHealthStub},
|
||||
{"ok+stub→stub", "ok", system.FSClassStub, netHealthStub},
|
||||
// Healthy idle trigger must NOT be downgraded (the over-eager autofs=stub mutant fails here).
|
||||
{"idle+autofs→idle", "idle", system.FSClassAutofs, "idle"},
|
||||
{"ok+network→ok", "ok", system.FSClassNetwork, "ok"},
|
||||
// A whole-server outage (agent unreachable) is more actionable and WINS over stub.
|
||||
{"unreachable+stub→unreachable", "unreachable", system.FSClassStub, netHealthUnreachable},
|
||||
// An inconclusive classification never manufactures a fault.
|
||||
{"idle+unknown→idle", "idle", system.FSClassUnknown, "idle"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
s := testServer(t)
|
||||
s.classifyFSPath = func(string) string { return c.class }
|
||||
if got := s.fuseNetHealth(c.agentHealth, "/mnt/felhom-drives/media"); got != c.want {
|
||||
t.Errorf("fuseNetHealth(%q, class=%q) = %q, want %q", c.agentHealth, c.class, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The row surfaced by the list handler reflects the fused stub verdict (end-to-end through
|
||||
// networkStorageItems). COMPANION red-proof: drop the fuseNetHealth call → the row shows the raw
|
||||
// agent health (unknown here, or idle live) and this stub assertion fails.
|
||||
func TestNetStorageItems_FusesStub(t *testing.T) {
|
||||
s := testServer(t)
|
||||
addNetworkPath(t, s, "media")
|
||||
// The consuming namespace sees a stub at the share path (the export-level outage the agent's
|
||||
// server-level dial is blind to).
|
||||
s.classifyFSPath = func(p string) string {
|
||||
if strings.HasSuffix(p, "/media") {
|
||||
return system.FSClassStub
|
||||
}
|
||||
return system.FSClassUnknown
|
||||
}
|
||||
items := s.networkStorageItems(context.Background())
|
||||
if len(items) != 1 {
|
||||
t.Fatalf("want 1 item, got %d", len(items))
|
||||
}
|
||||
if items[0].Health != netHealthStub {
|
||||
t.Errorf("share row health = %q, want %q (F8 fusion — the row must match the stacks stub badge)", items[0].Health, netHealthStub)
|
||||
}
|
||||
}
|
||||
|
||||
// F4 (CAMPAIGN-3): an out-of-range mapped_uid is refused at the door with a friendly 400 — the agent
|
||||
// is never reached (the campaign's 101000 previously leaked a raw agent_error).
|
||||
func TestNetStorageAdd_UIDRangeValidation(t *testing.T) {
|
||||
body := func(uid int) string {
|
||||
b, _ := json.Marshal(map[string]any{
|
||||
"name": "media", "protocol": "nfs", "server": "10.0.0.5", "export": "/srv/media",
|
||||
"mapped_uid": uid, "mapped_gid": uid,
|
||||
})
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// 101000 → 400 with the friendly message; nothing reaches the agent.
|
||||
t.Run("101000 refused with friendly 400", func(t *testing.T) {
|
||||
s := testServer(t)
|
||||
r := httptest.NewRequest(http.MethodPost, "/api/storage/netstorage/add", strings.NewReader(body(101000)))
|
||||
w := httptest.NewRecorder()
|
||||
s.handleNetStorageAdd(w, r)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("uid 101000: got %d want 400 (agent must never be reached)", w.Code)
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "érvénytelen") {
|
||||
t.Errorf("expected the friendly uid message, got: %s", w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
// 65534 (nobody) → 400 (boundary just above valid).
|
||||
t.Run("65534 refused", func(t *testing.T) {
|
||||
s := testServer(t)
|
||||
r := httptest.NewRequest(http.MethodPost, "/api/storage/netstorage/add", strings.NewReader(body(65534)))
|
||||
w := httptest.NewRecorder()
|
||||
s.handleNetStorageAdd(w, r)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("uid 65534: got %d want 400", w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
// Valid uids pass the range check — they proceed past validation and fail later at the agent
|
||||
// lookup (503, no agent in the test). NOT a 400 → the uid check let them through.
|
||||
for _, uid := range []int{1000, 65533, 0 /* defaults to 1000 */} {
|
||||
t.Run("valid uid passes range check", func(t *testing.T) {
|
||||
s := testServer(t)
|
||||
r := httptest.NewRequest(http.MethodPost, "/api/storage/netstorage/add", strings.NewReader(body(uid)))
|
||||
w := httptest.NewRecorder()
|
||||
s.handleNetStorageAdd(w, r)
|
||||
if w.Code == http.StatusBadRequest {
|
||||
t.Fatalf("uid %d was wrongly refused as out-of-range (400): %s", uid, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user