Files
Oxicloud/tests/webdav/test_dedup_webdav_ref_count.sh
T
Edouard Vanbelle 1e2882973b fix(test): correct due to commit 43cf4a2bg
- MKCOL is now better protected
    - Webdav now handle 201 (created) 204 (overritten)
2026-06-30 20:18:19 +02:00

242 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# =============================================================
# OxiCloud – Dedup ref_count via WebDAV (two uploads + overwrite)
# =============================================================
# Scenario:
# 1. PUT dedup-test.jpg via WebDAV as file A
# 2. PUT dedup-test-2.jpg (identical content) via WebDAV as file B
# → same blob, two distinct file records, ref_count == 2
# 3. GET /api/dedup/check/{hash} → assert ref_count == 2
# 4. Overwrite file B via WebDAV PUT with different content
# → file B now references a new blob; original ref_count drops
# 5. GET /api/dedup/check/{hash} → assert ref_count == 1
#
# BLAKE3 hash of dedup-test.jpg (== dedup-test-2.jpg — same content):
# cde1ca663a2e62e0dadb41c3194e11ecb7d971d84c7451db17063b55c09e8066
#
# Prerequisites:
# - Server running at base_url with credentials from test.env
# - OXICLOUD_ENABLE_AUTH=true (/webdav uses JWT Bearer auth)
# - jq in PATH
#
# Run (from repo root):
# bash tests/webdav/test_dedup_webdav_ref_count.sh
# =============================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
cd "$SCRIPT_DIR"
source test.env
source common.sh
# ── helpers ──────────────────────────────────────────────────────────────────
PASS=0
FAIL=0
pass() { PASS=$(( PASS + 1 )); echo " PASS: $*"; }
fail() { FAIL=$(( FAIL + 1 )); echo " FAIL: $*" >&2; exit 1; }
webdav_put() {
local remote_name="$1" local_file="$2" mime="${3:-application/octet-stream}"
curl -s -o /dev/null -w "%{http_code}" \
-X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: $mime" \
--data-binary "@$local_file" \
"$base_url/webdav/$remote_name"
}
webdav_delete() {
local remote_name="$1"
curl -s -o /dev/null -w "%{http_code}" \
-X DELETE \
-H "Authorization: Bearer $TOKEN" \
"$base_url/webdav/$remote_name"
}
rest_get() {
curl -s -H "Authorization: Bearer $TOKEN" "$base_url$1"
}
rest_delete() {
curl -s -o /dev/null -w "%{http_code}" \
-X DELETE \
-H "Authorization: Bearer $TOKEN" \
"$base_url$1"
}
dedup_check() {
curl -s -H "Authorization: Bearer $TOKEN" "$base_url/api/dedup/check/$1"
}
# ── fixtures ──────────────────────────────────────────────────────────────────
# BLAKE3 hash of dedup-test.jpg (= dedup-test-2.jpg — byte-identical content)
BLOB_HASH="cde1ca663a2e62e0dadb41c3194e11ecb7d971d84c7451db17063b55c09e8066"
FIXTURE_A="$REPO_ROOT/tests/fixtures/dedup-test.jpg"
FIXTURE_B="$REPO_ROOT/tests/fixtures/dedup-test-2.jpg"
FIXTURE_OTHER="$REPO_ROOT/tests/fixtures/oxicloud-logo.jpg"
[[ -f "$FIXTURE_A" ]] || { echo "Missing fixture: $FIXTURE_A" >&2; exit 1; }
[[ -f "$FIXTURE_B" ]] || { echo "Missing fixture: $FIXTURE_B" >&2; exit 1; }
[[ -f "$FIXTURE_OTHER" ]] || { echo "Missing fixture: $FIXTURE_OTHER" >&2; exit 1; }
FILE_A="webdav-dedup-ref-a.jpg"
FILE_B="webdav-dedup-ref-b.jpg"
echo
echo "=== Dedup ref_count: two WebDAV uploads + overwrite ==="
echo
# ── authenticate ──────────────────────────────────────────────────────────────
oxicloud_login
# ── home folder ───────────────────────────────────────────────────────────────
HOME_FOLDER_ID=$(rest_get "/api/folders" | jq -r '.[0].id')
[[ -n "$HOME_FOLDER_ID" && "$HOME_FOLDER_ID" != "null" ]] \
|| fail "Could not retrieve home folder ID"
echo " home folder id: $HOME_FOLDER_ID"
# ── idempotent pre-test cleanup ───────────────────────────────────────────────
for REMOTE in "$FILE_A" "$FILE_B"; do
EXISTING_ID=$(rest_get "/api/files?folder_id=$HOME_FOLDER_ID" \
| jq -r --arg n "$REMOTE" 'first(.[] | select(.name == $n) | .id) // empty')
if [[ -n "$EXISTING_ID" ]]; then
echo " cleanup: deleting existing $REMOTE (id=$EXISTING_ID)"
rest_delete "/api/files/$EXISTING_ID" > /dev/null
fi
STALE=$(rest_get "/api/trash/resources" \
| jq -r --arg n "$REMOTE" 'first(.items[] | select(.resource.name == $n) | .resource.id) // empty')
if [[ -n "$STALE" ]]; then
echo " cleanup: purging $REMOTE from trash (id=$STALE)"
rest_delete "/api/trash/$STALE" > /dev/null
fi
done
# ── Step 1: Upload file A ─────────────────────────────────────────────────────
# Post commit 43cf4a2b, PUT distinguishes create (201) from overwrite (204)
# per RFC 7231 §4.3.4. The wipe loop above ensures A and B are NEW resources
# here, so we expect 201. Step 5 below tests the overwrite case (expects 204).
echo " step 1: PUT $FILE_A (dedup-test.jpg)..."
STATUS=$(webdav_put "$FILE_A" "$FIXTURE_A" "image/jpeg")
[[ "$STATUS" == "201" ]] || fail "PUT $FILE_A expected 201, got $STATUS"
pass "PUT $FILE_A → 201"
# ── Step 2: Upload file B (identical content, different name) ─────────────────
# Distinct resource (new path), so PUT emits 201 even though the underlying
# blob dedup-hits. 201 vs 204 reflects URL freshness, not byte freshness.
echo " step 2: PUT $FILE_B (dedup-test-2.jpg, same bytes)..."
STATUS=$(webdav_put "$FILE_B" "$FIXTURE_B" "image/jpeg")
[[ "$STATUS" == "201" ]] || fail "PUT $FILE_B expected 201, got $STATUS"
pass "PUT $FILE_B → 201"
# ── Step 3: Resolve file IDs and assert two distinct records ──────────────────
FILE_LISTING=$(rest_get "/api/files?folder_id=$HOME_FOLDER_ID")
FILE_A_ID=$(jq -r --arg n "$FILE_A" '.[] | select(.name == $n) | .id' <<< "$FILE_LISTING")
FILE_B_ID=$(jq -r --arg n "$FILE_B" '.[] | select(.name == $n) | .id' <<< "$FILE_LISTING")
[[ -n "$FILE_A_ID" && "$FILE_A_ID" != "null" ]] || fail "File A not found in listing"
[[ -n "$FILE_B_ID" && "$FILE_B_ID" != "null" ]] || fail "File B not found in listing"
[[ "$FILE_A_ID" != "$FILE_B_ID" ]] \
|| fail "File A and B share the same ID — dedup must produce two distinct records"
pass "Two distinct file records: A=$FILE_A_ID B=$FILE_B_ID"
# ── Step 3b: server's content_hash matches our local BLAKE3 for both ─────────
# Both files were uploaded from byte-identical content — server's
# `content_hash` field (exposed via the etag-centralization refactor)
# must equal BLOB_HASH for both, proving the server's view of
# content identity agrees with our local computation.
FILE_A_HASH=$(jq -r --arg n "$FILE_A" '.[] | select(.name == $n) | .content_hash // empty' <<< "$FILE_LISTING")
FILE_B_HASH=$(jq -r --arg n "$FILE_B" '.[] | select(.name == $n) | .content_hash // empty' <<< "$FILE_LISTING")
[[ "$FILE_A_HASH" == "$BLOB_HASH" ]] \
|| fail "content_hash mismatch for A: server=$FILE_A_HASH expected=$BLOB_HASH"
[[ "$FILE_B_HASH" == "$BLOB_HASH" ]] \
|| fail "content_hash mismatch for B: server=$FILE_B_HASH expected=$BLOB_HASH"
pass "content_hash on both A and B matches local BLAKE3 ($BLOB_HASH)"
# ── Step 4: Dedup check → ref_count == 2 ─────────────────────────────────────
echo " step 4: GET /api/dedup/check/$BLOB_HASH..."
RESP=$(dedup_check "$BLOB_HASH")
EXISTS=$(jq -r '.exists' <<< "$RESP")
RC=$( jq -r '.ref_count' <<< "$RESP")
[[ "$EXISTS" == "true" ]] \
|| fail "dedup/check: expected exists=true, got $EXISTS (full response: $RESP)"
[[ "$RC" == "2" ]] \
|| fail "dedup/check: expected ref_count=2 after two identical uploads, got $RC"
pass "ref_count == 2: both files reference the same blob"
# ── Step 5: Overwrite file B with different content ───────────────────────────
# swap_blob_hash calls remove_reference on the old hash → manifest ref_count 2→1
echo " step 5: PUT $FILE_B (oxicloud-logo.jpg, new content)..."
STATUS=$(webdav_put "$FILE_B" "$FIXTURE_OTHER" "image/jpeg")
[[ "$STATUS" == "204" ]] || fail "PUT $FILE_B (overwrite) expected 204, got $STATUS"
pass "PUT $FILE_B overwrite → 204"
# ── Step 5b: B's content_hash flipped, A's unchanged ─────────────────────────
# After overwrite, B references a new blob (oxicloud-logo.jpg)
# whose BLAKE3 differs from BLOB_HASH; A still holds the original.
# A weaker but local-fixture-agnostic check than asserting B's new
# exact hash (avoids hardcoding a second BLAKE3) — proves that the
# COW-overwrite path swaps the blob identity rather than silently
# keeping the old one.
REFRESHED=$(rest_get "/api/files?folder_id=$HOME_FOLDER_ID")
FILE_A_HASH_AFTER=$(jq -r --arg n "$FILE_A" '.[] | select(.name == $n) | .content_hash // empty' <<< "$REFRESHED")
FILE_B_HASH_AFTER=$(jq -r --arg n "$FILE_B" '.[] | select(.name == $n) | .content_hash // empty' <<< "$REFRESHED")
[[ "$FILE_A_HASH_AFTER" == "$BLOB_HASH" ]] \
|| fail "File A's content_hash changed unexpectedly: $FILE_A_HASH_AFTER (overwrite of B must not touch A)"
[[ "$FILE_B_HASH_AFTER" != "$BLOB_HASH" ]] \
|| fail "File B's content_hash unchanged after overwrite — COW path didn't swap the blob"
[[ -n "$FILE_B_HASH_AFTER" ]] \
|| fail "File B has empty content_hash after overwrite — server didn't compute a new blob"
pass "post-overwrite: A still on $BLOB_HASH, B flipped to $FILE_B_HASH_AFTER"
# ── Step 6: Dedup check → ref_count == 1 ─────────────────────────────────────
# File B now references a different blob; file A still holds the original.
echo " step 6: GET /api/dedup/check/$BLOB_HASH..."
RESP=$(dedup_check "$BLOB_HASH")
EXISTS=$(jq -r '.exists' <<< "$RESP")
RC=$( jq -r '.ref_count' <<< "$RESP")
[[ "$EXISTS" == "true" ]] \
|| fail "dedup/check: expected exists=true (file A still references blob), got $EXISTS"
[[ "$RC" == "1" ]] \
|| fail "dedup/check: expected ref_count=1 after overwriting file B, got $RC"
pass "ref_count == 1: only file A still references the original blob"
# ── cleanup ───────────────────────────────────────────────────────────────────
echo " cleanup..."
for REMOTE in "$FILE_A" "$FILE_B"; do
ST=$(webdav_delete "$REMOTE")
[[ "$ST" == "204" ]] || fail "WebDAV DELETE $REMOTE expected 204, got $ST"
TRASH_ITEM=$(rest_get "/api/trash/resources" \
| jq -r --arg n "$REMOTE" 'first(.items[] | select(.resource.name == $n) | .resource.id) // empty')
if [[ -n "$TRASH_ITEM" ]]; then
rest_delete "/api/trash/$TRASH_ITEM" > /dev/null
fi
done
pass "Cleanup complete"
# ── summary ───────────────────────────────────────────────────────────────────
echo
echo "Results: $PASS passed, $FAIL failed."
[[ "$FAIL" -eq 0 ]] && echo "All tests passed." || exit 1