Files
Oxicloud/tests/api
Edouard Vanbelle 1ea3826660 feat(jobs): jobs describe themselves — description, mutates, repair_description
The admin panel had no repair toggle wired to anything but a hardcoded
name list naming the two refcount tenants, so `thumb_derived_import` and
`thumb_attached_import` could not be run in repair mode from the UI at
all despite supporting it. And nothing in the job list said what any
given job does or whether clicking Run on production writes anything.

Three defaulted methods on `JobHandler` and `RecoverableJobHandler`:

    fn description(&self) -> &'static str
    fn mutates(&self) -> Mutates          // Never | Always | OnRepairOnly
    fn repair_description(&self) -> Option<&'static str>

`RecoverableAdapter` forwards them — the registry only holds
`dyn JobHandler`, so a tenant's metadata is invisible otherwise, and
falling back to the defaults would report every recoverable job as
read-only, including the ones that delete files.

Three values rather than a boolean because a job can be read-only by
default and destructive under `?repair=true`; a boolean answers wrongly
for one of its two modes, and `false` on something that unlinks files is
the dangerous direction to be wrong in. `repair_description` returning
`Option` collapses "does it repair" and "what does repair do" into one
method: presence gates the toggle, content is the confirmation text —
which the frontend cannot invent, since correcting a counter and
deleting sidecars are not the same warning.

`OnRepairOnly` with no `repair_description` is rejected at registration:
it claims to mutate only under a flag it does not support.

All 17 registered jobs declare all three. The panel now renders the
description under each name, badges read-only jobs, confirms before a
plain run of a mutating one, and offers the repair variant off the
backend flag instead of the name list.

Descriptions are English in the trait, next to the behaviour: one in
`locales/*.json` rots invisibly the moment a job changes, and a
translator cannot know what `manifests_consistency` reconciles. i18n can
layer on later keyed by job name with these as the fallback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 13:41:05 +02:00
..
2026-05-11 20:00:22 +02:00
2026-05-11 00:53:29 +02:00
2026-05-11 10:53:59 +02:00

API functional tests

Tests are written using Hurl — a plain-text, CLI-first HTTP testing tool.

Prerequisites

  • Hurl ≥ 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

# 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).