3b31b8911b
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
193 lines
8.3 KiB
Plaintext
193 lines
8.3 KiB
Plaintext
# =============================================================
|
|
# 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
|