feat(calendar,addressbook): add tests for authz
This commit is contained in:
@@ -346,6 +346,18 @@ async fn handle_propfind(
|
||||
.header(header::CONTENT_TYPE, "application/xml; charset=utf-8")
|
||||
.body(Body::from(response_body))
|
||||
.unwrap())
|
||||
} else if first_is_uuid {
|
||||
// Path segment IS a UUID but the calendar isn't
|
||||
// accessible to the caller — could be another
|
||||
// owner's calendar or genuinely missing. Return
|
||||
// 404 (anti-enum, matches every other OxiCloud
|
||||
// surface post-D7). The pre-Round-3 fall-through
|
||||
// silently listed the caller's OWN calendars,
|
||||
// which was misleading (the URL claimed one calendar,
|
||||
// response returned unrelated ones) and violated
|
||||
// the anti-enumeration contract audited in
|
||||
// `docs/plan/authz_audit/caldav_carddav_wopi.md`.
|
||||
Err(AppError::not_found("Calendar not found"))
|
||||
} else {
|
||||
// Not a calendar ID — treat as user calendar home (e.g. /caldav/{username}/)
|
||||
// List all calendars for this user
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
# =============================================================
|
||||
# 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
|
||||
@@ -276,3 +276,318 @@ Authorization: Bearer {{token}}
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$" isCollection
|
||||
|
||||
|
||||
# ═════════════════════════════════════════════════════════════
|
||||
# Round 3 — CardDAV/AddressBook AuthZ regression
|
||||
# ═════════════════════════════════════════════════════════════
|
||||
# Post-Round-3, address-book access + sharing routes through
|
||||
# `AuthorizationEngine` and `storage.role_grants`. The dedicated
|
||||
# `carddav.address_book_shares` table stopped being consulted;
|
||||
# the generic `POST /api/grants` endpoint accepts
|
||||
# `resource.type = "address_book"` as a first-class ReBAC
|
||||
# resource.
|
||||
#
|
||||
# Coverage:
|
||||
# 15. Fresh book owned by admin (Alice).
|
||||
# 16. Non-member user (Bob) doesn't see the book.
|
||||
# 17. Bob's direct GET on the book → 404 (anti-enum, was 403
|
||||
# pre-Round-3).
|
||||
# 18. Alice shares with Bob as Viewer via `POST /api/grants`.
|
||||
# 19. Bob's listing includes the book with is_readonly=true.
|
||||
# 20. Viewer role's bundle has no Create — Bob's contact
|
||||
# write → 404 (anti-enum).
|
||||
# 21. Alice revokes via `DELETE /api/grants/{id}`.
|
||||
# 22. Bob no longer sees the book.
|
||||
# 23. Cleanup.
|
||||
# =============================================================
|
||||
|
||||
|
||||
# Step 15 — Alice creates a fresh book for the share regression.
|
||||
POST {{base_url}}/api/address-books
|
||||
Authorization: Bearer {{token}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"name": "Round3 Share Book",
|
||||
"description": "Book for the multi-user share regression",
|
||||
"is_public": false
|
||||
}
|
||||
|
||||
HTTP 201
|
||||
[Captures]
|
||||
share_book_id: jsonpath "$.id"
|
||||
|
||||
|
||||
# Step 16 — Provision Bob. Idempotent: accept 201 on first run,
|
||||
# 409 on subsequent runs; login is the actual precondition.
|
||||
POST {{base_url}}/api/admin/users
|
||||
Authorization: Bearer {{token}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"username": "carddav_bob",
|
||||
"password": "CarddavBobPassword1!",
|
||||
"email": "carddav_bob@example.com",
|
||||
"role": "user"
|
||||
}
|
||||
|
||||
HTTP *
|
||||
|
||||
|
||||
POST {{base_url}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
{
|
||||
"username": "carddav_bob",
|
||||
"password": "CarddavBobPassword1!"
|
||||
}
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
bob_token: jsonpath "$.access_token"
|
||||
bob_user_id: jsonpath "$.user.id"
|
||||
|
||||
|
||||
# Step 17 — Bob's book listing does NOT include Alice's book.
|
||||
GET {{base_url}}/api/address-books
|
||||
Authorization: Bearer {{bob_token}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$[*].id" not contains {{share_book_id}}
|
||||
|
||||
|
||||
# Step 18a — Direct GET on Alice's book: 404 (anti-enum).
|
||||
GET {{base_url}}/api/address-books/{{share_book_id}}/contacts
|
||||
Authorization: Bearer {{bob_token}}
|
||||
|
||||
HTTP 404
|
||||
|
||||
|
||||
# Step 18b — Contact-write into Alice's book: 404. Bob has no
|
||||
# grant, so authz.require(Create) rejects with NotFound.
|
||||
# Body is minimal on purpose — the endpoint's wire DTO
|
||||
# (`CreateContactRequest`) marks every collection field
|
||||
# `#[serde(default)]`, so `full_name` alone deserialises
|
||||
# fine and lets the request reach the authz gate. Any
|
||||
# body-side 422 here would mask the AuthZ regression the
|
||||
# step is meant to verify.
|
||||
POST {{base_url}}/api/address-books/{{share_book_id}}/contacts
|
||||
Authorization: Bearer {{bob_token}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"full_name": "Sneaky Insert"
|
||||
}
|
||||
|
||||
HTTP 404
|
||||
|
||||
|
||||
# Step 19 — Alice shares the book with Bob as Viewer via the
|
||||
# generic ReBAC grant endpoint. `resource.type = "address_book"`
|
||||
# is a first-class variant post-Round-3.
|
||||
POST {{base_url}}/api/grants
|
||||
Authorization: Bearer {{token}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"subject": { "type": "user", "id": "{{bob_user_id}}" },
|
||||
"resource": { "type": "address_book", "id": "{{share_book_id}}" },
|
||||
"role": "viewer"
|
||||
}
|
||||
|
||||
HTTP 201
|
||||
[Captures]
|
||||
share_grant_id: jsonpath "$.grants[0].id"
|
||||
[Asserts]
|
||||
jsonpath "$.grants[0].role" == "viewer"
|
||||
jsonpath "$.grants[0].resource.type" == "address_book"
|
||||
jsonpath "$.grants[0].resource.id" == "{{share_book_id}}"
|
||||
|
||||
|
||||
# Step 20 — Bob's listing now includes the book, marked readonly
|
||||
# because he's not the owner.
|
||||
GET {{base_url}}/api/address-books
|
||||
Authorization: Bearer {{bob_token}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$[?(@.id == '{{share_book_id}}')].is_readonly" == true
|
||||
|
||||
|
||||
# Step 21 — Viewer bundle has no Create permission — Bob's
|
||||
# contact write still 404s. Same minimal-body reasoning as
|
||||
# Step 18b: keep the request valid at the wire layer so any
|
||||
# rejection has to come from the AuthZ engine.
|
||||
POST {{base_url}}/api/address-books/{{share_book_id}}/contacts
|
||||
Authorization: Bearer {{bob_token}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"full_name": "Viewer Cannot Write"
|
||||
}
|
||||
|
||||
HTTP 404
|
||||
|
||||
|
||||
# Step 22 — Alice revokes the grant.
|
||||
DELETE {{base_url}}/api/grants/{{share_grant_id}}
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
HTTP 204
|
||||
|
||||
|
||||
# Step 23 — Bob's listing no longer includes the book.
|
||||
GET {{base_url}}/api/address-books
|
||||
Authorization: Bearer {{bob_token}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$[*].id" not contains {{share_book_id}}
|
||||
|
||||
|
||||
# Step 24 — Cleanup: Alice deletes the book.
|
||||
DELETE {{base_url}}/api/address-books/{{share_book_id}}
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
HTTP 204
|
||||
|
||||
|
||||
# ═════════════════════════════════════════════════════════════
|
||||
# Round 3 — CardDAV protocol coverage
|
||||
# ═════════════════════════════════════════════════════════════
|
||||
# Verifies the CardDAV surface end-to-end:
|
||||
#
|
||||
# * MKCOL creates an address book via the CardDAV protocol
|
||||
# (`ContactService::create_address_book` seeds an Owner
|
||||
# role_grant on the caller so the engine's cache warms).
|
||||
# * PROPFIND lists it in the caller's address-book home.
|
||||
# * A non-member's PROPFIND doesn't include the book.
|
||||
# * `POST /api/grants` with `resource.type = "address_book"`
|
||||
# grants Read to the non-member.
|
||||
# * The recipient's PROPFIND now includes the book.
|
||||
# * Revoke → book vanishes.
|
||||
# * DELETE cleans up.
|
||||
#
|
||||
# Book UUID is server-assigned at MKCOL time and appears in the
|
||||
# PROPFIND multistatus as `<D:href>/carddav/<uuid>/</D:href>`.
|
||||
# Regex-capture is unambiguous only if admin has zero
|
||||
# pre-existing CardDAV books — true on the CI DB (fresh from
|
||||
# `tests/webdav/run.sh`'s private Postgres), false in a
|
||||
# populated dev DB.
|
||||
# =============================================================
|
||||
|
||||
|
||||
# Step 25 — Alice creates a fresh book via CardDAV MKCOL.
|
||||
# Empty body — `handle_mkcol` derives the display name from the
|
||||
# path's last segment.
|
||||
MKCOL {{base_url}}/carddav/round3-carddav-book/
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
HTTP 201
|
||||
|
||||
|
||||
# Step 26 — Alice PROPFIND at Depth 1 lists her books. Capture
|
||||
# the server-assigned UUID with a regex on the `<D:href>` value.
|
||||
PROPFIND {{base_url}}/carddav/
|
||||
Authorization: Bearer {{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]
|
||||
carddav_book_id: body regex "/carddav/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/"
|
||||
|
||||
|
||||
# Step 27 — Bob PROPFIND: the book UUID is NOT in his response.
|
||||
# (Bob's lifecycle-provisioned books, if any, get their own
|
||||
# UUIDs — no collision.)
|
||||
PROPFIND {{base_url}}/carddav/
|
||||
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 "{{carddav_book_id}}"
|
||||
|
||||
|
||||
# Step 28 — Alice shares the book with Bob as Viewer via the
|
||||
# generic ReBAC grant endpoint (same wire format as the
|
||||
# calendar test, only the resource type differs).
|
||||
POST {{base_url}}/api/grants
|
||||
Authorization: Bearer {{token}}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"subject": { "type": "user", "id": "{{bob_user_id}}" },
|
||||
"resource": { "type": "address_book", "id": "{{carddav_book_id}}" },
|
||||
"role": "viewer"
|
||||
}
|
||||
|
||||
HTTP 201
|
||||
[Captures]
|
||||
carddav_grant_id: jsonpath "$.grants[0].id"
|
||||
|
||||
|
||||
# Step 29 — Bob PROPFIND now includes the shared book. The
|
||||
# CardDAV handler routes through the same
|
||||
# `list_user_address_books` as the REST API, so the shared
|
||||
# book flows in via the role_grants union.
|
||||
PROPFIND {{base_url}}/carddav/
|
||||
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 "{{carddav_book_id}}"
|
||||
|
||||
|
||||
# Step 30 — Alice revokes the grant.
|
||||
DELETE {{base_url}}/api/grants/{{carddav_grant_id}}
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
HTTP 204
|
||||
|
||||
|
||||
# Step 31 — Bob PROPFIND no longer includes the book.
|
||||
PROPFIND {{base_url}}/carddav/
|
||||
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 "{{carddav_book_id}}"
|
||||
|
||||
|
||||
# Step 32 — Cleanup: Alice deletes the book via CardDAV DELETE.
|
||||
DELETE {{base_url}}/carddav/{{carddav_book_id}}/
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
HTTP *
|
||||
[Asserts]
|
||||
status >= 200
|
||||
status < 300
|
||||
|
||||
@@ -161,6 +161,7 @@ hurl --variables-file "$API_DIR/test.env" --file-root "$REPO_ROOT/tests" --test
|
||||
"$API_DIR/batch_folder_copy.hurl" \
|
||||
"$API_DIR/dedup_blob_cleanup.hurl" \
|
||||
"$API_DIR/contacts.hurl" \
|
||||
"$API_DIR/calendar.hurl" \
|
||||
"$API_DIR/public_shares.hurl" \
|
||||
"$API_DIR/permissions.hurl" \
|
||||
"$API_DIR/grants.hurl" \
|
||||
|
||||
Reference in New Issue
Block a user