# =============================================================
# OxiCloud — ReBAC grant management (POST/DELETE/GET /api/grants)
# =============================================================
# Exercises cross-user grants, cascading, roles, revoke, lifecycle
# cleanup. Uses ONLY endpoints that route through the
# AuthorizationEngine — handler-layer inline checks (e.g.
# GET /api/folders/{id}) are scheduled for cleanup separately.
#
# Runs AFTER permissions.hurl (bob already exists). Self-contained
# resources (unique names) so it doesn't depend on prior state.
# =============================================================


# ─────────────────────────────────────────────────────────────
# Step 1 — Login as admin (Alice), capture token + home folder.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "{{username}}", "password": "{{password}}" }

HTTP 200
[Captures]
alice_token: jsonpath "$.access_token"

GET {{base_url}}/api/folders
Authorization: Bearer {{alice_token}}

HTTP 200
[Captures]
alice_home_id: jsonpath "$[0].id"


# ─────────────────────────────────────────────────────────────
# Step 2 — Create two test users specific to this file (dave + eve).
#           Avoids cross-file dependencies on bob from permissions.hurl
#           and gives us their user_id directly from the create response.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/admin/users
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{ "username": "dave", "password": "DavePassword1!", "email": "dave@example.com", "role": "user" }

HTTP 201
[Captures]
dave_user_id: jsonpath "$.id"

POST {{base_url}}/api/admin/users
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{ "username": "eve", "password": "EvePassword1!", "email": "eve@example.com", "role": "user" }

HTTP 201
[Captures]
eve_user_id: jsonpath "$.id"


# ─────────────────────────────────────────────────────────────
# Step 3 — Login dave and eve.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "dave", "password": "DavePassword1!" }

HTTP 200
[Captures]
dave_token: jsonpath "$.access_token"

POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "eve", "password": "EvePassword1!" }

HTTP 200
[Captures]
eve_token: jsonpath "$.access_token"


# ─────────────────────────────────────────────────────────────
# Step 4 — Alice creates a folder "grant-shared" + a child "grant-child".
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/folders
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{ "name": "grant-shared", "parent_id": "{{alice_home_id}}" }

HTTP 201
[Captures]
shared_folder_id: jsonpath "$.id"

POST {{base_url}}/api/folders
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{ "name": "grant-child", "parent_id": "{{shared_folder_id}}" }

HTTP 201
[Captures]
child_folder_id: jsonpath "$.id"


# ─────────────────────────────────────────────────────────────
# Step 5 — Without any grant, bob cannot rename Alice's folder.
#           PUT /api/folders/{id}/rename goes through the engine →
#           404 (anti-enumeration).
# ─────────────────────────────────────────────────────────────
PUT {{base_url}}/api/folders/{{shared_folder_id}}/rename
Authorization: Bearer {{dave_token}}
Content-Type: application/json
{ "name": "bob-tried" }

HTTP 404


# ─────────────────────────────────────────────────────────────
# Step 6 — Alice grants Bob the Viewer role. Server expands → [read].
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/grants
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{
  "subject":  { "type": "user",   "id": "{{dave_user_id}}" },
  "resource": { "type": "folder", "id": "{{shared_folder_id}}" },
  "role": "viewer"
}

HTTP 201
[Asserts]
jsonpath "$" count == 1
jsonpath "$[0].permission" == "read"


# ─────────────────────────────────────────────────────────────
# Step 7 — Viewer cannot rename (no update grant).
# ─────────────────────────────────────────────────────────────
PUT {{base_url}}/api/folders/{{shared_folder_id}}/rename
Authorization: Bearer {{dave_token}}
Content-Type: application/json
{ "name": "bob-tried-again" }

HTTP 404


# ─────────────────────────────────────────────────────────────
# Step 8 — Bob's incoming grants list contains the new grant.
# ─────────────────────────────────────────────────────────────
GET {{base_url}}/api/grants/incoming
Authorization: Bearer {{dave_token}}

HTTP 200
[Asserts]
jsonpath "$[?(@.resource.id=='{{shared_folder_id}}')].permission" == "read"


# ─────────────────────────────────────────────────────────────
# Step 9 — Promote Bob to Admin (adds comment, create, update, share, delete).
#           PUT /api/grants/role reconciles the row set in one call.
# ─────────────────────────────────────────────────────────────
PUT {{base_url}}/api/grants/role
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{
  "subject":  { "type": "user",   "id": "{{dave_user_id}}" },
  "resource": { "type": "folder", "id": "{{shared_folder_id}}" },
  "role": "admin"
}

