feat(drive): fix webdav back-compat
add env variable `OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX`
which is by default:
`OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX="@drive"`
so `/webdav/` -> points to user's personal drive (**backward compatibilit**y)
`/web/dav/@drive/{uuid|drive name}/` points to the respective drive
if admins want directly `/webdav/` pointing to list of drives they need to:
`OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX=""`
+ ensure lock is per user (RFC 4918 §9.11)
fix: #554
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
# =============================================================
|
||||
# OxiCloud — WebDAV drive-root URL scheme, `OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX=""` variant
|
||||
# =============================================================
|
||||
# Companion to `webdav_drive_root.hurl`. That file exercises the
|
||||
# default config (`OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX="@drive"`); this
|
||||
# one exercises the empty-string config where `/webdav/` IS the
|
||||
# drive listing and there's no default-drive shortcut.
|
||||
#
|
||||
# Server env for this test: `tests/common/server-webdav-drive-root.env`
|
||||
# sets `OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX=""`. This file assumes that
|
||||
# config is active — it is NOT part of the standard `run.sh`
|
||||
# invocation (which starts the default-config server).
|
||||
#
|
||||
# Coverage:
|
||||
# 1. Login, capture JWT
|
||||
# 2. Resolve caller's default drive (id + display name)
|
||||
# 3. Create a magic folder under the home root via REST
|
||||
# 4. PROPFIND `/webdav/` — drive listing (default drive
|
||||
# appears as a virtual child under its display name).
|
||||
# 5. PROPFIND `/webdav/<uuid>/` — descend into a drive by
|
||||
# UUID. Magic folder appears.
|
||||
# 6. PROPFIND `/webdav/<name>/` — descend into a drive by
|
||||
# display name. Magic folder appears.
|
||||
# 7. `/webdav/@drive/` returns 404 in this mode — the sigil
|
||||
# has no reserved meaning when `webdav_drive_listing_prefix=""`.
|
||||
# A drive genuinely named `@drive` would resolve here; the
|
||||
# 404 comes from "no such drive," not the sigil.
|
||||
# 8. Cleanup: DELETE the magic folder via REST.
|
||||
# =============================================================
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 1 — Login, capture JWT
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
{ "username": "{{username}}", "password": "{{password}}" }
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
token: jsonpath "$.access_token"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 2 — Resolve caller's default drive (id + display name).
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/api/drives
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
default_drive_id: jsonpath "$[0].id"
|
||||
default_drive_name: jsonpath "$[0].name"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 3 — Resolve the caller's home root folder id.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/api/folders
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
home_folder_id: jsonpath "$[0].id"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 4 — Create a magic folder under the home root via REST.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/folders
|
||||
Authorization: Bearer {{token}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"name": "hurl-drive-root-empty-magic-marker",
|
||||
"parent_id": "{{home_folder_id}}"
|
||||
}
|
||||
|
||||
HTTP 201
|
||||
[Captures]
|
||||
magic_folder_id: jsonpath "$.id"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 5 — PROPFIND on `/webdav/` (bare root). With
|
||||
# `OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX=""` this IS the drive
|
||||
# listing — the default drive appears as a virtual
|
||||
# child under its display name. The magic folder does
|
||||
# NOT appear here (it lives one level deeper).
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PROPFIND {{base_url}}/webdav/
|
||||
Authorization: Bearer {{token}}
|
||||
Depth: 1
|
||||
|
||||
HTTP 207
|
||||
[Asserts]
|
||||
xpath "//*[local-name()='response']/*[local-name()='href' and contains(text(), '{{default_drive_name}}')]" exists
|
||||
# Magic folder is one level deeper — must NOT show up at root.
|
||||
xpath "//*[local-name()='response']/*[local-name()='href' and contains(text(), 'hurl-drive-root-empty-magic-marker')]" not exists
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 6 — PROPFIND on `/webdav/<uuid>/`. Descends into the
|
||||
# default drive; magic folder is a top-level child.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PROPFIND {{base_url}}/webdav/{{default_drive_id}}/
|
||||
Authorization: Bearer {{token}}
|
||||
Depth: 1
|
||||
|
||||
HTTP 207
|
||||
[Asserts]
|
||||
xpath "//*[local-name()='response']/*[local-name()='href' and contains(text(), 'hurl-drive-root-empty-magic-marker')]" exists
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 7 — PROPFIND on `/webdav/<name>/`. Same descent via
|
||||
# display name.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PROPFIND {{base_url}}/webdav/{{default_drive_name}}/
|
||||
Authorization: Bearer {{token}}
|
||||
Depth: 1
|
||||
|
||||
HTTP 207
|
||||
[Asserts]
|
||||
xpath "//*[local-name()='response']/*[local-name()='href' and contains(text(), 'hurl-drive-root-empty-magic-marker')]" exists
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 8 — `/webdav/@drive/` has no reserved meaning in the
|
||||
# empty-config mode. `@drive` is treated as a plain
|
||||
# drive selector; no drive by that name → 404.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PROPFIND {{base_url}}/webdav/@drive/
|
||||
Authorization: Bearer {{token}}
|
||||
Depth: 1
|
||||
|
||||
HTTP 404
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 9 — Reject MKCOL at `/webdav/` (bare pseudo-root).
|
||||
# In the empty-config mode `/webdav/` IS the drive
|
||||
# listing — there's no writable parent, so 405
|
||||
# Method Not Allowed. This guard prevents a client
|
||||
# from creating something at "root" that shadows a
|
||||
# drive name.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
MKCOL {{base_url}}/webdav/
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
HTTP 405
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 10 — Reject MKCOL at `/webdav/<not-a-drive>`. The first
|
||||
# URL segment is the drive selector in this config;
|
||||
# an unknown selector yields 404. A client cannot
|
||||
# "create a drive" via MKCOL — the drive-create
|
||||
# surface is `POST /api/drives`.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
MKCOL {{base_url}}/webdav/hurl-not-a-real-drive
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
HTTP 404
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 11 — Reject PUT at `/webdav/<not-a-drive>/x.txt`. Same
|
||||
# rejection shape as MKCOL.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PUT {{base_url}}/webdav/hurl-not-a-real-drive/probe.txt
|
||||
Authorization: Bearer {{token}}
|
||||
Content-Type: text/plain
|
||||
```
|
||||
probe
|
||||
```
|
||||
|
||||
HTTP 404
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 12 — Cleanup: DELETE the magic folder via REST.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
DELETE {{base_url}}/api/folders/{{magic_folder_id}}
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
HTTP 204
|
||||
Executable
+106
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env bash
|
||||
# WebDAV drive-root URL-scheme variant runner.
|
||||
#
|
||||
# Exercises `OXICLOUD_WEBDAV_DRIVE_LISTING_PREFIX=""` — the config where the
|
||||
# WebDAV `@drive` path segment is disabled and `/webdav/` IS the
|
||||
# drive listing. `tests/api/webdav_drive_root.hurl` covers the
|
||||
# default `"@drive"` config in the main API run; this runner
|
||||
# starts a separately-configured server to cover the empty-string
|
||||
# case, mirroring the OIDC runner's shape.
|
||||
#
|
||||
# Usage (from repo root):
|
||||
# bash tests/webdav-drive-root/run.sh
|
||||
#
|
||||
# Prerequisites: docker, cargo, hurl ≥ 4.0
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
COMMON="$REPO_ROOT/tests/common"
|
||||
TEST_DIR="$REPO_ROOT/tests/webdav-drive-root"
|
||||
|
||||
# shellcheck source=test.env
|
||||
source "$TEST_DIR/test.env"
|
||||
|
||||
SERVER_PORT="${base_url##*:}"
|
||||
|
||||
log() { echo "[webdav-drive-root] $*"; }
|
||||
die() { echo "[webdav-drive-root] ERROR: $*" >&2; exit 1; }
|
||||
|
||||
wait_for_http() {
|
||||
local url="$1" timeout="${2:-60}"
|
||||
local deadline=$(( $(date +%s) + timeout ))
|
||||
until curl -sf "$url" >/dev/null 2>&1; do
|
||||
[[ $(date +%s) -ge $deadline ]] && die "Timeout waiting for $url"
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
# ── Teardown (always runs on exit) ────────────────────────────────────────────
|
||||
|
||||
SERVER_PID=""
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "$SERVER_PID" ]]; then
|
||||
log "Stopping OxiCloud server (pid $SERVER_PID)..."
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
wait "$SERVER_PID" 2>/dev/null || true
|
||||
fi
|
||||
bash "$COMMON/stop-db.sh"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
# ── 1. Start postgres ─────────────────────────────────────────────────────────
|
||||
|
||||
bash "$COMMON/spawn-db.sh"
|
||||
|
||||
# ── 2. Load the drive-root-variant server env + port ──────────────────────────
|
||||
|
||||
set -a
|
||||
# shellcheck source=../common/server-webdav-drive-root.env
|
||||
source "$COMMON/server-webdav-drive-root.env"
|
||||
OXICLOUD_SERVER_PORT=$SERVER_PORT
|
||||
OXICLOUD_STORAGE_PATH="$REPO_ROOT/tests/webdav-drive-root/storage"
|
||||
set +a
|
||||
|
||||
# shellcheck source=../common/wipe-storage.sh
|
||||
source "$COMMON/wipe-storage.sh"
|
||||
wipe_storage "$OXICLOUD_STORAGE_PATH"
|
||||
|
||||
# ── 3. Start OxiCloud server with the drive-root-variant config ───────────────
|
||||
|
||||
BUILD_TARGET="${BUILD_TARGET:-debug}"
|
||||
OXICLOUD_BIN="$REPO_ROOT/target/$BUILD_TARGET/oxicloud"
|
||||
|
||||
if [[ ! -x "$OXICLOUD_BIN" ]]; then
|
||||
log "Building OxiCloud server ($BUILD_TARGET)..."
|
||||
case "$BUILD_TARGET" in
|
||||
debug) (cd "$REPO_ROOT" && cargo build 2>&1 | tail -n 20) || die "cargo build failed" ;;
|
||||
release) (cd "$REPO_ROOT" && cargo build --release 2>&1 | tail -n 20) || die "cargo build --release failed" ;;
|
||||
*) die "Unsupported BUILD_TARGET='$BUILD_TARGET' (expected 'debug' or 'release')" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
log "Starting OxiCloud server with WEBDAV_DRIVE_LISTING_PREFIX='' on port $SERVER_PORT..."
|
||||
"$OXICLOUD_BIN" --config "$COMMON/server-webdav-drive-root.env" &
|
||||
SERVER_PID=$!
|
||||
log "Waiting for server at $base_url..."
|
||||
wait_for_http "$base_url/ready" 120
|
||||
log "Server is ready."
|
||||
|
||||
# ── 4. Run Hurl tests ─────────────────────────────────────────────────────────
|
||||
#
|
||||
# `setup.hurl` from the shared api/ suite bootstraps the initial admin
|
||||
# account via `POST /api/setup` — the endpoint locks after the first
|
||||
# admin exists, so it's a one-shot idempotency-by-server-state seed.
|
||||
# We reuse the file rather than duplicating the setup body so credential
|
||||
# / schema changes in the api tests automatically flow here.
|
||||
|
||||
log "Running Hurl tests..."
|
||||
hurl --variables-file "$TEST_DIR/test.env" \
|
||||
--file-root "$REPO_ROOT/tests" \
|
||||
--test --jobs 1 \
|
||||
"$REPO_ROOT/tests/api/setup.hurl" \
|
||||
"$TEST_DIR/drive_root_empty_config.hurl"
|
||||
|
||||
log "webdav-drive-root tests passed."
|
||||
@@ -0,0 +1,9 @@
|
||||
# Test credentials for the WebDAV drive-root variant runner — NOT real secrets.
|
||||
# Runs on a separate port from tests/api and tests/webdav so a
|
||||
# `just api-test` chain doesn't collide when the previous runner's
|
||||
# teardown is still in progress.
|
||||
base_url=http://localhost:8089
|
||||
username=admin
|
||||
email=admin@example.com
|
||||
# gitguardian:ignore
|
||||
password=TestPassword1!
|
||||
Reference in New Issue
Block a user