2026-05-11 19:26:52 +02:00
|
|
|
|
# =============================================================
|
|
|
|
|
|
# OxiCloud – Favorites API end-to-end scenario
|
|
|
|
|
|
# =============================================================
|
|
|
|
|
|
# Depends on files-folders.hurl having run first:
|
|
|
|
|
|
# - home folder exists with sub-folders test1 and test2-renamed
|
|
|
|
|
|
# - test2-renamed contains hello-renamed.txt
|
|
|
|
|
|
#
|
|
|
|
|
|
# Run:
|
|
|
|
|
|
# hurl --variables-file tests/api/test.env --test tests/api/favorites.hurl
|
|
|
|
|
|
# =============================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 1 – Login and capture the JWT token
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
POST {{base_url}}/api/auth/login
|
|
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
{
|
|
|
|
|
|
"username": "{{username}}",
|
|
|
|
|
|
"password": "{{password}}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Captures]
|
|
|
|
|
|
token: jsonpath "$.access_token"
|
|
|
|
|
|
[Asserts]
|
|
|
|
|
|
jsonpath "$.access_token" isString
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 2 – No favorites yet
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
2026-05-29 12:55:26 +02:00
|
|
|
|
GET {{base_url}}/api/favorites/resources
|
2026-05-11 19:26:52 +02:00
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Asserts]
|
2026-05-29 12:55:26 +02:00
|
|
|
|
jsonpath "$.items" isCollection
|
|
|
|
|
|
jsonpath "$.items" count == 0
|
2026-05-11 19:26:52 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 3 – Discover item IDs from the folder structure
|
|
|
|
|
|
# Folders are ORDER BY name: test1 ($[0]), test2-renamed ($[1])
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
GET {{base_url}}/api/folders
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Captures]
|
|
|
|
|
|
home_folder_id: jsonpath "$[0].id"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-31 20:21:33 +02:00
|
|
|
|
GET {{base_url}}/api/folders/{{home_folder_id}}/resources?resource_types=folder
|
2026-05-11 19:26:52 +02:00
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Captures]
|
2026-05-31 20:21:33 +02:00
|
|
|
|
test1_id: jsonpath "$.items[0].resource.id"
|
|
|
|
|
|
test2_id: jsonpath "$.items[1].resource.id"
|
2026-05-11 19:26:52 +02:00
|
|
|
|
[Asserts]
|
2026-05-31 20:21:33 +02:00
|
|
|
|
jsonpath "$.items[0].resource.name" == "test1"
|
|
|
|
|
|
jsonpath "$.items[1].resource.name" == "test2-renamed"
|
2026-05-11 19:26:52 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
GET {{base_url}}/api/files?folder_id={{test2_id}}
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Captures]
|
|
|
|
|
|
file_id: jsonpath "$[0].id"
|
|
|
|
|
|
[Asserts]
|
|
|
|
|
|
jsonpath "$[0].name" == "hello-renamed.txt"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 4 – Add hello-renamed.txt to favorites
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
POST {{base_url}}/api/favorites/file/{{file_id}}
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 201
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 5 – Favorites contains only hello-renamed.txt
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
2026-05-29 12:55:26 +02:00
|
|
|
|
GET {{base_url}}/api/favorites/resources
|
2026-05-11 19:26:52 +02:00
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Asserts]
|
2026-05-29 12:55:26 +02:00
|
|
|
|
jsonpath "$.items" count == 1
|
|
|
|
|
|
jsonpath "$.items[0].resource.id" == {{file_id}}
|
|
|
|
|
|
jsonpath "$.items[0].resource_type" == "file"
|
|
|
|
|
|
jsonpath "$.items[0].resource.name" == "hello-renamed.txt"
|
2026-07-22 00:36:14 +02:00
|
|
|
|
# Caller-flag contract — every row on /api/favorites/resources IS
|
|
|
|
|
|
# favorited by construction (the listing SQL hardcodes
|
|
|
|
|
|
# `TRUE AS is_favorite`). `is_shared` is a real per-row EXISTS.
|
|
|
|
|
|
jsonpath "$.items[0].resource.is_favorite" == true
|
|
|
|
|
|
jsonpath "$.items[0].resource.is_shared" == false
|
2026-05-11 19:26:52 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 6 – Add test1 folder to favorites
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
POST {{base_url}}/api/favorites/folder/{{test1_id}}
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 201
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 7 – Favorites contains both items (order-independent)
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
2026-05-29 12:55:26 +02:00
|
|
|
|
GET {{base_url}}/api/favorites/resources
|
2026-05-11 19:26:52 +02:00
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Asserts]
|
2026-05-29 12:55:26 +02:00
|
|
|
|
jsonpath "$.items" count == 2
|
|
|
|
|
|
jsonpath "$.items[*].resource.id" contains {{file_id}}
|
|
|
|
|
|
jsonpath "$.items[*].resource.id" contains {{test1_id}}
|
2026-05-11 19:26:52 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 8 – Remove hello-renamed.txt from favorites
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
DELETE {{base_url}}/api/favorites/file/{{file_id}}
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 9 – Favorites contains only test1 folder
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
2026-05-29 12:55:26 +02:00
|
|
|
|
GET {{base_url}}/api/favorites/resources
|
2026-05-11 19:26:52 +02:00
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Asserts]
|
2026-05-29 12:55:26 +02:00
|
|
|
|
jsonpath "$.items" count == 1
|
|
|
|
|
|
jsonpath "$.items[0].resource.id" == {{test1_id}}
|
|
|
|
|
|
jsonpath "$.items[0].resource_type" == "folder"
|
|
|
|
|
|
jsonpath "$.items[0].resource.name" == "test1"
|
2026-05-11 19:26:52 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 10 – Cleanup: remove test1 from favorites
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
DELETE {{base_url}}/api/favorites/folder/{{test1_id}}
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 11 – Favorites is empty again
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
2026-05-29 12:55:26 +02:00
|
|
|
|
GET {{base_url}}/api/favorites/resources
|
2026-05-11 19:26:52 +02:00
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Asserts]
|
2026-05-29 12:55:26 +02:00
|
|
|
|
jsonpath "$.items" count == 0
|
2026-07-04 23:31:10 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 12 — Cross-tenant regression (post-Drive AuthZ audit,
|
|
|
|
|
|
# Round 1 HIGH). Before this fix, `POST /api/favorites/…`
|
|
|
|
|
|
# accepted any UUID and enrolled it; the listing endpoint
|
|
|
|
|
|
# then JOINed back to storage.files/folders and returned
|
|
|
|
|
|
# name/mime/size/drive_id for anything the caller had
|
|
|
|
|
|
# managed to add — an information oracle over the whole
|
|
|
|
|
|
# tenant. Now the write path calls `authz.require(Read, …)`
|
|
|
|
|
|
# per item; a caller with no grant gets 404 (anti-enum)
|
|
|
|
|
|
# + `authz.denied` audit line. See
|
|
|
|
|
|
# `docs/plan/authz_audit/rest_storage.md`.
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
# Create a second, unprivileged user. Idempotent: `HTTP *` accepts
|
|
|
|
|
|
# either 201 (first run) or 409 (subsequent runs). The login below
|
|
|
|
|
|
# is the actual precondition — if it succeeds we know the user
|
|
|
|
|
|
# exists with the expected password.
|
|
|
|
|
|
POST {{base_url}}/api/admin/users
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
{ "username": "fav_mallory", "password": "FavMalloryPassword1!", "email": "fav_mallory@example.com", "role": "user" }
|
|
|
|
|
|
|
|
|
|
|
|
HTTP *
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
POST {{base_url}}/api/auth/login
|
|
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
{ "username": "fav_mallory", "password": "FavMalloryPassword1!" }
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Captures]
|
|
|
|
|
|
mallory_token: jsonpath "$.access_token"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Step 12a — Single-add on admin's file: 404 (anti-enum shape).
|
|
|
|
|
|
POST {{base_url}}/api/favorites/file/{{file_id}}
|
|
|
|
|
|
Authorization: Bearer {{mallory_token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Step 12b — Single-add on admin's folder: 404.
|
|
|
|
|
|
POST {{base_url}}/api/favorites/folder/{{test1_id}}
|
|
|
|
|
|
Authorization: Bearer {{mallory_token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Step 12c — Batch: must fail wholesale on the first denial. A partial
|
|
|
|
|
|
# success would still leak "which items are valid" — the same
|
|
|
|
|
|
# oracle we're closing.
|
|
|
|
|
|
POST {{base_url}}/api/favorites/batch
|
|
|
|
|
|
Authorization: Bearer {{mallory_token}}
|
|
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
{
|
|
|
|
|
|
"items": [
|
|
|
|
|
|
{ "item_id": "{{file_id}}", "item_type": "file" },
|
|
|
|
|
|
{ "item_id": "{{test1_id}}", "item_type": "folder" }
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Step 12d — Mallory's favorites list is EMPTY — no partial success
|
|
|
|
|
|
# slipped through.
|
|
|
|
|
|
GET {{base_url}}/api/favorites/resources
|
|
|
|
|
|
Authorization: Bearer {{mallory_token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Asserts]
|
|
|
|
|
|
jsonpath "$.items" count == 0
|