HTTP 200
[Asserts]
jsonpath "$" count == 6


# ─────────────────────────────────────────────────────────────
# Step 10 — Bob can now rename (Manager includes update).
# ─────────────────────────────────────────────────────────────
PUT {{base_url}}/api/folders/{{shared_folder_id}}/rename
Authorization: Bearer {{dave_token}}
Content-Type: application/json
{ "name": "renamed-by-bob-as-admin" }

HTTP 200


# ─────────────────────────────────────────────────────────────
# Step 11 — Cascading: Bob can also rename the CHILD folder, because
#           his Update grant on the parent cascades via ltree to the
#           child resource — even though no direct grant on the child.
# ─────────────────────────────────────────────────────────────
PUT {{base_url}}/api/folders/{{child_folder_id}}/rename
Authorization: Bearer {{dave_token}}
Content-Type: application/json
{ "name": "renamed-child-via-cascade" }

HTTP 200


# ─────────────────────────────────────────────────────────────
# Step 12 — Bob re-shares to Carol (he has Share via Admin).
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/grants
Authorization: Bearer {{dave_token}}
Content-Type: application/json
{
  "subject":  { "type": "user",   "id": "{{eve_user_id}}" },
  "resource": { "type": "folder", "id": "{{shared_folder_id}}" },
  "role": "viewer"
}

HTTP 201
[Captures]
eve_grant_id: jsonpath "$[0].id"


# ─────────────────────────────────────────────────────────────
# Step 13 — Carol can see the grant in her incoming list.
# ─────────────────────────────────────────────────────────────
GET {{base_url}}/api/grants/incoming
Authorization: Bearer {{eve_token}}

HTTP 200
[Asserts]
jsonpath "$[?(@.resource.id=='{{shared_folder_id}}')].permission" == "read"


# ─────────────────────────────────────────────────────────────
# Step 14 — Bob's outgoing grants list contains the grant to Carol.
# ─────────────────────────────────────────────────────────────
GET {{base_url}}/api/grants/outgoing
Authorization: Bearer {{dave_token}}

HTTP 200
[Asserts]
jsonpath "$[?(@.id=='{{eve_grant_id}}')].id" == "{{eve_grant_id}}"


# ─────────────────────────────────────────────────────────────
# Step 15 — Demote Bob to Viewer; he loses update/share/etc.
# ─────────────────────────────────────────────────────────────
PUT {{base_url}}/api/grants/role
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{
  "subject":  { "type": "user",   "id": "{{dave_user_id}}" },
  "resource": { "type": "folder", "id": "{{shared_folder_id}}" },
  "role": "viewer"
}

HTTP 200
[Asserts]
jsonpath "$" count == 1
jsonpath "$[0].permission" == "read"


# ─────────────────────────────────────────────────────────────
# Step 16 — Demoted Bob can no longer rename.
# ─────────────────────────────────────────────────────────────
PUT {{base_url}}/api/folders/{{shared_folder_id}}/rename
Authorization: Bearer {{dave_token}}
Content-Type: application/json
{ "name": "bob-tried-after-demote" }

HTTP 404


# ─────────────────────────────────────────────────────────────
# Step 17 — Lifecycle: Alice deletes the folder. The DB trigger
#           trg_cleanup_grants_folder removes both bob's and carol's
#           grants automatically (also for the cascade-deleted child).
# ─────────────────────────────────────────────────────────────
DELETE {{base_url}}/api/folders/{{child_folder_id}}
Authorization: Bearer {{alice_token}}

HTTP 204

DELETE {{base_url}}/api/folders/{{shared_folder_id}}
Authorization: Bearer {{alice_token}}

HTTP 204

DELETE {{base_url}}/api/trash/empty
Authorization: Bearer {{alice_token}}

HTTP 200


# ─────────────────────────────────────────────────────────────
# Step 18 — After permanent delete, Bob's incoming list no longer
#            contains the deleted folder's grant.
# ─────────────────────────────────────────────────────────────
GET {{base_url}}/api/grants/incoming
Authorization: Bearer {{dave_token}}

HTTP 200
[Asserts]
jsonpath "$" count == 0


# ─────────────────────────────────────────────────────────────
# Step 19 — Same for Carol.
# ─────────────────────────────────────────────────────────────
GET {{base_url}}/api/grants/incoming
Authorization: Bearer {{eve_token}}

HTTP 200
[Asserts]
jsonpath "$" count == 0


