e3823ce470
Add an end-to-end and unit test suite for the SvelteKit frontend:
- Playwright e2e specs (tests/e2e/spa) with a throwaway container stack,
codegen scenarios, and an Istanbul-based coverage report pipeline.
- Vitest unit tests across API endpoints, components, stores and composables.
- `data-testid` hooks on interactive elements (AppShell, FileViewer,
ShareDialog, search, photos, files breadcrumbs, login/Nextcloud flows,
public share pages) so the e2e suite can target them deterministically.
- Serve the SPA app-shell CSP from a <meta> policy (svelte.config.js) plus a
middleware that skips the CSP header on HTML; move the Nextcloud Login Flow
v2 grant page to the SvelteKit /nextcloud/login route.
- `just front-codegen` recipe and start-server-spa.sh harness.
Make the test environment robust and consistent:
- Install a deterministic in-memory localStorage/sessionStorage in the Vitest
setup so storage behaves identically across Node versions (Node 26 ships a
native Web Storage global that otherwise shadows jsdom's).
- Pin devenv to Node 26 + PostgreSQL 18 and pin every CI job to Node 26.3.0
so the dev shell and CI run the same toolchain versions.
Repair the API/WebDAV (hurl) suite, which had drifted from the backend:
- Migrate the removed `/api/folders/{id}/listing` endpoint to `/resources`
(cursor-paginated `{items:[{resource_type,resource}]}` shape) across the
batch-copy, grants, nested-group, and WebDAV NC tests + the dav_helpers
wipe routine.
- Stop photos_etag from uploading the dedup-tracked fixture so the dedup
blob-lifecycle test can own its content-addressed blob exclusively.
- dedup_create now asserts the idempotent same-content re-upload (201 +
existing file id) instead of the stale 409 expectation.
Generated coverage reports, nyc output and the e2e server runtime data dir
are gitignored rather than committed.
383 lines
21 KiB
Bash
Executable File
383 lines
21 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# =============================================================
|
|
# OxiCloud — Baseline: NC WebDAV PUT / MKCOL + BLAKE3 round-trip
|
|
# =============================================================
|
|
# Group F from BASELINE_TESTS_NC_WEBDAV.md.
|
|
#
|
|
# Headline assertions:
|
|
# F8 / F9 — local b3sum of the uploaded bytes equals the
|
|
# server's FileDto.content_hash retrieved via the
|
|
# REST API. Proves the hash-on-write streaming path
|
|
# actually produces the canonical BLAKE3 — the
|
|
# value every downstream dedup / lifecycle hook
|
|
# keys on. This is the load-bearing check that
|
|
# would have caught the `etag` vs `content_hash`
|
|
# confusion (the 0135930d regression).
|
|
#
|
|
# Pinned behaviour notes (not bugs, but worth catching if any
|
|
# change in either direction):
|
|
# F5 / F6 — NC PUT does NOT process If-None-Match / If-Match
|
|
# today. Both succeed with 201/204 instead of the
|
|
# strict-RFC-4918 412. If a later commit adds
|
|
# conditional support, the assertions here will
|
|
# trip and you can update them deliberately.
|
|
# F1 — NC PUT returns ETag + oc-etag headers but NO
|
|
# oc-fileid header (the file id is discoverable
|
|
# via PROPFIND or REST). Pinned as "absent".
|
|
# =============================================================
|
|
|
|
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 "=== NC WebDAV PUT / MKCOL + BLAKE3 (Group F baseline) ==="
|
|
echo
|
|
|
|
oxicloud_login
|
|
mint_app_password
|
|
resolve_home_folder_id
|
|
wipe_home_folder # defensive against cross-test contamination
|
|
|
|
NC_FILES_BASE="$base_url/remote.php/dav/files/$username"
|
|
|
|
PUT_FIXTURE_DIR=$(mktemp -d)
|
|
trap 'rm -rf "$PUT_FIXTURE_DIR"' EXIT
|
|
|
|
# ── Pre-flight: confirm `b3sum` is available ─────────────────────────────────
|
|
command -v b3sum >/dev/null 2>&1 \
|
|
|| fail "preflight: b3sum required for F8/F9 — install via 'brew install b3sum' or 'apt install b3sum'"
|
|
|
|
# ── Helper: extract the first ETag value from a curl `-D -` dump ─────────────
|
|
header_value() {
|
|
grep -i "^$1:" <<< "$2" | awk '{print $2}' | tr -d '\r"' | head -n 1
|
|
}
|
|
|
|
# ── Helper: list home folder, find file by name, capture id + content_hash ───
|
|
#
|
|
# Uses the cursor-paginated `/resources` listing (the `/listing` and
|
|
# `/contents` endpoints were removed). `?resource_types=file` returns only
|
|
# file rows as `{ items: [{ resource_type, resource }], next_cursor }`, so
|
|
# the file DTO (id + content_hash) is at `.items[].resource`.
|
|
nc_lookup_via_rest() {
|
|
local name="$1"
|
|
local response
|
|
response=$(api_curl "$base_url/api/folders/$HOME_FOLDER_ID/resources?resource_types=file&limit=200")
|
|
LAST_FILE_ID=$(jq -r --arg n "$name" '.items[]?.resource | select(.name == $n) | .id' <<< "$response")
|
|
LAST_FILE_CONTENT_HASH=$(jq -r --arg n "$name" '.items[]?.resource | select(.name == $n) | .content_hash' <<< "$response")
|
|
[[ -n "$LAST_FILE_ID" && "$LAST_FILE_ID" != "null" ]] \
|
|
|| fail "REST lookup for '$name' in home folder returned no id (response: $response)"
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# F1 — PUT a new file → 201 + ETag + oc-etag
|
|
# ─────────────────────────────────────────────────────────────
|
|
echo " F1: PUT new file → 201"
|
|
SMALL_CONTENT="hello from group F"
|
|
SMALL_PATH="$PUT_FIXTURE_DIR/f1-small.txt"
|
|
printf '%s' "$SMALL_CONTENT" > "$SMALL_PATH"
|
|
SMALL_LEN=$(wc -c < "$SMALL_PATH" | tr -d ' ')
|
|
|
|
HEADERS=$(nc_curl -D - -o /dev/null -X PUT \
|
|
-H "Content-Type: text/plain" \
|
|
--data-binary "@$SMALL_PATH" \
|
|
"$NC_FILES_BASE/f1-small.txt")
|
|
STATUS=$(awk 'NR==1{print $2}' <<< "$HEADERS" | tr -d '\r')
|
|
[[ "$STATUS" == "201" ]] \
|
|
|| fail "F1: PUT new expected 201, got $STATUS"
|
|
F1_ETAG=$(header_value "etag" "$HEADERS")
|
|
F1_OC_ETAG=$(header_value "oc-etag" "$HEADERS")
|
|
[[ -n "$F1_ETAG" ]] || fail "F1: response missing ETag header"
|
|
[[ -n "$F1_OC_ETAG" ]] || fail "F1: response missing oc-etag header"
|
|
[[ "$F1_ETAG" == "$F1_OC_ETAG" ]] \
|
|
|| fail "F1: ETag ($F1_ETAG) and oc-etag ($F1_OC_ETAG) should match"
|
|
# Pin "no oc-fileid header" current behaviour.
|
|
grep -qi '^oc-fileid:' <<< "$HEADERS" \
|
|
&& fail "F1: oc-fileid header is now present — pin needs updating"
|
|
pass "F1: PUT new → 201 + matching ETag/oc-etag, no oc-fileid header"
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# F2 — GET retrieves the bytes we just PUT
|
|
# ─────────────────────────────────────────────────────────────
|
|
echo " F2: GET file just PUT"
|
|
ACTUAL=$(nc_curl "$NC_FILES_BASE/f1-small.txt")
|
|
[[ "$ACTUAL" == "$SMALL_CONTENT" ]] \
|
|
|| fail "F2: body mismatch — got '$ACTUAL', expected '$SMALL_CONTENT'"
|
|
pass "F2: GET returns exact bytes from F1's PUT"
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# F3 — PUT overwrite same path → 204 + NEW ETag
|
|
# ─────────────────────────────────────────────────────────────
|
|
echo " F3: PUT overwrite → 204 + new ETag"
|
|
NEW_CONTENT="goodbye from group F"
|
|
NEW_PATH="$PUT_FIXTURE_DIR/f3-overwrite.txt"
|
|
printf '%s' "$NEW_CONTENT" > "$NEW_PATH"
|
|
|
|
HEADERS=$(nc_curl -D - -o /dev/null -X PUT \
|
|
-H "Content-Type: text/plain" \
|
|
--data-binary "@$NEW_PATH" \
|
|
"$NC_FILES_BASE/f1-small.txt")
|
|
STATUS=$(awk 'NR==1{print $2}' <<< "$HEADERS" | tr -d '\r')
|
|
[[ "$STATUS" == "204" ]] \
|
|
|| fail "F3: PUT overwrite expected 204, got $STATUS"
|
|
F3_ETAG=$(header_value "etag" "$HEADERS")
|
|
[[ -n "$F3_ETAG" ]] || fail "F3: overwrite response missing ETag"
|
|
[[ "$F3_ETAG" != "$F1_ETAG" ]] \
|
|
|| fail "F3: ETag must change on overwrite ($F1_ETAG → $F3_ETAG)"
|
|
pass "F3: PUT overwrite → 204 + new ETag (different from F1)"
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# F4 — GET after overwrite returns new content (cache regression
|
|
# guard — see commit f4ce4092)
|
|
# ─────────────────────────────────────────────────────────────
|
|
echo " F4: GET after overwrite returns NEW content"
|
|
ACTUAL=$(nc_curl "$NC_FILES_BASE/f1-small.txt")
|
|
[[ "$ACTUAL" == "$NEW_CONTENT" ]] \
|
|
|| fail "F4: STALE content after overwrite — got '$ACTUAL', expected '$NEW_CONTENT' (regression of f4ce4092?)"
|
|
pass "F4: GET after overwrite serves the new bytes (no stale-cache)"
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# F5 / F6 — Conditional PUT (RFC 7232 §3.1/§3.2, RFC 4918 §10)
|
|
#
|
|
# F5 covers `If-None-Match: *`: server MUST refuse the PUT with
|
|
# 412 when the target representation already exists (used by
|
|
# clients to do "create only if absent"). The mirror case — same
|
|
# header on a NEW path — must succeed; covered by F5b.
|
|
#
|
|
# F6 covers `If-Match: "<etag>"`: server MUST refuse the PUT with
|
|
# 412 when the supplied ETag doesn't strong-match the current
|
|
# representation (used by clients to do "update only if
|
|
# unchanged"). The mirror case — correct ETag → success — is
|
|
# covered by F6b.
|
|
# ─────────────────────────────────────────────────────────────
|
|
echo " F5: PUT with If-None-Match: * on existing path → 412"
|
|
HEADERS=$(nc_curl -D - -o /dev/null -X PUT \
|
|
-H "If-None-Match: *" -H "Content-Type: text/plain" \
|
|
--data-binary 'F5-payload' \
|
|
"$NC_FILES_BASE/f1-small.txt")
|
|
STATUS=$(awk 'NR==1{print $2}' <<< "$HEADERS" | tr -d '\r')
|
|
[[ "$STATUS" == "412" ]] \
|
|
|| fail "F5: expected 412 Precondition Failed for If-None-Match: * on existing path, got $STATUS"
|
|
pass "F5: If-None-Match: * on existing path → 412"
|
|
|
|
echo " F5b: PUT with If-None-Match: * on NEW path → 201/204"
|
|
HEADERS=$(nc_curl -D - -o /dev/null -X PUT \
|
|
-H "If-None-Match: *" -H "Content-Type: text/plain" \
|
|
--data-binary 'F5b-payload' \
|
|
"$NC_FILES_BASE/f5b-new.txt")
|
|
STATUS=$(awk 'NR==1{print $2}' <<< "$HEADERS" | tr -d '\r')
|
|
[[ "$STATUS" == "201" || "$STATUS" == "204" ]] \
|
|
|| fail "F5b: expected 201/204 for If-None-Match: * on new path, got $STATUS"
|
|
pass "F5b: If-None-Match: * on new path → $STATUS"
|
|
|
|
echo " F6: PUT with If-Match: \"wrong-etag\" → 412"
|
|
HEADERS=$(nc_curl -D - -o /dev/null -X PUT \
|
|
-H 'If-Match: "deadbeef-never-matches"' -H "Content-Type: text/plain" \
|
|
--data-binary 'F6-payload' \
|
|
"$NC_FILES_BASE/f1-small.txt")
|
|
STATUS=$(awk 'NR==1{print $2}' <<< "$HEADERS" | tr -d '\r')
|
|
[[ "$STATUS" == "412" ]] \
|
|
|| fail "F6: expected 412 Precondition Failed for non-matching If-Match, got $STATUS"
|
|
pass "F6: If-Match with non-matching ETag → 412"
|
|
|
|
echo " F6b: PUT with correct If-Match → 204"
|
|
# Fetch the current ETag of f1-small.txt via PROPFIND-ish HEAD,
|
|
# then re-PUT with that exact value as If-Match. Must succeed.
|
|
CURRENT_ETAG=$(nc_curl -D - -o /dev/null -X HEAD "$NC_FILES_BASE/f1-small.txt" \
|
|
| awk 'BEGIN{IGNORECASE=1} /^etag:/ {print $2}' | tr -d '\r')
|
|
[[ -n "$CURRENT_ETAG" ]] || fail "F6b: could not read current ETag via HEAD"
|
|
HEADERS=$(nc_curl -D - -o /dev/null -X PUT \
|
|
-H "If-Match: $CURRENT_ETAG" -H "Content-Type: text/plain" \
|
|
--data-binary 'F6b-payload' \
|
|
"$NC_FILES_BASE/f1-small.txt")
|
|
STATUS=$(awk 'NR==1{print $2}' <<< "$HEADERS" | tr -d '\r')
|
|
[[ "$STATUS" == "204" ]] \
|
|
|| fail "F6b: expected 204 for If-Match with correct ETag, got $STATUS"
|
|
pass "F6b: If-Match with current ETag → 204"
|
|
|
|
echo " F6c: PUT with If-Match: * on existing path → 204 (catch-all)"
|
|
HEADERS=$(nc_curl -D - -o /dev/null -X PUT \
|
|
-H 'If-Match: *' -H "Content-Type: text/plain" \
|
|
--data-binary 'F6c-payload' \
|
|
"$NC_FILES_BASE/f1-small.txt")
|
|
STATUS=$(awk 'NR==1{print $2}' <<< "$HEADERS" | tr -d '\r')
|
|
[[ "$STATUS" == "204" ]] \
|
|
|| fail "F6c: expected 204 for If-Match: * on existing path, got $STATUS"
|
|
pass "F6c: If-Match: * on existing path → 204"
|
|
|
|
echo " F6d: PUT with If-Match on NEW path → 412 (resource absent → cannot match)"
|
|
HEADERS=$(nc_curl -D - -o /dev/null -X PUT \
|
|
-H 'If-Match: "anything"' -H "Content-Type: text/plain" \
|
|
--data-binary 'F6d-payload' \
|
|
"$NC_FILES_BASE/f6d-new.txt")
|
|
STATUS=$(awk 'NR==1{print $2}' <<< "$HEADERS" | tr -d '\r')
|
|
[[ "$STATUS" == "412" ]] \
|
|
|| fail "F6d: expected 412 for If-Match on absent path, got $STATUS"
|
|
pass "F6d: If-Match on absent path → 412"
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# F7 — PUT a "large" file → succeeds, GET returns exact bytes
|
|
#
|
|
# Size is 3 MiB, deliberately just under `OXICLOUD_DIRECT_PUT_MAX_BYTES`
|
|
# (4 MiB in the test env — see `tests/common/server.env`). Files
|
|
# above that cap are expected to use the chunked-upload protocol,
|
|
# which is Group J territory. 3 MiB is still big enough to exercise
|
|
# the streaming-spool / hash-on-write path that F9 then validates
|
|
# end-to-end via the BLAKE3 round-trip. The BASELINE doc said "10 MB"
|
|
# but the test env constraint takes precedence.
|
|
# ─────────────────────────────────────────────────────────────
|
|
echo " F7: PUT 3 MiB random binary → 201/204 + GET returns same bytes"
|
|
LARGE_PATH="$PUT_FIXTURE_DIR/f7-large.bin"
|
|
dd if=/dev/urandom of="$LARGE_PATH" bs=1024 count=3072 status=none
|
|
LARGE_LEN=$(wc -c < "$LARGE_PATH" | tr -d ' ')
|
|
LARGE_LOCAL_HASH=$(b3sum --no-names "$LARGE_PATH" | awk '{print $1}')
|
|
|
|
# Disable `Expect: 100-continue` — curl sends it for large bodies,
|
|
# and the resulting interim "HTTP/1.1 100 Continue" line would be
|
|
# the FIRST line in the `-D -` dump, making `awk 'NR==1'` pick up
|
|
# 100 instead of the final 201/204. The Expect handshake serves
|
|
# no functional purpose for the test.
|
|
HEADERS=$(nc_curl -D - -o /dev/null -X PUT \
|
|
-H "Expect:" \
|
|
-H "Content-Type: application/octet-stream" \
|
|
--data-binary "@$LARGE_PATH" \
|
|
"$NC_FILES_BASE/f7-large.bin")
|
|
STATUS=$(awk 'NR==1{print $2}' <<< "$HEADERS" | tr -d '\r')
|
|
[[ "$STATUS" == "201" || "$STATUS" == "204" ]] \
|
|
|| fail "F7: PUT 3 MiB expected 201/204, got $STATUS"
|
|
DOWNLOADED="$PUT_FIXTURE_DIR/f7-large.downloaded"
|
|
nc_curl -o "$DOWNLOADED" "$NC_FILES_BASE/f7-large.bin"
|
|
cmp -s "$LARGE_PATH" "$DOWNLOADED" \
|
|
|| fail "F7: downloaded bytes differ from uploaded — streaming integrity broken"
|
|
pass "F7: 3 MiB streamed PUT round-trips byte-identically"
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# F8 — BLAKE3 round-trip (small file)
|
|
#
|
|
# Uses a dedicated path (`f8-blake3-probe.txt`) that no other
|
|
# scenario in this script touches. F1-F6 all overwrite
|
|
# `f1-small.txt` repeatedly, so by the time F8 runs the server
|
|
# holds whatever F6's last PUT wrote (`F6-payload`), not what
|
|
# F3 wrote — comparing F3's local b3sum against the server
|
|
# would be a false mismatch. A fresh single-write fixture
|
|
# isolates the BLAKE3 round-trip from the F1-F6 sequence.
|
|
#
|
|
# Verifies the streaming hash-on-write path produced the
|
|
# canonical BLAKE3 the dedup/lifecycle layer expects.
|
|
# ─────────────────────────────────────────────────────────────
|
|
echo " F8: BLAKE3 round-trip (small file — local b3sum vs server content_hash)"
|
|
F8_PATH="$PUT_FIXTURE_DIR/f8-probe.txt"
|
|
printf 'f8 blake3 round-trip probe — single write, known bytes' > "$F8_PATH"
|
|
F8_LOCAL_HASH=$(b3sum --no-names "$F8_PATH" | awk '{print $1}')
|
|
nc_curl -X PUT -H "Content-Type: text/plain" \
|
|
--data-binary "@$F8_PATH" \
|
|
"$NC_FILES_BASE/f8-blake3-probe.txt" > /dev/null
|
|
nc_lookup_via_rest "f8-blake3-probe.txt"
|
|
[[ -n "$LAST_FILE_CONTENT_HASH" && "$LAST_FILE_CONTENT_HASH" != "null" ]] \
|
|
|| fail "F8: REST returned empty content_hash for f8-blake3-probe.txt"
|
|
[[ "$LAST_FILE_CONTENT_HASH" == "$F8_LOCAL_HASH" ]] \
|
|
|| fail "F8: BLAKE3 mismatch — server '$LAST_FILE_CONTENT_HASH' vs local '$F8_LOCAL_HASH'"
|
|
pass "F8: small-file content_hash matches local b3sum ($F8_LOCAL_HASH)"
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# F9 — BLAKE3 round-trip (streamed file)
|
|
#
|
|
# Same check on the streaming hash-on-write path. The 3 MiB
|
|
# upload from F7 exercises the streaming spool /
|
|
# hasher.update / final blob promotion sequence — F8 only
|
|
# validates the small-buffer path. Size was 10 MB in the
|
|
# BASELINE doc; reduced to 3 MiB so it stays under the test
|
|
# env's direct-PUT cap (see F7 comment).
|
|
# ─────────────────────────────────────────────────────────────
|
|
echo " F9: BLAKE3 round-trip (3 MiB streamed file)"
|
|
nc_lookup_via_rest "f7-large.bin"
|
|
[[ -n "$LAST_FILE_CONTENT_HASH" && "$LAST_FILE_CONTENT_HASH" != "null" ]] \
|
|
|| fail "F9: REST returned empty content_hash for f7-large.bin"
|
|
[[ "$LAST_FILE_CONTENT_HASH" == "$LARGE_LOCAL_HASH" ]] \
|
|
|| fail "F9: BLAKE3 mismatch on 3 MiB — server '$LAST_FILE_CONTENT_HASH' vs local '$LARGE_LOCAL_HASH'"
|
|
pass "F9: 3 MiB streamed content_hash matches local b3sum"
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# F10 — MKCOL creates a folder → 201
|
|
# ─────────────────────────────────────────────────────────────
|
|
echo " F10: MKCOL new folder → 201"
|
|
STATUS=$(nc_curl -o /dev/null -w "%{http_code}" -X MKCOL "$NC_FILES_BASE/f10-folder/")
|
|
[[ "$STATUS" == "201" ]] \
|
|
|| fail "F10: MKCOL new expected 201, got $STATUS"
|
|
# PROPFIND it to confirm
|
|
BODY=$(nc_curl -X PROPFIND -H "Depth: 0" "$NC_FILES_BASE/f10-folder/")
|
|
grep -q '<d:collection/>' <<< "$BODY" \
|
|
|| fail "F10: PROPFIND of just-created folder lacks <d:collection/>"
|
|
pass "F10: MKCOL creates folder, PROPFIND sees it as a collection"
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# F11 / F11b / F11c — MKCOL parent semantics (RFC 4918 §9.3.1)
|
|
#
|
|
# F11 : missing intermediate parent → 409 Conflict
|
|
# F11b : parent exists, target new → 201 Created (positive case)
|
|
# F11c : target already exists → 405 Method Not Allowed
|
|
#
|
|
# Sabre/DAV and the actual NC server both 409 on a missing
|
|
# intermediate; our previous `mkdir -p` behaviour deviated. NC
|
|
# desktop walks ancestors one MKCOL at a time during sync so
|
|
# nothing real breaks from dropping the auto-create.
|
|
# ─────────────────────────────────────────────────────────────
|
|
echo " F11: MKCOL with missing intermediate parent → 409"
|
|
STATUS=$(nc_curl -o /dev/null -w "%{http_code}" -X MKCOL \
|
|
"$NC_FILES_BASE/f11-nonexistent-parent/inner/")
|
|
[[ "$STATUS" == "409" ]] \
|
|
|| fail "F11: expected 409 Conflict for MKCOL with missing parent, got $STATUS"
|
|
# The non-existent parent must NOT have been auto-created either.
|
|
[[ "$(nc_status_propfind_depth0 "$NC_FILES_BASE/f11-nonexistent-parent/")" == "404" ]] \
|
|
|| fail "F11: intermediate parent was silently created — auto-create still happening"
|
|
pass "F11: MKCOL with missing parent → 409, parent not silently created"
|
|
|
|
echo " F11b: MKCOL with existing parent + new target → 201"
|
|
nc_curl -o /dev/null -X MKCOL "$NC_FILES_BASE/f11b-parent/" > /dev/null
|
|
STATUS=$(nc_curl -o /dev/null -w "%{http_code}" -X MKCOL \
|
|
"$NC_FILES_BASE/f11b-parent/child/")
|
|
[[ "$STATUS" == "201" ]] \
|
|
|| fail "F11b: expected 201 Created for MKCOL with existing parent, got $STATUS"
|
|
[[ "$(nc_status_propfind_depth0 "$NC_FILES_BASE/f11b-parent/child/")" == "207" ]] \
|
|
|| fail "F11b: target collection not visible via PROPFIND after MKCOL"
|
|
pass "F11b: MKCOL with existing parent → 201, target reachable"
|
|
|
|
echo " F11c: MKCOL with target that already exists → 405"
|
|
STATUS=$(nc_curl -o /dev/null -w "%{http_code}" -X MKCOL \
|
|
"$NC_FILES_BASE/f11b-parent/child/")
|
|
[[ "$STATUS" == "405" ]] \
|
|
|| fail "F11c: expected 405 Method Not Allowed for MKCOL on existing collection, got $STATUS"
|
|
pass "F11c: MKCOL on existing target → 405"
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# F12 — MKCOL on existing folder → 405
|
|
# ─────────────────────────────────────────────────────────────
|
|
echo " F12: MKCOL on existing folder → 405"
|
|
STATUS=$(nc_curl -o /dev/null -w "%{http_code}" -X MKCOL "$NC_FILES_BASE/f10-folder/")
|
|
[[ "$STATUS" == "405" ]] \
|
|
|| fail "F12: existing-folder MKCOL expected 405, got $STATUS"
|
|
pass "F12: MKCOL on existing folder → 405"
|
|
|
|
# ── Cleanup ──────────────────────────────────────────────────────────────────
|
|
|
|
echo " cleanup: delete fixtures + empty trash"
|
|
# Use the NC DELETE (covered in group G) to round-trip through the
|
|
# same surface we're trying to baseline.
|
|
nc_curl -o /dev/null -X DELETE "$NC_FILES_BASE/f1-small.txt" || true
|
|
nc_curl -o /dev/null -X DELETE "$NC_FILES_BASE/f7-large.bin" || true
|
|
nc_curl -o /dev/null -X DELETE "$NC_FILES_BASE/f8-blake3-probe.txt" || true
|
|
nc_curl -o /dev/null -X DELETE "$NC_FILES_BASE/f10-folder/" || true
|
|
api_empty_trash || true
|
|
pass "cleanup done"
|
|
|
|
# ── summary ───────────────────────────────────────────────────────────────────
|
|
|
|
echo
|
|
echo "Results: $PASS passed, $FAIL failed."
|
|
[[ "$FAIL" -eq 0 ]] && echo "All tests passed." || exit 1
|