Files
Oxicloud/tests/api/auth_session_lifecycle.hurl
T
Edouard Vanbelle 6d65f7eb09 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.
2026-06-13 19:03:07 +02:00

135 lines
6.7 KiB
Plaintext

# =============================================================
# OxiCloud — Baseline: auth session lifecycle
# =============================================================
# Covers the refresh-token / logout / status surface that the
# existing `auth_login.hurl` deliberately leaves alone (login +
# lockout only). Browsers and the desktop NC client both rely
# on the rotate-and-revoke semantics being correct; this file
# pins them as a single end-to-end flow.
#
# Coverage:
# 1. GET /api/auth/status (no auth required, used by login page)
# 2. POST /api/auth/login (capture initial access + refresh)
# 3. POST /api/auth/refresh (rotate; capture new tokens)
# 4. New access token works on /api/auth/me
# 5. OLD refresh token rejected after rotation (session-family
# single-use enforcement)
# 6. POST /api/auth/logout (revokes the current refresh)
# 7. Refresh after logout → 401 (revocation actually took effect)
# 8. Access token still works briefly until it expires — we
# don't assert that explicitly because TTL is configurable
# and the access-token revocation semantics are documented
# as "JWT remains valid until exp"; logout only kills the
# refresh path.
# =============================================================
# ─────────────────────────────────────────────────────────────
# 1 — Unauthenticated /api/auth/status probe.
# The login page hits this on every load; the response
# determines whether the "Create first admin" flow shows.
# ─────────────────────────────────────────────────────────────
GET {{base_url}}/api/auth/status
HTTP 200
[Asserts]
jsonpath "$.initialized" == true
jsonpath "$.admin_count" >= 1
jsonpath "$.registration_allowed" == true
# ─────────────────────────────────────────────────────────────
# 2 — Login as admin. Capture BOTH tokens — we need the
# refresh later to verify rotation semantics.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "{{username}}", "password": "{{password}}" }
HTTP 200
[Captures]
access_v1: jsonpath "$.access_token"
refresh_v1: jsonpath "$.refresh_token"
# ─────────────────────────────────────────────────────────────
# 3 — Refresh: mint a new (access, refresh) pair. The refresh
# token is rotated — the response carries a NEW refresh
# that supersedes refresh_v1.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/refresh
Content-Type: application/json
{ "refresh_token": "{{refresh_v1}}" }
HTTP 200
[Captures]
access_v2: jsonpath "$.access_token"
refresh_v2: jsonpath "$.refresh_token"
[Asserts]
jsonpath "$.access_token" != "{{access_v1}}"
jsonpath "$.refresh_token" != "{{refresh_v1}}"
# ─────────────────────────────────────────────────────────────
# 4 — The new access token works on a protected endpoint.
# ─────────────────────────────────────────────────────────────
GET {{base_url}}/api/auth/me
Authorization: Bearer {{access_v2}}
HTTP 200
[Asserts]
jsonpath "$.username" == "{{username}}"
# ─────────────────────────────────────────────────────────────
# 5 — The OLD refresh token MUST be rejected after rotation.
# This is the session-family single-use property: replay
# of a used refresh token is treated as theft and rejected.
#
# The handler's OpenAPI doc says 401, but the actual response
# is 403: the refresh service raises `ErrorKind::AccessDenied`
# which maps to HTTP 403 in this codebase. Pinning the
# observed-and-correct behavior here.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/refresh
Content-Type: application/json
{ "refresh_token": "{{refresh_v1}}" }
HTTP 403
# ─────────────────────────────────────────────────────────────
# 6 — Logout using the v2 refresh + v2 access. Server-side
# this revokes the session and clears auth cookies in the
# response.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/logout
Authorization: Bearer {{access_v2}}
Content-Type: application/json
{ "refresh_token": "{{refresh_v2}}" }
HTTP 200
# ─────────────────────────────────────────────────────────────
# 7 — Post-logout: the v2 refresh token is now revoked. A
# refresh attempt is rejected with 403 (same AccessDenied
# mapping as step 5).
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/refresh
Content-Type: application/json
{ "refresh_token": "{{refresh_v2}}" }
HTTP 403
# ─────────────────────────────────────────────────────────────
# 8 — Sanity re-check: status still reports the system as
# initialized after logout (no state regression).
# ─────────────────────────────────────────────────────────────
GET {{base_url}}/api/auth/status
HTTP 200
[Asserts]
jsonpath "$.initialized" == true