# ════════════════════════════════════════════════════════════════════
# PHASE 2 — Comprehensive permission coverage with fresh user "adam".
# ════════════════════════════════════════════════════════════════════
# Exercises every engine-aware endpoint at each permission tier:
#
#   no grant   → all read/write/delete operations return 404
#   Viewer     → read endpoints OK, modify/delete endpoints return 404
#   Editor     → update + create + thumbnail-push OK, delete still 404
#   Admin      → everything including delete
#
# Endpoints in scope (all routed through the AuthorizationEngine):
#   Folders:  /contents · /contents/paginated · /listing · /download (zip)
#             · POST / · PUT /{id}/rename · PUT /{id}/move · DELETE /{id}
#   Files:    GET /  · GET /{id} (download)
#             · GET /{id}/metadata · GET /{id}/thumbnail/{size}
#             · PUT /{id}/thumbnail/{size} (push, Update)
#             · PUT /{id}/rename · PUT /{id}/move · DELETE /{id}
#             · POST /upload (via folder require_permission)
#
# Listing endpoints that are still owner-scoped (GET /api/folders root,
# GET /api/folders/paginated) are NOT covered here — they don't
# reflect grants today and are tracked as separate cleanup work.


# ─────────────────────────────────────────────────────────────
# Step 20 — Create user adam (fresh, no relationship to alice's tree).
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/admin/users
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{ "username": "adam", "password": "AdamPassword1!", "email": "adam@example.com", "role": "user" }

HTTP 201
[Captures]
adam_user_id: jsonpath "$.id"

POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "adam", "password": "AdamPassword1!" }

HTTP 200
[Captures]
adam_token: jsonpath "$.access_token"


# ─────────────────────────────────────────────────────────────
# Step 21 — Alice creates a fresh shareable folder, sub-folder, and
#           uploads a JPEG (which the server auto-thumbnails).
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/folders
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{ "name": "perm-test-folder", "parent_id": "{{alice_home_id}}" }

HTTP 201
[Captures]
perm_folder_id: jsonpath "$.id"

POST {{base_url}}/api/folders
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{ "name": "perm-test-child", "parent_id": "{{perm_folder_id}}" }

HTTP 201
[Captures]
perm_child_id: jsonpath "$.id"

POST {{base_url}}/api/files/upload
Authorization: Bearer {{alice_token}}
[MultipartFormData]
folder_id: {{perm_folder_id}}
file: file,fixtures/oxicloud-logo.jpg; image/jpeg

HTTP 201
[Captures]
perm_file_id: jsonpath "$.id"


# ════════════════════════════════════════════════════════════════════
# Phase 2A — Adam has NO grant. Every engine-aware endpoint denies.
# ════════════════════════════════════════════════════════════════════

# ── Folder reads ─────────────────────────────────────────────
GET {{base_url}}/api/folders/{{perm_folder_id}}/contents
Authorization: Bearer {{adam_token}}

HTTP 404

GET {{base_url}}/api/folders/{{perm_folder_id}}/contents/paginated
Authorization: Bearer {{adam_token}}

HTTP 404

GET {{base_url}}/api/folders/{{perm_folder_id}}/listing
Authorization: Bearer {{adam_token}}

HTTP 404

GET {{base_url}}/api/folders/{{perm_folder_id}}/download
Authorization: Bearer {{adam_token}}

HTTP 404

# ── File reads ───────────────────────────────────────────────
GET {{base_url}}/api/files?folder_id={{perm_folder_id}}
Authorization: Bearer {{adam_token}}

HTTP 404

GET {{base_url}}/api/files/{{perm_file_id}}
Authorization: Bearer {{adam_token}}

HTTP 404

GET {{base_url}}/api/files/{{perm_file_id}}/metadata
Authorization: Bearer {{adam_token}}

HTTP 404

GET {{base_url}}/api/files/{{perm_file_id}}/thumbnail/icon
Authorization: Bearer {{adam_token}}

HTTP 404

# ── Folder mutations ─────────────────────────────────────────
POST {{base_url}}/api/folders
Authorization: Bearer {{adam_token}}
Content-Type: application/json
{ "name": "adam-attack", "parent_id": "{{perm_folder_id}}" }

HTTP 404

PUT {{base_url}}/api/folders/{{perm_folder_id}}/rename
Authorization: Bearer {{adam_token}}
Content-Type: application/json
{ "name": "adam-rename-attempt" }

HTTP 404

DELETE {{base_url}}/api/folders/{{perm_folder_id}}
Authorization: Bearer {{adam_token}}

HTTP 404

# ── File mutations ───────────────────────────────────────────
POST {{base_url}}/api/files/upload
Authorization: Bearer {{adam_token}}
[MultipartFormData]
folder_id: {{perm_folder_id}}
file: file,fixtures/hello.txt; text/plain

