285 lines
14 KiB
Plaintext
285 lines
14 KiB
Plaintext
|
|
# =============================================================
|
||
|
|
# OxiCloud — NFC normalization on write (regression pin)
|
||
|
|
# =============================================================
|
||
|
|
# Regression pin for AtalayaLabs/OxiCloud#706. The bug: macOS Finder
|
||
|
|
# and other NFD-emitting clients uploaded folder names in decomposed
|
||
|
|
# form ("à" = "a" + U+0300 combining grave, 2 codepoints), which
|
||
|
|
# landed raw in storage.folders.name / storage.files.name. Clients
|
||
|
|
# doing NFC-normalized lookups (NextCloud desktop, DAVX5, well-
|
||
|
|
# behaved sync clients — the exact clients the reporter used) then
|
||
|
|
# failed to descend into or match their own uploads.
|
||
|
|
#
|
||
|
|
# The fix normalizes at the repository layer (folder_db_repository,
|
||
|
|
# file_blob_write_repository) so every write surface — REST, WebDAV,
|
||
|
|
# NextCloud DAV, batch, chunked, WOPI-fallback — is covered at one
|
||
|
|
# choke point. WebDAV/NC MKCOL/PUT handlers additionally emit
|
||
|
|
# `Content-Location` (RFC 7231 §3.1.4.2) when canonicalisation
|
||
|
|
# actually changed the URL, so well-behaved clients update their
|
||
|
|
# local index immediately without waiting for the next PROPFIND
|
||
|
|
# cycle.
|
||
|
|
#
|
||
|
|
# This file pins the three critical write paths from the reporter's
|
||
|
|
# scenario:
|
||
|
|
# 1. REST `POST /api/folders` — the browser-side upload path
|
||
|
|
# 2. WebDAV `MKCOL` — davfs / macOS Finder
|
||
|
|
# 3. NextCloud `MKCOL` — NC desktop client / DAVX5
|
||
|
|
#
|
||
|
|
# For each: post NFD, expect the DB row to hold NFC, and for the
|
||
|
|
# DAV surfaces expect `Content-Location` pointing at the canonical
|
||
|
|
# NFC URL, plus a follow-up NFC-URL lookup that succeeds (proving
|
||
|
|
# the two clients-and-server sides agree on the canonical form
|
||
|
|
# after the fix).
|
||
|
|
#
|
||
|
|
# Byte encoding conventions used below:
|
||
|
|
# * NFD `à` = U+0061 U+0300 → UTF-8 `61 CC 80` → URL `a%CC%80`
|
||
|
|
# * NFC `à` = U+00E0 → UTF-8 `C3 A0` → URL `%C3%A0`
|
||
|
|
# JSON bodies use `̀` (JSON-standard Unicode escape, always
|
||
|
|
# interpreted by the server's JSON parser). Assertions use Hurl's
|
||
|
|
# `\u{HHHH}` escape for the expected NFC codepoint.
|
||
|
|
# =============================================================
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
# Step 1 — Admin login.
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
POST {{base_url}}/api/auth/login
|
||
|
|
Content-Type: application/json
|
||
|
|
{ "username": "{{username}}", "password": "{{password}}" }
|
||
|
|
|
||
|
|
HTTP 200
|
||
|
|
[Captures]
|
||
|
|
admin_token: jsonpath "$.access_token"
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
# Step 2 — Resolve admin's home folder id (the default parent
|
||
|
|
# for REST folder create when no parent_id is supplied, but we
|
||
|
|
# pass it explicitly so this test doesn't depend on the auto-
|
||
|
|
# resolve fallback path).
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
GET {{base_url}}/api/folders
|
||
|
|
Authorization: Bearer {{admin_token}}
|
||
|
|
|
||
|
|
HTTP 200
|
||
|
|
[Captures]
|
||
|
|
home_id: jsonpath "$[0].id"
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
# Step 3 — REST `POST /api/folders` with an NFD name in the
|
||
|
|
# JSON body. The name field carries "nfc-rest-à" — that
|
||
|
|
# is the 11-byte NFD form ("nfc-rest-a" + U+0300). Post-fix,
|
||
|
|
# the repo NFC-normalizes at bind time, so the returned name
|
||
|
|
# must be the 10-byte NFC form "nfc-rest-\u{00e0}".
|
||
|
|
#
|
||
|
|
# Pre-fix: the response would echo the NFD input verbatim
|
||
|
|
# (`nfc-rest-à`), the DB would store 11 bytes, and a
|
||
|
|
# subsequent PROPFIND from an NFC-normalizing client would
|
||
|
|
# miss. That's the class of bug #706 reports.
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
POST {{base_url}}/api/folders
|
||
|
|
Authorization: Bearer {{admin_token}}
|
||
|
|
Content-Type: application/json
|
||
|
|
{
|
||
|
|
"name": "nfc-rest-à",
|
||
|
|
"parent_id": "{{home_id}}"
|
||
|
|
}
|
||
|
|
|
||
|
|
HTTP 201
|
||
|
|
[Captures]
|
||
|
|
rest_folder_id: jsonpath "$.id"
|
||
|
|
[Asserts]
|
||
|
|
# The stored (and returned) name must be NFC. If this fails, the
|
||
|
|
# repo-level normalize call was skipped or bypassed by a new code
|
||
|
|
# path — see folder_db_repository::create_folder.
|
||
|
|
jsonpath "$.name" == "nfc-rest-\u{00e0}"
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
# Step 4 — Follow-up GET verifies the DB persisted NFC (not just
|
||
|
|
# that the create response happened to canonicalise before
|
||
|
|
# echoing). Reads from the same row a client's PROPFIND would.
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
GET {{base_url}}/api/folders/{{rest_folder_id}}
|
||
|
|
Authorization: Bearer {{admin_token}}
|
||
|
|
|
||
|
|
HTTP 200
|
||
|
|
[Asserts]
|
||
|
|
jsonpath "$.name" == "nfc-rest-\u{00e0}"
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
# Step 5 — Cleanup the REST-created folder before moving on to
|
||
|
|
# the DAV surfaces. Ed's memory feedback_hurl_teardown_shared_db:
|
||
|
|
# hurl files share the DB across the suite; each file must clean
|
||
|
|
# up what it created.
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
DELETE {{base_url}}/api/folders/{{rest_folder_id}}
|
||
|
|
Authorization: Bearer {{admin_token}}
|
||
|
|
|
||
|
|
HTTP *
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
# Step 6 — WebDAV `MKCOL` with an NFD folder name in the URL
|
||
|
|
# path. Sends `nfc-dav-a%CC%80/` — the URL-encoded NFD form
|
||
|
|
# ("nfc-dav-a" + %CC%80 for U+0300). Post-fix, the handler
|
||
|
|
# canonicalises and emits `Content-Location` pointing at the
|
||
|
|
# NFC URL. Naive clients ignore the header (status stays 201);
|
||
|
|
# well-behaved clients update their local index to the
|
||
|
|
# canonical URL immediately.
|
||
|
|
#
|
||
|
|
# Bare `/webdav/<name>/` (no `@drive/<selector>/` prefix) maps
|
||
|
|
# to the caller's default drive contents — matches the shape
|
||
|
|
# `webdav_drive_root.hurl` Step 4 documents. Simpler than the
|
||
|
|
# picker, and covers the same code path (`handle_mkcol` runs
|
||
|
|
# either way; the last URL segment is what the normalize
|
||
|
|
# operates on).
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
MKCOL {{base_url}}/webdav/nfc-dav-a%CC%80/
|
||
|
|
Authorization: Bearer {{admin_token}}
|
||
|
|
|
||
|
|
HTTP 201
|
||
|
|
[Asserts]
|
||
|
|
# The canonical URL substitutes the NFC form (%C3%A0) for the
|
||
|
|
# NFD segment in the request. If Content-Location is missing or
|
||
|
|
# still contains %CC%80, either the handler skipped the
|
||
|
|
# normalize-and-diff or repo-level canonicalization didn't fire.
|
||
|
|
header "Content-Location" contains "%C3%A0"
|
||
|
|
header "Content-Location" not contains "%CC%80"
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
# Step 7 — PROPFIND on the CANONICAL (NFC) URL confirms the
|
||
|
|
# folder is reachable there. This is what a well-behaved sync
|
||
|
|
# client does on its next cycle after consuming Content-Location
|
||
|
|
# — and what the reporter's macOS/Android clients were doing
|
||
|
|
# already, hence their failure to find their own NFD uploads.
|
||
|
|
# Post-fix, this must return a matching 207.
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
PROPFIND {{base_url}}/webdav/nfc-dav-%C3%A0/
|
||
|
|
Authorization: Bearer {{admin_token}}
|
||
|
|
Depth: 0
|
||
|
|
Content-Type: application/xml
|
||
|
|
```
|
||
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
||
|
|
<D:propfind xmlns:D="DAV:">
|
||
|
|
<D:prop><D:displayname/><D:resourcetype/></D:prop>
|
||
|
|
</D:propfind>
|
||
|
|
```
|
||
|
|
|
||
|
|
HTTP 207
|
||
|
|
[Asserts]
|
||
|
|
# The response body must reference the canonical URL exactly.
|
||
|
|
body contains "nfc-dav-%C3%A0"
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
# Step 8 — PROPFIND on the ORIGINAL (NFD) URL returns 404. The
|
||
|
|
# server does not maintain a legacy-NFD-alias for post-fix rows
|
||
|
|
# — the canonical URL is the only one that resolves. This pins
|
||
|
|
# the intended one-way behaviour (write NFD → stored NFC →
|
||
|
|
# only NFC URL matches), which is exactly what NFC-normalizing
|
||
|
|
# clients want.
|
||
|
|
#
|
||
|
|
# (Pre-existing NFD rows in the DB, deliberately not touched by
|
||
|
|
# this fix per operator decision, remain reachable via their
|
||
|
|
# NFD URL. That's a separate scenario — historic content, not
|
||
|
|
# the write-time regression this file covers.)
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
PROPFIND {{base_url}}/webdav/nfc-dav-a%CC%80/
|
||
|
|
Authorization: Bearer {{admin_token}}
|
||
|
|
Depth: 0
|
||
|
|
Content-Type: application/xml
|
||
|
|
```
|
||
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
||
|
|
<D:propfind xmlns:D="DAV:">
|
||
|
|
<D:prop><D:resourcetype/></D:prop>
|
||
|
|
</D:propfind>
|
||
|
|
```
|
||
|
|
|
||
|
|
HTTP 404
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
# Step 9 — Cleanup the WebDAV-created folder.
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
DELETE {{base_url}}/webdav/nfc-dav-%C3%A0/
|
||
|
|
Authorization: Bearer {{admin_token}}
|
||
|
|
|
||
|
|
HTTP *
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
# Step 10 — Mint an app-password for NextCloud Basic Auth.
|
||
|
|
#
|
||
|
|
# NextCloud DAV endpoints (`/remote.php/dav/…`) never accept a
|
||
|
|
# plain JWT — they use HTTP Basic Auth with an app-password,
|
||
|
|
# same pattern as every existing `nc_*.hurl` test. Mint one
|
||
|
|
# here so steps 11-13 below can authenticate.
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
POST {{base_url}}/api/auth/app-passwords
|
||
|
|
Authorization: Bearer {{admin_token}}
|
||
|
|
Content-Type: application/json
|
||
|
|
{ "label": "nfc_normalization hurl test" }
|
||
|
|
|
||
|
|
HTTP 200
|
||
|
|
[Captures]
|
||
|
|
nc_user: jsonpath "$.username"
|
||
|
|
nc_pw: jsonpath "$.password"
|
||
|
|
nc_pw_id: jsonpath "$.id"
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
# Step 11 — NextCloud `MKCOL` with NFD name in the URL path.
|
||
|
|
# Same shape as WebDAV MKCOL but on the /remote.php/dav/files/
|
||
|
|
# surface — this is the code path macOS-based NC desktop
|
||
|
|
# clients hit. Same Content-Location contract.
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
MKCOL {{base_url}}/remote.php/dav/files/{{username}}/nfc-nc-a%CC%80/
|
||
|
|
[BasicAuth]
|
||
|
|
{{nc_user}}: {{nc_pw}}
|
||
|
|
|
||
|
|
HTTP 201
|
||
|
|
[Asserts]
|
||
|
|
header "Content-Location" contains "%C3%A0"
|
||
|
|
header "Content-Location" not contains "%CC%80"
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
# Step 12 — PROPFIND via NextCloud DAV on the canonical URL.
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/nfc-nc-%C3%A0/
|
||
|
|
Depth: 0
|
||
|
|
Content-Type: application/xml
|
||
|
|
[BasicAuth]
|
||
|
|
{{nc_user}}: {{nc_pw}}
|
||
|
|
```
|
||
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
||
|
|
<D:propfind xmlns:D="DAV:">
|
||
|
|
<D:prop><D:displayname/><D:resourcetype/></D:prop>
|
||
|
|
</D:propfind>
|
||
|
|
```
|
||
|
|
|
||
|
|
HTTP 207
|
||
|
|
[Asserts]
|
||
|
|
body contains "nfc-nc-%C3%A0"
|
||
|
|
|
||
|
|
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
# Step 13 — Cleanup the NC-created folder + retire the
|
||
|
|
# app-password so this test file leaves no side effects
|
||
|
|
# behind (per feedback_hurl_teardown_shared_db).
|
||
|
|
# ─────────────────────────────────────────────────────────────
|
||
|
|
DELETE {{base_url}}/remote.php/dav/files/{{username}}/nfc-nc-%C3%A0/
|
||
|
|
[BasicAuth]
|
||
|
|
{{nc_user}}: {{nc_pw}}
|
||
|
|
|
||
|
|
HTTP *
|
||
|
|
|
||
|
|
|
||
|
|
DELETE {{base_url}}/api/auth/app-passwords/{{nc_pw_id}}
|
||
|
|
Authorization: Bearer {{admin_token}}
|
||
|
|
|
||
|
|
HTTP *
|