refactor: flatten repository structure to root and archive research notes
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"chfs-ng/internal/auth"
|
||||
"chfs-ng/internal/storage"
|
||||
"github.com/emersion/go-webdav"
|
||||
)
|
||||
|
||||
func TestWebDAVAuthFSCheckTree(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(root, "test", "private"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "test", "public.txt"), []byte("public"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "test", "private", "secret.txt"), []byte("secret"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
account := &auth.Account{Mask: auth.Read | auth.Write | auth.Delete, Rules: []auth.PathRule{
|
||||
{Dir: "test", Mask: auth.Read | auth.Write | auth.Delete},
|
||||
{Dir: "test/private", Mask: auth.None},
|
||||
}}
|
||||
fs := &webdavAuthFS{inner: &webdavStorageFS{store: storage.Local{Root: root}}, account: account}
|
||||
|
||||
if err := fs.checkTree(context.Background(), "/test", auth.Delete); err == nil {
|
||||
t.Fatal("expected recursive delete permission check to reject inaccessible descendant")
|
||||
}
|
||||
if err := fs.checkTree(context.Background(), "/test/public.txt", auth.Delete); err != nil {
|
||||
t.Fatalf("public file should be deletable: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebDAVAuthFSOverwriteRequiresDelete(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(root, "src.txt"), []byte("src"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "dst.txt"), []byte("dst"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
account := &auth.Account{Mask: auth.Write}
|
||||
fs := &webdavAuthFS{inner: &webdavStorageFS{store: storage.Local{Root: root}}, account: account}
|
||||
if err := fs.authorizeOverwrite(context.Background(), "/dst.txt"); err == nil {
|
||||
t.Fatal("expected overwrite authorization to require delete permission")
|
||||
}
|
||||
account.Mask = auth.Delete
|
||||
if err := fs.authorizeOverwrite(context.Background(), "/dst.txt"); err != nil {
|
||||
t.Fatalf("delete permission should authorize overwrite: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSameWebDAVDestinationHost(t *testing.T) {
|
||||
req := httptest.NewRequest("MOVE", "/webdav/src.txt", nil)
|
||||
req.Host = "example.test"
|
||||
req.Header.Set("Destination", "http://other.test/webdav/dst.txt")
|
||||
if sameWebDAVDestinationHost(req) {
|
||||
t.Fatal("cross-host destination should be rejected")
|
||||
}
|
||||
req.Header.Set("Destination", "http://example.test/webdav/dst.txt")
|
||||
if !sameWebDAVDestinationHost(req) {
|
||||
t.Fatal("same-host absolute destination should be accepted")
|
||||
}
|
||||
req.Header.Set("Destination", "/webdav/dst.txt")
|
||||
if !sameWebDAVDestinationHost(req) {
|
||||
t.Fatal("relative destination should be accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebDAVStorageFSRejectsDestinationInsideSource(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(root, "src", "child"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "src", "file.txt"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fs := &webdavStorageFS{store: storage.Local{Root: root}}
|
||||
if _, err := fs.Copy(context.Background(), "/src", "/src/child/copy", &webdav.CopyOptions{}); err == nil {
|
||||
t.Fatal("copying a directory into its descendant should fail")
|
||||
}
|
||||
if _, err := fs.Move(context.Background(), "/src", "/src/child/moved", &webdav.MoveOptions{}); err == nil {
|
||||
t.Fatal("moving a directory into its descendant should fail")
|
||||
}
|
||||
if _, err := fs.Copy(context.Background(), "/src", "/src", &webdav.CopyOptions{}); err == nil {
|
||||
t.Fatal("copying a resource onto itself should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebDAVAuthFSMoveMissingSourcePrecedence(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(root, "existing.txt"), []byte("existing"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
account := &auth.Account{Mask: auth.Read | auth.Write | auth.Delete}
|
||||
fs := &webdavAuthFS{inner: &webdavStorageFS{store: storage.Local{Root: root}}, account: account}
|
||||
|
||||
_, err := fs.Move(context.Background(), "/missing.txt", "/new.txt", &webdav.MoveOptions{})
|
||||
if err == nil || err.Error() != "403 Forbidden: source not found" {
|
||||
t.Fatalf("missing source to free destination: got %v", err)
|
||||
}
|
||||
|
||||
_, err = fs.Move(context.Background(), "/missing.txt", "/existing.txt", &webdav.MoveOptions{})
|
||||
if err == nil || err.Error() != "412 Precondition Failed: destination exists" {
|
||||
t.Fatalf("missing source to occupied destination: got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebDAVStorageFSCopyPreservesExistingDestinationOnSourceReadFailure(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(root, "dst.txt"), []byte("original"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store := storage.Local{Root: root}
|
||||
if err := os.WriteFile(filepath.Join(root, "src.txt"), []byte("replacement"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fs := &webdavStorageFS{store: store}
|
||||
if _, err := fs.Copy(context.Background(), "/src.txt", "/dst.txt", &webdav.CopyOptions{}); err != nil {
|
||||
t.Fatalf("copy: %v", err)
|
||||
}
|
||||
got, err := os.ReadFile(filepath.Join(root, "dst.txt"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != "replacement" {
|
||||
t.Fatalf("destination content=%q", got)
|
||||
}
|
||||
matches, err := filepath.Glob(filepath.Join(root, "dst.txt.chfs-copy-*"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(matches) != 0 {
|
||||
t.Fatalf("temporary copy files leaked: %v", matches)
|
||||
}
|
||||
}
|
||||
|
||||
type failingReader struct{}
|
||||
|
||||
func (failingReader) Read([]byte) (int, error) { return 0, errors.New("source read failed") }
|
||||
|
||||
type readCloser struct{ io.Reader }
|
||||
|
||||
func (r readCloser) Close() error { return nil }
|
||||
|
||||
func TestWebDAVStorageFSCopyFailureDoesNotCorruptDestination(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
store := storage.Local{Root: root}
|
||||
if err := os.WriteFile(filepath.Join(root, "dst.txt"), []byte("original"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := copyFileToDestination(store, failingReader{}, "dst.txt"); err == nil {
|
||||
t.Fatal("expected source read failure")
|
||||
}
|
||||
got, err := os.ReadFile(filepath.Join(root, "dst.txt"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != "original" {
|
||||
t.Fatalf("destination was corrupted: %q", got)
|
||||
}
|
||||
matches, err := filepath.Glob(filepath.Join(root, "dst.txt.chfs-copy-*"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(matches) != 0 {
|
||||
t.Fatalf("temporary copy files leaked: %v", matches)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebDAVStorageFSCreateFailureDoesNotCorruptDestination(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
store := storage.Local{Root: root}
|
||||
if err := os.WriteFile(filepath.Join(root, "dst.txt"), []byte("original"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fs := &webdavStorageFS{store: store}
|
||||
_, _, err := fs.Create(context.Background(), "/dst.txt", readCloser{Reader: failingReader{}}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected PUT body failure")
|
||||
}
|
||||
got, err := os.ReadFile(filepath.Join(root, "dst.txt"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != "original" {
|
||||
t.Fatalf("destination was corrupted: %q", got)
|
||||
}
|
||||
matches, err := filepath.Glob(filepath.Join(root, "dst.txt.chfs-copy-*"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(matches) != 0 {
|
||||
t.Fatalf("temporary PUT files leaked: %v", matches)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebDAVStorageFSCopyAndMove(t *testing.T) {
|
||||
root1 := t.TempDir()
|
||||
root2 := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(root1, "a.txt"), []byte("one"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := storage.NewMulti([]string{root1, root2})
|
||||
fs := &webdavStorageFS{store: m}
|
||||
if _, err := fs.Copy(context.Background(), "/"+m.Names[0]+"/a.txt", "/"+m.Names[0]+"/b.txt", &webdav.CopyOptions{}); err != nil {
|
||||
t.Fatalf("copy: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root1, "b.txt")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := fs.Move(context.Background(), "/"+m.Names[0]+"/b.txt", "/"+m.Names[1]+"/b.txt", &webdav.MoveOptions{}); err == nil {
|
||||
t.Fatal("expected cross-root move rejection")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user