HTTP 404

PUT {{base_url}}/api/files/{{perm_file_id}}/rename
Authorization: Bearer {{adam_token}}
Content-Type: application/json
{ "name": "adam-file-rename" }

HTTP 404

PUT {{base_url}}/api/files/{{perm_file_id}}/thumbnail/icon
Authorization: Bearer {{adam_token}}
Content-Type: image/png
file,fixtures/blue-image.png;

HTTP 404

DELETE {{base_url}}/api/files/{{perm_file_id}}
Authorization: Bearer {{adam_token}}

HTTP 404

# ── Chunked upload: cannot start session in alice's folder ──
# create_upload_impl pre-checks Permission::Create via require_permission.
POST {{base_url}}/api/uploads
Authorization: Bearer {{adam_token}}
Content-Type: application/json
{
  "filename": "adam-chunked-attack.mp4",
  "folder_id": "{{perm_folder_id}}",
  "content_type": "video/mp4",
  "total_size": 2760653,
  "chunk_size": 3000000
}

HTTP 404


# ════════════════════════════════════════════════════════════════════
# Phase 2B — Alice grants adam Viewer. Read OK, mutate/delete denied.
# ════════════════════════════════════════════════════════════════════
POST {{base_url}}/api/grants
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{
  "subject":  { "type": "user",   "id": "{{adam_user_id}}" },
  "resource": { "type": "folder", "id": "{{perm_folder_id}}" },
  "role": "viewer"
}

HTTP 201

# ── Read endpoints now succeed ──────────────────────────────
GET {{base_url}}/api/folders/{{perm_folder_id}}/contents
Authorization: Bearer {{adam_token}}

HTTP 200
[Asserts]
jsonpath "$" count == 1
jsonpath "$[0].id" == "{{perm_child_id}}"

GET {{base_url}}/api/folders/{{perm_folder_id}}/contents/paginated
Authorization: Bearer {{adam_token}}

HTTP 200

GET {{base_url}}/api/folders/{{perm_folder_id}}/listing
Authorization: Bearer {{adam_token}}

HTTP 200

GET {{base_url}}/api/folders/{{perm_folder_id}}/download
Authorization: Bearer {{adam_token}}

HTTP 200
[Asserts]
header "Content-Type" contains "zip"

GET {{base_url}}/api/files?folder_id={{perm_folder_id}}
Authorization: Bearer {{adam_token}}

HTTP 200
[Asserts]
jsonpath "$" count == 1
jsonpath "$[0].id" == "{{perm_file_id}}"

GET {{base_url}}/api/files/{{perm_file_id}}
Authorization: Bearer {{adam_token}}

HTTP 200

GET {{base_url}}/api/files/{{perm_file_id}}/metadata
Authorization: Bearer {{adam_token}}

HTTP 200

GET {{base_url}}/api/files/{{perm_file_id}}/thumbnail/icon
Authorization: Bearer {{adam_token}}

HTTP 200
[Asserts]
header "Content-Type" startsWith "image/"

# ── Cascading: child folder also readable via parent's grant ─
GET {{base_url}}/api/folders/{{perm_child_id}}/contents
Authorization: Bearer {{adam_token}}

HTTP 200

# ── Mutations still denied (Viewer has no Update/Create/Delete) ─
POST {{base_url}}/api/folders
Authorization: Bearer {{adam_token}}
Content-Type: application/json
{ "name": "adam-attack-2", "parent_id": "{{perm_folder_id}}" }

HTTP 404

PUT {{base_url}}/api/folders/{{perm_folder_id}}/rename
Authorization: Bearer {{adam_token}}
Content-Type: application/json
{ "name": "adam-rename-as-viewer" }

HTTP 404

PUT {{base_url}}/api/files/{{perm_file_id}}/rename
Authorization: Bearer {{adam_token}}
Content-Type: application/json
{ "name": "adam-file-rename-as-viewer" }

HTTP 404

PUT {{base_url}}/api/files/{{perm_file_id}}/thumbnail/icon
Authorization: Bearer {{adam_token}}
Content-Type: image/png
file,fixtures/blue-image.png;

HTTP 404

POST {{base_url}}/api/files/upload
Authorization: Bearer {{adam_token}}
[MultipartFormData]
folder_id: {{perm_folder_id}}
file: file,fixtures/hello.txt; text/plain

HTTP 404

DELETE {{base_url}}/api/files/{{perm_file_id}}
Authorization: Bearer {{adam_token}}

HTTP 404

