package storageagent import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNFSNormalize(t *testing.T) { ok := []struct { in NFSSpec writable bool want NFSSpec }{ {NFSSpec{"nas.lan", "/export/music/", "rw,nfsvers=4.1,soft"}, false, NFSSpec{"nas.lan", "/export/music", "ro,nfsvers=4.1,soft"}}, {NFSSpec{" 192.168.1.10 ", "/m", ""}, true, NFSSpec{"192.168.1.10", "/m", "rw"}}, {NFSSpec{"fd00::1", "/m", "timeo=150,timeo=10"}, false, NFSSpec{"[fd00::1]", "/m", "ro,timeo=150"}}, } for _, tc := range ok { got, err := tc.in.Normalize(tc.writable) require.NoError(t, err, tc.in) assert.Equal(t, tc.want, got) } bad := []struct { in NFSSpec field string }{ {NFSSpec{"nas;rm -rf /", "/m", ""}, "nfs_server"}, {NFSSpec{"-oexec", "/m", ""}, "nfs_server"}, {NFSSpec{"nas", "relative", ""}, "nfs_export"}, {NFSSpec{"nas", "/a/../etc", ""}, "nfs_export"}, {NFSSpec{"nas", "/m,exec", ""}, "nfs_export"}, {NFSSpec{"nas", "/m", "exec"}, "nfs_options"}, {NFSSpec{"nas", "/m", "nfsvers=9"}, "nfs_options"}, {NFSSpec{"nas", "/m", "soft=1"}, "nfs_options"}, {NFSSpec{"nas", "/m", "port"}, "nfs_options"}, } for _, tc := range bad { _, err := tc.in.Normalize(false) var ve *ValidationError require.ErrorAs(t, err, &ve, tc.in) assert.Equal(t, tc.field, ve.Field, tc.in) } } type fakeMounter struct { mounted map[string]NFSSpec files int } func (f *fakeMounter) Mount(_ context.Context, spec NFSSpec, target string) error { if err := os.MkdirAll(target, 0o755); err != nil { return err } for i := range f.files { _ = os.WriteFile(filepath.Join(target, "t"+itoa(i)+".flac"), nil, 0o644) } f.mounted[target] = spec return nil } func (f *fakeMounter) BindReadOnly(_ context.Context, src, target string) error { f.mounted[target] = NFSSpec{Export: "bind:" + src} return os.MkdirAll(target, 0o755) } func (f *fakeMounter) Unmount(_ context.Context, target string) error { delete(f.mounted, target) return nil } func TestMountEndpoint(t *testing.T) { root := t.TempDir() m := &fakeMounter{mounted: map[string]NFSSpec{}, files: 3} h := NewRouterWithMounter(nil, "secret", root, m) post := func(path string, body any) *httptest.ResponseRecorder { raw, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, path, bytes.NewReader(raw)) req.Header.Set("Authorization", "Bearer secret") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) return rec } id := "0190a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b" rec := post("/internal/mounts", map[string]any{"source_id": id, "server": "nas", "export": "/music", "options": "soft"}) require.Equal(t, http.StatusOK, rec.Code, rec.Body.String()) var res MountResult require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &res)) assert.Equal(t, MountResult{Status: "mounted", AudioFiles: 3, Message: "found 3 audio files"}, res) assert.Equal(t, "ro,soft", m.mounted[filepath.Join(root, id)].Options) assert.Equal(t, http.StatusUnprocessableEntity, post("/internal/mounts", map[string]any{"source_id": "../etc", "server": "nas", "export": "/m"}).Code) assert.Equal(t, http.StatusUnprocessableEntity, post("/internal/mounts/test", map[string]any{"server": "nas", "export": "/m", "options": "exec"}).Code) rec = post("/internal/mounts/test", map[string]any{"server": "nas", "export": "/m"}) require.Equal(t, http.StatusOK, rec.Code) assert.Len(t, m.mounted, 1, "test mount is removed") }