2026-06-10 22:59:06 +02:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# =============================================================
|
|
|
|
|
# OxiCloud — Baseline: Native /webdav/ + LOCK / UNLOCK
|
|
|
|
|
# =============================================================
|
|
|
|
|
# Groups M + N from BASELINE_TESTS_NC_WEBDAV.md (11 scenarios).
|
|
|
|
|
#
|
|
|
|
|
# The native `/webdav/...` surface is the protocol layer
|
|
|
|
|
# rclone, davfs2, Cyberduck, Office (via WebDAV mount), and
|
|
|
|
|
# the other generic-DAV ecosystem use. It differs from the
|
|
|
|
|
# NC `/remote.php/dav/files/{user}/...` surface in several
|
|
|
|
|
# baseline-worthy ways:
|
|
|
|
|
#
|
|
|
|
|
# - Auth: JWT bearer (not Basic Auth with an app password)
|
|
|
|
|
# - Chroot: implicit "the user's home folder", NO {user}
|
|
|
|
|
# URL segment to validate
|
|
|
|
|
# - DAV class advertisement: `1, 2` (incl. Class 2 LOCK)
|
|
|
|
|
# versus NC's `1, 3`
|
|
|
|
|
#
|
|
|
|
|
# Coverage:
|
|
|
|
|
# M1 — OPTIONS / advertises DAV 1, 2 + Allow includes LOCK
|
|
|
|
|
# M2 — PROPFIND Depth: 1 trailing-slash semantics (the
|
|
|
|
|
# same regression guard as D9/D10 on the NC surface,
|
|
|
|
|
# run again on this surface)
|
|
|
|
|
# M3 — PUT sample.txt → 201
|
|
|
|
|
# M4 — Range GET (bytes=0-9) → 206
|
|
|
|
|
# M5 — MOVE sample.txt → moved.txt
|
|
|
|
|
# M6 — MKCOL sub/ → 201
|
|
|
|
|
# M7 — DELETE sub/ → 204
|
|
|
|
|
# M8 — COPY a.txt → b.txt (pin whatever current is — native
|
|
|
|
|
# COPY may or may not be implemented)
|
|
|
|
|
# N1 — LOCK locked.txt → 200 + Lock-Token header
|
|
|
|
|
# N2 — PUT locked.txt without If: <token> from a different
|
|
|
|
|
# context → 423 Locked
|
|
|
|
|
# N3 — UNLOCK with the token → 204; subsequent PUT succeeds
|
|
|
|
|
# =============================================================
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
|
cd "$SCRIPT_DIR"
|
|
|
|
|
|
|
|
|
|
source test.env
|
|
|
|
|
source common.sh
|
|
|
|
|
source lib/dav_helpers.sh
|
|
|
|
|
|
|
|
|
|
echo
|
|
|
|
|
echo "=== Native /webdav/ + LOCK / UNLOCK (Groups M + N baseline) ==="
|
|
|
|
|
echo
|
|
|
|
|
|
|
|
|
|
oxicloud_login
|
|
|
|
|
resolve_home_folder_id
|
|
|
|
|
wipe_home_folder
|
|
|
|
|
|
|
|
|
|
DAV_BASE="$base_url/webdav"
|
|
|
|
|
FIXTURE_DIR=$(mktemp -d)
|
|
|
|
|
trap 'rm -rf "$FIXTURE_DIR"; wipe_home_folder 2>/dev/null || true' EXIT
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
# M1 — OPTIONS advertises DAV 1, 2 + Allow includes LOCK
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
echo " M1: OPTIONS /webdav/ → DAV: 1, 2 + Allow includes LOCK/UNLOCK"
|
|
|
|
|
HEADERS=$(dav_curl -i -X OPTIONS "$DAV_BASE/" | tr -d '\r')
|
|
|
|
|
STATUS=$(awk 'NR==1{print $2}' <<< "$HEADERS")
|
|
|
|
|
[[ "$STATUS" == "200" ]] \
|
|
|
|
|
|| fail "M1: OPTIONS expected 200, got $STATUS"
|
|
|
|
|
grep -qi '^dav:.*1.*2' <<< "$HEADERS" \
|
|
|
|
|
|| fail "M1: missing 'DAV: 1, 2' header (native surface should advertise Class 2)"
|
|
|
|
|
grep -qi '^allow:.*LOCK' <<< "$HEADERS" || fail "M1: Allow missing LOCK"
|
|
|
|
|
grep -qi '^allow:.*UNLOCK' <<< "$HEADERS" || fail "M1: Allow missing UNLOCK"
|
|
|
|
|
pass "M1: OPTIONS advertises DAV 1, 2 + Allow includes LOCK/UNLOCK"
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
# Fixture setup via REST so M2 has stable mixed content.
|
|
|
|
|
# We deliberately PROPFIND a dedicated *sub-folder* of home,
|
|
|
|
|
# not bare `/webdav/`, because:
|
|
|
|
|
#
|
|
|
|
|
# 1. The native handler's `resolve_webdav_path` only fires
|
|
|
|
|
# when the URL subpath is non-empty (gated by
|
|
|
|
|
# `!path.is_empty() && method.as_str() != "OPTIONS"`).
|
|
|
|
|
# PROPFIND on bare `/webdav/` therefore returns the
|
|
|
|
|
# user's root-collections list, not the contents of
|
|
|
|
|
# their home folder.
|
|
|
|
|
# 2. The number of root collections varies per environment
|
|
|
|
|
# (default home + anything else the system or earlier
|
|
|
|
|
# tests created), so asserting a fixed count there is
|
|
|
|
|
# brittle. A test-owned sub-folder is fully under our
|
|
|
|
|
# control.
|
|
|
|
|
#
|
|
|
|
|
# Inside `m2-probe/`: 2 files + 2 sub-folders → PROPFIND
|
|
|
|
|
# Depth: 1 yields exactly 5 responses (self + 4 children) and
|
|
|
|
|
# exercises both the file-href (no trailing slash) and the
|
|
|
|
|
# folder-href (trailing slash) branches of the same
|
|
|
|
|
# regression guard that catches D9/D10 on the NC surface.
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
api_create_folder "m2-probe" "$HOME_FOLDER_ID"
|
|
|
|
|
M2_PROBE_ID="$LAST_FOLDER_ID"
|
|
|
|
|
echo "alpha" > "$FIXTURE_DIR/m2-alpha.txt"
|
|
|
|
|
echo "beta" > "$FIXTURE_DIR/m2-beta.txt"
|
|
|
|
|
api_upload_file "$FIXTURE_DIR/m2-alpha.txt" "$M2_PROBE_ID"
|
|
|
|
|
api_upload_file "$FIXTURE_DIR/m2-beta.txt" "$M2_PROBE_ID"
|
|
|
|
|
api_create_folder "m2-foldA" "$M2_PROBE_ID"
|
|
|
|
|
api_create_folder "m2-foldB" "$M2_PROBE_ID"
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
# M2 — PROPFIND Depth: 1 trailing-slash semantics (mixed)
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
echo " M2: PROPFIND /webdav/m2-probe/ Depth: 1 (mixed children — trailing-slash regression guard)"
|
|
|
|
|
BODY=$(dav_curl -X PROPFIND -H "Depth: 1" "$DAV_BASE/m2-probe/")
|
|
|
|
|
N=$(count_responses "$BODY")
|
|
|
|
|
# Collection (1) + 2 files + 2 folders = 5
|
|
|
|
|
[[ "$N" == "5" ]] \
|
|
|
|
|
|| fail "M2: expected 5 responses (collection + 2 files + 2 folders), got $N"
|
|
|
|
|
assert_collection_hrefs_have_trailing_slash "$BODY" "M2"
|
|
|
|
|
pass "M2: 5 responses, trailing-slash semantics correct on native /webdav/ surface"
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
# M3 — PUT a new file
|
|
|
|
|
#
|
|
|
|
|
# Pinned current behaviour: the native handler always returns
|
|
|
|
|
# 204 NO_CONTENT regardless of new-vs-overwrite. The NC handler
|
|
|
|
|
# differentiates (201 for new, 204 for overwrite, see F1/F3)
|
|
|
|
|
# but the native one in `interfaces/api/handlers/webdav_handler.rs::handle_put`
|
|
|
|
|
# unconditionally builds a 204 response on success (line ~1026
|
|
|
|
|
# at time of writing). RFC 4918 §9.7.1 actually allows either
|
|
|
|
|
# — both indicate success — so this is current behaviour, not
|
|
|
|
|
# a bug. NC desktop / generic DAV clients accept both.
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
# M3-M8 use ROOT-level paths (just `/webdav/<name>`) NOT nested
|
|
|
|
|
# under `m2-probe/`. Why: there's a real bug in the native PUT
|
|
|
|
|
# handler where a PUT to `/webdav/m2-probe/foo.txt` writes the
|
|
|
|
|
# file with `folder_id=NULL` (`get_parent_folder_id` doesn't
|
|
|
|
|
# correctly look up REST-created parent folders), so the file
|
|
|
|
|
# effectively ends up at root. The lenient GET path
|
|
|
|
|
# (`get_file_by_path`) still finds it, but the strict
|
|
|
|
|
# `resolve_path_for_user` (which MOVE / COPY / DELETE use) does
|
|
|
|
|
# not. PUT then GET works on nested paths; PUT then MOVE 404s.
|
|
|
|
|
# Pinning this as KNOWN BUG at M5 below.
|
|
|
|
|
#
|
|
|
|
|
# Until that's fixed, M3-M8 use root-level paths so the rest of
|
|
|
|
|
# the lifecycle (which the existing test_dedup_webdav_* scripts
|
|
|
|
|
# also exercise at root) actually validates.
|
|
|
|
|
|
|
|
|
|
echo " M3: PUT /webdav/m3-sample.txt (pinned: native always 204, NC would be 201 on new)"
|
|
|
|
|
STATUS=$(dav_curl -o /dev/null -w "%{http_code}" -X PUT \
|
|
|
|
|
-H "Content-Type: text/plain" \
|
|
|
|
|
--data-binary 'sample contents — exactly 31 bytes' \
|
|
|
|
|
"$DAV_BASE/m3-sample.txt")
|
|
|
|
|
case "$STATUS" in
|
|
|
|
|
204)
|
|
|
|
|
pass "M3: native PUT new → 204 (pinned current behaviour; differs from NC's 201/204 split)"
|
|
|
|
|
;;
|
|
|
|
|
201)
|
|
|
|
|
fail "M3: native PUT now returns 201 for new — handler differentiates new-vs-overwrite. Update pin if intentional."
|
|
|
|
|
;;
|
|
|
|
|
*)
|
|
|
|
|
fail "M3: unexpected status $STATUS"
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
# M4 — Range GET bytes=0-9 → 206 + 10 bytes
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
echo " M4: GET /webdav/m3-sample.txt with Range: bytes=0-9 → 206 + 10 bytes"
|
|
|
|
|
HEADERS=$(dav_curl -D - -o /dev/null -H "Range: bytes=0-9" "$DAV_BASE/m3-sample.txt")
|
|
|
|
|
STATUS=$(awk 'NR==1{print $2}' <<< "$HEADERS" | tr -d '\r')
|
|
|
|
|
[[ "$STATUS" == "206" ]] \
|
|
|
|
|
|| fail "M4: Range GET expected 206, got $STATUS"
|
|
|
|
|
BODY_SIZE=$(dav_curl -H "Range: bytes=0-9" "$DAV_BASE/m3-sample.txt" | wc -c | tr -d ' ')
|
|
|
|
|
[[ "$BODY_SIZE" == "10" ]] \
|
|
|
|
|
|| fail "M4: Range body size expected 10, got $BODY_SIZE"
|
|
|
|
|
pass "M4: Range bytes=0-9 → 206 + 10 bytes"
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
# M5 — MOVE sample.txt → moved.txt
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
# M5 — root-level MOVE
|
|
|
|
|
#
|
|
|
|
|
# After all the nested-path diagnostics above (which surfaced the
|
|
|
|
|
# bug pinned in the M3 comment), this assertion finally tests the
|
|
|
|
|
# code path where it should actually work: root-level MOVE of a
|
|
|
|
|
# file PUT at root. If even this 404s, the bug is broader and
|
|
|
|
|
# native MOVE is unusable, not just nested.
|
|
|
|
|
echo " M5: MOVE /webdav/m3-sample.txt → /webdav/m5-moved.txt"
|
|
|
|
|
STATUS=$(dav_curl -o /dev/null -w "%{http_code}" -X MOVE \
|
|
|
|
|
-H "Destination: $DAV_BASE/m5-moved.txt" \
|
|
|
|
|
"$DAV_BASE/m3-sample.txt")
|
|
|
|
|
case "$STATUS" in
|
|
|
|
|
201|204)
|
|
|
|
|
pass "M5: root-level MOVE → $STATUS"
|
|
|
|
|
;;
|
|
|
|
|
404)
|
|
|
|
|
# KNOWN BUG: native MOVE returns 404 on a file that was
|
|
|
|
|
# PUT at the same path, even at root level. The strict
|
|
|
|
|
# `resolve_path_for_user` SQL query doesn't match what
|
|
|
|
|
# the PUT's `save_file_from_temp_with_dedup` stored —
|
|
|
|
|
# most likely because the WebDAV dispatcher's path
|
|
|
|
|
# prepending (`resolve_webdav_path` → "My Folder - X/foo")
|
|
|
|
|
# doesn't match the user's actual home folder path
|
|
|
|
|
# field in the DB. Same root cause makes nested MOVE
|
|
|
|
|
# (see M3 comment) unusable too.
|
|
|
|
|
#
|
|
|
|
|
# Where the fix lives:
|
|
|
|
|
# `interfaces/api/handlers/webdav_handler.rs::handle_move`
|
|
|
|
|
# currently calls `resolver.resolve_path_for_user`. It
|
|
|
|
|
# should either:
|
|
|
|
|
# (a) fall back to `file_retrieval_service.get_file_by_path`
|
|
|
|
|
# (the same lookup GET uses successfully), or
|
|
|
|
|
# (b) normalise the source path through the same
|
|
|
|
|
# transformer the PUT writes through.
|
|
|
|
|
pass "M5: root-level MOVE → 404 (KNOWN BUG: resolve_path_for_user mismatch — pinned)"
|
|
|
|
|
;;
|
|
|
|
|
*)
|
|
|
|
|
fail "M5: unexpected status $STATUS"
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
# M6 — MKCOL sub/ → 201
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
echo " M6: MKCOL /webdav/m6-sub/ → 201"
|
|
|
|
|
STATUS=$(dav_curl -o /dev/null -w "%{http_code}" -X MKCOL "$DAV_BASE/m6-sub/")
|
|
|
|
|
[[ "$STATUS" == "201" ]] \
|
|
|
|
|
|| fail "M6: MKCOL expected 201, got $STATUS"
|
|
|
|
|
pass "M6: native MKCOL → 201"
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
# M7 — DELETE sub/ → 204
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
echo " M7: DELETE /webdav/m6-sub/ → 204"
|
|
|
|
|
STATUS=$(dav_curl -o /dev/null -w "%{http_code}" -X DELETE "$DAV_BASE/m6-sub/")
|
|
|
|
|
case "$STATUS" in
|
|
|
|
|
204) pass "M7: native DELETE → 204" ;;
|
|
|
|
|
404)
|
|
|
|
|
# If DELETE also hits the resolve_path_for_user 404 trap
|
|
|
|
|
# (it uses the same resolver), pin as same root-cause
|
|
|
|
|
# KNOWN BUG.
|
|
|
|
|
pass "M7: native DELETE → 404 (KNOWN BUG: same resolve_path_for_user mismatch as M5 — pinned)"
|
|
|
|
|
;;
|
|
|
|
|
*) fail "M7: unexpected status $STATUS" ;;
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
# M8 — COPY a.txt → b.txt (pin whatever current behaviour is)
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
# M8 source depends on whether M5 MOVE actually worked. If M5 was
|
|
|
|
|
# pinned as KNOWN BUG (404), the source for M8 is still
|
|
|
|
|
# m3-sample.txt at root, not m5-moved.txt.
|
|
|
|
|
echo " M8: COPY native source → /webdav/m8-copy.txt"
|
|
|
|
|
M8_SOURCE_URL="$DAV_BASE/m3-sample.txt"
|
|
|
|
|
# If M5 actually moved the file, the source name changed.
|
|
|
|
|
if dav_curl -o /dev/null -w "%{http_code}" -X PROPFIND -H "Depth: 0" "$DAV_BASE/m5-moved.txt" | grep -q "207"; then
|
|
|
|
|
M8_SOURCE_URL="$DAV_BASE/m5-moved.txt"
|
|
|
|
|
fi
|
|
|
|
|
STATUS=$(dav_curl -o /dev/null -w "%{http_code}" -X COPY \
|
|
|
|
|
-H "Destination: $DAV_BASE/m8-copy.txt" \
|
|
|
|
|
"$M8_SOURCE_URL")
|
|
|
|
|
case "$STATUS" in
|
|
|
|
|
201|204)
|
|
|
|
|
# Confirm source still exists (COPY != MOVE).
|
|
|
|
|
SRC_STATUS=$(dav_curl -o /dev/null -w "%{http_code}" -X PROPFIND -H "Depth: 0" "$M8_SOURCE_URL")
|
|
|
|
|
DST_STATUS=$(dav_curl -o /dev/null -w "%{http_code}" -X PROPFIND -H "Depth: 0" "$DAV_BASE/m8-copy.txt")
|
|
|
|
|
[[ "$SRC_STATUS" == "207" ]] \
|
|
|
|
|
|| fail "M8: COPY removed source ($SRC_STATUS instead of 207) — that's MOVE behaviour, not COPY"
|
|
|
|
|
[[ "$DST_STATUS" == "207" ]] \
|
|
|
|
|
|| fail "M8: destination not present after COPY ($DST_STATUS)"
|
|
|
|
|
pass "M8: native COPY → $STATUS, source preserved, destination present"
|
|
|
|
|
;;
|
|
|
|
|
405)
|
|
|
|
|
pass "M8: native COPY → 405 METHOD_NOT_ALLOWED — handler not implemented, pinned"
|
|
|
|
|
;;
|
|
|
|
|
404)
|
|
|
|
|
pass "M8: native COPY → 404 (KNOWN BUG: same resolve_path_for_user mismatch as M5/M7 — pinned)"
|
|
|
|
|
;;
|
|
|
|
|
500)
|
|
|
|
|
# KNOWN BUG: the COPY file branch at
|
|
|
|
|
# `interfaces/api/handlers/webdav_handler.rs::handle_copy`
|
|
|
|
|
# line ~1639 passes `(file.id, user.id, target_folder_id)`
|
|
|
|
|
# to `copy_file_with_perms` — no destination NAME. The
|
|
|
|
|
# copy therefore lands in the target folder under the
|
|
|
|
|
# SOURCE's name, ignoring the rename the client requested.
|
|
|
|
|
# When source and destination resolve to the same folder
|
|
|
|
|
# (common for root-level COPY), this collides with the
|
|
|
|
|
# source itself → AlreadyExists → leaks as 500.
|
|
|
|
|
#
|
|
|
|
|
# Where the fix lives: same handler — either
|
|
|
|
|
# (a) extend `copy_file_with_perms` to accept an
|
|
|
|
|
# optional new name (the folder-tree branch on
|
|
|
|
|
# line ~1591 already passes a name into
|
|
|
|
|
# `copy_folder_tree_with_perms`), or
|
|
|
|
|
# (b) follow the copy with a `rename_file_with_perms`
|
|
|
|
|
# call if `dest_filename != source.name` (mirrors
|
|
|
|
|
# what MOVE does at line ~1347).
|
|
|
|
|
pass "M8: native COPY → 500 (KNOWN BUG: dest filename discarded, collides with source — pinned)"
|
|
|
|
|
;;
|
|
|
|
|
*)
|
|
|
|
|
fail "M8: unexpected COPY status $STATUS"
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
# ═════════════════════════════════════════════════════════════
|
|
|
|
|
# Group N — LOCK / UNLOCK
|
|
|
|
|
# ═════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
# Set up a file for the lock scenarios.
|
|
|
|
|
dav_curl -o /dev/null -X PUT \
|
|
|
|
|
-H "Content-Type: text/plain" \
|
|
|
|
|
--data-binary 'lockable contents' \
|
|
|
|
|
"$DAV_BASE/n-locked.txt" > /dev/null
|
|
|
|
|
|
|
|
|
|
LOCK_BODY='<?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>baseline-test-owner</d:owner>
|
|
|
|
|
</d:lockinfo>'
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
# N1 — LOCK → 200 with Lock-Token header
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
echo " N1: LOCK /webdav/n-locked.txt → 200 + Lock-Token header"
|
|
|
|
|
HEADERS=$(dav_curl -D - -o /dev/null -X LOCK \
|
|
|
|
|
-H "Content-Type: application/xml" \
|
|
|
|
|
-H "Timeout: Second-60" \
|
|
|
|
|
--data "$LOCK_BODY" \
|
|
|
|
|
"$DAV_BASE/n-locked.txt")
|
|
|
|
|
STATUS=$(awk 'NR==1{print $2}' <<< "$HEADERS" | tr -d '\r')
|
|
|
|
|
[[ "$STATUS" == "200" ]] \
|
|
|
|
|
|| fail "N1: LOCK expected 200, got $STATUS"
|
|
|
|
|
LOCK_TOKEN=$(grep -i '^lock-token:' <<< "$HEADERS" | awk '{print $2}' | tr -d '\r<>')
|
|
|
|
|
[[ -n "$LOCK_TOKEN" ]] \
|
|
|
|
|
|| fail "N1: LOCK response missing Lock-Token header"
|
|
|
|
|
pass "N1: LOCK → 200 + Lock-Token=$LOCK_TOKEN"
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
2026-06-16 23:17:40 +02:00
|
|
|
# N2 — PUT to a locked file without the token → 423 Locked
|
2026-06-10 22:59:06 +02:00
|
|
|
#
|
|
|
|
|
# RFC 4918 §9.10.4 + §6: a writeable resource under an
|
|
|
|
|
# exclusive lock MUST reject conflicting writes with 423
|
2026-06-16 23:17:40 +02:00
|
|
|
# Locked unless the request submits the lock token in `If:`.
|
|
|
|
|
# N2b verifies the inverse: same PUT with the correct
|
|
|
|
|
# `If: (<token>)` header succeeds, proving the gate isn't
|
|
|
|
|
# blocking legitimate updates from the lock owner.
|
2026-06-10 22:59:06 +02:00
|
|
|
# ─────────────────────────────────────────────────────────────
|
2026-06-16 23:17:40 +02:00
|
|
|
echo " N2: PUT /webdav/n-locked.txt without If:(<token>) → 423"
|
2026-06-10 22:59:06 +02:00
|
|
|
STATUS=$(dav_curl -o /dev/null -w "%{http_code}" -X PUT \
|
|
|
|
|
-H "Content-Type: text/plain" \
|
|
|
|
|
--data-binary 'tampered contents' \
|
|
|
|
|
"$DAV_BASE/n-locked.txt")
|
2026-06-16 23:17:40 +02:00
|
|
|
[[ "$STATUS" == "423" ]] \
|
|
|
|
|
|| fail "N2: expected 423 Locked for PUT to locked path without token, got $STATUS"
|
|
|
|
|
pass "N2: PUT to locked path without token → 423"
|
|
|
|
|
|
|
|
|
|
echo " N2b: PUT /webdav/n-locked.txt WITH If:(<token>) → 204"
|
|
|
|
|
STATUS=$(dav_curl -o /dev/null -w "%{http_code}" -X PUT \
|
|
|
|
|
-H "Content-Type: text/plain" \
|
|
|
|
|
-H "If: (<$LOCK_TOKEN>)" \
|
|
|
|
|
--data-binary 'authorised update' \
|
|
|
|
|
"$DAV_BASE/n-locked.txt")
|
|
|
|
|
[[ "$STATUS" == "204" ]] \
|
|
|
|
|
|| fail "N2b: expected 204 No Content for PUT with correct lock token, got $STATUS"
|
|
|
|
|
pass "N2b: PUT with matching If:(<token>) → 204"
|
2026-06-10 22:59:06 +02:00
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
# N3 — UNLOCK with token → 204; subsequent PUT succeeds
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
echo " N3: UNLOCK /webdav/n-locked.txt + follow-up PUT succeeds"
|
|
|
|
|
STATUS=$(dav_curl -o /dev/null -w "%{http_code}" -X UNLOCK \
|
|
|
|
|
-H "Lock-Token: <$LOCK_TOKEN>" \
|
|
|
|
|
"$DAV_BASE/n-locked.txt")
|
|
|
|
|
[[ "$STATUS" == "204" ]] \
|
|
|
|
|
|| fail "N3: UNLOCK expected 204, got $STATUS"
|
|
|
|
|
# Now PUT without any token — should succeed since lock is released.
|
|
|
|
|
STATUS=$(dav_curl -o /dev/null -w "%{http_code}" -X PUT \
|
|
|
|
|
-H "Content-Type: text/plain" \
|
|
|
|
|
--data-binary 'post-unlock contents' \
|
|
|
|
|
"$DAV_BASE/n-locked.txt")
|
|
|
|
|
[[ "$STATUS" == "201" || "$STATUS" == "204" ]] \
|
|
|
|
|
|| fail "N3: post-unlock PUT expected 201/204, got $STATUS"
|
|
|
|
|
pass "N3: UNLOCK → 204 + subsequent PUT succeeds ($STATUS)"
|
|
|
|
|
|
|
|
|
|
# ── summary ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
echo
|
|
|
|
|
echo "Results: $PASS passed, $FAIL failed."
|
|
|
|
|
[[ "$FAIL" -eq 0 ]] && echo "All tests passed." || exit 1
|