DELETE {{base_url}}/api/folders/{{perm_folder_id}}
Authorization: Bearer {{adam_token}}

HTTP 404

# ── Viewer cannot start a chunked upload (no Create grant) ──
POST {{base_url}}/api/uploads
Authorization: Bearer {{adam_token}}
Content-Type: application/json
{
  "filename": "viewer-chunked-attempt.mp4",
  "folder_id": "{{perm_folder_id}}",
  "content_type": "video/mp4",
  "total_size": 2760653,
  "chunk_size": 3000000
}

HTTP 404


# ════════════════════════════════════════════════════════════════════
# Phase 2C — Promote adam to Editor (read + comment + create + update).
#           Create + Update endpoints now succeed; Delete still denied.
# ════════════════════════════════════════════════════════════════════
PUT {{base_url}}/api/grants/role
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{
  "subject":  { "type": "user",   "id": "{{adam_user_id}}" },
  "resource": { "type": "folder", "id": "{{perm_folder_id}}" },
  "role": "editor"
}

HTTP 200

# ── Update succeeds ─────────────────────────────────────────
PUT {{base_url}}/api/folders/{{perm_folder_id}}/rename
Authorization: Bearer {{adam_token}}
Content-Type: application/json
{ "name": "renamed-by-adam-as-editor" }

HTTP 200

PUT {{base_url}}/api/files/{{perm_file_id}}/rename
Authorization: Bearer {{adam_token}}
Content-Type: application/json
{ "name": "adam-renamed-logo.jpg" }

HTTP 200

# ── Thumbnail push (Update) succeeds ────────────────────────
PUT {{base_url}}/api/files/{{perm_file_id}}/thumbnail/preview
Authorization: Bearer {{adam_token}}
Content-Type: image/png
file,fixtures/blue-image.png;

HTTP 201

# ── Create succeeds ─────────────────────────────────────────
POST {{base_url}}/api/folders
Authorization: Bearer {{adam_token}}
Content-Type: application/json
{ "name": "adam-created-child", "parent_id": "{{perm_folder_id}}" }

HTTP 201

POST {{base_url}}/api/files/upload
Authorization: Bearer {{adam_token}}
[MultipartFormData]
folder_id: {{perm_folder_id}}
file: file,fixtures/hello.txt; text/plain

HTTP 201

# ── Chunked upload full lifecycle as Editor ─────────────────
# 1. Open session (server pre-checks Create on folder)
POST {{base_url}}/api/uploads
Authorization: Bearer {{adam_token}}
Content-Type: application/json
{
  "filename": "adam-chunked-video.mp4",
  "folder_id": "{{perm_folder_id}}",
  "content_type": "video/mp4",
  "total_size": 2760653,
  "chunk_size": 3000000
}

HTTP 201
[Captures]
adam_upload_id: jsonpath "$.upload_id"

# 2. Send the single chunk (chunk_size > total_size → 1 chunk).
PATCH {{base_url}}/api/uploads/{{adam_upload_id}}?chunk_index=0
Authorization: Bearer {{adam_token}}
Content-Type: application/octet-stream
file,fixtures/free_video_over_1MB.mp4;

HTTP 200

# 3. Status query: dave (different user) cannot peek at adam's session.
HEAD {{base_url}}/api/uploads/{{adam_upload_id}}
Authorization: Bearer {{dave_token}}

HTTP 404

# 4. Cancel attempt by a different user is rejected.
DELETE {{base_url}}/api/uploads/{{adam_upload_id}}
Authorization: Bearer {{dave_token}}

HTTP 404

# 5. Adam completes the upload — file is created in alice's folder.
POST {{base_url}}/api/uploads/{{adam_upload_id}}/complete
Authorization: Bearer {{adam_token}}

HTTP 201
[Captures]
adam_chunked_file_id: jsonpath "$.file_id"

# 6. The new file is visible in the folder listing (caller-of-listing is alice).
GET {{base_url}}/api/files?folder_id={{perm_folder_id}}
Authorization: Bearer {{alice_token}}

HTTP 200
[Asserts]
jsonpath "$[?(@.id=='{{adam_chunked_file_id}}')].name" == "adam-chunked-video.mp4"

# 7. A second session that adam cancels before completing — cleanup path.
POST {{base_url}}/api/uploads
Authorization: Bearer {{adam_token}}
Content-Type: application/json
{
  "filename": "adam-cancelled.mp4",
  "folder_id": "{{perm_folder_id}}",
  "content_type": "video/mp4",
  "total_size": 2760653,
  "chunk_size": 3000000
}

