Merge pull request #570 from swissiety/rfc-4331-quota-properties

feat(webdav): RFC 4331 quota-available-bytes/quota-used-bytes
This commit is contained in:
Dionisio Pozo
2026-07-14 12:58:22 +02:00
committed by GitHub
11 changed files with 622 additions and 37 deletions
+171
View File
@@ -0,0 +1,171 @@
# =============================================================
# OxiCloud — NC WebDAV quota properties (RFC 4331)
# =============================================================
# The NextCloud-compatible WebDAV surface
# (`interfaces/nextcloud/webdav_handler.rs`) previously had NO
# `d:quota-used-bytes`/`d:quota-available-bytes` support at all. This
# pins the new coverage added alongside the native surface's
# drive-awareness fix (`AppState::resolve_webdav_quota`):
#
# 1. Personal-drive PROPFIND (no drive marker in the Basic Auth
# username) reports the caller's account envelope.
# 2. A SHARED drive with its own finite quota reports THAT quota —
# not the owner's personal envelope — when addressed via the
# multi-drive POC's `{user}~{root_folder_id}` Basic Auth marker
# (see `basic_auth_middleware.rs` — the marker is the drive's
# ROOT FOLDER id, not the drive's own id).
# 3. quota-used-bytes on the shared drive increases after a PUT.
#
# This file always emits the full property set regardless of the
# PROPFIND request body (see `handle_propfind`'s doc comment — "the
# NC response always emits the full property set"), so no XML body
# is needed to request the props; a bare PROPFIND suffices.
# =============================================================
# ─────────────────────────────────────────────────────────────
# Step 1 — Admin login + mint admin's own NC app password (for
# the personal-envelope case).
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "{{username}}", "password": "{{password}}" }
HTTP 200
[Captures]
admin_token: jsonpath "$.access_token"
POST {{base_url}}/api/auth/app-passwords
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{ "label": "nc_webdav_quota_properties personal" }
HTTP 200
[Captures]
admin_nc_username: jsonpath "$.username"
admin_nc_password: jsonpath "$.password"
# ─────────────────────────────────────────────────────────────
# Step 2 — Personal-drive PROPFIND (no `~` marker) surfaces the
# account envelope. `>= 0` / `> 0` rather than exact
# numbers since admin's envelope already has content
# from earlier tests in the suite.
# ─────────────────────────────────────────────────────────────
PROPFIND {{base_url}}/remote.php/dav/files/{{username}}/
Depth: 0
[BasicAuth]
{{admin_nc_username}}: {{admin_nc_password}}
HTTP 207
[Asserts]
xpath "number(//*[local-name()='quota-used-bytes'])" >= 0
xpath "number(//*[local-name()='quota-available-bytes'])" > 0
# ─────────────────────────────────────────────────────────────
# Step 3 — Fresh user + a 500-byte shared drive owned by them.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/admin/users
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{
"username": "ncq_owner",
"password": "NcqOwnerPwd1!",
"email": "ncq_owner@example.com",
"role": "user"
}
HTTP 201
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "ncq_owner", "password": "NcqOwnerPwd1!" }
HTTP 200
[Captures]
ncq_owner_jwt: jsonpath "$.access_token"
ncq_owner_id: jsonpath "$.user.id"
POST {{base_url}}/api/auth/app-passwords
Authorization: Bearer {{ncq_owner_jwt}}
Content-Type: application/json
{ "label": "nc_webdav_quota_properties shared" }
HTTP 200
[Captures]
ncq_nc_username: jsonpath "$.username"
ncq_nc_password: jsonpath "$.password"
POST {{base_url}}/api/drives
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{
"kind": "shared",
"name": "ncq-shared",
"owner": { "type": "user", "id": "{{ncq_owner_id}}" },
"quota_bytes": 500
}
HTTP 201
[Captures]
ncq_root_folder_id: jsonpath "$.root_folder_id"
# ─────────────────────────────────────────────────────────────
# Step 4 — PROPFIND the shared drive via the multi-drive POC's
# `{user}~{root_folder_id}` Basic Auth marker. Brand-new
# drive → used == 0, available == quota exactly.
# ─────────────────────────────────────────────────────────────
PROPFIND {{base_url}}/remote.php/dav/files/{{ncq_nc_username}}~{{ncq_root_folder_id}}/
Depth: 0
[Options]
variable: ncq_composite_user={{ncq_nc_username}}~{{ncq_root_folder_id}}
[BasicAuth]
{{ncq_composite_user}}: {{ncq_nc_password}}
HTTP 207
[Asserts]
xpath "number(//*[local-name()='quota-used-bytes'])" == 0
xpath "number(//*[local-name()='quota-available-bytes'])" == 500
# ─────────────────────────────────────────────────────────────
# Step 5 — PUT a file into the shared drive; quota-used-bytes
# must reflect it, quota-available-bytes must shrink.
# ─────────────────────────────────────────────────────────────
PUT {{base_url}}/remote.php/dav/files/{{ncq_nc_username}}~{{ncq_root_folder_id}}/quota-probe.txt
Content-Type: text/plain
[Options]
variable: ncq_composite_user={{ncq_nc_username}}~{{ncq_root_folder_id}}
[BasicAuth]
{{ncq_composite_user}}: {{ncq_nc_password}}
```
32-byte-ish payload for nc
```
HTTP 201
# The drive-usage bump is fire-and-forget on a tokio task (see
# `file_upload_service.rs::maybe_update_storage_usage`), so retry
# until `used_bytes` catches up — same shape as `drive_quota.hurl`
# Step 5.
PROPFIND {{base_url}}/remote.php/dav/files/{{ncq_nc_username}}~{{ncq_root_folder_id}}/
Depth: 0
[Options]
variable: ncq_composite_user={{ncq_nc_username}}~{{ncq_root_folder_id}}
retry: 10
retry-interval: 200ms
[BasicAuth]
{{ncq_composite_user}}: {{ncq_nc_password}}
HTTP 207
[Asserts]
xpath "number(//*[local-name()='quota-used-bytes'])" > 0
xpath "number(//*[local-name()='quota-available-bytes'])" < 500
# No further cleanup needed — `tests/api/storage_cleanup_check.sh`
# drains/deletes every non-admin-default drive at suite end.
+2
View File
@@ -195,6 +195,8 @@ hurl --variables-file "$API_DIR/test.env" --file-root "$REPO_ROOT/tests" --test
"$API_DIR/webdav_dead_properties.hurl" \
"$API_DIR/nc_webdav_dead_properties.hurl" \
"$API_DIR/webdav_protected_properties.hurl" \
"$API_DIR/webdav_quota_properties.hurl" \
"$API_DIR/nc_webdav_quota_properties.hurl" \
"$API_DIR/webdav_drive_root.hurl" \
"$API_DIR/webdav_permissions.hurl" \
"$API_DIR/webdav_nested_move_cascade.hurl" \
+235
View File
@@ -0,0 +1,235 @@
# =============================================================
# OxiCloud — WebDAV quota properties (RFC 4331)
# =============================================================
# `DAV:quota-available-bytes` / `DAV:quota-used-bytes` are resolved once
# per PROPFIND request via `AppState::resolve_webdav_quota` — see
# webdav_handler.rs / webdav_adapter.rs::write_quota_props.
# Unlimited accounts/drives (quota <= 0 or unset) omit
# quota-available-bytes entirely per RFC 4331 §3, rather than reporting
# a sentinel value.
#
# Coverage:
# 1. Named-prop PROPFIND for both properties on the WebDAV root → 207,
# both present with numeric values.
# 2. allprop PROPFIND also includes both properties.
# 3. quota-used-bytes increases by (at least) the size of a file
# just uploaded through WebDAV.
# 4. A SHARED drive with its own finite quota reports THAT quota on
# `/webdav/@drive/<id>/`, distinct from the caller's personal
# envelope — the drive-awareness fix (previously `resolve_quota`
# ignored `drive_id` entirely and always reported the envelope).
# =============================================================
# ─────────────────────────────────────────────────────────────
# Step 1 — Login, capture JWT
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "{{username}}", "password": "{{password}}" }
HTTP 200
[Captures]
token: jsonpath "$.access_token"
# ─────────────────────────────────────────────────────────────
# Step 2 — Named-prop PROPFIND for the two quota properties.
# ─────────────────────────────────────────────────────────────
PROPFIND {{base_url}}/webdav/
Authorization: Bearer {{token}}
Depth: 0
Content-Type: application/xml; charset=utf-8
```
<?xml version="1.0" encoding="utf-8"?>
<D:propfind xmlns:D="DAV:">
<D:prop>
<D:quota-available-bytes/>
<D:quota-used-bytes/>
</D:prop>
</D:propfind>
```
HTTP 207
[Asserts]
xpath "string(//*[local-name()='propstat'][1]/*[local-name()='status'])" contains "200 OK"
xpath "number(//*[local-name()='quota-used-bytes'])" >= 0
xpath "number(//*[local-name()='quota-available-bytes'])" > 0
# ─────────────────────────────────────────────────────────────
# Step 3 — allprop PROPFIND also surfaces both properties.
# ─────────────────────────────────────────────────────────────
PROPFIND {{base_url}}/webdav/
Authorization: Bearer {{token}}
Depth: 0
Content-Type: application/xml; charset=utf-8
```
<?xml version="1.0" encoding="utf-8"?>
<D:propfind xmlns:D="DAV:">
<D:allprop/>
</D:propfind>
```
HTTP 207
[Asserts]
xpath "number(//*[local-name()='quota-used-bytes'])" >= 0
xpath "number(//*[local-name()='quota-available-bytes'])" > 0
[Captures]
used_before: xpath "number(//*[local-name()='quota-used-bytes'])"
# ─────────────────────────────────────────────────────────────
# Step 4 — Upload a file, then confirm quota-used-bytes reflects it.
# ─────────────────────────────────────────────────────────────
PUT {{base_url}}/webdav/quota-probe.txt
Authorization: Bearer {{token}}
Content-Type: text/plain
```
quota accounting probe payload
```
HTTP 201
PROPFIND {{base_url}}/webdav/
Authorization: Bearer {{token}}
Depth: 0
Content-Type: application/xml; charset=utf-8
```
<?xml version="1.0" encoding="utf-8"?>
<D:propfind xmlns:D="DAV:">
<D:prop>
<D:quota-used-bytes/>
</D:prop>
</D:propfind>
```
HTTP 207
[Asserts]
xpath "number(//*[local-name()='quota-used-bytes'])" >= {{used_before}}
# ─────────────────────────────────────────────────────────────
# Cleanup
# ─────────────────────────────────────────────────────────────
DELETE {{base_url}}/webdav/quota-probe.txt
Authorization: Bearer {{token}}
HTTP 204
# ─────────────────────────────────────────────────────────────
# Step 5 — Shared drive with its own finite quota reports THAT
# quota, not the owner's personal envelope.
#
# Provision a fresh user + a 500-byte shared drive owned
# by them, then PROPFIND `/webdav/@drive/<id>/` (see
# `webdav_drive_root.hurl` for the URL-scheme contract).
# A brand-new drive has `used_bytes == 0`, so
# quota-available-bytes must equal the quota exactly —
# a value that cannot coincide with the personal envelope
# asserted above (that account already had files on it
# from earlier steps).
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/admin/users
Authorization: Bearer {{token}}
Content-Type: application/json
{
"username": "wq_owner",
"password": "WqOwnerPwd1!",
"email": "wq_owner@example.com",
"role": "user"
}
HTTP 201
POST {{base_url}}/api/auth/login
Content-Type: application/json
{ "username": "wq_owner", "password": "WqOwnerPwd1!" }
HTTP 200
[Captures]
wq_owner_token: jsonpath "$.access_token"
wq_owner_id: jsonpath "$.user.id"
POST {{base_url}}/api/drives
Authorization: Bearer {{token}}
Content-Type: application/json
{
"kind": "shared",
"name": "wq-shared",
"owner": { "type": "user", "id": "{{wq_owner_id}}" },
"quota_bytes": 500
}
HTTP 201
[Captures]
wq_drive_id: jsonpath "$.id"
PROPFIND {{base_url}}/webdav/@drive/{{wq_drive_id}}/
Authorization: Bearer {{wq_owner_token}}
Depth: 0
Content-Type: application/xml; charset=utf-8
```
<?xml version="1.0" encoding="utf-8"?>
<D:propfind xmlns:D="DAV:">
<D:prop>
<D:quota-available-bytes/>
<D:quota-used-bytes/>
</D:prop>
</D:propfind>
```
HTTP 207
[Asserts]
xpath "number(//*[local-name()='quota-used-bytes'])" == 0
xpath "number(//*[local-name()='quota-available-bytes'])" == 500
# Upload a 32-byte file into the shared drive; quota-used-bytes must
# reflect it and quota-available-bytes must shrink accordingly —
# confirms the shared-drive branch reads `storage.drives` live, not
# a cached/stale value.
PUT {{base_url}}/webdav/@drive/{{wq_drive_id}}/quota-probe.txt
Authorization: Bearer {{wq_owner_token}}
Content-Type: text/plain
```
32-byte-ish payload for drv
```
HTTP 201
# The drive-usage bump is fire-and-forget on a tokio task (see
# `file_upload_service.rs::maybe_update_storage_usage`), so the SQL
# UPDATE may not have landed yet when the PUT above returned. Retry
# the PROPFIND until `used_bytes` catches up — same shape as
# `drive_quota.hurl` Step 5.
PROPFIND {{base_url}}/webdav/@drive/{{wq_drive_id}}/
Authorization: Bearer {{wq_owner_token}}
Depth: 0
Content-Type: application/xml; charset=utf-8
[Options]
retry: 10
retry-interval: 200ms
```
<?xml version="1.0" encoding="utf-8"?>
<D:propfind xmlns:D="DAV:">
<D:prop>
<D:quota-available-bytes/>
<D:quota-used-bytes/>
</D:prop>
</D:propfind>
```
HTTP 207
[Asserts]
xpath "number(//*[local-name()='quota-used-bytes'])" > 0
xpath "number(//*[local-name()='quota-available-bytes'])" < 500
# No further cleanup needed — `tests/api/storage_cleanup_check.sh`
# drains/deletes every non-admin-default drive at suite end.