b409f5eee2
Live validation caught it: the 'new folder' flow passed the storage root through sharingResolvePath, which (correctly) refuses the drive root as a share target — so share creation silently failed. sharingResolveStorageRoot accepts EXACTLY a registered live root (strictly tighter) and is used only as the new-folder parent. Regression test asserts both halves.
167 lines
6.0 KiB
Go
167 lines
6.0 KiB
Go
package web
|
|
|
|
import (
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
)
|
|
|
|
// newSharingServer returns a Server with ONE registered storage root laid out like a real drive
|
|
// (appdata/, backups/, media/filmek, shares/) so the guard runs against a realistic tree.
|
|
func newSharingServer(t *testing.T) (*Server, string) {
|
|
t.Helper()
|
|
lg := log.New(io.Discard, "", 0)
|
|
root := t.TempDir()
|
|
drive := filepath.Join(root, "drive")
|
|
for _, d := range []string{"appdata/paperless", "backups/unit", "media/filmek", "shares"} {
|
|
if err := os.MkdirAll(filepath.Join(drive, filepath.FromSlash(d)), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
sett, err := settings.Load(filepath.Join(root, "settings.json"), lg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := sett.AddStoragePath(settings.StoragePath{Path: drive, Label: "teszt", Schedulable: true}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return &Server{settings: sett, logger: lg, cfg: &config.Config{}}, drive
|
|
}
|
|
|
|
// Scenario C: the picker/create guard matrix. Every refusal must ALSO be a non-effect.
|
|
func TestSharingResolvePath_GuardMatrix(t *testing.T) {
|
|
s, drive := newSharingServer(t)
|
|
outside := t.TempDir() // a real dir, but under no registered storage root
|
|
|
|
refused := []struct{ name, path string }{
|
|
{"appdata subtree (live app DB)", filepath.Join(drive, "appdata", "paperless")},
|
|
{"appdata root", filepath.Join(drive, "appdata")},
|
|
{"backups subtree", filepath.Join(drive, "backups", "unit")},
|
|
{"the drive root itself", drive},
|
|
{"outside every registered root", outside},
|
|
{"relative path", "nem/abszolut"},
|
|
{"empty", ""},
|
|
{"traversal out of the root", filepath.Join(drive, "..", "escape")},
|
|
{"nonexistent", filepath.Join(drive, "nincs-ilyen")},
|
|
}
|
|
for _, tc := range refused {
|
|
if got, err := s.sharingResolvePath(tc.path); err == nil {
|
|
t.Errorf("%s: must be refused, got %q", tc.name, got)
|
|
}
|
|
}
|
|
|
|
// The guard is not "deny everything": a real user-data folder IS shareable (Scenario B shares
|
|
// media/filmek). Without this the refusal tests above would pass on a broken always-deny guard.
|
|
good := filepath.Join(drive, "media", "filmek")
|
|
if _, err := s.sharingResolvePath(good); err != nil {
|
|
t.Errorf("a user-data folder must be shareable, got %v", err)
|
|
}
|
|
if _, err := s.sharingResolvePath(filepath.Join(drive, "shares")); err != nil {
|
|
t.Errorf("the shares dir must be shareable, got %v", err)
|
|
}
|
|
}
|
|
|
|
// A symlink planted INSIDE a registered root that points OUTSIDE it must not escape the guard —
|
|
// this is why EvalSymlinks runs before the containment assert.
|
|
func TestSharingResolvePath_SymlinkEscapeRefused(t *testing.T) {
|
|
s, drive := newSharingServer(t)
|
|
outside := t.TempDir()
|
|
link := filepath.Join(drive, "media", "escape-link")
|
|
if err := os.Symlink(outside, link); err != nil {
|
|
t.Skipf("symlinks unavailable on this platform/privilege level: %v", err)
|
|
}
|
|
if got, err := s.sharingResolvePath(link); err == nil {
|
|
t.Errorf("a symlink escaping the storage root must be refused, resolved to %q", got)
|
|
}
|
|
}
|
|
|
|
// A decommissioned storage root stops being shareable.
|
|
func TestSharingResolvePath_DecommissionedRootRefused(t *testing.T) {
|
|
s, drive := newSharingServer(t)
|
|
good := filepath.Join(drive, "media", "filmek")
|
|
if _, err := s.sharingResolvePath(good); err != nil {
|
|
t.Fatalf("precondition: %v", err)
|
|
}
|
|
if err := s.settings.SetDecommissioned(drive, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := s.sharingResolvePath(good); err == nil {
|
|
t.Error("a decommissioned storage root must not be shareable")
|
|
}
|
|
}
|
|
|
|
// Every refusal returns the SAME message — a per-reason message would make the picker an oracle
|
|
// for the existence/contents of paths outside the customer's storage.
|
|
func TestSharingResolvePath_UniformRefusal(t *testing.T) {
|
|
s, drive := newSharingServer(t)
|
|
outside := t.TempDir()
|
|
for _, p := range []string{
|
|
filepath.Join(drive, "appdata"),
|
|
outside,
|
|
filepath.Join(drive, "nincs-ilyen"),
|
|
} {
|
|
_, err := s.sharingResolvePath(p)
|
|
if err == nil {
|
|
t.Fatalf("%s should refuse", p)
|
|
}
|
|
if err.Error() != errNotShareable.Error() {
|
|
t.Errorf("refusal message must be uniform, got %q for %s", err.Error(), p)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The "new folder" flow validates the storage ROOT, which the share-target guard rightly refuses.
|
|
// Regression guard for the live-validation bug: reusing sharingResolvePath there broke share
|
|
// creation entirely (the root is never a valid share TARGET, but is the valid PARENT).
|
|
func TestSharingResolveStorageRoot(t *testing.T) {
|
|
s, drive := newSharingServer(t)
|
|
|
|
// The registered root is accepted here...
|
|
if got, err := s.sharingResolveStorageRoot(drive); err != nil || got == "" {
|
|
t.Errorf("a registered storage root must be accepted as a new-folder parent, got %q err=%v", got, err)
|
|
}
|
|
// ...even though it is (correctly) refused as a share TARGET.
|
|
if _, err := s.sharingResolvePath(drive); err == nil {
|
|
t.Error("the drive root must still be refused as a share target")
|
|
}
|
|
|
|
// Anything that is not EXACTLY a registered live root is refused.
|
|
for _, bad := range []string{
|
|
filepath.Join(drive, "media"), // a subdir is not a root
|
|
filepath.Join(drive, "appdata"),
|
|
t.TempDir(), // unregistered
|
|
"relative",
|
|
"",
|
|
} {
|
|
if _, err := s.sharingResolveStorageRoot(bad); err == nil {
|
|
t.Errorf("%q must not be accepted as a storage root", bad)
|
|
}
|
|
}
|
|
|
|
// A decommissioned root stops being a valid parent.
|
|
if err := s.settings.SetDecommissioned(drive, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := s.sharingResolveStorageRoot(drive); err == nil {
|
|
t.Error("a decommissioned root must not accept new folders")
|
|
}
|
|
}
|
|
|
|
// pathWithin must be segment-wise: a sibling directory sharing a name PREFIX is not containment.
|
|
func TestPathWithin_SiblingPrefixIsNotContainment(t *testing.T) {
|
|
if pathWithin("/mnt/drive-evil/x", "/mnt/drive") {
|
|
t.Error("/mnt/drive-evil must NOT count as inside /mnt/drive")
|
|
}
|
|
if !pathWithin("/mnt/drive/x", "/mnt/drive") {
|
|
t.Error("/mnt/drive/x must count as inside /mnt/drive")
|
|
}
|
|
if !pathWithin("/mnt/drive", "/mnt/drive") {
|
|
t.Error("a root is within itself")
|
|
}
|
|
}
|