From 5a18ec4bac55f85f5bef9fa51e84919b1026541d Mon Sep 17 00:00:00 2001 From: Edouard Vanbelle Date: Sat, 6 Jun 2026 17:21:44 +0200 Subject: [PATCH] test(hash_content): check that hash matches --- tests/api/dedup_blob_cleanup.hurl | 10 ++++++ tests/webdav/test_chunked_upload_dedup.sh | 12 +++++++ tests/webdav/test_dedup_webdav_multichunk.sh | 16 ++++++++++ tests/webdav/test_dedup_webdav_ref_count.sh | 33 ++++++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/tests/api/dedup_blob_cleanup.hurl b/tests/api/dedup_blob_cleanup.hurl index c2563c34..63849750 100644 --- a/tests/api/dedup_blob_cleanup.hurl +++ b/tests/api/dedup_blob_cleanup.hurl @@ -90,6 +90,12 @@ file1_id: jsonpath "$.id" [Asserts] jsonpath "$.name" == "dedup-test.jpg" jsonpath "$.folder_id" == {{test_folder_id}} +# Cross-check that the server's view of the uploaded content matches +# the BLAKE3 we computed locally over fixtures/dedup-test.jpg. The +# `content_hash` field is the raw blob hash, distinct from `etag` +# (which folds in modified_at) — exposed in REST JSON by the +# etag-centralization refactor. +jsonpath "$.content_hash" == "cde1ca663a2e62e0dadb41c3194e11ecb7d971d84c7451db17063b55c09e8066" # ref_count == 1: blob has exactly one file reference after first upload @@ -118,6 +124,10 @@ file2_id: jsonpath "$.id" [Asserts] jsonpath "$.name" == "dedup-test-2.jpg" jsonpath "$.id" != "{{file1_id}}" +# Same content as fixtures/dedup-test.jpg → identical content_hash. +# This is the actual "dedup happened" assertion at the API surface, +# independent of the /api/dedup/check probe below. +jsonpath "$.content_hash" == "cde1ca663a2e62e0dadb41c3194e11ecb7d971d84c7451db17063b55c09e8066" # ref_count == 2: dedup hit — same blob now referenced by two file records diff --git a/tests/webdav/test_chunked_upload_dedup.sh b/tests/webdav/test_chunked_upload_dedup.sh index 51094cf1..c9cd10d7 100755 --- a/tests/webdav/test_chunked_upload_dedup.sh +++ b/tests/webdav/test_chunked_upload_dedup.sh @@ -146,6 +146,18 @@ MIME=$(jq -r '.mime_type' <<< "$LISTED_FILE") || fail "Expected MIME type video/mp4, got: $MIME" pass "File listed with MIME type: $MIME" +# ── Step 4b: server's content_hash matches our local BLAKE3 ────────────────── +# Cross-check the file we just uploaded: the server's view of its +# content identity (FileDto.content_hash, exposed in REST JSON since +# the etag-centralization refactor) must equal the BLAKE3 we know +# the fixture has. Catches any chunk-assembly bug that would +# silently produce a different blob than the source bytes. + +LISTED_HASH=$(jq -r '.content_hash // empty' <<< "$LISTED_FILE") +[[ "$LISTED_HASH" == "$BLOB_HASH" ]] \ + || fail "content_hash mismatch: server=$LISTED_HASH expected=$BLOB_HASH" +pass "content_hash matches local BLAKE3 ($BLOB_HASH)" + # ── Step 5: Dedup check → ref_count == 1 ───────────────────────────────────── echo " step 5: GET /api/dedup/check/$BLOB_HASH..." diff --git a/tests/webdav/test_dedup_webdav_multichunk.sh b/tests/webdav/test_dedup_webdav_multichunk.sh index 967124a8..dd742afc 100755 --- a/tests/webdav/test_dedup_webdav_multichunk.sh +++ b/tests/webdav/test_dedup_webdav_multichunk.sh @@ -141,6 +141,22 @@ FILE_B_ID=$(jq -r --arg n "$FILE_B" '.[] | select(.name == $n) | .id' <<< "$LIST || fail "File A and B share the same ID — dedup must create two distinct records" pass "Two distinct file records: A=$FILE_A_ID B=$FILE_B_ID" +# ── Step 2b: server's content_hash matches our local BLAKE3 for both ───────── +# Both files were uploaded from byte-identical bytes, so the server +# MUST report the same content_hash for both — and that hash MUST +# equal the BLAKE3 we computed locally. Without this check, a +# subtle CDC-assembly bug could produce two distinct blobs that +# happen to map to the same dedup key but differ from the source — +# the ref_count assertions below would still pass. + +FILE_A_HASH=$(jq -r --arg n "$FILE_A" '.[] | select(.name == $n) | .content_hash // empty' <<< "$LISTING") +FILE_B_HASH=$(jq -r --arg n "$FILE_B" '.[] | select(.name == $n) | .content_hash // empty' <<< "$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 3: ref_count == 2 ──────────────────────────────────────────────────── echo " step 3: dedup/check → expect ref_count=2..." diff --git a/tests/webdav/test_dedup_webdav_ref_count.sh b/tests/webdav/test_dedup_webdav_ref_count.sh index 7a87762d..e1bf400a 100755 --- a/tests/webdav/test_dedup_webdav_ref_count.sh +++ b/tests/webdav/test_dedup_webdav_ref_count.sh @@ -147,6 +147,20 @@ FILE_B_ID=$(jq -r --arg n "$FILE_B" '.[] | select(.name == $n) | .id' <<< "$FILE || 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..." @@ -168,6 +182,25 @@ 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.