276 lines
9.5 KiB
Plaintext
276 lines
9.5 KiB
Plaintext
|
|
# =============================================================
|
|||
|
|
# OxiCloud – An UPLOADED thumbnail survives both copy paths
|
|||
|
|
# =============================================================
|
|||
|
|
# A user-supplied preview is not derivable from the file's content, so
|
|||
|
|
# nothing can regenerate it. If a copy loses it, it is gone — and the loss
|
|||
|
|
# is silent, because the server quietly falls back to rendering one from
|
|||
|
|
# the source (or to 204 for a PDF, which has no render path at all).
|
|||
|
|
#
|
|||
|
|
# That was the behaviour before `storage.file_attached_blobs`: the PUT
|
|||
|
|
# wrote `ext-{file_id}.jpg`, keyed by file id, which no copy path
|
|||
|
|
# duplicates and no other instance can see.
|
|||
|
|
#
|
|||
|
|
# The test distinguishes "preserved" from "re-rendered" by making the two
|
|||
|
|
# visibly different: the FILE is red-image.png, the uploaded thumbnail is
|
|||
|
|
# derived from green-image.png. A server-side render of the file could
|
|||
|
|
# only ever produce the red one. So byte-equality with the post-upload
|
|||
|
|
# bytes proves the copy served the ATTACHMENT, not a fresh render.
|
|||
|
|
#
|
|||
|
|
# Step 4 is what makes that airtight — it captures the rendered thumbnail
|
|||
|
|
# BEFORE the upload and requires the upload to change it. Without that,
|
|||
|
|
# byte-equality across copies could be satisfied by three identical
|
|||
|
|
# renders.
|
|||
|
|
#
|
|||
|
|
# Prerequisites: setup.hurl must have run (admin user exists).
|
|||
|
|
# =============================================================
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
# Step 1 – Login
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
POST {{base_url}}/api/auth/login
|
|||
|
|
Content-Type: application/json
|
|||
|
|
{
|
|||
|
|
"username": "{{username}}",
|
|||
|
|
"password": "{{password}}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
HTTP 200
|
|||
|
|
[Captures]
|
|||
|
|
token: jsonpath "$.access_token"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
# Step 2 – Source and destination folders
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
POST {{base_url}}/api/folders
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
Content-Type: application/json
|
|||
|
|
{
|
|||
|
|
"name": "hurl-attach-src"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
HTTP 201
|
|||
|
|
[Captures]
|
|||
|
|
src_folder_id: jsonpath "$.id"
|
|||
|
|
|
|||
|
|
|
|||
|
|
POST {{base_url}}/api/folders
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
Content-Type: application/json
|
|||
|
|
{
|
|||
|
|
"name": "hurl-attach-dst"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
HTTP 201
|
|||
|
|
[Captures]
|
|||
|
|
dst_folder_id: jsonpath "$.id"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
# Step 3 – Upload the file (RED)
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
POST {{base_url}}/api/files/upload
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
[MultipartFormData]
|
|||
|
|
folder_id: {{src_folder_id}}
|
|||
|
|
file: file,fixtures/red-image.png; image/png
|
|||
|
|
|
|||
|
|
HTTP 201
|
|||
|
|
[Captures]
|
|||
|
|
orig_file_id: jsonpath "$.id"
|
|||
|
|
orig_file_name: jsonpath "$.name"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
# Step 4 – The server-rendered thumbnail, before any upload.
|
|||
|
|
# Captured so the upload can be shown to have replaced it.
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
GET {{base_url}}/api/files/{{orig_file_id}}/thumbnail/preview
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
|
|||
|
|
HTTP 200
|
|||
|
|
[Captures]
|
|||
|
|
rendered_thumb: bytes
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
# Step 5 – Upload a custom thumbnail (GREEN) for that file
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
PUT {{base_url}}/api/files/{{orig_file_id}}/thumbnail/preview
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
Content-Type: image/png
|
|||
|
|
file,fixtures/green-image.png;
|
|||
|
|
|
|||
|
|
HTTP 201
|
|||
|
|
|
|||
|
|
|
|||
|
|
# It must now serve the upload, not the render.
|
|||
|
|
GET {{base_url}}/api/files/{{orig_file_id}}/thumbnail/preview
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
|
|||
|
|
HTTP 200
|
|||
|
|
[Captures]
|
|||
|
|
uploaded_thumb: bytes
|
|||
|
|
[Asserts]
|
|||
|
|
bytes != {{rendered_thumb}}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
# Step 6 – Single-file copy → the attachment comes with it.
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
POST {{base_url}}/api/batch/files/copy
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
Content-Type: application/json
|
|||
|
|
{
|
|||
|
|
"file_ids": ["{{orig_file_id}}"],
|
|||
|
|
"target_folder_id": "{{dst_folder_id}}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
HTTP 200
|
|||
|
|
[Captures]
|
|||
|
|
file_copy_id: jsonpath "$.successful[0].id"
|
|||
|
|
[Asserts]
|
|||
|
|
jsonpath "$.successful[0].id" != "{{orig_file_id}}"
|
|||
|
|
|
|||
|
|
|
|||
|
|
GET {{base_url}}/api/files/{{file_copy_id}}/thumbnail/preview
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
|
|||
|
|
HTTP 200
|
|||
|
|
[Asserts]
|
|||
|
|
bytes == {{uploaded_thumb}}
|
|||
|
|
bytes != {{rendered_thumb}}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
# Step 7 – Folder copy → same, through storage.copy_folder_tree.
|
|||
|
|
#
|
|||
|
|
# The other copy path. It reaches the attachment through the same
|
|||
|
|
# `copy_file_satellites` call, and this is the leg that would break if
|
|||
|
|
# the tree path ever grew its own fan-out again.
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
POST {{base_url}}/api/batch/folders/copy
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
Content-Type: application/json
|
|||
|
|
{
|
|||
|
|
"folder_ids": ["{{src_folder_id}}"],
|
|||
|
|
"target_folder_id": "{{dst_folder_id}}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
HTTP 200
|
|||
|
|
[Captures]
|
|||
|
|
tree_root_id: jsonpath "$.successful[0].new_root_folder_id"
|
|||
|
|
[Asserts]
|
|||
|
|
jsonpath "$.stats.failed" == 0
|
|||
|
|
|
|||
|
|
|
|||
|
|
GET {{base_url}}/api/files?folder_id={{tree_root_id}}
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
|
|||
|
|
HTTP 200
|
|||
|
|
[Captures]
|
|||
|
|
tree_copy_id: jsonpath "$[0].id"
|
|||
|
|
[Asserts]
|
|||
|
|
jsonpath "$" count == 1
|
|||
|
|
jsonpath "$[0].name" == "{{orig_file_name}}"
|
|||
|
|
jsonpath "$[0].id" != "{{orig_file_id}}"
|
|||
|
|
|
|||
|
|
|
|||
|
|
GET {{base_url}}/api/files/{{tree_copy_id}}/thumbnail/preview
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
|
|||
|
|
HTTP 200
|
|||
|
|
[Asserts]
|
|||
|
|
bytes == {{uploaded_thumb}}
|
|||
|
|
bytes != {{rendered_thumb}}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
# Step 8 – Delete the ORIGINAL, run GC, and require both copies to keep
|
|||
|
|
# serving the upload.
|
|||
|
|
#
|
|||
|
|
# Each copy holds its own reference on the attached blob — the rows are
|
|||
|
|
# duplicated, not shared, because the table is file-keyed. If the copy
|
|||
|
|
# had failed to take one, deleting the original would walk the count to
|
|||
|
|
# zero and GC would reap bytes that cannot be regenerated.
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
DELETE {{base_url}}/api/files/{{orig_file_id}}
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
|
|||
|
|
HTTP 204
|
|||
|
|
|
|||
|
|
|
|||
|
|
GET {{base_url}}/api/trash/resources
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
|
|||
|
|
HTTP 200
|
|||
|
|
[Captures]
|
|||
|
|
trash_orig_id: jsonpath "$.items[?(@.resource.id == '{{orig_file_id}}')].resource.id"
|
|||
|
|
|
|||
|
|
|
|||
|
|
DELETE {{base_url}}/api/trash/{{trash_orig_id}}
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
|
|||
|
|
HTTP 200
|
|||
|
|
|
|||
|
|
|
|||
|
|
POST {{base_url}}/api/admin/jobs/dedup_gc/trigger
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
[Options]
|
|||
|
|
delay: 500ms
|
|||
|
|
|
|||
|
|
HTTP 200
|
|||
|
|
|
|||
|
|
|
|||
|
|
GET {{base_url}}/api/files/{{file_copy_id}}/thumbnail/preview
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
|
|||
|
|
HTTP 200
|
|||
|
|
[Asserts]
|
|||
|
|
bytes == {{uploaded_thumb}}
|
|||
|
|
|
|||
|
|
|
|||
|
|
GET {{base_url}}/api/files/{{tree_copy_id}}/thumbnail/preview
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
|
|||
|
|
HTTP 200
|
|||
|
|
[Asserts]
|
|||
|
|
bytes == {{uploaded_thumb}}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
# Step 9 – Teardown. Hurl files share one database within run.sh.
|
|||
|
|
# ─────────────────────────────────────────────────────────────
|
|||
|
|
DELETE {{base_url}}/api/folders/{{src_folder_id}}
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
|
|||
|
|
HTTP 204
|
|||
|
|
|
|||
|
|
|
|||
|
|
DELETE {{base_url}}/api/folders/{{dst_folder_id}}
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
|
|||
|
|
HTTP 204
|
|||
|
|
|
|||
|
|
|
|||
|
|
GET {{base_url}}/api/trash/resources
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
|
|||
|
|
HTTP 200
|
|||
|
|
[Captures]
|
|||
|
|
trash_src_id: jsonpath "$.items[?(@.resource.id == '{{src_folder_id}}')].resource.id"
|
|||
|
|
trash_dst_id: jsonpath "$.items[?(@.resource.id == '{{dst_folder_id}}')].resource.id"
|
|||
|
|
|
|||
|
|
|
|||
|
|
DELETE {{base_url}}/api/trash/{{trash_src_id}}
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
|
|||
|
|
HTTP 200
|
|||
|
|
|
|||
|
|
|
|||
|
|
DELETE {{base_url}}/api/trash/{{trash_dst_id}}
|
|||
|
|
Authorization: Bearer {{token}}
|
|||
|
|
|
|||
|
|
HTTP 200
|