Files
Oxicloud/tests/api/registration.hurl
T
Edouard Vanbelle 9a49ab44d8 feat(passwordless): pass3: passwordless account (via emailed magic-link)
Backend
  - RegisterDto — username and password both become Option<String> with #[serde(default)] so JSON can omit them entirely.
  - AuthApplicationService::register — username uniqueness check skipped when None (multiple NULLs OK under the UNIQUE index); password hashing skipped when None; User::new called with the actual Options instead of forcing Some(...).
  - auth_handler::register — branches on dto.password.is_none(). With password → existing 201 + UserDto. Without → triggers MagicLinkInviteService::send_login_link(&email) best-effort, then returns 200 + {"message": "Check your email…"}. The
  OIDC-mode-disables-password-registration gate now only fires for the password path (email-only signup is still allowed even in OIDC-only mode, because it doesn't store a password).
  - magic_link_handler::redirect_target — new 3-way decision tree:
    - Resource target (folder invitation) → /#/files/folder/{id} (existing)
    - NULL resource + is_external = false → /#/files (the welcome path for new internal users — they have a home folder)
    - NULL resource + is_external = true → /#/sharedwithme (the existing external-user landing)

  Tests
  - New tests/api/registration.hurl with 9 requests covering: classic (with-password) register → 201 + UserDto, email-only register → 200 + uniform message + welcome magic-link captured, redemption → 302 to /#/files + cookies set, profile read → username
  absent + is_external: false, resend magic-link works (eligible while passwordless), cleanup deletes both new users.
  - Wired into tests/api/run.sh right after auth_login.hurl.

  Plan additions
  - auth-simplification.md gained PR 22 at the bottom of the PR sequence — device-bound magic-link redemption via challenge cookie + asymmetric TTLs (login: 10 min, invitation: 24 h). Full design recap, schema migration, config knobs
  (OXICLOUD_MAGIC_LINK_LOGIN_TTL_MINUTES / _INVITE_TTL_HOURS), and Hurl coverage outline are in the plan. Slots in before PR 21's docs so the architecture page describes the final state from the start.

  Checks — cargo fmt, cargo clippy --all-features --all-targets -- -D warnings, cargo test --lib (297 passed), biome, stylelint, tsc, full Hurl suite (16 files) all green.
2026-06-03 00:35:25 +02:00

138 lines
6.4 KiB
Plaintext

# =============================================================
# OxiCloud — email-only registration (PR 18)
# =============================================================
# PR 18 makes `password` (and `username`) optional in
# `POST /api/auth/register`. Email-only signup:
# - returns a uniform 200 message (no JWT, no UserDto)
# - mints a welcome magic-link mailed to `email`
# - redemption lands the new internal user on `/#/files`
# (not `/#/sharedwithme`, which is for externals)
#
# Requires `OXICLOUD_SMTP_MOCK=true` (set in tests/common/server.env).
# =============================================================
# ─────────────────────────────────────────────────────────────
# Step 1 — admin login (cleanup ops at the end need her token).
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "{{username}}", "password": "{{password}}" }
HTTP 200
[Captures]
alice_token: jsonpath "$.access_token"
# ─────────────────────────────────────────────────────────────
# Step 2 — Classic registration (with password) still works.
# Returns 201 + UserDto (existing behaviour, unchanged).
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/register
Content-Type: application/json
{
"username": "charlie",
"email": "charlie@example.com",
"password": "TestPassword1!"
}
HTTP 201
[Asserts]
jsonpath "$.username" == "charlie"
jsonpath "$.email" == "charlie@example.com"
jsonpath "$.is_external" == false
[Captures]
charlie_user_id: jsonpath "$.id"
# ─────────────────────────────────────────────────────────────
# Step 3 — Email-only registration. No username, no password.
# Returns 200 + uniform message; welcome magic-link
# is captured by the MockEmailSender.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/register
Content-Type: application/json
{
"email": "pr18-emailonly@example.com"
}
HTTP 200
[Asserts]
jsonpath "$.message" contains "sign-in link"
# ─────────────────────────────────────────────────────────────
# Step 4 — Capture the welcome mail + extract the magic-link.
# ─────────────────────────────────────────────────────────────
GET {{base_url}}/api/admin/smtp/test/captured?to=pr18-emailonly@example.com
Authorization: Bearer {{alice_token}}
HTTP 200
[Asserts]
jsonpath "$.to" == "pr18-emailonly@example.com"
jsonpath "$.text_body" matches "/magic/v1/[A-Za-z0-9_-]+"
[Captures]
pr18_magic_url: jsonpath "$.text_body" regex "(https?://[^\\s]+/magic/v1/[A-Za-z0-9_-]+)"
# ─────────────────────────────────────────────────────────────
# Step 5 — Redeem the welcome link. Internal user with no
# resource target → lands on `/#/files` (NOT
# `/#/sharedwithme`, which is the external-user
# landing).
# ─────────────────────────────────────────────────────────────
GET {{pr18_magic_url}}
HTTP 302
[Asserts]
header "Location" == "/#/files"
[Captures]
pr18_access_token: cookie "oxicloud_access"
# ─────────────────────────────────────────────────────────────
# Step 6 — The new user can read their own profile. After PR 18
# the username field is omitted (no handle claimed yet),
# and `is_external` is false (they're an internal user
# who signed up directly, not via invitation).
# ─────────────────────────────────────────────────────────────
GET {{base_url}}/api/auth/me
Authorization: Bearer {{pr18_access_token}}
HTTP 200
[Asserts]
jsonpath "$.email" == "pr18-emailonly@example.com"
jsonpath "$.is_external" == false
jsonpath "$.username" not exists
[Captures]
pr18_user_id: jsonpath "$.id"
# ─────────────────────────────────────────────────────────────
# Step 7 — Dave can request another magic-link (he has no
# password configured → eligible). Anti-enumeration
# 200 either way.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/magic-link/send
Content-Type: application/json
{ "email": "pr18-emailonly@example.com" }
HTTP 200
[Asserts]
jsonpath "$.message" contains "sign-in link"
# ─────────────────────────────────────────────────────────────
# Cleanup — admin deletes charlie + dave so the DB-clean sweep
# at run.sh end sees no stragglers.
# ─────────────────────────────────────────────────────────────
DELETE {{base_url}}/api/admin/users/{{charlie_user_id}}
Authorization: Bearer {{alice_token}}
HTTP *
DELETE {{base_url}}/api/admin/users/{{pr18_user_id}}
Authorization: Bearer {{alice_token}}
HTTP *