Files
pipu-music/services/api/internal/storageagent/nfs_test.go
T
mrleo1nidandClaude Opus 5.5 c583991d14
ci / api / api (push) Successful in 26s
ci / mobile / mobile (Metro-бандл Android) (push) Failing after 22s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 37s
ci / ts / ts (packages + web) (push) Successful in 51s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 53s
feat(api): library sources, scanner and catalog (M2)
- schema for sources, path patterns, media files, artists, albums,
  tracks, genres (migration 00003)
- path patterns (RE2 with named groups, skip), merge with tags by
  source priority, fallbacks from the file name and folder, name
  normalization and multi-artist split
- scanner: walk, skip unchanged files by size and mtime, ffprobe in 8
  workers with a single catalog writer, renames by content hash keep
  the track, duplicates fold into one track with the best file as
  primary, missing files are kept 30 days
- storage-agent mounts NFS with validated server, export and an option
  whitelist, no shell; test mount counts audio files
- admin API: sources CRUD, connection test, patterns with preview,
  scan, library status, file errors, host folder picker; scheduled
  scans and health checks; mounts are restored on start
- catalog API: artist, artist albums, album with tracks, track
- api image is alpine with ffmpeg for ffprobe

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 06:48:37 +03:00

106 lines
3.3 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) 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")
}