Files
pipu-music/services/api/internal/storageagent/nfs_test.go
T
mrleo1nidandClaude Opus 5.5 f66be2ba0a
ci / api / api (push) Successful in 33s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 43s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m11s
ci / ts / ts (packages + web) (push) Successful in 1m10s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 51s
feat: storage writes via storage-agent, accepts_uploads and write_template (M5.5)
- storage-agent: POST /internal/files copies a staged file into a source
  (temp file, fsync, SHA-256 check, hard link so nothing is overwritten,
  " (2)" suffix on conflict, owner PIPU_FILE_UID/GID) and
  POST /internal/files/test. NFS sources that accept uploads are mounted
  rw in an agent-only /library-rw and bind-mounted read-only for others;
  local sources are written through /library-host-rw.
- meta: write templates ({track:02}, {disc:1-}, [optional]) with name
  sanitising and length limits, suggestion from the source's regexes
  and a reversibility check against 50 catalogue tracks.
- API and admin UI: accepts_uploads / write_template on sources,
  write-template suggest and check endpoints, write test result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 13:46:56 +03:00

111 lines
3.5 KiB
Go

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