e3823ce470
Add an end-to-end and unit test suite for the SvelteKit frontend:
- Playwright e2e specs (tests/e2e/spa) with a throwaway container stack,
codegen scenarios, and an Istanbul-based coverage report pipeline.
- Vitest unit tests across API endpoints, components, stores and composables.
- `data-testid` hooks on interactive elements (AppShell, FileViewer,
ShareDialog, search, photos, files breadcrumbs, login/Nextcloud flows,
public share pages) so the e2e suite can target them deterministically.
- Serve the SPA app-shell CSP from a <meta> policy (svelte.config.js) plus a
middleware that skips the CSP header on HTML; move the Nextcloud Login Flow
v2 grant page to the SvelteKit /nextcloud/login route.
- `just front-codegen` recipe and start-server-spa.sh harness.
Make the test environment robust and consistent:
- Install a deterministic in-memory localStorage/sessionStorage in the Vitest
setup so storage behaves identically across Node versions (Node 26 ships a
native Web Storage global that otherwise shadows jsdom's).
- Pin devenv to Node 26 + PostgreSQL 18 and pin every CI job to Node 26.3.0
so the dev shell and CI run the same toolchain versions.
Repair the API/WebDAV (hurl) suite, which had drifted from the backend:
- Migrate the removed `/api/folders/{id}/listing` endpoint to `/resources`
(cursor-paginated `{items:[{resource_type,resource}]}` shape) across the
batch-copy, grants, nested-group, and WebDAV NC tests + the dav_helpers
wipe routine.
- Stop photos_etag from uploading the dedup-tracked fixture so the dedup
blob-lifecycle test can own its content-addressed blob exclusively.
- dedup_create now asserts the idempotent same-content re-upload (201 +
existing file id) instead of the stale 409 expectation.
Generated coverage reports, nyc output and the e2e server runtime data dir
are gitignored rather than committed.
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).