Files
nidus/internal/webdav/handler_test.go
T
arnef 2fd39c8180 refactor: unify data directory structure
Unify user data directory from fragmented layout to consistent nested format:

- Move WebDAV from: data/files/<username> → data/<username>/files/
- Move CalDAV from: data/<username>/cal-<name> → data/<username>/calendars/<name>
- Move CardDAV from: data/<username>/card-<name> → data/<username>/addressbooks/<name>

Changes:
- internal/store/store.go: Update collectionPath() to map collection names
- internal/store/migrate.go: Add idempotent Migrate() method
- internal/store/migrate_test.go: Comprehensive migration tests
- internal/webdav/handler.go: Use new unified path structure
- cmd/server/main.go: Auto-run migration on startup
- tools/nidusctl/main.go: Add migrate subcommand
- Update tests to verify new structure

URL endpoints unchanged - only on-disk structure modified. All tests pass.
2026-08-30 12:15:31 +02:00

101 lines
3.5 KiB
Go

package filewebdav_test
import (
"io"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/yourusername/caldav-server/internal/auth"
filewebdav "github.com/yourusername/caldav-server/internal/webdav"
)
func testLogger() *slog.Logger {
return slog.New(slog.NewTextHandler(io.Discard, nil))
}
func doAs(t *testing.T, h http.Handler, user, method, path string, body string) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(method, path, strings.NewReader(body))
ctx := auth.NewContext(req.Context(), &auth.Principal{Username: user})
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
// TestPerUserIsolationAndPrefix verifies that all users share the same
// "/files/" URL, but each is served from (and can only see) their own
// directory on disk, resolved from the Basic Auth identity.
func TestPerUserIsolationAndPrefix(t *testing.T) {
dir := t.TempDir()
h := filewebdav.NewHandler(nil, dir, testLogger())
putRec := doAs(t, h, "alice", http.MethodPut, "/files/note.txt", "hello alice")
if putRec.Code != http.StatusCreated && putRec.Code != http.StatusNoContent {
t.Fatalf("PUT as alice: unexpected status %d: %s", putRec.Code, putRec.Body.String())
}
getRec := doAs(t, h, "alice", http.MethodGet, "/files/note.txt", "")
if getRec.Code != http.StatusOK {
t.Fatalf("GET as alice: unexpected status %d", getRec.Code)
}
if getRec.Body.String() != "hello alice" {
t.Fatalf("unexpected body: %q", getRec.Body.String())
}
// bob hits the exact same URL, but must not see alice's file — his own
// directory on disk is empty.
bobRec := doAs(t, h, "bob", http.MethodGet, "/files/note.txt", "")
if bobRec.Code != http.StatusNotFound {
t.Fatalf("expected bob to get 404 for alice's file, got %d", bobRec.Code)
}
// Confirm the file physically landed under dataDir/alice/files/, not
// nested under an extra files/files/... path.
if _, err := os.Stat(dir + "/alice/files/note.txt"); err != nil {
t.Fatalf("expected file at dataDir/alice/files/note.txt: %v", err)
}
}
// TestLockPersistsAcrossRequests ensures the LockSystem used by the handler
// is not recreated (and thus reset) on every request.
func TestLockPersistsAcrossRequests(t *testing.T) {
dir := t.TempDir()
h := filewebdav.NewHandler(nil, dir, testLogger())
// Create the file first.
doAs(t, h, "alice", http.MethodPut, "/files/locked.txt", "v1")
lockBody := `<?xml version="1.0" encoding="utf-8" ?>
<D:lockinfo xmlns:D="DAV:">
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
<D:owner><D:href>test</D:href></D:owner>
</D:lockinfo>`
lockRec := doAs(t, h, "alice", "LOCK", "/files/locked.txt", lockBody)
if lockRec.Code != http.StatusOK {
t.Fatalf("LOCK: unexpected status %d: %s", lockRec.Code, lockRec.Body.String())
}
locktoken := lockRec.Header().Get("Lock-Token")
if locktoken == "" {
t.Fatal("expected Lock-Token header in LOCK response")
}
// A second, unrelated request must still see the lock as active,
// proving the LockSystem instance was reused rather than reset.
req := httptest.NewRequest(http.MethodPut, "/files/locked.txt", strings.NewReader("v2 without token"))
ctx := auth.NewContext(req.Context(), &auth.Principal{Username: "alice"})
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusLocked {
t.Fatalf("expected 423 Locked for PUT without lock token, got %d: %s", rec.Code, rec.Body.String())
}
}