chore(test): add new API coverage
ensure better API test coverage on important routes 1. tests/api/public_shares.hurl — create a share token, verify, list contents, fetch a file, fetch a folder zip, then revoke and re-verify with the token. Same pattern as grants.hurl. ~30 min, biggest security ROI. 2. tests/api/auth_session_lifecycle.hurl — login → refresh → use new token → logout → refresh-rejected → login-again. Covers the session-family invalidation contract. 3. tests/api/admin_user_ops.hurl — admin disables / re-enables / changes role / resets password / sets quota for a fixture user. Five POSTs. 4. tests/api/groups_effective_members.hurl — nested groups: A contains B contains user X; effective-members returns X. Two scenarios, but it's the ReBAC contract under the Drive refactor. 5. tests/api/search_basic.hurl — upload foo.txt, search "foo", get the result; cross-user: bob can't search alice's foo.
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
# =============================================================
|
||||
# OxiCloud — Baseline: admin user-mutation surface
|
||||
# =============================================================
|
||||
# Exercises the cluster of admin-only `PUT /api/admin/users/{id}/*`
|
||||
# endpoints that operators rely on for incident response:
|
||||
# - disable a compromised account
|
||||
# - rotate its password
|
||||
# - change its role
|
||||
# - set / clear its quota
|
||||
#
|
||||
# Each is a one-shot mutation, but the failure mode of any one
|
||||
# is severe (operator can't lock out an attacker, can't reset a
|
||||
# password). Pinning them together keeps the cluster intact
|
||||
# under refactors.
|
||||
#
|
||||
# Coverage:
|
||||
# 1. Admin creates a fresh fixture user via POST /api/admin/users
|
||||
# 2. Fixture user logs in successfully (baseline)
|
||||
# 3. PUT /quota → fixture user's /me reports updated quota
|
||||
# 4. PUT /role → fixture user becomes admin
|
||||
# 5. PUT /password (admin reset) → old password no longer works,
|
||||
# new password works
|
||||
# 6. PUT /active=false → fixture user login → 403
|
||||
# 7. PUT /active=true → fixture user login works again
|
||||
# 8. Cleanup via DELETE /api/admin/users/{id}
|
||||
# =============================================================
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Setup — admin login
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
{ "username": "{{username}}", "password": "{{password}}" }
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
admin_token: jsonpath "$.access_token"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 1 — Admin creates the fixture user "charlie-ops"
|
||||
# Uses a name that doesn't collide with charlie in
|
||||
# registration.hurl (which uses just "charlie"), so this
|
||||
# file is order-independent.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/admin/users
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"username": "charlie-ops",
|
||||
"password": "OriginalPassword1!",
|
||||
"email": "charlie-ops@example.com",
|
||||
"role": "user"
|
||||
}
|
||||
|
||||
HTTP 201
|
||||
[Captures]
|
||||
charlie_id: jsonpath "$.id"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 2 — Baseline: fixture user can log in with the password
|
||||
# admin assigned.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
{ "username": "charlie-ops", "password": "OriginalPassword1!" }
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
charlie_token_v1: jsonpath "$.access_token"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 3 — Set a non-default quota; charlie's own /me must reflect it.
|
||||
# 200 MiB = 209715200 bytes — keeps the assertion exact while
|
||||
# still being a believable per-user cap.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PUT {{base_url}}/api/admin/users/{{charlie_id}}/quota
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: application/json
|
||||
{ "quota_bytes": 209715200 }
|
||||
|
||||
HTTP 200
|
||||
|
||||
GET {{base_url}}/api/auth/me
|
||||
Authorization: Bearer {{charlie_token_v1}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$.storage_quota_bytes" == 209715200
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 4 — Promote charlie to admin. After this the /me payload's
|
||||
# role field must reflect the change.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PUT {{base_url}}/api/admin/users/{{charlie_id}}/role
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: application/json
|
||||
{ "role": "admin" }
|
||||
|
||||
HTTP 200
|
||||
|
||||
GET {{base_url}}/api/auth/me
|
||||
Authorization: Bearer {{charlie_token_v1}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$.role" == "admin"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 5 — Admin resets charlie's password.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PUT {{base_url}}/api/admin/users/{{charlie_id}}/password
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: application/json
|
||||
{ "new_password": "AdminResetPassword2!" }
|
||||
|
||||
HTTP 200
|
||||
|
||||
# Old password no longer works. Login failures map to 403
|
||||
# (AccessDenied) in this codebase — both "invalid credentials"
|
||||
# and "account deactivated" go through the same error kind.
|
||||
POST {{base_url}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
{ "username": "charlie-ops", "password": "OriginalPassword1!" }
|
||||
|
||||
HTTP 403
|
||||
|
||||
# New password works.
|
||||
POST {{base_url}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
{ "username": "charlie-ops", "password": "AdminResetPassword2!" }
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
charlie_token_v2: jsonpath "$.access_token"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 6 — Disable the account. The next login attempt must report
|
||||
# 403 (account disabled) — distinct from 401 (bad creds)
|
||||
# so operators can tell "I locked you out" from "you typed
|
||||
# the wrong password".
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PUT {{base_url}}/api/admin/users/{{charlie_id}}/active
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: application/json
|
||||
{ "active": false }
|
||||
|
||||
HTTP 200
|
||||
|
||||
POST {{base_url}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
{ "username": "charlie-ops", "password": "AdminResetPassword2!" }
|
||||
|
||||
HTTP 403
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 7 — Re-enable; login works again.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PUT {{base_url}}/api/admin/users/{{charlie_id}}/active
|
||||
Authorization: Bearer {{admin_token}}
|
||||
Content-Type: application/json
|
||||
{ "active": true }
|
||||
|
||||
HTTP 200
|
||||
|
||||
POST {{base_url}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
{ "username": "charlie-ops", "password": "AdminResetPassword2!" }
|
||||
|
||||
HTTP 200
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# 8 — Teardown
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
DELETE {{base_url}}/api/admin/users/{{charlie_id}}
|
||||
Authorization: Bearer {{admin_token}}
|
||||
|
||||
HTTP 200
|
||||
Reference in New Issue
Block a user