2026-05-11 00:50:39 +02:00
|
|
|
|
# =============================================================
|
|
|
|
|
|
# OxiCloud – Contacts API end-to-end scenario
|
|
|
|
|
|
# =============================================================
|
|
|
|
|
|
# Run:
|
|
|
|
|
|
# hurl --variables-file tests/api/hurl.vars --test tests/api/contacts.hurl
|
|
|
|
|
|
#
|
|
|
|
|
|
# Variables required (see hurl.vars):
|
|
|
|
|
|
# base_url – e.g. http://localhost:8087
|
|
|
|
|
|
# username – OxiCloud username
|
|
|
|
|
|
# password – OxiCloud password
|
|
|
|
|
|
# =============================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 1 – Login and capture the JWT token
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
POST {{base_url}}/api/auth/login
|
|
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
{
|
|
|
|
|
|
"username": "{{username}}",
|
|
|
|
|
|
"password": "{{password}}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Captures]
|
|
|
|
|
|
token: jsonpath "$.access_token"
|
|
|
|
|
|
[Asserts]
|
|
|
|
|
|
jsonpath "$.access_token" isString
|
|
|
|
|
|
jsonpath "$.token_type" == "Bearer"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 2 – List address books (at least the system book)
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
GET {{base_url}}/api/address-books
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Asserts]
|
|
|
|
|
|
jsonpath "$" isCollection
|
|
|
|
|
|
# System address book must always be present
|
|
|
|
|
|
jsonpath "$[?(@.id == 'system')].is_system" == true
|
|
|
|
|
|
jsonpath "$[?(@.id == 'system')].is_readonly" == true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 3 – Create a personal address book
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
POST {{base_url}}/api/address-books
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
{
|
|
|
|
|
|
"name": "Personal",
|
|
|
|
|
|
"description": "Personal address book created by Hurl tests",
|
|
|
|
|
|
"is_public": false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 201
|
|
|
|
|
|
[Captures]
|
|
|
|
|
|
book_id: jsonpath "$.id"
|
|
|
|
|
|
[Asserts]
|
|
|
|
|
|
jsonpath "$.id" isString
|
|
|
|
|
|
jsonpath "$.name" == "Personal"
|
|
|
|
|
|
jsonpath "$.description" == "Personal address book created by Hurl tests"
|
|
|
|
|
|
jsonpath "$.is_public" == false
|
|
|
|
|
|
jsonpath "$.is_system" == false
|
|
|
|
|
|
jsonpath "$.is_readonly" == false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 4 – List contacts in the new book – must be empty
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
GET {{base_url}}/api/address-books/{{book_id}}/contacts
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Asserts]
|
|
|
|
|
|
jsonpath "$" isCollection
|
|
|
|
|
|
jsonpath "$" count == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 5 – Create contact John Doe
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
POST {{base_url}}/api/address-books/{{book_id}}/contacts
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
{
|
|
|
|
|
|
"first_name": "John",
|
|
|
|
|
|
"last_name": "Doe",
|
|
|
|
|
|
"full_name": "John Doe",
|
|
|
|
|
|
"email": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"email": "john.doe@example.com",
|
|
|
|
|
|
"type": "work",
|
|
|
|
|
|
"is_primary": true
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
"phone": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"number": "+1-555-0100",
|
|
|
|
|
|
"type": "mobile",
|
|
|
|
|
|
"is_primary": true
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 201
|
|
|
|
|
|
[Captures]
|
|
|
|
|
|
contact_id: jsonpath "$.id"
|
|
|
|
|
|
[Asserts]
|
|
|
|
|
|
jsonpath "$.id" isString
|
|
|
|
|
|
jsonpath "$.address_book_id" == {{book_id}}
|
|
|
|
|
|
jsonpath "$.first_name" == "John"
|
|
|
|
|
|
jsonpath "$.last_name" == "Doe"
|
|
|
|
|
|
jsonpath "$.full_name" == "John Doe"
|
|
|
|
|
|
jsonpath "$.email" count == 1
|
|
|
|
|
|
jsonpath "$.email[0].email" == "john.doe@example.com"
|
|
|
|
|
|
jsonpath "$.email[0].type" == "work"
|
|
|
|
|
|
jsonpath "$.email[0].is_primary" == true
|
|
|
|
|
|
jsonpath "$.phone" count == 1
|
|
|
|
|
|
jsonpath "$.phone[0].number" == "+1-555-0100"
|
|
|
|
|
|
jsonpath "$.phone[0].type" == "mobile"
|
|
|
|
|
|
jsonpath "$.phone[0].is_primary" == true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 6 – List contacts – exactly 1 result, must be John Doe
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
GET {{base_url}}/api/address-books/{{book_id}}/contacts
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Asserts]
|
|
|
|
|
|
jsonpath "$" count == 1
|
|
|
|
|
|
jsonpath "$[*].id" contains {{contact_id}}
|
|
|
|
|
|
jsonpath "$[0].first_name" == "John"
|
|
|
|
|
|
jsonpath "$[0].last_name" == "Doe"
|
|
|
|
|
|
jsonpath "$[0].full_name" == "John Doe"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 7 – Get John Doe by id and verify all fields
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
GET {{base_url}}/api/address-books/{{book_id}}/contacts/{{contact_id}}
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Captures]
|
|
|
|
|
|
etag: header "ETag"
|
|
|
|
|
|
[Asserts]
|
|
|
|
|
|
header "ETag" exists
|
|
|
|
|
|
jsonpath "$.id" == {{contact_id}}
|
|
|
|
|
|
jsonpath "$.address_book_id" == {{book_id}}
|
|
|
|
|
|
jsonpath "$.first_name" == "John"
|
|
|
|
|
|
jsonpath "$.last_name" == "Doe"
|
|
|
|
|
|
jsonpath "$.full_name" == "John Doe"
|
|
|
|
|
|
jsonpath "$.email[0].email" == "john.doe@example.com"
|
|
|
|
|
|
jsonpath "$.email[0].type" == "work"
|
|
|
|
|
|
jsonpath "$.email[0].is_primary" == true
|
|
|
|
|
|
jsonpath "$.phone[0].number" == "+1-555-0100"
|
|
|
|
|
|
jsonpath "$.phone[0].type" == "mobile"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 8 – Update John Doe: add nickname, notes, and organisation
|
|
|
|
|
|
# Uses If-Match for optimistic concurrency
|
2026-05-11 09:54:35 +02:00
|
|
|
|
# Captures the refreshed ETag into etag_updated so that
|
|
|
|
|
|
# the original etag remains available as a stale value.
|
2026-05-11 00:50:39 +02:00
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
PUT {{base_url}}/api/address-books/{{book_id}}/contacts/{{contact_id}}
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
Content-Type: application/json
|
|
|
|
|
|
If-Match: {{etag}}
|
|
|
|
|
|
{
|
|
|
|
|
|
"first_name": "John",
|
|
|
|
|
|
"last_name": "Doe",
|
|
|
|
|
|
"full_name": "John Doe",
|
|
|
|
|
|
"nickname": "JD",
|
|
|
|
|
|
"organization": "ACME Corp",
|
|
|
|
|
|
"notes": "Updated via Hurl test",
|
|
|
|
|
|
"email": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"email": "john.doe@example.com",
|
|
|
|
|
|
"type": "work",
|
|
|
|
|
|
"is_primary": true
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
"phone": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"number": "+1-555-0100",
|
|
|
|
|
|
"type": "mobile",
|
|
|
|
|
|
"is_primary": true
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Captures]
|
2026-05-11 09:54:35 +02:00
|
|
|
|
etag_updated: header "ETag"
|
2026-05-11 00:50:39 +02:00
|
|
|
|
[Asserts]
|
|
|
|
|
|
header "ETag" exists
|
|
|
|
|
|
jsonpath "$.id" == {{contact_id}}
|
|
|
|
|
|
jsonpath "$.first_name" == "John"
|
|
|
|
|
|
jsonpath "$.last_name" == "Doe"
|
|
|
|
|
|
jsonpath "$.nickname" == "JD"
|
|
|
|
|
|
jsonpath "$.organization" == "ACME Corp"
|
|
|
|
|
|
jsonpath "$.notes" == "Updated via Hurl test"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
2026-05-11 09:54:35 +02:00
|
|
|
|
# Step 9 – Stale ETag rejection: update with the pre-step-8 ETag
|
|
|
|
|
|
# The contact was already modified so this must return 412
|
2026-05-11 00:50:39 +02:00
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
2026-05-11 09:54:35 +02:00
|
|
|
|
PUT {{base_url}}/api/address-books/{{book_id}}/contacts/{{contact_id}}
|
2026-05-11 00:50:39 +02:00
|
|
|
|
Authorization: Bearer {{token}}
|
2026-05-11 09:54:35 +02:00
|
|
|
|
Content-Type: application/json
|
2026-05-11 00:50:39 +02:00
|
|
|
|
If-Match: {{etag}}
|
2026-05-11 09:54:35 +02:00
|
|
|
|
{
|
|
|
|
|
|
"first_name": "John",
|
|
|
|
|
|
"last_name": "Doe",
|
|
|
|
|
|
"full_name": "John Doe",
|
|
|
|
|
|
"nickname": "should-not-be-saved"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 412
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
# Step 10 – Delete John Doe (uses refreshed ETag from step 8)
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
DELETE {{base_url}}/api/address-books/{{book_id}}/contacts/{{contact_id}}
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
If-Match: {{etag_updated}}
|
2026-05-11 00:50:39 +02:00
|
|
|
|
|
|
|
|
|
|
HTTP 204
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
2026-05-11 09:54:35 +02:00
|
|
|
|
# Step 11 – Address book must be empty again
|
2026-05-11 00:50:39 +02:00
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
GET {{base_url}}/api/address-books/{{book_id}}/contacts
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Asserts]
|
|
|
|
|
|
jsonpath "$" count == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
2026-05-11 09:54:35 +02:00
|
|
|
|
# Step 12 – Delete the personal address book
|
2026-05-11 00:50:39 +02:00
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
DELETE {{base_url}}/api/address-books/{{book_id}}
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 204
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
2026-05-11 09:54:35 +02:00
|
|
|
|
# Step 13 – Verify the address book is gone from the list
|
2026-05-11 00:50:39 +02:00
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
GET {{base_url}}/api/address-books
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Asserts]
|
|
|
|
|
|
jsonpath "$[*].id" not contains {{book_id}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
2026-05-11 09:54:35 +02:00
|
|
|
|
# Step 14 – List the system address book (OxiCloud users)
|
2026-05-11 00:50:39 +02:00
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
GET {{base_url}}/api/address-books/system/contacts
|
|
|
|
|
|
Authorization: Bearer {{token}}
|
|
|
|
|
|
|
|
|
|
|
|
HTTP 200
|
|
|
|
|
|
[Asserts]
|
|
|
|
|
|
jsonpath "$" isCollection
|