Files
Oxicloud/tests/api/calendar.hurl
T
2026-07-08 01:03:25 +02:00

251 lines
10 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================
# OxiCloud – CalDAV + Round-3 AuthZ end-to-end scenario
# =============================================================
# Verifies the full CalDAV surface post-Round-3:
#
# * MKCALENDAR / PROPFIND / DELETE against `/caldav/*` all
# route through `CalendarService`, which enforces
# `authz.require` on every method.
# * Cross-user access uses the 404 anti-enum shape (was 403
# in the bespoke `check_calendar_access` era).
# * Sharing goes through the generic `POST /api/grants` with
# `resource.type = "calendar"` — a first-class ReBAC
# resource variant added in Round 3 Phase 1.
# * A shared calendar shows up in the recipient's PROPFIND
# listing while the grant is live and disappears again
# after revoke.
#
# The `calendar_id` is server-assigned at MKCALENDAR time and
# surfaces in the PROPFIND response as `/caldav/<uuid>/`. We
# extract it with a regex on the response body — the fresh CI
# database (`tests/webdav/run.sh` spawns a private Postgres)
# guarantees admin has zero pre-existing calendars, so the
# first-match regex is unambiguous.
#
# CalDAV auth is JWT via the same middleware the REST API uses
# (`/caldav/*` and `/carddav/*` are both wrapped in
# `auth_middleware + require_internal_user_layer` in main.rs).
# =============================================================
# ─────────────────────────────────────────────────────────────
# Step 1 – Alice (admin) logs in.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/auth/login
Content-Type: application/json
{
"username": "{{username}}",
"password": "{{password}}"
}
HTTP 200
[Captures]
alice_token: jsonpath "$.access_token"
# ─────────────────────────────────────────────────────────────
# Step 2 – MKCALENDAR: create a fresh calendar for the test.
# Empty body → the CalDAV handler derives the display name
# from the last path segment ("round3-cal" here). The response
# is 201 with an empty body — CalDAV convention. The
# server-assigned UUID is captured in Step 3 via PROPFIND.
# ─────────────────────────────────────────────────────────────
MKCALENDAR {{base_url}}/caldav/round3-cal/
Authorization: Bearer {{alice_token}}
HTTP 201
# ─────────────────────────────────────────────────────────────
# Step 3 – Alice PROPFIND at Depth 1 lists her calendars.
# The response is a `<D:multistatus>` — each calendar surfaces
# as `<D:href>/caldav/<uuid>/</D:href>`. Regex-capture the
# UUID (first `/caldav/<uuid>/` in the body — the root href
# is `/caldav/` alone, no UUID, so it can't match).
# ─────────────────────────────────────────────────────────────
PROPFIND {{base_url}}/caldav/
Authorization: Bearer {{alice_token}}
Depth: 1
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
[Captures]
calendar_id: body regex "/caldav/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/"
# ─────────────────────────────────────────────────────────────
# Step 4 – Provision Bob. Idempotent: `HTTP *` accepts 201
# on the first run and 409 on subsequent ones. Login is the
# actual precondition.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/admin/users
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{
"username": "caldav_bob",
"password": "CaldavBobPassword1!",
"email": "caldav_bob@example.com",
"role": "user"
}
HTTP *
POST {{base_url}}/api/auth/login
Content-Type: application/json
{
"username": "caldav_bob",
"password": "CaldavBobPassword1!"
}
HTTP 200
[Captures]
bob_token: jsonpath "$.access_token"
bob_user_id: jsonpath "$.user.id"
# ─────────────────────────────────────────────────────────────
# Step 5 – Cross-user PROPFIND. Bob has no grant on Alice's
# calendar; his listing does NOT include the calendar's UUID.
# (Bob's OWN response body will list his lifecycle-provisioned
# calendars — none of them collide with Alice's UUID.)
# ─────────────────────────────────────────────────────────────
PROPFIND {{base_url}}/caldav/
Authorization: Bearer {{bob_token}}
Depth: 1
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]
body not contains "{{calendar_id}}"
# ─────────────────────────────────────────────────────────────
# Step 6 – Cross-user direct PROPFIND on Alice's calendar
# → 404. `authz.require(Read)` denies with `NotFound` for
# anti-enumeration parity with files/folders/drives.
# ─────────────────────────────────────────────────────────────
PROPFIND {{base_url}}/caldav/{{calendar_id}}/
Authorization: Bearer {{bob_token}}
Depth: 0
Content-Type: application/xml
```
<?xml version="1.0" encoding="UTF-8"?>
<D:propfind xmlns:D="DAV:">
<D:prop><D:displayname/></D:prop>
</D:propfind>
```
HTTP *
[Asserts]
status >= 400
status < 500
# ─────────────────────────────────────────────────────────────
# Step 7 – Alice shares the calendar with Bob as Viewer via
# the generic ReBAC grant endpoint. `resource.type = "calendar"`
# is a first-class variant post-Round-3.
# ─────────────────────────────────────────────────────────────
POST {{base_url}}/api/grants
Authorization: Bearer {{alice_token}}
Content-Type: application/json
{
"subject": { "type": "user", "id": "{{bob_user_id}}" },
"resource": { "type": "calendar", "id": "{{calendar_id}}" },
"role": "viewer"
}
HTTP 201
[Captures]
share_grant_id: jsonpath "$.grants[0].id"
[Asserts]
jsonpath "$.grants[0].role" == "viewer"
jsonpath "$.grants[0].resource.type" == "calendar"
jsonpath "$.grants[0].resource.id" == "{{calendar_id}}"
# ─────────────────────────────────────────────────────────────
# Step 8 – Bob PROPFIND now includes Alice's calendar. The
# `list_my_calendars` service method reads
# `authz.list_incoming_grants(user)` and unions across
# owned + shared, replacing the pre-Round-3 owner-only query.
# ─────────────────────────────────────────────────────────────
PROPFIND {{base_url}}/caldav/
Authorization: Bearer {{bob_token}}
Depth: 1
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]
body contains "{{calendar_id}}"
# ─────────────────────────────────────────────────────────────
# Step 9 – Alice revokes the grant. `DELETE /api/grants/{id}`
# maps to a single `role_grants` row delete.
# ─────────────────────────────────────────────────────────────
DELETE {{base_url}}/api/grants/{{share_grant_id}}
Authorization: Bearer {{alice_token}}
HTTP 204
# ─────────────────────────────────────────────────────────────
# Step 10 – Bob PROPFIND no longer includes Alice's calendar.
# The role_grants row is gone, so `list_incoming_grants` won't
# surface it and `list_my_calendars` collapses back to Bob's
# own.
# ─────────────────────────────────────────────────────────────
PROPFIND {{base_url}}/caldav/
Authorization: Bearer {{bob_token}}
Depth: 1
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]
body not contains "{{calendar_id}}"
# ─────────────────────────────────────────────────────────────
# Step 11 – Cleanup: Alice deletes the calendar. The service
# runs `authz.require(Delete)` (owner passes via the seeded
# Owner grant), then `revoke_all_for_resource` wipes any
# remaining grants on the calendar in case a share slipped
# through.
# ─────────────────────────────────────────────────────────────
DELETE {{base_url}}/caldav/{{calendar_id}}/
Authorization: Bearer {{alice_token}}
HTTP *
[Asserts]
status >= 200
status < 300