Files
Oxicloud/tests/api/lowercase_usernames.hurl
T
Edouard Vanbelle a95a6b106c feat(username): normalize username into lowercase
- normalize username into lowercase (this is already ASCII only)
- permit users to login with their username with insensitive case
- if a disabled account is reactivated and got a collision, it will normalize it too
- server will stop on collision (ex: 2 entries with `Alice` and `alice`)
  in a such case admin can run:

```
oxicloud migrate lowercase-usernames --dry-run
```
then
```
oxicloud migrate lowercase-usernames
```
2026-09-13 19:53:09 +02:00

161 lines
6.7 KiB
Plaintext

# =============================================================
# OxiCloud — usernames are case-insensitive (silent lowercase on ingest)
# =============================================================
# Pin for `docs/plan/username-lowercase.md`. The plan makes usernames
# case-insensitive by canonicalising to lowercase in
# `User::validate_username`. This file covers the wire surface:
#
# 1. Registration with a mixed-case username lands as lowercase.
# 2. Login by the ORIGINAL mixed-case string succeeds (server
# normalises on lookup).
# 3. Login by ALL-CAPS of the same name also succeeds.
# 4. Login by the canonical lowercase form succeeds.
# 5. Profile rename to a mixed-case name lands as lowercase.
# 6. NC Basic Auth accepts every case variant of the same account.
#
# Character-set + boundary rules (no leading dot, no `@`, etc.) are
# NOT re-tested here — that's the Rust unit-test surface. This file
# only pins the end-to-end normalization behaviour.
#
# Runs after `setup.hurl` (admin exists). Uses a self-contained user
# to avoid interfering with other scenarios.
# =============================================================
# ─────────────────────────────────────────────────────────────
# Step 1 — Register with a mixed-case username. Expect the server
# to silently store the lowercase form.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/register
Content-Type: application/json
{
"username": "MixedCaseUser",
"email": "mixedcase@example.com",
"password": "MixedCasePassword1!"
}
HTTP 200
# ─────────────────────────────────────────────────────────────
# Step 2 — Log in with the ORIGINAL mixed-case string. Server
# should normalise on lookup and accept.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "MixedCaseUser", "password": "MixedCasePassword1!" }
HTTP 200
[Asserts]
jsonpath "$.access_token" isString
# The `/auth/me` response inside the login reply exposes the canonical
# stored username. Post-migration, it MUST be lowercase regardless of
# what the caller typed at registration.
jsonpath "$.user.full.user.username" == "mixedcaseuser"
# ─────────────────────────────────────────────────────────────
# Step 3 — Log in with ALL-CAPS. Same account, different casing.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "MIXEDCASEUSER", "password": "MixedCasePassword1!" }
HTTP 200
[Asserts]
jsonpath "$.user.full.user.username" == "mixedcaseuser"
# ─────────────────────────────────────────────────────────────
# Step 4 — Log in with the canonical lowercase form. Same account.
# Capture the token for the profile-rename step below.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "mixedcaseuser", "password": "MixedCasePassword1!" }
HTTP 200
[Captures]
mixed_token: jsonpath "$.access_token"
[Asserts]
jsonpath "$.user.full.user.username" == "mixedcaseuser"
# ─────────────────────────────────────────────────────────────
# Step 5 — Rename via profile PATCH. New name is mixed-case; server
# must store it as lowercase. Same rule as registration, applied on
# the mutation path.
# ─────────────────────────────────────────────────────────────
PATCH {{base_url}}/api/auth/me
Authorization: Bearer {{mixed_token}}
Content-Type: application/json
{ "username": "RenamedTarget" }
HTTP 200
[Asserts]
# The response echoes the stored (canonical) form.
jsonpath "$.full.user.username" == "renamedtarget"
# Old (pre-rename) username no longer resolves — login fails 401.
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "MixedCaseUser", "password": "MixedCasePassword1!" }
HTTP 401
# New (post-rename) mixed-case login succeeds.
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "RENAMEDTARGET", "password": "MixedCasePassword1!" }
HTTP 200
[Asserts]
jsonpath "$.user.full.user.username" == "renamedtarget"
# ─────────────────────────────────────────────────────────────
# Step 6 — NextCloud Basic Auth accepts every case variant.
# The middleware lowercases the decoded username on the auth path,
# and `extract_url_user` lowercases the URL segment. A cached
# client URL like `.../dav/files/RenamedTarget` continues to work
# indefinitely across the migration.
#
# Uses PROPFIND `Depth: 0` on `/remote.php/dav/files/{user}/` — a
# well-formed request that touches Basic Auth + URL parse + chroot
# resolve in one hop. 207 Multi-Status is the expected success shape.
# ─────────────────────────────────────────────────────────────
PROPFIND {{base_url}}/remote.php/dav/files/renamedtarget/
Depth: 0
[BasicAuth]
renamedtarget: MixedCasePassword1!
HTTP 207
PROPFIND {{base_url}}/remote.php/dav/files/RenamedTarget/
Depth: 0
[BasicAuth]
RenamedTarget: MixedCasePassword1!
HTTP 207
PROPFIND {{base_url}}/remote.php/dav/files/RENAMEDTARGET/
Depth: 0
[BasicAuth]
RENAMEDTARGET: MixedCasePassword1!
HTTP 207
# Mixed case in URL, lowercase in Basic Auth — still works because
# both surfaces normalise before comparison.
PROPFIND {{base_url}}/remote.php/dav/files/RenamedTarget/
Depth: 0
[BasicAuth]
renamedtarget: MixedCasePassword1!
HTTP 207