HTTP 201
[Captures]
adam_cancel_id: jsonpath "$.upload_id"

DELETE {{base_url}}/api/uploads/{{adam_cancel_id}}
Authorization: Bearer {{adam_token}}

HTTP 204

# ── Delete still denied (Editor excludes Delete) ────────────
DELETE {{base_url}}/api/files/{{perm_file_id}}
Authorization: Bearer {{adam_token}}

HTTP 404

DELETE {{base_url}}/api/folders/{{perm_folder_id}}
Authorization: Bearer {{adam_token}}

HTTP 404


# ════════════════════════════════════════════════════════════════════
# Phase 2D — Promote adam to Admin (all 6 permissions). Delete OK.
# ════════════════════════════════════════════════════════════════════
PUT {{base_url}}/api/grants/role
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{
  "subject":  { "type": "user",   "id": "{{adam_user_id}}" },
  "resource": { "type": "folder", "id": "{{perm_folder_id}}" },
  "role": "admin"
}

HTTP 200

DELETE {{base_url}}/api/files/{{perm_file_id}}
Authorization: Bearer {{adam_token}}

HTTP 204


# ════════════════════════════════════════════════════════════════════
# Phase 2E — Lifecycle cleanup. Alice (still the owner) trashes &
#           empties; the trigger removes all access_grants rows.
# ════════════════════════════════════════════════════════════════════
DELETE {{base_url}}/api/folders/{{perm_folder_id}}
Authorization: Bearer {{alice_token}}

HTTP 204

DELETE {{base_url}}/api/trash/empty
Authorization: Bearer {{alice_token}}

HTTP 200

# Adam's incoming list is empty.
GET {{base_url}}/api/grants/incoming
Authorization: Bearer {{adam_token}}

HTTP 200
[Asserts]
jsonpath "$" count == 0


# ════════════════════════════════════════════════════════════════════
# PHASE 3 — Batch operations (/api/batch/*)
# ════════════════════════════════════════════════════════════════════
# Every batch endpoint passes caller_id through to the batch service,
# which delegates per-item to engine-aware *_with_perms methods. The
# handler aggregates results: 200 (all OK), 206 (mixed), 400 (all failed).
#
# Endpoints exercised:
#   POST   /api/batch/files/get      · /api/batch/files/move
#   POST   /api/batch/files/copy     · /api/batch/files/delete
#   POST   /api/batch/folders/get    · /api/batch/folders/create
#   POST   /api/batch/folders/move   · /api/batch/folders/copy
#   POST   /api/batch/folders/delete · /api/batch/trash
#   POST   /api/batch/download       · GET /api/batch/download (querystring)
#
# Fresh user "frank" — no grants from earlier phases.


# ─────────────────────────────────────────────────────────────
# Step P3.1 — Create frank and login.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/admin/users
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{ "username": "frank", "password": "FrankPassword1!", "email": "frank@example.com", "role": "user" }

HTTP 201
[Captures]
frank_user_id: jsonpath "$.id"

POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "frank", "password": "FrankPassword1!" }

HTTP 200
[Captures]
frank_token: jsonpath "$.access_token"


# ─────────────────────────────────────────────────────────────
# Step P3.2 — Alice creates a batch-test folder with 2 sub-folders
#             and 2 files (all owned by alice).
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/folders
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{ "name": "batch-test", "parent_id": "{{alice_home_id}}" }

HTTP 201
[Captures]
batch_root_id: jsonpath "$.id"

POST {{base_url}}/api/folders
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{ "name": "batch-sub-A", "parent_id": "{{batch_root_id}}" }

HTTP 201
[Captures]
batch_sub_a_id: jsonpath "$.id"

POST {{base_url}}/api/folders
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{ "name": "batch-sub-B", "parent_id": "{{batch_root_id}}" }

HTTP 201
[Captures]
batch_sub_b_id: jsonpath "$.id"

POST {{base_url}}/api/files/upload
Authorization: Bearer {{alice_token}}
[MultipartFormData]
folder_id: {{batch_root_id}}
file: file,fixtures/red-image.png; image/png

HTTP 201
[Captures]
batch_file_1_id: jsonpath "$.id"

POST {{base_url}}/api/files/upload
Authorization: Bearer {{alice_token}}
[MultipartFormData]
folder_id: {{batch_root_id}}
file: file,fixtures/green-image.png; image/png

HTTP 201
[Captures]
batch_file_2_id: jsonpath "$.id"


# ════════════════════════════════════════════════════════════════════
# Phase 3A — frank has NO grant. Every batch op returns 400 (all failed).
# ════════════════════════════════════════════════════════════════════

