19a8186c66
The upload omitted folder_id, which the handler needs to resolve the file's owner — it answers 500, not a root upload. Every other upload in the suite passes it; this was the only one that did not, which is why nothing caught it earlier. The folder also gives the WebDAV overwrite a deterministic path (/webdav/hurl-etag-src/<name>) instead of depending on where a folder-less upload would have landed. Teardown now removes it and purges it from trash, keeping the shared database clean for the files that run after.
API functional tests
Tests are written using Hurl — a plain-text, CLI-first HTTP testing tool.
Prerequisites
- Hurl ≥ 4.0
Install:
cargo install hurlor via your package manager
Configuration
Edit test.env to match your local instance. This file is the single source of
truth: run.sh sources it for shell variables and passes it to Hurl as
--variables-file.
base_url=http://localhost:8087
username=admin
email=admin@example.com
password=TestPassword1!
Running the tests
# First-time setup (run once on a fresh instance)
hurl --variables-file tests/api/test.env --test tests/api/setup.hurl
# Contacts CRUD scenario
hurl --variables-file tests/api/test.env --test tests/api/contacts.hurl
# All scenarios at once
hurl --variables-file tests/api/test.env --test tests/api/setup.hurl tests/api/contacts.hurl
# With full request/response output
hurl --variables-file tests/api/test.env --test --verbose tests/api/contacts.hurl
# Generate an HTML report
hurl --variables-file tests/api/test.env --test --report-html /tmp/hurl-report tests/api/contacts.hurl
Test files
| File | Description |
|---|---|
setup.hurl |
One-time admin account creation; also asserts the endpoint is locked afterwards |
files-folders.hurl |
Files & folders CRUD scenario (22 steps, see below) |
favorites.hurl |
Favorites add/list/remove scenario (11 steps); depends on files-folders.hurl state |
trash.hurl |
Trash move/restore/purge scenario (16 steps); depends on files-folders.hurl state |
recent.hurl |
Recent items record/list/clear scenario (6 steps); depends on files-folders.hurl state |
contacts.hurl |
Full contacts CRUD scenario (14 steps, see below) |
test.env |
Variables: base_url, username, email, password — used by both Hurl and run.sh |
Scenario: files-folders.hurl
| Step | Description |
|---|---|
| 1 | Login – capture JWT token |
| 2 | List root folders – assert exactly 1 (home folder), capture home_folder_id, assert parent_id is null |
| 3 | Browse home folder sub-folders – assert empty |
| 4 | Browse home folder files – assert empty |
| 5 | Create folder named "/" – assert HTTP 400, error_type == "Invalid Input" |
| 6 | Create test1 inside home folder – capture test1_id, assert parent_id == home_folder_id |
| 7 | Create test2 inside test1 – capture test2_id, assert parent_id == test1_id |
| 8 | Browse home folder – assert exactly 1 sub-folder (test1) |
| 9 | Browse test1 – assert exactly 1 sub-folder (test2), assert parent_id |
| 10 | Upload fixtures/hello.txt into test2 – capture file_id, assert name/size/mime_type/folder_id |
| 11 | List files in test2 – assert count=1, mime_type == text/plain, icon_class == fas fa-file-alt |
| 12 | Move test2 from test1 into home folder – assert parent_id == home_folder_id |
| 13 | Browse test1 – assert empty (no more children) |
| 14 | Browse home folder – assert 2 sub-folders (test1 and test2) |
| 15 | Rename test2 → test2-renamed – assert new name, parent_id unchanged |
| 16 | Rename hello.txt → hello-renamed.txt – assert new name, folder_id unchanged |
| 17 | Rename hello-renamed.txt to "." – assert HTTP 400, error_type == "Invalid Input" |
| 18 | Upload fixtures/oxicloud-logo.jpg into home folder – capture logo_id, assert mime_type == image/jpeg |
| 19 | List files in home folder – assert count=1, icon_class == fas fa-file-image |
| 20 | GET /api/files/{logo_id}/thumbnail/icon → HTTP 200 |
| 21 | GET /api/files/{logo_id}/thumbnail/preview → HTTP 200 |
| 22 | GET /api/files/{logo_id}/thumbnail/large → HTTP 200 |
Scenario: favorites.hurl
Depends on files-folders.hurl having run first (test1, test2-renamed/hello-renamed.txt must exist).
| Step | Description |
|---|---|
| 1 | Login – capture JWT token |
| 2 | Assert no favorites yet |
| 3 | Discover item IDs: home folder → contents (test1=$[0], test2-renamed=$[1]) → files in test2-renamed |
| 4 | POST /api/favorites/file/{file_id} – add hello-renamed.txt → HTTP 201 |
| 5 | List favorites – assert count=1, item_type=file, item_name=hello-renamed.txt |
| 6 | POST /api/favorites/folder/{test1_id} – add test1 → HTTP 201 |
| 7 | List favorites – assert count=2, both IDs present (order-independent) |
| 8 | DELETE /api/favorites/file/{file_id} – remove hello-renamed.txt → HTTP 200 |
| 9 | List favorites – assert count=1, item_type=folder, item_name=test1 |
| 10 | Cleanup: DELETE /api/favorites/folder/{test1_id} → HTTP 200 |
| 11 | List favorites – assert count=0 |
Scenario: trash.hurl
Depends on files-folders.hurl having run first (home folder must exist).
| Step | Description |
|---|---|
| 1 | Login – capture JWT token |
| 2 | Assert trash is empty |
| 3 | Capture home folder ID |
| 4 | Create to-delete folder in home folder – capture to_delete_id |
| 5 | Upload hello.txt into to-delete |
| 6 | DELETE /api/folders/{to_delete_id} – moves folder to trash → HTTP 204 |
| 7 | List trash – assert count=1, item_type=folder, name=to-delete, original_id matches; capture trash_id |
| 8 | List home folder contents – assert to-delete is not present |
| 9 | POST /api/trash/{trash_id}/restore → HTTP 200, success == true |
| 10 | List home folder contents – assert to-delete is present |
| 11 | List files in to-delete – assert count=1, name=hello.txt |
| 12 | List trash – assert empty (restore removed the entry) |
| 13 | DELETE /api/folders/{to_delete_id} – move restored folder to trash again → HTTP 204 |
| 14 | DELETE /api/trash/empty – purge trash → HTTP 200, success == true |
| 15 | List trash – assert empty after purge |
| 16 | List home folder contents – assert to-delete is not present (permanently gone) |
Scenario: recent.hurl
Depends on files-folders.hurl having run first (test2-renamed/hello-renamed.txt must exist).
Recent items are not auto-recorded on upload — step 3 explicitly registers the access.
| Step | Description |
|---|---|
| 1 | Login – capture JWT token |
| 2 | Discover file_id of hello-renamed.txt via home folder → test2-renamed contents |
| 3 | POST /api/recent/file/{file_id} – record access → HTTP 200 |
| 4 | GET /api/recent – assert count=1, item_type=file, item_name=hello-renamed.txt |
| 5 | DELETE /api/recent/clear – clear all recent items → HTTP 200 |
| 6 | GET /api/recent – assert count=0 |
Scenario: contacts.hurl
| Step | Description |
|---|---|
| 1 | Login – capture JWT token |
| 2 | List address books – assert system book is present and read-only |
| 3 | Create personal address book – capture book_id |
| 4 | List contacts in new book – assert empty |
| 5 | Create contact John Doe – capture contact_id |
| 6 | List contacts – assert exactly 1 result with John Doe's id |
| 7 | Get John Doe – assert all fields, capture ETag |
| 8 | Update John Doe (nickname, org, notes) with If-Match – assert new values, capture refreshed ETag |
| 9 | Delete John Doe with If-Match |
| 10 | List contacts – assert empty again |
| 11 | Delete personal address book |
| 12 | List address books – assert book_id no longer present |
| 13 | List system address book – assert non-empty collection of OxiCloud users |
Legacy bash tests
test.sh and common.sh are the original curl/bash scripts kept for reference.
Run them with bash tests/api/test.sh from the repo root (requires jq).