Files
Oxicloud/tests/api/auth_session_lifecycle.hurl
T
2026-08-21 23:56:25 +02:00

135 lines
6.8 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 "$.full.user.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