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
- 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>
111 lines
3.5 KiB
Go
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")
|
|
}
|