feat(breadcrumb): build breadcrumb in 1 API call
add /api/folders/{id}/ancestors
this API to iterate parent up to the drive root or the shared folder
this will help UI to build the breadcrumb in 1 API call
and to identify the root element (is it a drive users has access to or
a shared folder ?)
ui: now only 1 API call is now required to build the breadcrumb
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
# =============================================================
|
||||
# OxiCloud — GET /api/folders/{id}/ancestors
|
||||
# =============================================================
|
||||
# Pins the shared-breadcrumb endpoint. Coverage:
|
||||
# 1. Own personal drive: leaf returns full chain [root, sub, leaf]
|
||||
# with access_source.kind = "drive" + drive info.
|
||||
# 2. Drive-root leaf: chain has a single element (the root itself).
|
||||
# 3. Anti-enum: unknown UUID / no-Read → 404 (not 403).
|
||||
# 4. Cross-user: ancestors_stranger can't read admin's folder → 404.
|
||||
# =============================================================
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Setup — admin login + ancestors_stranger provisioning
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
{ "username": "{{username}}", "password": "{{password}}" }
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
admin_token: jsonpath "$.access_token"
|
||||
|
||||
|
||||
GET {{base_url}}/api/folders
|
||||
Authorization: Bearer {{admin_token}}
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
admin_home_id: jsonpath "$[0].id"
|
||||
|
||||
|
||||
# Anti-enum registration: 200 whether ancestors_stranger existed or not.
|
||||
POST {{base_url}}/api/auth/register
|
||||
Content-Type: application/json
|
||||
{
|
||||
"username": "ancestors_stranger",
|
||||
"email": "ancestors_stranger@example.com",
|
||||
"password": "BobPassword1!"
|
||||
}
|
||||
|
||||
HTTP 200
|
||||
|
||||
|
||||
POST {{base_url}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
{ "username": "ancestors_stranger", "password": "BobPassword1!" }
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
ancestors_stranger_token: jsonpath "$.access_token"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 1 — Create a small tree under admin's home:
|
||||
# home > ancestors-test > child > grandchild
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/folders
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: application/json
|
||||
{ "name": "ancestors-test", "parent_id": "{{admin_home_id}}" }
|
||||
|
||||
HTTP 201
|
||||
[Captures]
|
||||
mid_folder_id: jsonpath "$.id"
|
||||
|
||||
|
||||
POST {{base_url}}/api/folders
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: application/json
|
||||
{ "name": "child", "parent_id": "{{mid_folder_id}}" }
|
||||
|
||||
HTTP 201
|
||||
[Captures]
|
||||
child_folder_id: jsonpath "$.id"
|
||||
|
||||
|
||||
POST {{base_url}}/api/folders
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: application/json
|
||||
{ "name": "grandchild", "parent_id": "{{child_folder_id}}" }
|
||||
|
||||
HTTP 201
|
||||
[Captures]
|
||||
leaf_folder_id: jsonpath "$.id"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 2 — Ancestors on the deepest leaf.
|
||||
# Chain must be root → mid → child → grandchild.
|
||||
# access_source.kind = "drive" (admin owns the personal drive).
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/api/folders/{{leaf_folder_id}}/ancestors
|
||||
Authorization: Bearer {{admin_token}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$.ancestors" count == 4
|
||||
jsonpath "$.ancestors[0].id" == "{{admin_home_id}}"
|
||||
jsonpath "$.ancestors[0].parent_id" == null
|
||||
# Every ancestor carries drive_id (post-2026-07-26 addition) so
|
||||
# /files can derive the current drive without an extra `getFolder`.
|
||||
# Folders in one chain share a drive; checking `isString` on the
|
||||
# leaf is enough — Hurl can't cleanly assert field equality across
|
||||
# path indices.
|
||||
jsonpath "$.ancestors[0].drive_id" isString
|
||||
jsonpath "$.ancestors[3].drive_id" isString
|
||||
jsonpath "$.ancestors[1].id" == "{{mid_folder_id}}"
|
||||
jsonpath "$.ancestors[1].name" == "ancestors-test"
|
||||
jsonpath "$.ancestors[1].parent_id" == "{{admin_home_id}}"
|
||||
jsonpath "$.ancestors[2].id" == "{{child_folder_id}}"
|
||||
jsonpath "$.ancestors[2].name" == "child"
|
||||
jsonpath "$.ancestors[3].id" == "{{leaf_folder_id}}"
|
||||
jsonpath "$.ancestors[3].name" == "grandchild"
|
||||
jsonpath "$.access_source.kind" == "drive"
|
||||
jsonpath "$.access_source.drive.id" isString
|
||||
jsonpath "$.access_source.drive.name" isString
|
||||
jsonpath "$.access_source.drive.kind" == "personal"
|
||||
# Subject enrichment (2026-07-27): field carries the SHARER
|
||||
# (`role_grants.granted_by`), not the grantee. On admin's own personal
|
||||
# drive the drive grant is self-seeded with `granted_by = admin`, so
|
||||
# the assertion still resolves to `{{username}}` — but the semantic is
|
||||
# "who shared this?" and would surface a different name on a folder
|
||||
# shared with admin by someone else.
|
||||
jsonpath "$.access_source.subject.kind" == "user"
|
||||
jsonpath "$.access_source.subject.id" isString
|
||||
jsonpath "$.access_source.subject.name" == "{{username}}"
|
||||
# Caller's role via the boundary grant (2026-07-27) — piggybacked on
|
||||
# the same `role_grants` row that carries `granted_by`. Personal
|
||||
# drive owner grant is `owner`.
|
||||
jsonpath "$.access_source.caller_role" == "owner"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 3 — Drive-root leaf. Chain is one element (the root).
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/api/folders/{{admin_home_id}}/ancestors
|
||||
Authorization: Bearer {{admin_token}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$.ancestors" count == 1
|
||||
jsonpath "$.ancestors[0].id" == "{{admin_home_id}}"
|
||||
jsonpath "$.ancestors[0].parent_id" == null
|
||||
jsonpath "$.access_source.kind" == "drive"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 4 — Anti-enum: unknown-UUID and cross-user access both
|
||||
# return 404 (never 403). A well-formed UUID that
|
||||
# doesn't exist and a real folder the caller can't
|
||||
# Read produce the same shape — attackers can't
|
||||
# distinguish "no such folder" from "not yours."
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/api/folders/00000000-0000-0000-0000-000000000000/ancestors
|
||||
Authorization: Bearer {{admin_token}}
|
||||
|
||||
HTTP 404
|
||||
|
||||
|
||||
GET {{base_url}}/api/folders/{{leaf_folder_id}}/ancestors
|
||||
Authorization: Bearer {{ancestors_stranger_token}}
|
||||
|
||||
HTTP 404
|
||||
|
||||
|
||||
# Middleware-level 401 (no auth) is deliberately NOT tested here.
|
||||
# Prior login steps in this file leave Hurl's cookie jar populated,
|
||||
# so an omitted `Authorization:` header still authenticates via cookie
|
||||
# and lands on the handler — which returns the endpoint's anti-enum
|
||||
# 404 rather than the middleware 401. The middleware 401 case is
|
||||
# pinned separately at the TOP of `search_basic.hurl`, before any
|
||||
# login has run.
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 5 — Teardown: recursive delete of the top folder takes
|
||||
# the whole subtree. `DELETE /api/folders/{id}` is a
|
||||
# soft-delete-to-trash — every downstream test that
|
||||
# expects an empty trash (`trash.hurl`, `trash_resources.hurl`,
|
||||
# …) would find our orphan. Follow up with `empty` so
|
||||
# the trash returns to its clean baseline.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
DELETE {{base_url}}/api/folders/{{mid_folder_id}}
|
||||
Authorization: Bearer {{admin_token}}
|
||||
|
||||
HTTP 204
|
||||
|
||||
|
||||
DELETE {{base_url}}/api/trash/empty
|
||||
Authorization: Bearer {{admin_token}}
|
||||
|
||||
HTTP 200
|
||||
@@ -157,6 +157,7 @@ hurl --variables-file "$API_DIR/test.env" --file-root "$REPO_ROOT/tests" --test
|
||||
"$API_DIR/nc_ocs_user_info.hurl" \
|
||||
"$API_DIR/nc_avatar_preview.hurl" \
|
||||
"$API_DIR/files-folders.hurl" \
|
||||
"$API_DIR/folder_ancestors.hurl" \
|
||||
"$API_DIR/photos_etag.hurl" \
|
||||
"$API_DIR/favorites.hurl" \
|
||||
"$API_DIR/trash.hurl" \
|
||||
|
||||
Reference in New Issue
Block a user