# API functional tests Tests are written using [Hurl](https://hurl.dev) — a plain-text, CLI-first HTTP testing tool. ## Prerequisites - [Hurl](https://hurl.dev/docs/installation.html) ≥ 4.0 Install: `cargo install hurl` or 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 ```bash # 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`).