# Files — get
POST {{base_url}}/api/batch/files/get
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "file_ids": ["{{batch_file_1_id}}", "{{batch_file_2_id}}"] }

HTTP 400
[Asserts]
jsonpath "$.stats.successful" == 0
jsonpath "$.stats.failed" == 2

# Files — move
POST {{base_url}}/api/batch/files/move
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "file_ids": ["{{batch_file_1_id}}"], "target_folder_id": "{{batch_sub_a_id}}" }

HTTP 400
[Asserts]
jsonpath "$.stats.failed" == 1

# Files — copy
POST {{base_url}}/api/batch/files/copy
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "file_ids": ["{{batch_file_1_id}}"], "target_folder_id": "{{batch_sub_a_id}}" }

HTTP 400
[Asserts]
jsonpath "$.stats.failed" == 1

# Files — delete
POST {{base_url}}/api/batch/files/delete
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "file_ids": ["{{batch_file_1_id}}"] }

HTTP 400
[Asserts]
jsonpath "$.stats.failed" == 1

# Folders — get
POST {{base_url}}/api/batch/folders/get
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "folder_ids": ["{{batch_sub_a_id}}", "{{batch_sub_b_id}}"] }

HTTP 400
[Asserts]
jsonpath "$.stats.failed" == 2

# Folders — create child (no Create on batch_root)
POST {{base_url}}/api/batch/folders/create
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "folders": [{ "name": "frank-attack", "parent_id": "{{batch_root_id}}" }] }

HTTP 400

# Folders — move
POST {{base_url}}/api/batch/folders/move
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "folder_ids": ["{{batch_sub_a_id}}"], "target_folder_id": "{{batch_sub_b_id}}" }

HTTP 400

# Folders — copy
POST {{base_url}}/api/batch/folders/copy
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "folder_ids": ["{{batch_sub_a_id}}"], "target_folder_id": "{{batch_sub_b_id}}" }

HTTP 400

# Folders — delete
POST {{base_url}}/api/batch/folders/delete
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "folder_ids": ["{{batch_sub_a_id}}"], "recursive": false }

HTTP 400

# Trash (mixed)
POST {{base_url}}/api/batch/trash
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "file_ids": ["{{batch_file_1_id}}"], "folder_ids": ["{{batch_sub_a_id}}"] }

HTTP 400
[Asserts]
jsonpath "$.stats.failed" == 2

# Download POST — engine rejects each item; batch service tracks
# `items_added` and bails out with NotFound when none were authorized.
POST {{base_url}}/api/batch/download
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "file_ids": ["{{batch_file_1_id}}"], "folder_ids": [] }

HTTP 404

# Download GET (querystring variant) — same behavior
GET {{base_url}}/api/batch/download?file_ids={{batch_file_1_id}}
Authorization: Bearer {{frank_token}}

HTTP 404


# ════════════════════════════════════════════════════════════════════
# Phase 3B — Alice grants frank Viewer. Read endpoints succeed;
#            mutating batch ops still all-fail.
# ════════════════════════════════════════════════════════════════════
POST {{base_url}}/api/grants
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{
  "subject":  { "type": "user",   "id": "{{frank_user_id}}" },
  "resource": { "type": "folder", "id": "{{batch_root_id}}" },
  "role": "viewer"
}

HTTP 201

# get_files succeeds (Read cascades to all descendants)
POST {{base_url}}/api/batch/files/get
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "file_ids": ["{{batch_file_1_id}}", "{{batch_file_2_id}}"] }

HTTP 200
[Asserts]
jsonpath "$.stats.successful" == 2
jsonpath "$.stats.failed" == 0

# get_folders succeeds
POST {{base_url}}/api/batch/folders/get
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "folder_ids": ["{{batch_sub_a_id}}", "{{batch_sub_b_id}}"] }

HTTP 200
[Asserts]
jsonpath "$.stats.successful" == 2

# Download POST as Viewer — succeeds (Read sufficient)
POST {{base_url}}/api/batch/download
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "file_ids": ["{{batch_file_1_id}}", "{{batch_file_2_id}}"], "folder_ids": [] }

HTTP 200
[Asserts]
header "Content-Type" == "application/zip"

# Download GET — same
GET {{base_url}}/api/batch/download?file_ids={{batch_file_1_id}},{{batch_file_2_id}}
Authorization: Bearer {{frank_token}}

HTTP 200
[Asserts]
header "Content-Type" == "application/zip"

# Mutations still rejected
POST {{base_url}}/api/batch/files/move
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "file_ids": ["{{batch_file_1_id}}"], "target_folder_id": "{{batch_sub_a_id}}" }

