feat(drive): personal drives have the user's quota in commun
This commit is contained in:
+2
-1
@@ -162,7 +162,8 @@ hurl --variables-file "$API_DIR/test.env" --file-root "$REPO_ROOT/tests" --test
|
||||
"$API_DIR/nc_auth_failures.hurl" \
|
||||
"$API_DIR/dedup_create.hurl" \
|
||||
"$API_DIR/trash_per_drive.hurl" \
|
||||
"$API_DIR/drive_quota.hurl"
|
||||
"$API_DIR/drive_quota.hurl" \
|
||||
"$API_DIR/user_envelope_quota.hurl"
|
||||
|
||||
#bash "$API_DIR/dedup_bulk_upload.sh"
|
||||
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
# =============================================================
|
||||
# OxiCloud – User envelope quota (sum of personal drives)
|
||||
# =============================================================
|
||||
# Run:
|
||||
# hurl --variables-file tests/api/test.env --file-root tests \
|
||||
# --test tests/api/user_envelope_quota.hurl
|
||||
#
|
||||
# The model under test (`docs/plan/drive.md` §7):
|
||||
# `auth.users.storage_quota_bytes` caps the SUM of `used_bytes`
|
||||
# across the user's PERSONAL drives only. Shared drives never
|
||||
# count against any user envelope.
|
||||
#
|
||||
# Cases:
|
||||
# 1. Baseline — fresh user: `/me.storage_used_bytes == 0`.
|
||||
# 2. Shared-drive upload does NOT touch the envelope —
|
||||
# `/me.storage_used_bytes` stays 0 after upload + sweep.
|
||||
# 3. Personal-drive upload DOES bump the envelope —
|
||||
# `/me.storage_used_bytes == file_size` after upload + sweep.
|
||||
# 4. Sweep self-heals — after trashing the personal file and
|
||||
# `trigger-sweep`, `/me.storage_used_bytes` returns to 0.
|
||||
#
|
||||
# `trigger-sweep` is the deterministic synchronisation point:
|
||||
# it runs the drive-side sweep then the user-side sweep
|
||||
# (`StorageUsageService::start_reconciliation_job`), so both
|
||||
# cached counters are authoritative ground-truth by the time
|
||||
# the assertion fires. Gated by
|
||||
# `OXICLOUD_ENABLE_ADMIN_INTERNAL_ENDPOINTS=true`
|
||||
# (set in `tests/common/server.env`).
|
||||
#
|
||||
# Self-contained: provisions `ue_owner` so it can run alongside
|
||||
# the rest of the suite.
|
||||
# =============================================================
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 1 — Admin login.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
{ "username": "{{username}}", "password": "{{password}}" }
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
admin_token: jsonpath "$.access_token"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 2 — Provision `ue_owner` (user envelope under test).
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/admin/users
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"username": "ue_owner",
|
||||
"password": "UeOwnerPwd1!",
|
||||
"email": "ue_owner@example.com",
|
||||
"role": "user"
|
||||
}
|
||||
|
||||
HTTP 201
|
||||
|
||||
POST {{base_url}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
{ "username": "ue_owner", "password": "UeOwnerPwd1!" }
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
owner_token: jsonpath "$.access_token"
|
||||
owner_user_id: jsonpath "$.user.id"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 3 — Fetch the user's default Personal drive root folder.
|
||||
# `GET /api/folders` returns root folders for the
|
||||
# caller; for a fresh user that's a single entry — the
|
||||
# Personal drive's root provisioned by
|
||||
# `PersonalDriveLifecycleHook`.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/api/folders
|
||||
Authorization: Bearer {{owner_token}}
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
personal_root_id: jsonpath "$[0].id"
|
||||
[Asserts]
|
||||
jsonpath "$" count == 1
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 4 — Baseline. Fresh user's envelope is zero.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
GET {{base_url}}/api/auth/me
|
||||
Authorization: Bearer {{owner_token}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$.storage_used_bytes" == 0
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 5 — Admin creates a shared drive with `ue_owner` as
|
||||
# direct user-Owner. No per-drive quota (NULL = unlim).
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/drives
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"kind": "shared",
|
||||
"name": "ue-shared",
|
||||
"owner": { "type": "user", "id": "{{owner_user_id}}" }
|
||||
}
|
||||
|
||||
HTTP 201
|
||||
[Captures]
|
||||
shared_drive_id: jsonpath "$.id"
|
||||
shared_root_id: jsonpath "$.root_folder_id"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 6 — Case 2: upload hello.txt (32 B) to the SHARED drive.
|
||||
# The drive's `used_bytes` will move; the user envelope
|
||||
# must NOT.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/files/upload
|
||||
Authorization: Bearer {{owner_token}}
|
||||
[MultipartFormData]
|
||||
folder_id: {{shared_root_id}}
|
||||
file: file,fixtures/hello.txt; text/plain
|
||||
|
||||
HTTP 201
|
||||
|
||||
|
||||
# Wait for the drive-side fire-and-forget delta to settle.
|
||||
# Acts as the synchronisation point: by the time `drives.used_bytes`
|
||||
# reflects the upload, the sibling user-side delta task spawned in
|
||||
# the same call has had its chance to run too.
|
||||
GET {{base_url}}/api/drives
|
||||
Authorization: Bearer {{owner_token}}
|
||||
[Options]
|
||||
retry: 10
|
||||
retry-interval: 200ms
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$[?(@.id=='{{shared_drive_id}}')].used_bytes" == 32
|
||||
|
||||
|
||||
# Force the user-side sweep to run, authoritative ground-truth.
|
||||
# If the delta path incorrectly fired the user counter, the sweep
|
||||
# would still correct it back to 0 (the new SQL excludes shared
|
||||
# drives) — this also validates the sweep formula.
|
||||
POST {{base_url}}/api/admin/internal/trigger-sweep
|
||||
Authorization: Bearer {{admin_token}}
|
||||
|
||||
HTTP 200
|
||||
|
||||
|
||||
# Envelope untouched by the shared upload. Both delta path and
|
||||
# sweep path agree on `0` for a user with no personal-drive
|
||||
# content.
|
||||
GET {{base_url}}/api/auth/me
|
||||
Authorization: Bearer {{owner_token}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$.storage_used_bytes" == 0
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 7 — Case 3: upload hello.txt (32 B) to the user's own
|
||||
# default Personal drive. The envelope MUST move now.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/files/upload
|
||||
Authorization: Bearer {{owner_token}}
|
||||
[MultipartFormData]
|
||||
folder_id: {{personal_root_id}}
|
||||
file: file,fixtures/hello.txt; text/plain
|
||||
|
||||
HTTP 201
|
||||
[Captures]
|
||||
personal_file_id: jsonpath "$.id"
|
||||
|
||||
|
||||
# Retry until the user-side delta lands. If the conditional-fire
|
||||
# logic is broken (delta never fires for personal), retries time
|
||||
# out at `0` and the test fails — this is the regression catch.
|
||||
GET {{base_url}}/api/auth/me
|
||||
Authorization: Bearer {{owner_token}}
|
||||
[Options]
|
||||
retry: 10
|
||||
retry-interval: 200ms
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$.storage_used_bytes" == 32
|
||||
|
||||
|
||||
# Confirm the sweep agrees with the delta — both code paths must
|
||||
# give the same number.
|
||||
POST {{base_url}}/api/admin/internal/trigger-sweep
|
||||
Authorization: Bearer {{admin_token}}
|
||||
|
||||
HTTP 200
|
||||
|
||||
GET {{base_url}}/api/auth/me
|
||||
Authorization: Bearer {{owner_token}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$.storage_used_bytes" == 32
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 8 — Case 4: trash + empty the personal file, then sweep.
|
||||
# Per-drive (and per-user) counters are NOT decremented
|
||||
# on delete (same design as the per-drive quota model);
|
||||
# the sweep is the correctness backstop. Asserts it
|
||||
# actually closes the drift back to 0.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
DELETE {{base_url}}/api/files/{{personal_file_id}}
|
||||
Authorization: Bearer {{owner_token}}
|
||||
|
||||
HTTP 204
|
||||
|
||||
DELETE {{base_url}}/api/trash/empty
|
||||
Authorization: Bearer {{owner_token}}
|
||||
|
||||
HTTP 200
|
||||
|
||||
POST {{base_url}}/api/admin/internal/trigger-sweep
|
||||
Authorization: Bearer {{admin_token}}
|
||||
|
||||
HTTP 200
|
||||
|
||||
GET {{base_url}}/api/auth/me
|
||||
Authorization: Bearer {{owner_token}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$.storage_used_bytes" == 0
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 9 — Cleanup. Deleting `ue_owner` cascades through
|
||||
# `default_for_user` (default Personal drive + root
|
||||
# folder + files) and removes the `role_grants` rows
|
||||
# tying them to the shared drive. The shared drive
|
||||
# itself is owned by admin (the creator) and gets
|
||||
# drained by `storage_cleanup_check.sh` later.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
DELETE {{base_url}}/api/admin/users/{{owner_user_id}}
|
||||
Authorization: Bearer {{admin_token}}
|
||||
|
||||
HTTP 200
|
||||
Reference in New Issue
Block a user