HTTP 400

POST {{base_url}}/api/batch/files/delete
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "file_ids": ["{{batch_file_1_id}}"] }

HTTP 400


# ════════════════════════════════════════════════════════════════════
# Phase 3C — Promote frank to Editor (read + comment + create + update).
#            Move + copy + create succeed; delete still fails.
# ════════════════════════════════════════════════════════════════════
PUT {{base_url}}/api/grants/role
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{
  "subject":  { "type": "user",   "id": "{{frank_user_id}}" },
  "resource": { "type": "folder", "id": "{{batch_root_id}}" },
  "role": "editor"
}

HTTP 200

# Batch folder create (Create on parent)
POST {{base_url}}/api/batch/folders/create
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{
  "folders": [
    { "name": "frank-batch-1", "parent_id": "{{batch_root_id}}" },
    { "name": "frank-batch-2", "parent_id": "{{batch_root_id}}" }
  ]
}

HTTP 201
[Asserts]
jsonpath "$.stats.successful" == 2

# Batch file move (Update on file + Create on target)
POST {{base_url}}/api/batch/files/move
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "file_ids": ["{{batch_file_1_id}}"], "target_folder_id": "{{batch_sub_a_id}}" }

HTTP 200
[Asserts]
jsonpath "$.stats.successful" == 1

# Batch file copy (Read on src + Create on dst)
POST {{base_url}}/api/batch/files/copy
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "file_ids": ["{{batch_file_2_id}}"], "target_folder_id": "{{batch_sub_b_id}}" }

HTTP 200
[Asserts]
jsonpath "$.stats.successful" == 1

# Batch folder move
POST {{base_url}}/api/batch/folders/move
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "folder_ids": ["{{batch_sub_a_id}}"], "target_folder_id": "{{batch_sub_b_id}}" }

HTTP 200

# Batch folder copy — copy sub_a (now nested inside sub_b after the
# move above) back to batch_root. Avoids name collision with the
# existing sub_b at the root.
POST {{base_url}}/api/batch/folders/copy
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "folder_ids": ["{{batch_sub_a_id}}"], "target_folder_id": "{{batch_root_id}}" }

HTTP 200

# Batch delete still denied (Editor excludes Delete)
POST {{base_url}}/api/batch/files/delete
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "file_ids": ["{{batch_file_2_id}}"] }

HTTP 400

POST {{base_url}}/api/batch/folders/delete
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "folder_ids": ["{{batch_sub_b_id}}"], "recursive": true }

HTTP 400


# ════════════════════════════════════════════════════════════════════
# Phase 3D — Promote frank to Admin. Delete + trash succeed.
# ════════════════════════════════════════════════════════════════════
PUT {{base_url}}/api/grants/role
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{
  "subject":  { "type": "user",   "id": "{{frank_user_id}}" },
  "resource": { "type": "folder", "id": "{{batch_root_id}}" },
  "role": "admin"
}

HTTP 200

# Frank (Admin grant = Delete) trashes batch_file_2 — item goes to
# Alice's trash because file.user_id is unchanged (Alice is still owner).
POST {{base_url}}/api/batch/trash
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "file_ids": ["{{batch_file_2_id}}"], "folder_ids": [] }

HTTP 200
[Asserts]
jsonpath "$.stats.successful" == 1

# Alice trashes batch_file_1 (which frank moved into batch_sub_a in Phase 3C).
POST {{base_url}}/api/batch/trash
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{ "file_ids": ["{{batch_file_1_id}}"], "folder_ids": [] }

HTTP 200
[Asserts]
jsonpath "$.stats.successful" == 1

# Batch permanent-delete a folder
POST {{base_url}}/api/batch/folders/delete
Authorization: Bearer {{frank_token}}
Content-Type: application/json
{ "folder_ids": ["{{batch_sub_b_id}}"], "recursive": true }

HTTP 200


# ════════════════════════════════════════════════════════════════════
# Phase 3E — Lifecycle cleanup. Alice deletes the batch-test root.
#            Trigger removes all of frank's grants.
# ════════════════════════════════════════════════════════════════════
DELETE {{base_url}}/api/folders/{{batch_root_id}}
Authorization: Bearer {{alice_token}}

HTTP 204

DELETE {{base_url}}/api/trash/empty
Authorization: Bearer {{alice_token}}

HTTP 200

GET {{base_url}}/api/grants/incoming
Authorization: Bearer {{frank_token}}

HTTP 200
[Asserts]
jsonpath "$" count == 0
